mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: subscription callback + canceled date
This commit is contained in:
parent
b573962f78
commit
abfcbcf3bf
@ -27,6 +27,7 @@ import 'package:appflowy/workspace/application/settings/appearance/desktop_appea
|
||||
import 'package:appflowy/workspace/application/settings/appearance/mobile_appearance.dart';
|
||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||
import 'package:appflowy/workspace/application/sidebar/rename_view/rename_view_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/subscription_success_listenable/subscription_success_listenable.dart';
|
||||
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/user/prelude.dart';
|
||||
import 'package:appflowy/workspace/application/view/prelude.dart';
|
||||
@ -168,6 +169,9 @@ void _resolveUserDeps(GetIt getIt, IntegrationMode mode) {
|
||||
getIt.registerFactory<SplashBloc>(() => SplashBloc());
|
||||
getIt.registerLazySingleton<NetworkListener>(() => NetworkListener());
|
||||
getIt.registerLazySingleton<CachedRecentService>(() => CachedRecentService());
|
||||
getIt.registerLazySingleton<SubscriptionSuccessListenable>(
|
||||
() => SubscriptionSuccessListenable(),
|
||||
);
|
||||
}
|
||||
|
||||
void _resolveHomeDeps(GetIt getIt) {
|
||||
|
@ -12,6 +12,7 @@ import 'package:appflowy/user/application/auth/auth_error.dart';
|
||||
import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||
import 'package:appflowy/user/application/auth/device_id.dart';
|
||||
import 'package:appflowy/user/application/user_auth_listener.dart';
|
||||
import 'package:appflowy/workspace/application/subscription_success_listenable/subscription_success_listenable.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/toast.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
@ -93,6 +94,10 @@ class AppFlowyCloudDeepLink {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_isPaymentSuccessUri(uri)) {
|
||||
return getIt<SubscriptionSuccessListenable>().onPaymentSuccess();
|
||||
}
|
||||
|
||||
return _isAuthCallbackDeepLink(uri).fold(
|
||||
(_) async {
|
||||
final deviceId = await getDeviceId();
|
||||
@ -161,6 +166,10 @@ class AppFlowyCloudDeepLink {
|
||||
..msg = uri.path,
|
||||
);
|
||||
}
|
||||
|
||||
bool _isPaymentSuccessUri(Uri uri) {
|
||||
return uri.host == 'payment-success';
|
||||
}
|
||||
}
|
||||
|
||||
class InitAppFlowyCloudTask extends LaunchTask {
|
||||
|
@ -1,5 +1,7 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart';
|
||||
@ -7,8 +9,6 @@ import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:fixnum/fixnum.dart';
|
||||
|
||||
const _deepLinkSubscriptionUrl = 'appflowy-flutter://subscription-callback';
|
||||
|
||||
class UserBackendService {
|
||||
UserBackendService({required this.userId});
|
||||
|
||||
@ -234,8 +234,7 @@ class UserBackendService {
|
||||
..recurringInterval = RecurringIntervalPB.Month
|
||||
..workspaceSubscriptionPlan = plan
|
||||
..successUrl =
|
||||
'http://$_deepLinkSubscriptionUrl'; // TODO(Mathias): Change once Zack has resolved
|
||||
|
||||
'${getIt<AppFlowyCloudSharedEnv>().appflowyCloudConfig.base_url}/web/payment-success';
|
||||
return UserEventSubscribeWorkspace(request).send();
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,112 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:appflowy/user/application/user_service.dart';
|
||||
import 'package:appflowy/workspace/application/workspace/workspace_service.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/code.pbenum.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/workspace.pbserver.dart';
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'settings_billing_bloc.freezed.dart';
|
||||
|
||||
class SettingsBillingBloc
|
||||
extends Bloc<SettingsBillingEvent, SettingsBillingState> {
|
||||
SettingsBillingBloc({
|
||||
required this.workspaceId,
|
||||
}) : super(const _Initial()) {
|
||||
_service = WorkspaceService(workspaceId: workspaceId);
|
||||
|
||||
on<SettingsBillingEvent>((event, emit) async {
|
||||
await event.when(
|
||||
started: () async {
|
||||
emit(const SettingsBillingState.loading());
|
||||
|
||||
final snapshots = await Future.wait([
|
||||
UserBackendService.getWorkspaceSubscriptions(),
|
||||
_service.getBillingPortal(),
|
||||
]);
|
||||
|
||||
FlowyError? error;
|
||||
|
||||
final subscription = snapshots.first.fold(
|
||||
(s) =>
|
||||
(s as RepeatedWorkspaceSubscriptionPB)
|
||||
.items
|
||||
.firstWhereOrNull((i) => i.workspaceId == workspaceId) ??
|
||||
WorkspaceSubscriptionPB(
|
||||
workspaceId: workspaceId,
|
||||
subscriptionPlan: SubscriptionPlanPB.None,
|
||||
isActive: true,
|
||||
),
|
||||
(e) {
|
||||
// Not a Cjstomer yet
|
||||
if (e.code == ErrorCode.InvalidParams) {
|
||||
return WorkspaceSubscriptionPB(
|
||||
workspaceId: workspaceId,
|
||||
subscriptionPlan: SubscriptionPlanPB.None,
|
||||
isActive: true,
|
||||
);
|
||||
}
|
||||
|
||||
error = e;
|
||||
return null;
|
||||
},
|
||||
);
|
||||
|
||||
final billingPortalResult = snapshots.last;
|
||||
final billingPortal = billingPortalResult.fold(
|
||||
(s) => s as BillingPortalPB,
|
||||
(e) {
|
||||
// Not a customer yet
|
||||
if (e.code == ErrorCode.InvalidParams) {
|
||||
return BillingPortalPB();
|
||||
}
|
||||
|
||||
error = e;
|
||||
return null;
|
||||
},
|
||||
);
|
||||
|
||||
if (subscription == null || billingPortal == null || error != null) {
|
||||
return emit(SettingsBillingState.error(error: error));
|
||||
}
|
||||
|
||||
emit(
|
||||
SettingsBillingState.ready(
|
||||
subscription: subscription,
|
||||
billingPortal: billingPortal,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
late final String workspaceId;
|
||||
late final WorkspaceService _service;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SettingsBillingEvent with _$SettingsBillingEvent {
|
||||
const factory SettingsBillingEvent.started() = _Started;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SettingsBillingState with _$SettingsBillingState {
|
||||
const factory SettingsBillingState.initial() = _Initial;
|
||||
|
||||
const factory SettingsBillingState.loading() = _Loading;
|
||||
|
||||
const factory SettingsBillingState.error({
|
||||
@Default(null) FlowyError? error,
|
||||
}) = _Error;
|
||||
|
||||
const factory SettingsBillingState.ready({
|
||||
required WorkspaceSubscriptionPB subscription,
|
||||
required BillingPortalPB? billingPortal,
|
||||
}) = _Ready;
|
||||
}
|
@ -1,7 +1,9 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:appflowy/core/helpers/url_launcher.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/user_service.dart';
|
||||
import 'package:appflowy/workspace/application/subscription_success_listenable/subscription_success_listenable.dart';
|
||||
import 'package:appflowy/workspace/application/workspace/workspace_service.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/code.pbenum.dart';
|
||||
@ -20,6 +22,8 @@ class SettingsPlanBloc extends Bloc<SettingsPlanEvent, SettingsPlanState> {
|
||||
required this.workspaceId,
|
||||
}) : super(const _Initial()) {
|
||||
_service = WorkspaceService(workspaceId: workspaceId);
|
||||
_successListenable = getIt<SubscriptionSuccessListenable>();
|
||||
_successListenable.addListener(_onPaymentSuccessful);
|
||||
|
||||
on<SettingsPlanEvent>((event, emit) async {
|
||||
await event.when(
|
||||
@ -101,12 +105,32 @@ class SettingsPlanBloc extends Bloc<SettingsPlanEvent, SettingsPlanState> {
|
||||
cancelSubscription: () async {
|
||||
await UserBackendService.cancelSubscription(workspaceId);
|
||||
},
|
||||
paymentSuccessful: () {
|
||||
final readyState = state.mapOrNull(ready: (state) => state);
|
||||
if (readyState == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
emit(readyState.copyWith(showSuccessDialog: true));
|
||||
emit(readyState.copyWith(showSuccessDialog: false));
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
late final String workspaceId;
|
||||
late final WorkspaceService _service;
|
||||
late final SubscriptionSuccessListenable _successListenable;
|
||||
|
||||
void _onPaymentSuccessful() {
|
||||
add(const SettingsPlanEvent.paymentSuccessful());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
_successListenable.removeListener(_onPaymentSuccessful);
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
@ -115,6 +139,7 @@ class SettingsPlanEvent with _$SettingsPlanEvent {
|
||||
const factory SettingsPlanEvent.addSubscription(SubscriptionPlanPB plan) =
|
||||
_AddSubscription;
|
||||
const factory SettingsPlanEvent.cancelSubscription() = _CancelSubscription;
|
||||
const factory SettingsPlanEvent.paymentSuccessful() = _PaymentSuccessful;
|
||||
}
|
||||
|
||||
@freezed
|
||||
@ -131,5 +156,6 @@ class SettingsPlanState with _$SettingsPlanState {
|
||||
required WorkspaceUsagePB workspaceUsage,
|
||||
required WorkspaceSubscriptionPB subscription,
|
||||
required BillingPortalPB? billingPortal,
|
||||
@Default(false) bool showSuccessDialog,
|
||||
}) = _Ready;
|
||||
}
|
||||
|
@ -0,0 +1,7 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
class SubscriptionSuccessListenable extends ChangeNotifier {
|
||||
SubscriptionSuccessListenable();
|
||||
|
||||
void onPaymentSuccess() => notifyListeners();
|
||||
}
|
@ -1,48 +1,123 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/core/helpers/url_launcher.dart';
|
||||
import 'package:appflowy/workspace/application/settings/billing/settings_billing_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/settings/plan/settings_plan_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/settings/plan/workspace_subscription_ext.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/pages/settings_plan_comparison_dialog.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/shared/settings_category.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/shared/single_setting_action.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/workspace.pbenum.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flowy_infra_ui/widget/error_page.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../../../generated/locale_keys.g.dart';
|
||||
|
||||
class SettingsBillingView extends StatelessWidget {
|
||||
const SettingsBillingView({super.key});
|
||||
const SettingsBillingView({super.key, required this.workspaceId});
|
||||
|
||||
final String workspaceId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SettingsBody(
|
||||
title: LocaleKeys.settings_billingPage_title.tr(),
|
||||
description: LocaleKeys.settings_billingPage_description.tr(),
|
||||
children: [
|
||||
SettingsCategory(
|
||||
title: LocaleKeys.settings_billingPage_plan_title.tr(),
|
||||
children: [
|
||||
SingleSettingAction(
|
||||
label: LocaleKeys.settings_billingPage_plan_freeLabel.tr(),
|
||||
buttonLabel:
|
||||
LocaleKeys.settings_billingPage_plan_planButtonLabel.tr(),
|
||||
return BlocProvider<SettingsBillingBloc>(
|
||||
create: (context) => SettingsBillingBloc(workspaceId: workspaceId)
|
||||
..add(const SettingsBillingEvent.started()),
|
||||
child: BlocBuilder<SettingsBillingBloc, SettingsBillingState>(
|
||||
builder: (context, state) {
|
||||
return state.map(
|
||||
initial: (_) => const SizedBox.shrink(),
|
||||
loading: (_) => const Center(
|
||||
child: SizedBox(
|
||||
height: 24,
|
||||
width: 24,
|
||||
child: CircularProgressIndicator.adaptive(strokeWidth: 3),
|
||||
),
|
||||
),
|
||||
SingleSettingAction(
|
||||
label: LocaleKeys.settings_billingPage_plan_billingPeriod.tr(),
|
||||
buttonLabel:
|
||||
LocaleKeys.settings_billingPage_plan_periodButtonLabel.tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
SettingsCategory(
|
||||
title: LocaleKeys.settings_billingPage_paymentDetails_title.tr(),
|
||||
children: [
|
||||
SingleSettingAction(
|
||||
label: LocaleKeys.settings_billingPage_paymentDetails_methodLabel
|
||||
.tr(),
|
||||
buttonLabel: LocaleKeys
|
||||
.settings_billingPage_paymentDetails_methodButtonLabel
|
||||
.tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
error: (state) {
|
||||
if (state.error != null) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: FlowyErrorPage.message(
|
||||
state.error!.msg,
|
||||
howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ErrorWidget.withDetails(message: 'Something went wrong!');
|
||||
},
|
||||
ready: (state) {
|
||||
return SettingsBody(
|
||||
title: LocaleKeys.settings_billingPage_title.tr(),
|
||||
children: [
|
||||
SettingsCategory(
|
||||
title: LocaleKeys.settings_billingPage_plan_title.tr(),
|
||||
children: [
|
||||
SingleSettingAction(
|
||||
onPressed: () => _openPricingDialog(
|
||||
context,
|
||||
workspaceId,
|
||||
state.subscription.subscriptionPlan,
|
||||
),
|
||||
label: state.subscription.label,
|
||||
buttonLabel: LocaleKeys
|
||||
.settings_billingPage_plan_planButtonLabel
|
||||
.tr(),
|
||||
),
|
||||
SingleSettingAction(
|
||||
onPressed: () =>
|
||||
afLaunchUrlString(state.billingPortal!.url),
|
||||
label: LocaleKeys
|
||||
.settings_billingPage_plan_billingPeriod
|
||||
.tr(),
|
||||
buttonLabel: LocaleKeys
|
||||
.settings_billingPage_plan_periodButtonLabel
|
||||
.tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
SettingsCategory(
|
||||
title: LocaleKeys.settings_billingPage_paymentDetails_title
|
||||
.tr(),
|
||||
children: [
|
||||
SingleSettingAction(
|
||||
onPressed: () =>
|
||||
afLaunchUrlString(state.billingPortal!.url),
|
||||
label: LocaleKeys
|
||||
.settings_billingPage_paymentDetails_methodLabel
|
||||
.tr(),
|
||||
buttonLabel: LocaleKeys
|
||||
.settings_billingPage_paymentDetails_methodButtonLabel
|
||||
.tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _openPricingDialog(
|
||||
BuildContext context,
|
||||
String workspaceId,
|
||||
SubscriptionPlanPB plan,
|
||||
) =>
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (_) => BlocProvider<SettingsPlanBloc>(
|
||||
create: (_) => SettingsPlanBloc(workspaceId: workspaceId)
|
||||
..add(const SettingsPlanEvent.started()),
|
||||
child: SettingsPlanComparisonDialog(
|
||||
workspaceId: workspaceId,
|
||||
currentPlan: plan,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -39,139 +39,147 @@ class _SettingsPlanComparisonDialogState
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FlowyDialog(
|
||||
constraints: const BoxConstraints(maxWidth: 784, minWidth: 674),
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 24, left: 24, right: 24),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const FlowyText.semibold(
|
||||
'Compare & select plan',
|
||||
fontSize: 24,
|
||||
),
|
||||
const Spacer(),
|
||||
GestureDetector(
|
||||
onTap: Navigator.of(context).pop,
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: FlowySvg(
|
||||
FlowySvgs.m_close_m,
|
||||
size: const Size.square(20),
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: SingleChildScrollView(
|
||||
controller: horizontalController,
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: SingleChildScrollView(
|
||||
controller: verticalController,
|
||||
padding: const EdgeInsets.only(left: 24, right: 24, bottom: 24),
|
||||
child: Column(
|
||||
return BlocBuilder<SettingsPlanBloc, SettingsPlanState>(
|
||||
builder: (context, state) {
|
||||
return FlowyDialog(
|
||||
constraints: const BoxConstraints(maxWidth: 784, minWidth: 674),
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 24, left: 24, right: 24),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const VSpace(18),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 248,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const VSpace(22),
|
||||
const SizedBox(
|
||||
height: 100,
|
||||
child: FlowyText.semibold(
|
||||
'Plan\nFeatures',
|
||||
fontSize: 24,
|
||||
maxLines: 2,
|
||||
color: Color(0xFF5C3699),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 64),
|
||||
const SizedBox(height: 56),
|
||||
..._planLabels.map(
|
||||
(e) => _ComparisonCell(
|
||||
label: e.label,
|
||||
tooltip: e.tooltip,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
FlowyText.semibold(
|
||||
LocaleKeys.settings_comparePlanDialog_title.tr(),
|
||||
fontSize: 24,
|
||||
),
|
||||
const Spacer(),
|
||||
GestureDetector(
|
||||
onTap: Navigator.of(context).pop,
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: FlowySvg(
|
||||
FlowySvgs.m_close_m,
|
||||
size: const Size.square(20),
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
_PlanTable(
|
||||
title: LocaleKeys
|
||||
.settings_comparePlanDialog_freePlan_title
|
||||
.tr(),
|
||||
description: LocaleKeys
|
||||
.settings_comparePlanDialog_freePlan_description
|
||||
.tr(),
|
||||
price: LocaleKeys
|
||||
.settings_comparePlanDialog_freePlan_price
|
||||
.tr(),
|
||||
priceInfo: LocaleKeys
|
||||
.settings_comparePlanDialog_freePlan_priceInfo
|
||||
.tr(),
|
||||
cells: _freeLabels,
|
||||
isCurrent:
|
||||
widget.currentPlan == SubscriptionPlanPB.None,
|
||||
canDowngrade:
|
||||
widget.currentPlan != SubscriptionPlanPB.None,
|
||||
onSelected: () async {
|
||||
if (widget.currentPlan == SubscriptionPlanPB.None) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.read<SettingsPlanBloc>().add(
|
||||
const SettingsPlanEvent.cancelSubscription(),
|
||||
);
|
||||
},
|
||||
),
|
||||
_PlanTable(
|
||||
title: LocaleKeys
|
||||
.settings_comparePlanDialog_proPlan_title
|
||||
.tr(),
|
||||
description: LocaleKeys
|
||||
.settings_comparePlanDialog_proPlan_description
|
||||
.tr(),
|
||||
price: LocaleKeys
|
||||
.settings_comparePlanDialog_proPlan_price
|
||||
.tr(),
|
||||
priceInfo: LocaleKeys
|
||||
.settings_comparePlanDialog_proPlan_priceInfo
|
||||
.tr(),
|
||||
cells: _proLabels,
|
||||
isCurrent:
|
||||
widget.currentPlan == SubscriptionPlanPB.Pro,
|
||||
canUpgrade:
|
||||
widget.currentPlan == SubscriptionPlanPB.None,
|
||||
onSelected: () =>
|
||||
context.read<SettingsPlanBloc>().add(
|
||||
const SettingsPlanEvent.addSubscription(
|
||||
SubscriptionPlanPB.Pro,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: SingleChildScrollView(
|
||||
controller: horizontalController,
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: SingleChildScrollView(
|
||||
controller: verticalController,
|
||||
padding:
|
||||
const EdgeInsets.only(left: 24, right: 24, bottom: 24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 250,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const VSpace(22),
|
||||
SizedBox(
|
||||
height: 100,
|
||||
child: FlowyText.semibold(
|
||||
LocaleKeys
|
||||
.settings_comparePlanDialog_planFeatures
|
||||
.tr(),
|
||||
fontSize: 24,
|
||||
maxLines: 2,
|
||||
color: const Color(0xFF5C3699),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 64),
|
||||
const SizedBox(height: 56),
|
||||
..._planLabels.map(
|
||||
(e) => _ComparisonCell(
|
||||
label: e.label,
|
||||
tooltip: e.tooltip,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
_PlanTable(
|
||||
title: LocaleKeys
|
||||
.settings_comparePlanDialog_freePlan_title
|
||||
.tr(),
|
||||
description: LocaleKeys
|
||||
.settings_comparePlanDialog_freePlan_description
|
||||
.tr(),
|
||||
price: LocaleKeys
|
||||
.settings_comparePlanDialog_freePlan_price
|
||||
.tr(),
|
||||
priceInfo: LocaleKeys
|
||||
.settings_comparePlanDialog_freePlan_priceInfo
|
||||
.tr(),
|
||||
cells: _freeLabels,
|
||||
isCurrent:
|
||||
widget.currentPlan == SubscriptionPlanPB.None,
|
||||
canDowngrade:
|
||||
widget.currentPlan != SubscriptionPlanPB.None,
|
||||
onSelected: () async {
|
||||
if (widget.currentPlan ==
|
||||
SubscriptionPlanPB.None) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.read<SettingsPlanBloc>().add(
|
||||
const SettingsPlanEvent
|
||||
.cancelSubscription(),
|
||||
);
|
||||
},
|
||||
),
|
||||
_PlanTable(
|
||||
title: LocaleKeys
|
||||
.settings_comparePlanDialog_proPlan_title
|
||||
.tr(),
|
||||
description: LocaleKeys
|
||||
.settings_comparePlanDialog_proPlan_description
|
||||
.tr(),
|
||||
price: LocaleKeys
|
||||
.settings_comparePlanDialog_proPlan_price
|
||||
.tr(),
|
||||
priceInfo: LocaleKeys
|
||||
.settings_comparePlanDialog_proPlan_priceInfo
|
||||
.tr(),
|
||||
cells: _proLabels,
|
||||
isCurrent:
|
||||
widget.currentPlan == SubscriptionPlanPB.Pro,
|
||||
canUpgrade:
|
||||
widget.currentPlan == SubscriptionPlanPB.None,
|
||||
onSelected: () =>
|
||||
context.read<SettingsPlanBloc>().add(
|
||||
const SettingsPlanEvent.addSubscription(
|
||||
SubscriptionPlanPB.Pro,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -205,7 +213,7 @@ class _PlanTable extends StatelessWidget {
|
||||
final highlightPlan = !isCurrent && !canDowngrade && canUpgrade;
|
||||
|
||||
return Container(
|
||||
width: 200,
|
||||
width: 210,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
gradient: !highlightPlan
|
||||
@ -248,11 +256,27 @@ class _PlanTable extends StatelessWidget {
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 12),
|
||||
child: _ActionButton(
|
||||
label: canUpgrade && !canDowngrade
|
||||
? LocaleKeys.settings_comparePlanDialog_actions_upgrade
|
||||
.tr()
|
||||
: LocaleKeys.settings_comparePlanDialog_actions_downgrade
|
||||
.tr(),
|
||||
onPressed: onSelected,
|
||||
isUpgrade: canUpgrade && !canDowngrade,
|
||||
useGradientBorder: !isCurrent && canUpgrade,
|
||||
),
|
||||
),
|
||||
] else if (isCurrent) ...[
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 12),
|
||||
child: _ActionButton(
|
||||
label: LocaleKeys.settings_comparePlanDialog_actions_current
|
||||
.tr(),
|
||||
onPressed: () {},
|
||||
isUpgrade: canUpgrade && !canDowngrade,
|
||||
useGradientBorder: !isCurrent && canUpgrade,
|
||||
),
|
||||
),
|
||||
] else ...[
|
||||
const SizedBox(height: 56),
|
||||
],
|
||||
@ -285,8 +309,7 @@ class _ComparisonCell extends StatelessWidget {
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
FlowyText.medium(label),
|
||||
const Spacer(),
|
||||
Expanded(child: FlowyText.medium(label)),
|
||||
if (tooltip != null)
|
||||
FlowyTooltip(
|
||||
message: tooltip,
|
||||
@ -300,11 +323,13 @@ class _ComparisonCell extends StatelessWidget {
|
||||
|
||||
class _ActionButton extends StatelessWidget {
|
||||
const _ActionButton({
|
||||
required this.label,
|
||||
required this.onPressed,
|
||||
required this.isUpgrade,
|
||||
this.useGradientBorder = false,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final VoidCallback onPressed;
|
||||
final bool isUpgrade;
|
||||
final bool useGradientBorder;
|
||||
@ -341,13 +366,7 @@ class _ActionButton extends StatelessWidget {
|
||||
),
|
||||
child: Center(
|
||||
child: _drawText(
|
||||
isUpgrade
|
||||
? LocaleKeys
|
||||
.settings_comparePlanDialog_actions_upgrade
|
||||
.tr()
|
||||
: LocaleKeys
|
||||
.settings_comparePlanDialog_actions_downgrade
|
||||
.tr(),
|
||||
label,
|
||||
isLM,
|
||||
),
|
||||
),
|
||||
|
@ -2,6 +2,9 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/util/int64_extension.dart';
|
||||
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
|
||||
import 'package:appflowy/workspace/application/settings/date_time/date_format_ext.dart';
|
||||
import 'package:appflowy/workspace/application/settings/plan/settings_plan_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/settings/plan/workspace_subscription_ext.dart';
|
||||
import 'package:appflowy/workspace/application/settings/plan/workspace_usage_ext.dart';
|
||||
@ -117,6 +120,19 @@ class _CurrentPlanBox extends StatelessWidget {
|
||||
subscription.subscriptionPlan,
|
||||
),
|
||||
),
|
||||
if (subscription.hasCanceled) ...[
|
||||
const VSpace(12),
|
||||
FlowyText(
|
||||
LocaleKeys
|
||||
.settings_planPage_planUsage_currentPlan_canceledInfo
|
||||
.tr(
|
||||
args: [_canceledDate(context)],
|
||||
),
|
||||
maxLines: 5,
|
||||
fontSize: 12,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -197,6 +213,15 @@ class _CurrentPlanBox extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
String _canceledDate(BuildContext context) {
|
||||
final appearance = context.read<AppearanceSettingsCubit>().state;
|
||||
return appearance.dateFormat.formatDate(
|
||||
subscription.canceledAt.toDateTime(),
|
||||
true,
|
||||
appearance.timeFormat,
|
||||
);
|
||||
}
|
||||
|
||||
void _openPricingDialog(
|
||||
BuildContext context,
|
||||
String workspaceId,
|
||||
@ -282,16 +307,19 @@ class _PlanUsageSummary extends StatelessWidget {
|
||||
usage.totalBlobBytesLimit.toInt(),
|
||||
),
|
||||
),
|
||||
// TODO(Mathias): Implement AI Usage once it's ready in backend
|
||||
Expanded(
|
||||
child: _UsageBox(
|
||||
title:
|
||||
LocaleKeys.settings_planPage_planUsage_aiResponseLabel.tr(),
|
||||
label:
|
||||
LocaleKeys.settings_planPage_planUsage_aiResponseUsage.tr(
|
||||
args: ['750', '1,000'],
|
||||
title: LocaleKeys.settings_planPage_planUsage_collaboratorsLabel
|
||||
.tr(),
|
||||
label: LocaleKeys.settings_planPage_planUsage_collaboratorsUsage
|
||||
.tr(
|
||||
args: [
|
||||
usage.memberCount.toString(),
|
||||
usage.memberCountLimit.toString(),
|
||||
],
|
||||
),
|
||||
value: .75,
|
||||
value: usage.totalBlobBytes.toInt() /
|
||||
usage.totalBlobBytesLimit.toInt(),
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -301,7 +329,7 @@ class _PlanUsageSummary extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_ToggleMore(
|
||||
value: false,
|
||||
value: currentPlan == SubscriptionPlanPB.Pro,
|
||||
label:
|
||||
LocaleKeys.settings_planPage_planUsage_memberProToggle.tr(),
|
||||
currentPlan: currentPlan,
|
||||
@ -309,7 +337,7 @@ class _PlanUsageSummary extends StatelessWidget {
|
||||
),
|
||||
const VSpace(8),
|
||||
_ToggleMore(
|
||||
value: false,
|
||||
value: currentPlan == SubscriptionPlanPB.Pro,
|
||||
label:
|
||||
LocaleKeys.settings_planPage_planUsage_guestCollabToggle.tr(),
|
||||
currentPlan: currentPlan,
|
||||
@ -446,12 +474,12 @@ class _PlanProgressIndicator extends StatelessWidget {
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: AFThemeExtension.of(context).progressBarBGColor,
|
||||
border: Border.all(
|
||||
color: const Color(0xFFDDF1F7).withOpacity(
|
||||
theme.brightness == Brightness.light ? 1 : 0.1,
|
||||
),
|
||||
),
|
||||
color: AFThemeExtension.of(context).progressBarBGColor,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
|
@ -97,7 +97,7 @@ class SettingsDialog extends StatelessWidget {
|
||||
case SettingsPage.plan:
|
||||
return SettingsPlanView(workspaceId: workspaceId);
|
||||
case SettingsPage.billing:
|
||||
return const SettingsBillingView();
|
||||
return SettingsBillingView(workspaceId: workspaceId);
|
||||
case SettingsPage.featureFlags:
|
||||
return const FeatureFlagsPage();
|
||||
default:
|
||||
|
@ -508,10 +508,12 @@
|
||||
"title": "Plan usage summary",
|
||||
"storageLabel": "Storage",
|
||||
"storageUsage": "{} of {} GB",
|
||||
"collaboratorsLabel": "Collaborators",
|
||||
"collaboratorsUsage": "{} of {}",
|
||||
"aiResponseLabel": "AI Responses",
|
||||
"aiResponseUsage": "{} of {}",
|
||||
"proBadge": "Pro",
|
||||
"memberProToggle": "Unlimited members",
|
||||
"memberProToggle": "Up to 10 members",
|
||||
"guestCollabToggle": "10 guest collaborators",
|
||||
"aiCredit": {
|
||||
"title": "Add AppFlowy AI Credit",
|
||||
@ -536,7 +538,8 @@
|
||||
"freeConOne": "30 day revision history",
|
||||
"freeConTwo": "Guest collaborators (edit access)",
|
||||
"freeConThree": "unlimited storage",
|
||||
"freeConFour": "6 month revision history"
|
||||
"freeConFour": "6 month revision history",
|
||||
"canceledInfo": "Your plan is cancelled, you will be downgraded to the Free plan on {}."
|
||||
},
|
||||
"deal": {
|
||||
"bannerLabel": "New year deal!",
|
||||
@ -549,7 +552,6 @@
|
||||
"billingPage": {
|
||||
"menuLabel": "Billing",
|
||||
"title": "Billing",
|
||||
"description": "Customize your profile, manage account security, open AI keys, or login into your account.",
|
||||
"plan": {
|
||||
"title": "Plan",
|
||||
"freeLabel": "Free",
|
||||
@ -565,9 +567,12 @@
|
||||
}
|
||||
},
|
||||
"comparePlanDialog": {
|
||||
"title": "Compare & select plan",
|
||||
"planFeatures": "Plan\nFeatures",
|
||||
"actions": {
|
||||
"upgrade": "Upgrade",
|
||||
"downgrade": "Downgrade"
|
||||
"downgrade": "Downgrade",
|
||||
"current": "Current"
|
||||
},
|
||||
"freePlan": {
|
||||
"title": "Free",
|
||||
@ -585,7 +590,7 @@
|
||||
"itemOne": "Workspaces",
|
||||
"itemTwo": "Members",
|
||||
"itemThree": "Guests",
|
||||
"tooltipThree": "Guests have read-only permission to the specifically chared content",
|
||||
"tooltipThree": "Guests have read-only permission to the specifically shared content",
|
||||
"itemFour": "Guest collaborators",
|
||||
"tooltipFour": "Guest collaborators are billed as one seat",
|
||||
"itemFive": "Storage",
|
||||
@ -612,7 +617,7 @@
|
||||
"itemFive": "unlimited",
|
||||
"itemSix": "yes",
|
||||
"itemSeven": "yes",
|
||||
"itemEight": "100,000 monthly"
|
||||
"itemEight": "10,000 monthly"
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
|
Loading…
Reference in New Issue
Block a user