feat: downgrade/upgrade dialogs

This commit is contained in:
Mathias Mogensen 2024-06-10 13:41:57 +02:00
parent 89f6056592
commit 08fd136f39
9 changed files with 266 additions and 160 deletions

View File

@ -27,7 +27,7 @@ class SettingsPlanBloc extends Bloc<SettingsPlanEvent, SettingsPlanState> {
on<SettingsPlanEvent>((event, emit) async {
await event.when(
started: () async {
started: (withShowSuccessful) async {
emit(const SettingsPlanState.loading());
final snapshots = await Future.wait([
@ -88,8 +88,19 @@ class SettingsPlanBloc extends Bloc<SettingsPlanEvent, SettingsPlanState> {
workspaceUsage: usageResult,
subscription: subscription,
billingPortal: billingPortal,
showSuccessDialog: withShowSuccessful,
),
);
if (withShowSuccessful) {
emit(
SettingsPlanState.ready(
workspaceUsage: usageResult,
subscription: subscription,
billingPortal: billingPortal,
),
);
}
},
addSubscription: (plan) async {
final result = await UserBackendService.createSubscription(
@ -104,6 +115,7 @@ class SettingsPlanBloc extends Bloc<SettingsPlanEvent, SettingsPlanState> {
},
cancelSubscription: () async {
await UserBackendService.cancelSubscription(workspaceId);
add(const SettingsPlanEvent.started());
},
paymentSuccessful: () {
final readyState = state.mapOrNull(ready: (state) => state);
@ -111,8 +123,7 @@ class SettingsPlanBloc extends Bloc<SettingsPlanEvent, SettingsPlanState> {
return;
}
emit(readyState.copyWith(showSuccessDialog: true));
emit(readyState.copyWith(showSuccessDialog: false));
add(const SettingsPlanEvent.started(withShowSuccessful: true));
},
);
});
@ -135,7 +146,9 @@ class SettingsPlanBloc extends Bloc<SettingsPlanEvent, SettingsPlanState> {
@freezed
class SettingsPlanEvent with _$SettingsPlanEvent {
const factory SettingsPlanEvent.started() = _Started;
const factory SettingsPlanEvent.started({
@Default(false) bool withShowSuccessful,
}) = _Started;
const factory SettingsPlanEvent.addSubscription(SubscriptionPlanPB plan) =
_AddSubscription;
const factory SettingsPlanEvent.cancelSubscription() = _CancelSubscription;

View File

@ -8,7 +8,7 @@ import 'package:appflowy/workspace/presentation/settings/pages/settings_plan_com
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:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/widget/error_page.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@ -63,8 +63,9 @@ class SettingsBillingView extends StatelessWidget {
onPressed: () => _openPricingDialog(
context,
workspaceId,
state.subscription.subscriptionPlan,
state.subscription,
),
fontWeight: FontWeight.w500,
label: state.subscription.label,
buttonLabel: LocaleKeys
.settings_billingPage_plan_planButtonLabel
@ -77,6 +78,7 @@ class SettingsBillingView extends StatelessWidget {
label: LocaleKeys
.settings_billingPage_plan_billingPeriod
.tr(),
fontWeight: FontWeight.w500,
buttonLabel: LocaleKeys
.settings_billingPage_plan_periodButtonLabel
.tr(),
@ -95,6 +97,7 @@ class SettingsBillingView extends StatelessWidget {
label: LocaleKeys
.settings_billingPage_paymentDetails_methodLabel
.tr(),
fontWeight: FontWeight.w500,
buttonLabel: LocaleKeys
.settings_billingPage_paymentDetails_methodButtonLabel
.tr(),
@ -113,7 +116,7 @@ class SettingsBillingView extends StatelessWidget {
void _openPricingDialog(
BuildContext context,
String workspaceId,
SubscriptionPlanPB plan,
WorkspaceSubscriptionPB subscription,
) =>
showDialog(
context: context,
@ -122,7 +125,7 @@ class SettingsBillingView extends StatelessWidget {
..add(const SettingsPlanEvent.started()),
child: SettingsPlanComparisonDialog(
workspaceId: workspaceId,
currentPlan: plan,
subscription: subscription,
),
),
);

View File

@ -3,6 +3,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/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/shared/settings_alert_dialog.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
@ -14,11 +17,11 @@ class SettingsPlanComparisonDialog extends StatefulWidget {
const SettingsPlanComparisonDialog({
super.key,
required this.workspaceId,
required this.currentPlan,
required this.subscription,
});
final String workspaceId;
final SubscriptionPlanPB currentPlan;
final WorkspaceSubscriptionPB subscription;
@override
State<SettingsPlanComparisonDialog> createState() =>
@ -30,6 +33,8 @@ class _SettingsPlanComparisonDialogState
final horizontalController = ScrollController();
final verticalController = ScrollController();
late WorkspaceSubscriptionPB currentSubscription = widget.subscription;
@override
void dispose() {
horizontalController.dispose();
@ -39,147 +44,208 @@ class _SettingsPlanComparisonDialogState
@override
Widget build(BuildContext context) {
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,
children: [
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,
),
),
),
],
return BlocListener<SettingsPlanBloc, SettingsPlanState>(
listener: (context, state) {
final readyState = state.mapOrNull(ready: (state) => state);
if (readyState == null) {
return;
}
if (readyState.showSuccessDialog) {
SettingsAlertDialog(
icon: Center(
child: SizedBox(
height: 90,
width: 90,
child: FlowySvg(
FlowySvgs.check_circle_s,
color: AFThemeExtension.of(context).success,
),
),
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;
}
),
title:
LocaleKeys.settings_comparePlanDialog_paymentSuccess_title.tr(
args: [readyState.subscription.label],
),
subtitle: LocaleKeys
.settings_comparePlanDialog_paymentSuccess_description
.tr(
args: [readyState.subscription.label],
),
hideCancelButton: true,
confirm: Navigator.of(context).pop,
confirmLabel: LocaleKeys.button_close.tr(),
).show(context);
}
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,
),
),
),
],
),
],
setState(() {
currentSubscription = readyState.subscription;
});
},
child: 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: [
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,
),
),
),
],
),
),
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: currentSubscription.subscriptionPlan ==
SubscriptionPlanPB.None,
canDowngrade:
currentSubscription.subscriptionPlan !=
SubscriptionPlanPB.None,
onSelected: () async {
if (currentSubscription.subscriptionPlan ==
SubscriptionPlanPB.None ||
currentSubscription.hasCanceled) {
return;
}
context.read<SettingsPlanBloc>().add(
const SettingsPlanEvent
.cancelSubscription(),
);
await SettingsAlertDialog(
icon: Center(
child: SizedBox(
height: 90,
width: 90,
child: FlowySvg(
FlowySvgs.check_circle_s,
color:
AFThemeExtension.of(context).success,
),
),
),
title: LocaleKeys
.settings_comparePlanDialog_downgradeSuccess_title
.tr(args: [currentSubscription.label]),
subtitle: LocaleKeys
.settings_comparePlanDialog_downgradeSuccess_description
.tr(),
hideCancelButton: true,
confirm: Navigator.of(context).pop,
confirmLabel: LocaleKeys.button_close.tr(),
).show(context);
},
),
_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: currentSubscription.subscriptionPlan ==
SubscriptionPlanPB.Pro,
canUpgrade: currentSubscription.subscriptionPlan ==
SubscriptionPlanPB.None,
onSelected: () =>
context.read<SettingsPlanBloc>().add(
const SettingsPlanEvent.addSubscription(
SubscriptionPlanPB.Pro,
),
),
),
],
),
],
),
),
),
],
),
);
},
),
],
),
),
);
}
}

View File

@ -61,7 +61,7 @@ class SettingsPlanView extends StatelessWidget {
children: [
_PlanUsageSummary(
usage: state.workspaceUsage,
currentPlan: state.subscription.subscriptionPlan,
subscription: state.subscription,
),
_CurrentPlanBox(subscription: state.subscription),
],
@ -117,7 +117,7 @@ class _CurrentPlanBox extends StatelessWidget {
onPressed: () => _openPricingDialog(
context,
context.read<SettingsPlanBloc>().workspaceId,
subscription.subscriptionPlan,
subscription,
),
),
if (subscription.hasCanceled) ...[
@ -225,7 +225,7 @@ class _CurrentPlanBox extends StatelessWidget {
void _openPricingDialog(
BuildContext context,
String workspaceId,
SubscriptionPlanPB plan,
WorkspaceSubscriptionPB subscription,
) =>
showDialog(
context: context,
@ -233,7 +233,7 @@ class _CurrentPlanBox extends StatelessWidget {
value: context.read<SettingsPlanBloc>(),
child: SettingsPlanComparisonDialog(
workspaceId: workspaceId,
currentPlan: plan,
subscription: subscription,
),
),
);
@ -274,10 +274,10 @@ class _ProConItem extends StatelessWidget {
}
class _PlanUsageSummary extends StatelessWidget {
const _PlanUsageSummary({required this.usage, required this.currentPlan});
const _PlanUsageSummary({required this.usage, required this.subscription});
final WorkspaceUsagePB usage;
final SubscriptionPlanPB currentPlan;
final WorkspaceSubscriptionPB subscription;
@override
Widget build(BuildContext context) {
@ -329,18 +329,18 @@ class _PlanUsageSummary extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_ToggleMore(
value: currentPlan == SubscriptionPlanPB.Pro,
value: subscription.subscriptionPlan == SubscriptionPlanPB.Pro,
label:
LocaleKeys.settings_planPage_planUsage_memberProToggle.tr(),
currentPlan: currentPlan,
subscription: subscription,
badgeLabel: LocaleKeys.settings_planPage_planUsage_proBadge.tr(),
),
const VSpace(8),
_ToggleMore(
value: currentPlan == SubscriptionPlanPB.Pro,
value: subscription.subscriptionPlan == SubscriptionPlanPB.Pro,
label:
LocaleKeys.settings_planPage_planUsage_guestCollabToggle.tr(),
currentPlan: currentPlan,
subscription: subscription,
badgeLabel: LocaleKeys.settings_planPage_planUsage_proBadge.tr(),
),
],
@ -381,13 +381,13 @@ class _ToggleMore extends StatefulWidget {
const _ToggleMore({
required this.value,
required this.label,
required this.currentPlan,
required this.subscription,
this.badgeLabel,
});
final bool value;
final String label;
final SubscriptionPlanPB currentPlan;
final WorkspaceSubscriptionPB subscription;
final String? badgeLabel;
@override
@ -422,7 +422,7 @@ class _ToggleMoreState extends State<_ToggleMore> {
value: context.read<SettingsPlanBloc>(),
child: SettingsPlanComparisonDialog(
workspaceId: context.read<SettingsPlanBloc>().workspaceId,
currentPlan: widget.currentPlan,
subscription: widget.subscription,
),
),
).then((_) {

View File

@ -1,3 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';
@ -40,8 +43,6 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:google_fonts/google_fonts.dart';

View File

@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart';
@ -10,6 +11,7 @@ import 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart';
class SettingsAlertDialog extends StatefulWidget {
const SettingsAlertDialog({
super.key,
this.icon,
required this.title,
this.subtitle,
this.children,
@ -21,6 +23,7 @@ class SettingsAlertDialog extends StatefulWidget {
this.implyLeading = false,
});
final Widget? icon;
final String title;
final String? subtitle;
final List<Widget>? children;
@ -86,6 +89,10 @@ class _SettingsAlertDialogState extends State<SettingsAlertDialog> {
),
],
),
if (widget.icon != null) ...[
widget.icon!,
const VSpace(16),
],
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
@ -168,6 +175,7 @@ class _Actions extends StatelessWidget {
fontColor: AFThemeExtension.of(context).textColor,
fillColor: Colors.transparent,
hoverColor: Colors.transparent,
radius: Corners.s12Border,
onPressed: () {
cancel?.call();
Navigator.of(context).pop();
@ -187,6 +195,7 @@ class _Actions extends StatelessWidget {
horizontal: 24,
vertical: 12,
),
radius: Corners.s12Border,
fontColor: isDangerous ? Colors.white : null,
fontHoverColor: Colors.white,
fillColor: isDangerous

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
@ -65,6 +66,7 @@ class SingleSettingAction extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 7),
fillColor:
isDangerous ? null : Theme.of(context).colorScheme.primary,
radius: Corners.s12Border,
hoverColor: isDangerous ? null : const Color(0xFF005483),
fontColor: isDangerous ? Theme.of(context).colorScheme.error : null,
fontHoverColor: Colors.white,

View File

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.88 11.68L12.52 6.04L11.4 4.92L6.88 9.44L4.6 7.16L3.48 8.28L6.88 11.68ZM8 16C6.89333 16 5.85333 15.79 4.88 15.37C3.90667 14.95 3.06 14.38 2.34 13.66C1.62 12.94 1.05 12.0933 0.63 11.12C0.21 10.1467 0 9.10667 0 8C0 6.89333 0.21 5.85333 0.63 4.88C1.05 3.90667 1.62 3.06 2.34 2.34C3.06 1.62 3.90667 1.05 4.88 0.63C5.85333 0.21 6.89333 0 8 0C9.10667 0 10.1467 0.21 11.12 0.63C12.0933 1.05 12.94 1.62 13.66 2.34C14.38 3.06 14.95 3.90667 15.37 4.88C15.79 5.85333 16 6.89333 16 8C16 9.10667 15.79 10.1467 15.37 11.12C14.95 12.0933 14.38 12.94 13.66 13.66C12.94 14.38 12.0933 14.95 11.12 15.37C10.1467 15.79 9.10667 16 8 16Z" fill="#66CF80"/>
</svg>

After

Width:  |  Height:  |  Size: 748 B

View File

@ -330,7 +330,8 @@
"signInGoogle": "Sign in with Google",
"signInGithub": "Sign in with Github",
"signInDiscord": "Sign in with Discord",
"more": "More"
"more": "More",
"close": "Close"
},
"label": {
"welcome": "Welcome!",
@ -618,6 +619,14 @@
"itemSix": "yes",
"itemSeven": "yes",
"itemEight": "10,000 monthly"
},
"paymentSuccess": {
"title": "You are now on the {} plan!",
"description": "Your payment has been successfully processed and your plan is upgraded to AppFlowy {}. You can view your plan details on the Plan page"
},
"downgradeSuccess": {
"title": "You have canceled the {} plan!",
"description": "Your plan has been canceled, until the end of the billing cycle you will retain your previous plan benefits."
}
},
"common": {