feat: refine UI and add business logic for AI

This commit is contained in:
Mathias Mogensen 2024-07-11 09:47:59 +02:00
parent 792e6f1370
commit 61419e305c
11 changed files with 281 additions and 128 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View File

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

View File

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