feat: subscription callback + canceled date

This commit is contained in:
Mathias Mogensen 2024-06-10 12:00:56 +02:00
parent b573962f78
commit abfcbcf3bf
11 changed files with 473 additions and 189 deletions

View File

@ -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) {

View File

@ -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 {

View File

@ -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();
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -0,0 +1,7 @@
import 'package:flutter/foundation.dart';
class SubscriptionSuccessListenable extends ChangeNotifier {
SubscriptionSuccessListenable();
void onPaymentSuccess() => notifyListeners();
}

View File

@ -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,
),
),
);
}

View File

@ -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,
),
),

View File

@ -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),

View File

@ -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:

View File

@ -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": {