mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: refine UI and add business logic for AI
This commit is contained in:
parent
792e6f1370
commit
61419e305c
@ -86,10 +86,11 @@ class SettingsBillingBloc
|
||||
},
|
||||
billingPortalFetched: (billingPortal) async => state.maybeWhen(
|
||||
orElse: () {},
|
||||
ready: (subscriptionInfo, _) => emit(
|
||||
ready: (subscriptionInfo, _, plan) => emit(
|
||||
SettingsBillingState.ready(
|
||||
subscriptionInfo: subscriptionInfo,
|
||||
billingPortal: billingPortal,
|
||||
successfulPlanUpgrade: plan,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -114,7 +115,7 @@ class SettingsBillingBloc
|
||||
await _userService.cancelSubscription(workspaceId, plan);
|
||||
await _onPaymentSuccessful();
|
||||
},
|
||||
paymentSuccessful: () async {
|
||||
paymentSuccessful: (plan) async {
|
||||
final result = await UserBackendService.getWorkspaceSubscriptionInfo(
|
||||
workspaceId,
|
||||
);
|
||||
@ -152,7 +153,11 @@ class SettingsBillingBloc
|
||||
// Invalidate cache for this workspace
|
||||
await UserBackendService.invalidateWorkspaceSubscriptionCache(workspaceId);
|
||||
|
||||
add(const SettingsBillingEvent.paymentSuccessful());
|
||||
add(
|
||||
SettingsBillingEvent.paymentSuccessful(
|
||||
plan: _successListenable.subscribedPlan,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -168,7 +173,9 @@ class SettingsBillingEvent with _$SettingsBillingEvent {
|
||||
const factory SettingsBillingEvent.cancelSubscription(
|
||||
SubscriptionPlanPB plan,
|
||||
) = _CancelSubscription;
|
||||
const factory SettingsBillingEvent.paymentSuccessful() = _PaymentSuccessful;
|
||||
const factory SettingsBillingEvent.paymentSuccessful({
|
||||
SubscriptionPlanPB? plan,
|
||||
}) = _PaymentSuccessful;
|
||||
}
|
||||
|
||||
@freezed
|
||||
@ -186,12 +193,17 @@ class SettingsBillingState extends Equatable with _$SettingsBillingState {
|
||||
const factory SettingsBillingState.ready({
|
||||
required WorkspaceSubscriptionInfoPB subscriptionInfo,
|
||||
required BillingPortalPB? billingPortal,
|
||||
@Default(null) SubscriptionPlanPB? successfulPlanUpgrade,
|
||||
}) = _Ready;
|
||||
|
||||
@override
|
||||
List<Object?> get props => maybeWhen(
|
||||
orElse: () => const [],
|
||||
error: (error) => [error],
|
||||
ready: (subscription, billingPortal) => [subscription, billingPortal],
|
||||
ready: (subscription, billingPortal, plan) => [
|
||||
subscription,
|
||||
billingPortal,
|
||||
plan,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ class SettingsPlanBloc extends Bloc<SettingsPlanEvent, SettingsPlanState> {
|
||||
|
||||
on<SettingsPlanEvent>((event, emit) async {
|
||||
await event.when(
|
||||
started: (withShowSuccessful) async {
|
||||
started: (withSuccessfulUpgrade) async {
|
||||
emit(const SettingsPlanState.loading());
|
||||
|
||||
final snapshots = await Future.wait([
|
||||
@ -64,11 +64,11 @@ class SettingsPlanBloc extends Bloc<SettingsPlanEvent, SettingsPlanState> {
|
||||
SettingsPlanState.ready(
|
||||
workspaceUsage: usageResult,
|
||||
subscriptionInfo: subscriptionInfo,
|
||||
showSuccessDialog: withShowSuccessful,
|
||||
successfulPlanUpgrade: withSuccessfulUpgrade,
|
||||
),
|
||||
);
|
||||
|
||||
if (withShowSuccessful) {
|
||||
if (withSuccessfulUpgrade != null) {
|
||||
emit(
|
||||
SettingsPlanState.ready(
|
||||
workspaceUsage: usageResult,
|
||||
@ -80,7 +80,7 @@ class SettingsPlanBloc extends Bloc<SettingsPlanEvent, SettingsPlanState> {
|
||||
addSubscription: (plan) async {
|
||||
final result = await _userService.createSubscription(
|
||||
workspaceId,
|
||||
SubscriptionPlanPB.Pro,
|
||||
plan,
|
||||
);
|
||||
|
||||
result.fold(
|
||||
@ -103,13 +103,13 @@ class SettingsPlanBloc extends Bloc<SettingsPlanEvent, SettingsPlanState> {
|
||||
|
||||
add(const SettingsPlanEvent.started());
|
||||
},
|
||||
paymentSuccessful: () {
|
||||
paymentSuccessful: (plan) {
|
||||
final readyState = state.mapOrNull(ready: (state) => state);
|
||||
if (readyState == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
add(const SettingsPlanEvent.started(withShowSuccessful: true));
|
||||
add(SettingsPlanEvent.started(withSuccessfulUpgrade: plan));
|
||||
},
|
||||
);
|
||||
});
|
||||
@ -124,7 +124,11 @@ class SettingsPlanBloc extends Bloc<SettingsPlanEvent, SettingsPlanState> {
|
||||
// Invalidate cache for this workspace
|
||||
await UserBackendService.invalidateWorkspaceSubscriptionCache(workspaceId);
|
||||
|
||||
add(const SettingsPlanEvent.paymentSuccessful());
|
||||
add(
|
||||
SettingsPlanEvent.paymentSuccessful(
|
||||
plan: _successListenable.subscribedPlan,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -137,7 +141,7 @@ class SettingsPlanBloc extends Bloc<SettingsPlanEvent, SettingsPlanState> {
|
||||
@freezed
|
||||
class SettingsPlanEvent with _$SettingsPlanEvent {
|
||||
const factory SettingsPlanEvent.started({
|
||||
@Default(false) bool withShowSuccessful,
|
||||
@Default(null) SubscriptionPlanPB? withSuccessfulUpgrade,
|
||||
}) = _Started;
|
||||
|
||||
const factory SettingsPlanEvent.addSubscription(SubscriptionPlanPB plan) =
|
||||
@ -145,7 +149,9 @@ class SettingsPlanEvent with _$SettingsPlanEvent {
|
||||
|
||||
const factory SettingsPlanEvent.cancelSubscription() = _CancelSubscription;
|
||||
|
||||
const factory SettingsPlanEvent.paymentSuccessful() = _PaymentSuccessful;
|
||||
const factory SettingsPlanEvent.paymentSuccessful({
|
||||
@Default(null) SubscriptionPlanPB? plan,
|
||||
}) = _PaymentSuccessful;
|
||||
}
|
||||
|
||||
@freezed
|
||||
@ -161,7 +167,7 @@ class SettingsPlanState with _$SettingsPlanState {
|
||||
const factory SettingsPlanState.ready({
|
||||
required WorkspaceUsagePB workspaceUsage,
|
||||
required WorkspaceSubscriptionInfoPB subscriptionInfo,
|
||||
@Default(false) bool showSuccessDialog,
|
||||
@Default(null) SubscriptionPlanPB? successfulPlanUpgrade,
|
||||
@Default(false) bool downgradeProcessing,
|
||||
}) = _Ready;
|
||||
}
|
||||
|
@ -26,6 +26,22 @@ extension SubscriptionLabels on WorkspaceSubscriptionInfoPB {
|
||||
};
|
||||
}
|
||||
|
||||
extension AllSubscriptionLabels on SubscriptionPlanPB {
|
||||
String get label => switch (this) {
|
||||
SubscriptionPlanPB.None =>
|
||||
LocaleKeys.settings_planPage_planUsage_currentPlan_freeTitle.tr(),
|
||||
SubscriptionPlanPB.Pro =>
|
||||
LocaleKeys.settings_planPage_planUsage_currentPlan_proTitle.tr(),
|
||||
SubscriptionPlanPB.Team =>
|
||||
LocaleKeys.settings_planPage_planUsage_currentPlan_teamTitle.tr(),
|
||||
SubscriptionPlanPB.AiMax =>
|
||||
LocaleKeys.settings_billingPage_addons_aiMax_label.tr(),
|
||||
SubscriptionPlanPB.AiLocal =>
|
||||
LocaleKeys.settings_billingPage_addons_aiOnDevice_label.tr(),
|
||||
_ => 'N/A',
|
||||
};
|
||||
}
|
||||
|
||||
extension WorkspaceSubscriptionStatusExt on WorkspaceSubscriptionInfoPB {
|
||||
bool get isCanceled =>
|
||||
planSubscription.status == WorkspaceSubscriptionStatusPB.Canceled;
|
||||
|
@ -1,12 +1,17 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/util/int64_extension.dart';
|
||||
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
|
||||
import 'package:appflowy/workspace/application/settings/billing/settings_billing_bloc.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/presentation/settings/pages/settings_plan_comparison_dialog.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/shared/settings_alert_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/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@ -35,7 +40,8 @@ class SettingsBillingView extends StatelessWidget {
|
||||
workspaceId: workspaceId,
|
||||
userId: user.id,
|
||||
)..add(const SettingsBillingEvent.started()),
|
||||
child: BlocBuilder<SettingsBillingBloc, SettingsBillingState>(
|
||||
child: BlocConsumer<SettingsBillingBloc, SettingsBillingState>(
|
||||
listener: (context, state) {},
|
||||
builder: (context, state) {
|
||||
return state.map(
|
||||
initial: (_) => const SizedBox.shrink(),
|
||||
@ -124,7 +130,6 @@ class SettingsBillingView extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
// TODO(Mathias): Implement the business logic for AI Add-ons
|
||||
SettingsCategory(
|
||||
title: LocaleKeys.settings_billingPage_addons_title.tr(),
|
||||
children: [
|
||||
@ -134,8 +139,11 @@ class SettingsBillingView extends StatelessWidget {
|
||||
.settings_billingPage_addons_aiMax_label
|
||||
.tr(),
|
||||
description: LocaleKeys
|
||||
.settings_billingPage_addons_aiMax_description
|
||||
.tr(),
|
||||
.settings_billingPage_addons_aiMax_description,
|
||||
activeDescription: LocaleKeys
|
||||
.settings_billingPage_addons_aiMax_activeDescription,
|
||||
canceledDescription: LocaleKeys
|
||||
.settings_billingPage_addons_aiMax_canceledDescription,
|
||||
subscriptionInfo:
|
||||
state.subscriptionInfo.addOns.firstWhereOrNull(
|
||||
(a) => a.type == WorkspaceAddOnPBType.AddOnAiMax,
|
||||
@ -147,8 +155,11 @@ class SettingsBillingView extends StatelessWidget {
|
||||
.settings_billingPage_addons_aiOnDevice_label
|
||||
.tr(),
|
||||
description: LocaleKeys
|
||||
.settings_billingPage_addons_aiOnDevice_description
|
||||
.tr(),
|
||||
.settings_billingPage_addons_aiOnDevice_description,
|
||||
activeDescription: LocaleKeys
|
||||
.settings_billingPage_addons_aiOnDevice_activeDescription,
|
||||
canceledDescription: LocaleKeys
|
||||
.settings_billingPage_addons_aiOnDevice_canceledDescription,
|
||||
subscriptionInfo:
|
||||
state.subscriptionInfo.addOns.firstWhereOrNull(
|
||||
(a) => a.type == WorkspaceAddOnPBType.AddOnAiLocal,
|
||||
@ -195,12 +206,16 @@ class _AITile extends StatelessWidget {
|
||||
const _AITile({
|
||||
required this.label,
|
||||
required this.description,
|
||||
required this.canceledDescription,
|
||||
required this.activeDescription,
|
||||
required this.plan,
|
||||
this.subscriptionInfo,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final String description;
|
||||
final String canceledDescription;
|
||||
final String activeDescription;
|
||||
final SubscriptionPlanPB plan;
|
||||
final WorkspaceAddOnPB? subscriptionInfo;
|
||||
|
||||
@ -209,9 +224,29 @@ class _AITile extends StatelessWidget {
|
||||
final isCanceled = subscriptionInfo?.addOnSubscription.status ==
|
||||
WorkspaceSubscriptionStatusPB.Canceled;
|
||||
|
||||
final dateFormat = context.read<AppearanceSettingsCubit>().state.dateFormat;
|
||||
|
||||
return SingleSettingAction(
|
||||
label: label,
|
||||
description: description,
|
||||
description: subscriptionInfo != null && isCanceled
|
||||
? canceledDescription.tr(
|
||||
args: [
|
||||
dateFormat.formatDate(
|
||||
subscriptionInfo!.addOnSubscription.endDate.toDateTime(),
|
||||
false,
|
||||
),
|
||||
],
|
||||
)
|
||||
: subscriptionInfo != null
|
||||
? activeDescription.tr(
|
||||
args: [
|
||||
dateFormat.formatDate(
|
||||
subscriptionInfo!.addOnSubscription.endDate.toDateTime(),
|
||||
false,
|
||||
),
|
||||
],
|
||||
)
|
||||
: description.tr(),
|
||||
buttonLabel: subscriptionInfo != null
|
||||
? isCanceled
|
||||
? LocaleKeys.settings_billingPage_addons_renewLabel.tr()
|
||||
@ -220,13 +255,25 @@ class _AITile extends StatelessWidget {
|
||||
fontWeight: FontWeight.w500,
|
||||
minWidth: _buttonsMinWidth,
|
||||
onPressed: () {
|
||||
if (subscriptionInfo != null && !isCanceled) {
|
||||
// Cancel the addon
|
||||
if (subscriptionInfo != null && isCanceled) {
|
||||
// Show customer portal to renew
|
||||
context
|
||||
.read<SettingsBillingBloc>()
|
||||
.add(SettingsBillingEvent.cancelSubscription(plan));
|
||||
.add(const SettingsBillingEvent.openCustomerPortal());
|
||||
} else if (subscriptionInfo != null) {
|
||||
SettingsAlertDialog(
|
||||
title: 'Remove AI Max',
|
||||
subtitle:
|
||||
'Are you sure you want to remove AI Max? You will keep the benefits until the end of the billing period.',
|
||||
confirm: () {
|
||||
Navigator.of(context).pop();
|
||||
context
|
||||
.read<SettingsBillingBloc>()
|
||||
.add(SettingsBillingEvent.cancelSubscription(plan));
|
||||
},
|
||||
).show(context);
|
||||
} else {
|
||||
// Add/renew the addon
|
||||
// Add the addon
|
||||
context
|
||||
.read<SettingsBillingBloc>()
|
||||
.add(SettingsBillingEvent.addSubscription(plan));
|
||||
|
@ -56,13 +56,13 @@ class _SettingsPlanComparisonDialogState
|
||||
return;
|
||||
}
|
||||
|
||||
if (readyState.showSuccessDialog) {
|
||||
if (readyState.successfulPlanUpgrade != null) {
|
||||
SettingsAlertDialog(
|
||||
title: LocaleKeys.settings_comparePlanDialog_paymentSuccess_title
|
||||
.tr(args: [readyState.subscriptionInfo.label]),
|
||||
.tr(args: [readyState.successfulPlanUpgrade!.label]),
|
||||
subtitle: LocaleKeys
|
||||
.settings_comparePlanDialog_paymentSuccess_description
|
||||
.tr(args: [readyState.subscriptionInfo.label]),
|
||||
.tr(args: [readyState.successfulPlanUpgrade!.label]),
|
||||
hideCancelButton: true,
|
||||
confirm: Navigator.of(context).pop,
|
||||
confirmLabel: LocaleKeys.button_close.tr(),
|
||||
|
@ -1,5 +1,6 @@
|
||||
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/util/theme_extension.dart';
|
||||
@ -108,6 +109,7 @@ class SettingsPlanView extends StatelessWidget {
|
||||
.settings_planPage_planUsage_addons_addLabel
|
||||
.tr(),
|
||||
isActive: state.subscriptionInfo.hasAIMax,
|
||||
plan: SubscriptionPlanPB.AiMax,
|
||||
),
|
||||
),
|
||||
const HSpace(8),
|
||||
@ -136,6 +138,7 @@ class SettingsPlanView extends StatelessWidget {
|
||||
.settings_planPage_planUsage_addons_addLabel
|
||||
.tr(),
|
||||
isActive: state.subscriptionInfo.hasAIOnDevice,
|
||||
plan: SubscriptionPlanPB.AiLocal,
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -328,11 +331,10 @@ class _PlanUsageSummary extends StatelessWidget {
|
||||
Expanded(
|
||||
child: _UsageBox(
|
||||
title: LocaleKeys.settings_planPage_planUsage_storageLabel.tr(),
|
||||
replacementText: subscriptionInfo.plan ==
|
||||
WorkspacePlanPB.ProPlan
|
||||
? LocaleKeys.settings_planPage_planUsage_storageUnlimited
|
||||
.tr()
|
||||
: null,
|
||||
unlimitedLabel: LocaleKeys
|
||||
.settings_planPage_planUsage_unlimitedStorageLabel
|
||||
.tr(),
|
||||
unlimited: usage.storageBytesUnlimited,
|
||||
label: LocaleKeys.settings_planPage_planUsage_storageUsage.tr(
|
||||
args: [
|
||||
usage.currentBlobInGb,
|
||||
@ -345,17 +347,21 @@ class _PlanUsageSummary extends StatelessWidget {
|
||||
),
|
||||
Expanded(
|
||||
child: _UsageBox(
|
||||
title: LocaleKeys.settings_planPage_planUsage_collaboratorsLabel
|
||||
.tr(),
|
||||
label: LocaleKeys.settings_planPage_planUsage_collaboratorsUsage
|
||||
.tr(
|
||||
title:
|
||||
LocaleKeys.settings_planPage_planUsage_aiResponseLabel.tr(),
|
||||
label:
|
||||
LocaleKeys.settings_planPage_planUsage_aiResponseUsage.tr(
|
||||
args: [
|
||||
usage.memberCount.toString(),
|
||||
usage.memberCountLimit.toString(),
|
||||
usage.aiResponsesCount.toString(),
|
||||
usage.aiResponsesCountLimit.toString(),
|
||||
],
|
||||
),
|
||||
value:
|
||||
usage.memberCount.toInt() / usage.memberCountLimit.toInt(),
|
||||
unlimitedLabel: LocaleKeys
|
||||
.settings_planPage_planUsage_unlimitedAILabel
|
||||
.tr(),
|
||||
unlimited: usage.aiResponsesUnlimited,
|
||||
value: usage.aiResponsesCount.toInt() /
|
||||
usage.aiResponsesCountLimit.toInt(),
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -396,18 +402,23 @@ class _UsageBox extends StatelessWidget {
|
||||
required this.title,
|
||||
required this.label,
|
||||
required this.value,
|
||||
this.replacementText,
|
||||
required this.unlimitedLabel,
|
||||
this.unlimited = false,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String label;
|
||||
final double value;
|
||||
|
||||
/// Replaces the progress indicator if not null
|
||||
final String? replacementText;
|
||||
final String unlimitedLabel;
|
||||
|
||||
// Replaces the progress bar with an unlimited badge
|
||||
final bool unlimited;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isLM = Theme.of(context).isLightMode;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@ -416,17 +427,36 @@ class _UsageBox extends StatelessWidget {
|
||||
fontSize: 11,
|
||||
color: AFThemeExtension.of(context).secondaryTextColor,
|
||||
),
|
||||
if (replacementText != null) ...[
|
||||
Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: FlowyText.medium(
|
||||
replacementText!,
|
||||
fontSize: 11,
|
||||
color: AFThemeExtension.of(context).secondaryTextColor,
|
||||
),
|
||||
if (unlimited) ...[
|
||||
const VSpace(4),
|
||||
DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: isLM ? const Color(0xFFE8E2EE) : const Color(0xFF9C00FB),
|
||||
),
|
||||
],
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 4,
|
||||
horizontal: 8,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const FlowySvg(
|
||||
FlowySvgs.check_circle_outlined_s,
|
||||
color: Color(0xFF9C00FB),
|
||||
),
|
||||
const HSpace(4),
|
||||
FlowyText(
|
||||
unlimitedLabel,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 11,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
] else ...[
|
||||
_PlanProgressIndicator(label: label, progress: value),
|
||||
@ -569,6 +599,7 @@ class _AddOnBox extends StatelessWidget {
|
||||
required this.billingInfo,
|
||||
required this.buttonText,
|
||||
required this.isActive,
|
||||
required this.plan,
|
||||
});
|
||||
|
||||
final String title;
|
||||
@ -578,6 +609,7 @@ class _AddOnBox extends StatelessWidget {
|
||||
final String billingInfo;
|
||||
final String buttonText;
|
||||
final bool isActive;
|
||||
final SubscriptionPlanPB plan;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -658,7 +690,11 @@ class _AddOnBox extends StatelessWidget {
|
||||
? const Color(0xFFE8E2EE)
|
||||
: const Color(0xFF5C3699),
|
||||
fontSize: 12,
|
||||
onPressed: isActive ? null : () {},
|
||||
onPressed: isActive
|
||||
? null
|
||||
: () => context
|
||||
.read<SettingsPlanBloc>()
|
||||
.add(SettingsPlanEvent.addSubscription(plan)),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -153,6 +153,7 @@ class WorkspaceMemberBloc
|
||||
),
|
||||
);
|
||||
},
|
||||
upgradePlan: () {},
|
||||
);
|
||||
});
|
||||
}
|
||||
@ -209,6 +210,8 @@ class WorkspaceMemberEvent with _$WorkspaceMemberEvent {
|
||||
String email,
|
||||
AFRolePB role,
|
||||
) = UpdateWorkspaceMember;
|
||||
|
||||
const factory WorkspaceMemberEvent.upgradePlan() = UpgradePlan;
|
||||
}
|
||||
|
||||
enum WorkspaceMemberActionType {
|
||||
|
@ -3,16 +3,17 @@ import 'package:flutter/material.dart';
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/shared/af_role_pb_extension.dart';
|
||||
import 'package:appflowy/util/theme_extension.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/toast.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/code.pbenum.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||
import 'package:flowy_infra_ui/widget/rounded_button.dart';
|
||||
@ -29,12 +30,16 @@ class WorkspaceMembersPage extends StatelessWidget {
|
||||
return BlocProvider<WorkspaceMemberBloc>(
|
||||
create: (context) => WorkspaceMemberBloc(userProfile: userProfile)
|
||||
..add(const WorkspaceMemberEvent.initial()),
|
||||
child: BlocConsumer<WorkspaceMemberBloc, WorkspaceMemberState>(
|
||||
listener: _showResultDialog,
|
||||
child: BlocBuilder<WorkspaceMemberBloc, WorkspaceMemberState>(
|
||||
builder: (context, state) {
|
||||
return SettingsBody(
|
||||
title: LocaleKeys.settings_appearance_members_title.tr(),
|
||||
children: [
|
||||
if (state.actionResult != null)
|
||||
_showMemberLimitWarning(
|
||||
context,
|
||||
state.actionResult!,
|
||||
),
|
||||
if (state.myRole.canInvite) const _InviteMember(),
|
||||
if (state.members.isNotEmpty)
|
||||
_MemberList(
|
||||
@ -49,62 +54,85 @@ class WorkspaceMembersPage extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
void _showResultDialog(BuildContext context, WorkspaceMemberState state) {
|
||||
final actionResult = state.actionResult;
|
||||
if (actionResult == null) {
|
||||
return;
|
||||
Widget _showMemberLimitWarning(
|
||||
BuildContext context,
|
||||
WorkspaceMemberActionResult result,
|
||||
) {
|
||||
final isLM = Theme.of(context).isLightMode;
|
||||
|
||||
final actionType = result.actionType;
|
||||
final actionResult = result.result;
|
||||
|
||||
if (actionType == WorkspaceMemberActionType.invite &&
|
||||
actionResult.isFailure) {
|
||||
final error = actionResult.getFailure().code;
|
||||
if (error == ErrorCode.WorkspaceMemberLimitExceeded) {
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: isLM
|
||||
? const Color(0xFFFFF4E5)
|
||||
: const Color.fromARGB(255, 255, 200, 125),
|
||||
borderRadius: Corners.s8Border,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 6,
|
||||
horizontal: 16,
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
FlowySvg(
|
||||
FlowySvgs.warning_m,
|
||||
color: isLM
|
||||
? const Color(0xFFEF6C00)
|
||||
: const Color.fromARGB(255, 160, 75, 0),
|
||||
),
|
||||
const HSpace(12),
|
||||
Expanded(
|
||||
child: FlowyText(
|
||||
LocaleKeys.settings_appearance_members_memberLimitExceeded
|
||||
.tr(),
|
||||
color: const Color(0xFF663C00),
|
||||
maxLines: 3,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
onTap: () {},
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: const Color(0xFF5F2120),
|
||||
),
|
||||
borderRadius: Corners.s4Border,
|
||||
),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 5,
|
||||
),
|
||||
child: FlowyText(
|
||||
// TODO(Mathias): Localization
|
||||
'UPGRADE',
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF663C00),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final actionType = actionResult.actionType;
|
||||
final result = actionResult.result;
|
||||
|
||||
// only show the result dialog when the action is WorkspaceMemberActionType.add
|
||||
if (actionType == WorkspaceMemberActionType.add) {
|
||||
result.fold(
|
||||
(s) {
|
||||
showSnackBarMessage(
|
||||
context,
|
||||
LocaleKeys.settings_appearance_members_addMemberSuccess.tr(),
|
||||
);
|
||||
},
|
||||
(f) {
|
||||
Log.error('add workspace member failed: $f');
|
||||
final message = f.code == ErrorCode.WorkspaceMemberLimitExceeded
|
||||
? LocaleKeys.settings_appearance_members_memberLimitExceeded.tr()
|
||||
: LocaleKeys.settings_appearance_members_failedToAddMember.tr();
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => NavigatorOkCancelDialog(message: message),
|
||||
);
|
||||
},
|
||||
);
|
||||
} else if (actionType == WorkspaceMemberActionType.invite) {
|
||||
result.fold(
|
||||
(s) {
|
||||
showSnackBarMessage(
|
||||
context,
|
||||
LocaleKeys.settings_appearance_members_inviteMemberSuccess.tr(),
|
||||
);
|
||||
},
|
||||
(f) {
|
||||
Log.error('invite workspace member failed: $f');
|
||||
final message = f.code == ErrorCode.WorkspaceMemberLimitExceeded
|
||||
? LocaleKeys.settings_appearance_members_memberLimitExceeded.tr()
|
||||
: LocaleKeys.settings_appearance_members_failedToInviteMember
|
||||
.tr();
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => NavigatorOkCancelDialog(message: message),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
result.onFailure((f) {
|
||||
Log.error(
|
||||
'[Member] Failed to perform ${actionType.toString()} action: $f',
|
||||
);
|
||||
});
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
|
||||
|
5
frontend/resources/flowy_icons/24x/warning.svg
Normal file
5
frontend/resources/flowy_icons/24x/warning.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.0003 5.949L17.9028 17.8748H4.09783L11.0003 5.949ZM11.0003 2.2915L0.916992 19.7082H21.0837L11.0003 2.2915Z" fill="#EF6C00"/>
|
||||
<path d="M11.917 15.1248H10.0837V16.9582H11.917V15.1248Z" fill="#EF6C00"/>
|
||||
<path d="M11.917 9.62484H10.0837V14.2082H11.917V9.62484Z" fill="#EF6C00"/>
|
||||
</svg>
|
After Width: | Height: | Size: 390 B |
@ -639,13 +639,14 @@
|
||||
"title": "Plan usage summary",
|
||||
"storageLabel": "Storage",
|
||||
"storageUsage": "{} of {} GB",
|
||||
"unlimitedStorageLabel": "Unlimited storage",
|
||||
"collaboratorsLabel": "Members",
|
||||
"collaboratorsUsage": "{} of {}",
|
||||
"aiResponseLabel": "AI Responses",
|
||||
"aiResponseUsage": "{} of {}",
|
||||
"unlimitedAILabel": "Unlimited responses",
|
||||
"proBadge": "Pro",
|
||||
"memberProToggle": "More members & unlimited AI",
|
||||
"storageUnlimited": "Unlimited storage with Pro Plan",
|
||||
"aiCredit": {
|
||||
"title": "Add @:appName AI Credit",
|
||||
"price": "5$",
|
||||
@ -716,11 +717,15 @@
|
||||
"renewLabel": "Renew",
|
||||
"aiMax": {
|
||||
"label": "AI Max",
|
||||
"description": "US$8 /user per month billed annually or US$10 billed monthly"
|
||||
"description": "US$8 /user per month billed annually or US$10 billed monthly",
|
||||
"activeDescription": "Next invoice due on {}",
|
||||
"canceledDescription": "AI Max will be removed on {}"
|
||||
},
|
||||
"aiOnDevice": {
|
||||
"label": "AI On-device",
|
||||
"description": "US$8 /user per month billed annually or US$10 billed monthly"
|
||||
"description": "US$8 /user per month billed annually or US$10 billed monthly",
|
||||
"activeDescription": "Next invoice due on {}",
|
||||
"canceledDescription": "AI On-device will be removed on {}"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -946,7 +951,7 @@
|
||||
"one": "{} member",
|
||||
"other": "{} members"
|
||||
},
|
||||
"memberLimitExceeded": "You've reached the maximum member limit allowed for your account. If you want to add more additional members to continue your work, please request on Github",
|
||||
"memberLimitExceeded": "Member limit reached. Upgrade to invite additional members.",
|
||||
"failedToAddMember": "Failed to add member",
|
||||
"addMemberSuccess": "Member added successfully",
|
||||
"removeMember": "Remove Member",
|
||||
|
@ -1,6 +1,6 @@
|
||||
use chrono::Utc;
|
||||
use client_api::entity::billing_dto::{
|
||||
RecurringInterval, SubscriptionPlan, SubscriptionStatus, WorkspaceSubscriptionStatus,
|
||||
RecurringInterval, SubscriptionPlan, WorkspaceSubscriptionStatus,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::str::FromStr;
|
||||
@ -631,7 +631,11 @@ impl From<WorkspaceSubscriptionStatus> for WorkspaceSubscriptionV2PB {
|
||||
Self {
|
||||
workspace_id: sub.workspace_id,
|
||||
subscription_plan: sub.workspace_plan.clone().into(),
|
||||
status: sub.subscription_status.into(),
|
||||
status: if sub.cancel_at.is_some() {
|
||||
WorkspaceSubscriptionStatusPB::Canceled
|
||||
} else {
|
||||
WorkspaceSubscriptionStatusPB::Active
|
||||
},
|
||||
end_date: sub.current_period_end,
|
||||
}
|
||||
}
|
||||
@ -658,12 +662,3 @@ impl From<i64> for WorkspaceSubscriptionStatusPB {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SubscriptionStatus> for WorkspaceSubscriptionStatusPB {
|
||||
fn from(status: SubscriptionStatus) -> Self {
|
||||
match status {
|
||||
SubscriptionStatus::Active => WorkspaceSubscriptionStatusPB::Active,
|
||||
_ => WorkspaceSubscriptionStatusPB::Canceled,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user