mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: cache workspace subscription + minor fixes (#5705)
This commit is contained in:
@ -1,14 +1,20 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
import 'package:appflowy/core/helpers/url_launcher.dart';
|
||||||
import 'package:appflowy/user/application/user_service.dart';
|
import 'package:appflowy/user/application/user_service.dart';
|
||||||
import 'package:appflowy/workspace/application/workspace/workspace_service.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';
|
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-error/errors.pb.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.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.pb.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-user/workspace.pbserver.dart';
|
import 'package:appflowy_backend/protobuf/flowy-user/workspace.pbserver.dart';
|
||||||
|
import 'package:appflowy_result/appflowy_result.dart';
|
||||||
import 'package:bloc/bloc.dart';
|
import 'package:bloc/bloc.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
part 'settings_billing_bloc.freezed.dart';
|
part 'settings_billing_bloc.freezed.dart';
|
||||||
@ -25,25 +31,19 @@ class SettingsBillingBloc
|
|||||||
started: () async {
|
started: () async {
|
||||||
emit(const SettingsBillingState.loading());
|
emit(const SettingsBillingState.loading());
|
||||||
|
|
||||||
final snapshots = await Future.wait([
|
|
||||||
UserBackendService.getWorkspaceSubscriptions(),
|
|
||||||
_service.getBillingPortal(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
FlowyError? error;
|
FlowyError? error;
|
||||||
|
|
||||||
final subscription = snapshots.first.fold(
|
final subscription =
|
||||||
|
(await UserBackendService.getWorkspaceSubscriptions()).fold(
|
||||||
(s) =>
|
(s) =>
|
||||||
(s as RepeatedWorkspaceSubscriptionPB)
|
s.items.firstWhereOrNull((i) => i.workspaceId == workspaceId) ??
|
||||||
.items
|
|
||||||
.firstWhereOrNull((i) => i.workspaceId == workspaceId) ??
|
|
||||||
WorkspaceSubscriptionPB(
|
WorkspaceSubscriptionPB(
|
||||||
workspaceId: workspaceId,
|
workspaceId: workspaceId,
|
||||||
subscriptionPlan: SubscriptionPlanPB.None,
|
subscriptionPlan: SubscriptionPlanPB.None,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
),
|
),
|
||||||
(e) {
|
(e) {
|
||||||
// Not a Cjstomer yet
|
// Not a Customer yet
|
||||||
if (e.code == ErrorCode.InvalidParams) {
|
if (e.code == ErrorCode.InvalidParams) {
|
||||||
return WorkspaceSubscriptionPB(
|
return WorkspaceSubscriptionPB(
|
||||||
workspaceId: workspaceId,
|
workspaceId: workspaceId,
|
||||||
@ -57,46 +57,88 @@ class SettingsBillingBloc
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
final billingPortalResult = snapshots.last;
|
if (subscription == null || error != null) {
|
||||||
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));
|
return emit(SettingsBillingState.error(error: error));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!_billingPortalCompleter.isCompleted) {
|
||||||
|
unawaited(_fetchBillingPortal());
|
||||||
|
unawaited(
|
||||||
|
_billingPortalCompleter.future.then(
|
||||||
|
(result) {
|
||||||
|
result.fold(
|
||||||
|
(portal) {
|
||||||
|
_billingPortal = portal;
|
||||||
|
add(
|
||||||
|
SettingsBillingEvent.billingPortalFetched(
|
||||||
|
billingPortal: portal,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
(e) => Log.error('Error fetching billing portal: $e'),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
emit(
|
emit(
|
||||||
SettingsBillingState.ready(
|
SettingsBillingState.ready(
|
||||||
subscription: subscription,
|
subscription: subscription,
|
||||||
billingPortal: billingPortal,
|
billingPortal: _billingPortal,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
billingPortalFetched: (billingPortal) {
|
||||||
|
state.maybeWhen(
|
||||||
|
orElse: () {},
|
||||||
|
ready: (subscription, _) => emit(
|
||||||
|
SettingsBillingState.ready(
|
||||||
|
subscription: subscription,
|
||||||
|
billingPortal: billingPortal,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
openCustomerPortal: () async {
|
||||||
|
if (_billingPortalCompleter.isCompleted && _billingPortal != null) {
|
||||||
|
await afLaunchUrlString(_billingPortal!.url);
|
||||||
|
}
|
||||||
|
await _billingPortalCompleter.future;
|
||||||
|
if (_billingPortal != null) {
|
||||||
|
await afLaunchUrlString(_billingPortal!.url);
|
||||||
|
}
|
||||||
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
late final String workspaceId;
|
late final String workspaceId;
|
||||||
late final WorkspaceService _service;
|
late final WorkspaceService _service;
|
||||||
|
final _billingPortalCompleter =
|
||||||
|
Completer<FlowyResult<BillingPortalPB, FlowyError>>();
|
||||||
|
|
||||||
|
BillingPortalPB? _billingPortal;
|
||||||
|
|
||||||
|
Future<void> _fetchBillingPortal() async {
|
||||||
|
final billingPortalResult = await _service.getBillingPortal();
|
||||||
|
_billingPortalCompleter.complete(billingPortalResult);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
class SettingsBillingEvent with _$SettingsBillingEvent {
|
class SettingsBillingEvent with _$SettingsBillingEvent {
|
||||||
const factory SettingsBillingEvent.started() = _Started;
|
const factory SettingsBillingEvent.started() = _Started;
|
||||||
|
const factory SettingsBillingEvent.billingPortalFetched({
|
||||||
|
required BillingPortalPB billingPortal,
|
||||||
|
}) = _BillingPortalFetched;
|
||||||
|
const factory SettingsBillingEvent.openCustomerPortal() = _OpenCustomerPortal;
|
||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
class SettingsBillingState with _$SettingsBillingState {
|
class SettingsBillingState extends Equatable with _$SettingsBillingState {
|
||||||
|
const SettingsBillingState._();
|
||||||
|
|
||||||
const factory SettingsBillingState.initial() = _Initial;
|
const factory SettingsBillingState.initial() = _Initial;
|
||||||
|
|
||||||
const factory SettingsBillingState.loading() = _Loading;
|
const factory SettingsBillingState.loading() = _Loading;
|
||||||
@ -109,4 +151,11 @@ class SettingsBillingState with _$SettingsBillingState {
|
|||||||
required WorkspaceSubscriptionPB subscription,
|
required WorkspaceSubscriptionPB subscription,
|
||||||
required BillingPortalPB? billingPortal,
|
required BillingPortalPB? billingPortal,
|
||||||
}) = _Ready;
|
}) = _Ready;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => maybeWhen(
|
||||||
|
orElse: () => const [],
|
||||||
|
error: (error) => [error],
|
||||||
|
ready: (subscription, billingPortal) => [subscription, billingPortal],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,6 @@ 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/subscription_success_listenable/subscription_success_listenable.dart';
|
||||||
import 'package:appflowy/workspace/application/workspace/workspace_service.dart';
|
import 'package:appflowy/workspace/application/workspace/workspace_service.dart';
|
||||||
import 'package:appflowy_backend/log.dart';
|
import 'package:appflowy_backend/log.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-error/errors.pb.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.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.pb.dart';
|
||||||
@ -36,7 +35,6 @@ class SettingsPlanBloc extends Bloc<SettingsPlanEvent, SettingsPlanState> {
|
|||||||
final snapshots = await Future.wait([
|
final snapshots = await Future.wait([
|
||||||
_service.getWorkspaceUsage(),
|
_service.getWorkspaceUsage(),
|
||||||
UserBackendService.getWorkspaceSubscriptions(),
|
UserBackendService.getWorkspaceSubscriptions(),
|
||||||
_service.getBillingPortal(),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
FlowyError? error;
|
FlowyError? error;
|
||||||
@ -65,24 +63,7 @@ class SettingsPlanBloc extends Bloc<SettingsPlanEvent, SettingsPlanState> {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
final billingPortalResult = snapshots.last;
|
if (usageResult == null || subscription == null || error != null) {
|
||||||
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 (usageResult == null ||
|
|
||||||
subscription == null ||
|
|
||||||
billingPortal == null ||
|
|
||||||
error != null) {
|
|
||||||
return emit(SettingsPlanState.error(error: error));
|
return emit(SettingsPlanState.error(error: error));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,7 +71,6 @@ class SettingsPlanBloc extends Bloc<SettingsPlanEvent, SettingsPlanState> {
|
|||||||
SettingsPlanState.ready(
|
SettingsPlanState.ready(
|
||||||
workspaceUsage: usageResult,
|
workspaceUsage: usageResult,
|
||||||
subscription: subscription,
|
subscription: subscription,
|
||||||
billingPortal: billingPortal,
|
|
||||||
showSuccessDialog: withShowSuccessful,
|
showSuccessDialog: withShowSuccessful,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -100,7 +80,6 @@ class SettingsPlanBloc extends Bloc<SettingsPlanEvent, SettingsPlanState> {
|
|||||||
SettingsPlanState.ready(
|
SettingsPlanState.ready(
|
||||||
workspaceUsage: usageResult,
|
workspaceUsage: usageResult,
|
||||||
subscription: subscription,
|
subscription: subscription,
|
||||||
billingPortal: billingPortal,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -176,7 +155,6 @@ class SettingsPlanState with _$SettingsPlanState {
|
|||||||
const factory SettingsPlanState.ready({
|
const factory SettingsPlanState.ready({
|
||||||
required WorkspaceUsagePB workspaceUsage,
|
required WorkspaceUsagePB workspaceUsage,
|
||||||
required WorkspaceSubscriptionPB subscription,
|
required WorkspaceSubscriptionPB subscription,
|
||||||
required BillingPortalPB? billingPortal,
|
|
||||||
@Default(false) bool showSuccessDialog,
|
@Default(false) bool showSuccessDialog,
|
||||||
@Default(false) bool downgradeProcessing,
|
@Default(false) bool downgradeProcessing,
|
||||||
}) = _Ready;
|
}) = _Ready;
|
||||||
|
@ -58,8 +58,7 @@ class SettingsBillingView extends StatelessWidget {
|
|||||||
return ErrorWidget.withDetails(message: 'Something went wrong!');
|
return ErrorWidget.withDetails(message: 'Something went wrong!');
|
||||||
},
|
},
|
||||||
ready: (state) {
|
ready: (state) {
|
||||||
final billingPortalEnabled = state.billingPortal != null &&
|
final billingPortalEnabled =
|
||||||
state.billingPortal!.url.isNotEmpty &&
|
|
||||||
state.subscription.subscriptionPlan !=
|
state.subscription.subscriptionPlan !=
|
||||||
SubscriptionPlanPB.None;
|
SubscriptionPlanPB.None;
|
||||||
|
|
||||||
@ -85,8 +84,11 @@ class SettingsBillingView extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
if (billingPortalEnabled)
|
if (billingPortalEnabled)
|
||||||
SingleSettingAction(
|
SingleSettingAction(
|
||||||
onPressed: () =>
|
onPressed: () => context
|
||||||
afLaunchUrlString(state.billingPortal!.url),
|
.read<SettingsBillingBloc>()
|
||||||
|
.add(
|
||||||
|
const SettingsBillingEvent.openCustomerPortal(),
|
||||||
|
),
|
||||||
label: LocaleKeys
|
label: LocaleKeys
|
||||||
.settings_billingPage_plan_billingPeriod
|
.settings_billingPage_plan_billingPeriod
|
||||||
.tr(),
|
.tr(),
|
||||||
@ -105,8 +107,11 @@ class SettingsBillingView extends StatelessWidget {
|
|||||||
.tr(),
|
.tr(),
|
||||||
children: [
|
children: [
|
||||||
SingleSettingAction(
|
SingleSettingAction(
|
||||||
onPressed: () =>
|
onPressed: () => context
|
||||||
afLaunchUrlString(state.billingPortal!.url),
|
.read<SettingsBillingBloc>()
|
||||||
|
.add(
|
||||||
|
const SettingsBillingEvent.openCustomerPortal(),
|
||||||
|
),
|
||||||
label: LocaleKeys
|
label: LocaleKeys
|
||||||
.settings_billingPage_paymentDetails_methodLabel
|
.settings_billingPage_paymentDetails_methodLabel
|
||||||
.tr(),
|
.tr(),
|
||||||
@ -119,24 +124,34 @@ class SettingsBillingView extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
// TODO(Mathias): Implement the business logic for AI Add-ons
|
// TODO(Mathias): Implement the business logic for AI Add-ons
|
||||||
const SettingsCategory(
|
SettingsCategory(
|
||||||
title: 'Add-ons',
|
title: LocaleKeys.settings_billingPage_addons_title.tr(),
|
||||||
children: [
|
children: [
|
||||||
SingleSettingAction(
|
SingleSettingAction(
|
||||||
buttonType: SingleSettingsButtonType.highlight,
|
buttonType: SingleSettingsButtonType.highlight,
|
||||||
label: 'AppFlowy AI Max',
|
label: LocaleKeys
|
||||||
description:
|
.settings_billingPage_addons_aiMax_label
|
||||||
"\$8 /user per month billed annually or \$10 billed monthly",
|
.tr(),
|
||||||
buttonLabel: 'Add AI Max',
|
description: LocaleKeys
|
||||||
|
.settings_billingPage_addons_aiMax_description
|
||||||
|
.tr(),
|
||||||
|
buttonLabel: LocaleKeys
|
||||||
|
.settings_billingPage_addons_aiMax_buttonLabel
|
||||||
|
.tr(),
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
minWidth: _buttonsMinWidth,
|
minWidth: _buttonsMinWidth,
|
||||||
),
|
),
|
||||||
SingleSettingAction(
|
SingleSettingAction(
|
||||||
buttonType: SingleSettingsButtonType.highlight,
|
buttonType: SingleSettingsButtonType.highlight,
|
||||||
label: 'AppFlowy AI Offline',
|
label: LocaleKeys
|
||||||
description:
|
.settings_billingPage_addons_aiOnDevice_label
|
||||||
"\$8 /user per month billed annually or \$10 billed monthly",
|
.tr(),
|
||||||
buttonLabel: 'Add AI Offline',
|
description: LocaleKeys
|
||||||
|
.settings_billingPage_addons_aiOnDevice_description
|
||||||
|
.tr(),
|
||||||
|
buttonLabel: LocaleKeys
|
||||||
|
.settings_billingPage_addons_aiOnDevice_buttonLabel
|
||||||
|
.tr(),
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
minWidth: _buttonsMinWidth,
|
minWidth: _buttonsMinWidth,
|
||||||
),
|
),
|
||||||
|
@ -57,16 +57,6 @@ class _SettingsPlanComparisonDialogState
|
|||||||
|
|
||||||
if (readyState.showSuccessDialog) {
|
if (readyState.showSuccessDialog) {
|
||||||
SettingsAlertDialog(
|
SettingsAlertDialog(
|
||||||
icon: Center(
|
|
||||||
child: SizedBox(
|
|
||||||
height: 90,
|
|
||||||
width: 90,
|
|
||||||
child: FlowySvg(
|
|
||||||
FlowySvgs.check_circle_s,
|
|
||||||
color: AFThemeExtension.of(context).success,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
title: LocaleKeys.settings_comparePlanDialog_paymentSuccess_title
|
title: LocaleKeys.settings_comparePlanDialog_paymentSuccess_title
|
||||||
.tr(args: [readyState.subscription.label]),
|
.tr(args: [readyState.subscription.label]),
|
||||||
subtitle: LocaleKeys
|
subtitle: LocaleKeys
|
||||||
@ -588,21 +578,28 @@ class _Heading extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
width: 175,
|
width: 185,
|
||||||
height: height,
|
height: height,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.only(left: 12 + (!isPrimary ? 12 : 0)),
|
padding: EdgeInsets.only(left: 12 + (!isPrimary ? 12 : 0)),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
FlowyText.semibold(
|
Row(
|
||||||
title,
|
children: [
|
||||||
fontSize: 24,
|
Expanded(
|
||||||
color: isPrimary
|
child: FlowyText.semibold(
|
||||||
? AFThemeExtension.of(context).strongText
|
title,
|
||||||
: Theme.of(context).isLightMode
|
fontSize: 24,
|
||||||
? const Color(0xFF5C3699)
|
overflow: TextOverflow.ellipsis,
|
||||||
: const Color(0xFFC49BEC),
|
color: isPrimary
|
||||||
|
? AFThemeExtension.of(context).strongText
|
||||||
|
: Theme.of(context).isLightMode
|
||||||
|
? const Color(0xFF5C3699)
|
||||||
|
: const Color(0xFFC49BEC),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
if (description != null && description!.isNotEmpty) ...[
|
if (description != null && description!.isNotEmpty) ...[
|
||||||
const VSpace(4),
|
const VSpace(4),
|
||||||
@ -636,24 +633,20 @@ final _planLabels = [
|
|||||||
),
|
),
|
||||||
_PlanItem(
|
_PlanItem(
|
||||||
label: LocaleKeys.settings_comparePlanDialog_planLabels_itemThree.tr(),
|
label: LocaleKeys.settings_comparePlanDialog_planLabels_itemThree.tr(),
|
||||||
tooltip: LocaleKeys.settings_comparePlanDialog_planLabels_tooltipThree.tr(),
|
|
||||||
),
|
),
|
||||||
_PlanItem(
|
_PlanItem(
|
||||||
label: LocaleKeys.settings_comparePlanDialog_planLabels_itemFour.tr(),
|
label: LocaleKeys.settings_comparePlanDialog_planLabels_itemFour.tr(),
|
||||||
tooltip: LocaleKeys.settings_comparePlanDialog_planLabels_tooltipFour.tr(),
|
|
||||||
),
|
),
|
||||||
_PlanItem(
|
_PlanItem(
|
||||||
label: LocaleKeys.settings_comparePlanDialog_planLabels_itemFive.tr(),
|
label: LocaleKeys.settings_comparePlanDialog_planLabels_itemFive.tr(),
|
||||||
),
|
),
|
||||||
_PlanItem(
|
_PlanItem(
|
||||||
label: LocaleKeys.settings_comparePlanDialog_planLabels_itemSix.tr(),
|
label: LocaleKeys.settings_comparePlanDialog_planLabels_itemSix.tr(),
|
||||||
|
tooltip: LocaleKeys.settings_comparePlanDialog_planLabels_tooltipSix.tr(),
|
||||||
),
|
),
|
||||||
_PlanItem(
|
_PlanItem(
|
||||||
label: LocaleKeys.settings_comparePlanDialog_planLabels_itemSeven.tr(),
|
label: LocaleKeys.settings_comparePlanDialog_planLabels_itemSeven.tr(),
|
||||||
),
|
tooltip: LocaleKeys.settings_comparePlanDialog_planLabels_tooltipSeven.tr(),
|
||||||
_PlanItem(
|
|
||||||
label: LocaleKeys.settings_comparePlanDialog_planLabels_itemEight.tr(),
|
|
||||||
tooltip: LocaleKeys.settings_comparePlanDialog_planLabels_tooltipEight.tr(),
|
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -676,20 +669,17 @@ final List<_CellItem> _freeLabels = [
|
|||||||
),
|
),
|
||||||
_CellItem(
|
_CellItem(
|
||||||
LocaleKeys.settings_comparePlanDialog_freeLabels_itemFour.tr(),
|
LocaleKeys.settings_comparePlanDialog_freeLabels_itemFour.tr(),
|
||||||
|
icon: FlowySvgs.check_m,
|
||||||
),
|
),
|
||||||
_CellItem(
|
_CellItem(
|
||||||
LocaleKeys.settings_comparePlanDialog_freeLabels_itemFive.tr(),
|
LocaleKeys.settings_comparePlanDialog_freeLabels_itemFive.tr(),
|
||||||
|
icon: FlowySvgs.check_m,
|
||||||
),
|
),
|
||||||
_CellItem(
|
_CellItem(
|
||||||
LocaleKeys.settings_comparePlanDialog_freeLabels_itemSix.tr(),
|
LocaleKeys.settings_comparePlanDialog_freeLabels_itemSix.tr(),
|
||||||
icon: FlowySvgs.check_m,
|
|
||||||
),
|
),
|
||||||
_CellItem(
|
_CellItem(
|
||||||
LocaleKeys.settings_comparePlanDialog_freeLabels_itemSeven.tr(),
|
LocaleKeys.settings_comparePlanDialog_freeLabels_itemSeven.tr(),
|
||||||
icon: FlowySvgs.check_m,
|
|
||||||
),
|
|
||||||
_CellItem(
|
|
||||||
LocaleKeys.settings_comparePlanDialog_freeLabels_itemEight.tr(),
|
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -705,19 +695,17 @@ final List<_CellItem> _proLabels = [
|
|||||||
),
|
),
|
||||||
_CellItem(
|
_CellItem(
|
||||||
LocaleKeys.settings_comparePlanDialog_proLabels_itemFour.tr(),
|
LocaleKeys.settings_comparePlanDialog_proLabels_itemFour.tr(),
|
||||||
|
icon: FlowySvgs.check_m,
|
||||||
),
|
),
|
||||||
_CellItem(
|
_CellItem(
|
||||||
LocaleKeys.settings_comparePlanDialog_proLabels_itemFive.tr(),
|
LocaleKeys.settings_comparePlanDialog_proLabels_itemFive.tr(),
|
||||||
|
icon: FlowySvgs.check_m,
|
||||||
),
|
),
|
||||||
_CellItem(
|
_CellItem(
|
||||||
LocaleKeys.settings_comparePlanDialog_proLabels_itemSix.tr(),
|
LocaleKeys.settings_comparePlanDialog_proLabels_itemSix.tr(),
|
||||||
icon: FlowySvgs.check_m,
|
|
||||||
),
|
),
|
||||||
_CellItem(
|
_CellItem(
|
||||||
LocaleKeys.settings_comparePlanDialog_proLabels_itemSeven.tr(),
|
LocaleKeys.settings_comparePlanDialog_proLabels_itemSeven.tr(),
|
||||||
icon: FlowySvgs.check_m,
|
icon: FlowySvgs.check_m,
|
||||||
),
|
),
|
||||||
_CellItem(
|
|
||||||
LocaleKeys.settings_comparePlanDialog_proLabels_itemEight.tr(),
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
|
@ -69,7 +69,6 @@ class SettingsPlanView extends StatelessWidget {
|
|||||||
_PlanUsageSummary(
|
_PlanUsageSummary(
|
||||||
usage: state.workspaceUsage,
|
usage: state.workspaceUsage,
|
||||||
subscription: state.subscription,
|
subscription: state.subscription,
|
||||||
billingPortal: state.billingPortal,
|
|
||||||
),
|
),
|
||||||
const VSpace(16),
|
const VSpace(16),
|
||||||
_CurrentPlanBox(subscription: state.subscription),
|
_CurrentPlanBox(subscription: state.subscription),
|
||||||
@ -78,7 +77,7 @@ class SettingsPlanView extends StatelessWidget {
|
|||||||
FlowyText(
|
FlowyText(
|
||||||
'Add-ons',
|
'Add-ons',
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
color: AFThemeExtension.of(context).secondaryTextColor,
|
color: AFThemeExtension.of(context).strongText,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
const VSpace(8),
|
const VSpace(8),
|
||||||
@ -88,8 +87,8 @@ class SettingsPlanView extends StatelessWidget {
|
|||||||
child: _AddOnBox(
|
child: _AddOnBox(
|
||||||
title: "AI Max",
|
title: "AI Max",
|
||||||
description:
|
description:
|
||||||
"Unlimited AI responses with access to the latest advanced AI models.",
|
"Unlimited AI models and access to advanced models",
|
||||||
price: "\$8",
|
price: "US\$8",
|
||||||
priceInfo: "billed annually or \$10 billed monthly",
|
priceInfo: "billed annually or \$10 billed monthly",
|
||||||
buttonText: "Add AI Max",
|
buttonText: "Add AI Max",
|
||||||
),
|
),
|
||||||
@ -99,8 +98,8 @@ class SettingsPlanView extends StatelessWidget {
|
|||||||
child: _AddOnBox(
|
child: _AddOnBox(
|
||||||
title: "AI Offline",
|
title: "AI Offline",
|
||||||
description:
|
description:
|
||||||
"Run AI locally on your device for maximum privacy.",
|
"Local AI on your own hardware for ultimate privacy",
|
||||||
price: "\$8",
|
price: "US\$8",
|
||||||
priceInfo: "billed annually or \$10 billed monthly",
|
priceInfo: "billed annually or \$10 billed monthly",
|
||||||
buttonText: "Add AI Offline",
|
buttonText: "Add AI Offline",
|
||||||
),
|
),
|
||||||
@ -247,8 +246,7 @@ class _CurrentPlanBoxState extends State<_CurrentPlanBox> {
|
|||||||
final appearance = context.read<AppearanceSettingsCubit>().state;
|
final appearance = context.read<AppearanceSettingsCubit>().state;
|
||||||
return appearance.dateFormat.formatDate(
|
return appearance.dateFormat.formatDate(
|
||||||
widget.subscription.canceledAt.toDateTime(),
|
widget.subscription.canceledAt.toDateTime(),
|
||||||
true,
|
false,
|
||||||
appearance.timeFormat,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -273,12 +271,10 @@ class _PlanUsageSummary extends StatelessWidget {
|
|||||||
const _PlanUsageSummary({
|
const _PlanUsageSummary({
|
||||||
required this.usage,
|
required this.usage,
|
||||||
required this.subscription,
|
required this.subscription,
|
||||||
this.billingPortal,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
final WorkspaceUsagePB usage;
|
final WorkspaceUsagePB usage;
|
||||||
final WorkspaceSubscriptionPB subscription;
|
final WorkspaceSubscriptionPB subscription;
|
||||||
final BillingPortalPB? billingPortal;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -342,19 +338,17 @@ class _PlanUsageSummary extends StatelessWidget {
|
|||||||
subscription: subscription,
|
subscription: subscription,
|
||||||
badgeLabel:
|
badgeLabel:
|
||||||
LocaleKeys.settings_planPage_planUsage_proBadge.tr(),
|
LocaleKeys.settings_planPage_planUsage_proBadge.tr(),
|
||||||
onTap: billingPortal?.url == null
|
onTap: () async {
|
||||||
? null
|
context.read<SettingsPlanBloc>().add(
|
||||||
: () async {
|
const SettingsPlanEvent.addSubscription(
|
||||||
context.read<SettingsPlanBloc>().add(
|
SubscriptionPlanPB.Pro,
|
||||||
const SettingsPlanEvent.addSubscription(
|
),
|
||||||
SubscriptionPlanPB.Pro,
|
);
|
||||||
),
|
await Future.delayed(
|
||||||
);
|
const Duration(seconds: 2),
|
||||||
await Future.delayed(
|
() {},
|
||||||
const Duration(seconds: 2),
|
);
|
||||||
() {},
|
},
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@ -553,7 +547,7 @@ class _AddOnBox extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
return Container(
|
||||||
height: 200,
|
height: 220,
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 16,
|
horizontal: 16,
|
||||||
vertical: 12,
|
vertical: 12,
|
||||||
@ -571,7 +565,6 @@ class _AddOnBox extends StatelessWidget {
|
|||||||
color: AFThemeExtension.of(context).secondaryTextColor,
|
color: AFThemeExtension.of(context).secondaryTextColor,
|
||||||
),
|
),
|
||||||
const VSpace(4),
|
const VSpace(4),
|
||||||
const VSpace(4),
|
|
||||||
FlowyText.regular(
|
FlowyText.regular(
|
||||||
description,
|
description,
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
@ -579,24 +572,17 @@ class _AddOnBox extends StatelessWidget {
|
|||||||
maxLines: 4,
|
maxLines: 4,
|
||||||
),
|
),
|
||||||
const VSpace(4),
|
const VSpace(4),
|
||||||
Row(
|
FlowyText(
|
||||||
children: [
|
price,
|
||||||
FlowyText(
|
fontSize: 24,
|
||||||
price,
|
color: AFThemeExtension.of(context).strongText,
|
||||||
fontSize: 24,
|
|
||||||
color: AFThemeExtension.of(context).strongText,
|
|
||||||
),
|
|
||||||
const HSpace(4),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 4),
|
|
||||||
child: FlowyText(
|
|
||||||
'/user per month',
|
|
||||||
fontSize: 11,
|
|
||||||
color: AFThemeExtension.of(context).strongText,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
|
FlowyText(
|
||||||
|
'/user per month',
|
||||||
|
fontSize: 11,
|
||||||
|
color: AFThemeExtension.of(context).strongText,
|
||||||
|
),
|
||||||
|
const VSpace(6),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
@ -617,6 +603,7 @@ class _AddOnBox extends StatelessWidget {
|
|||||||
padding:
|
padding:
|
||||||
const EdgeInsets.symmetric(horizontal: 16, vertical: 7),
|
const EdgeInsets.symmetric(horizontal: 16, vertical: 7),
|
||||||
fillColor: Colors.transparent,
|
fillColor: Colors.transparent,
|
||||||
|
constraints: const BoxConstraints(minWidth: 115),
|
||||||
radius: Corners.s16Border,
|
radius: Corners.s16Border,
|
||||||
hoverColor: const Color(0xFF5C3699),
|
hoverColor: const Color(0xFF5C3699),
|
||||||
fontColor: const Color(0xFF5C3699),
|
fontColor: const Color(0xFF5C3699),
|
||||||
|
@ -631,13 +631,13 @@
|
|||||||
"title": "Plan usage summary",
|
"title": "Plan usage summary",
|
||||||
"storageLabel": "Storage",
|
"storageLabel": "Storage",
|
||||||
"storageUsage": "{} of {} GB",
|
"storageUsage": "{} of {} GB",
|
||||||
"collaboratorsLabel": "Collaborators",
|
"collaboratorsLabel": "Members",
|
||||||
"collaboratorsUsage": "{} of {}",
|
"collaboratorsUsage": "{} of {}",
|
||||||
"aiResponseLabel": "AI Responses",
|
"aiResponseLabel": "AI Responses",
|
||||||
"aiResponseUsage": "{} of {}",
|
"aiResponseUsage": "{} of {}",
|
||||||
"proBadge": "Pro",
|
"proBadge": "Pro",
|
||||||
"memberProToggle": "10 members and unlimited AI responses",
|
"memberProToggle": "More members & unlimited AI",
|
||||||
"storageUnlimited": "Unlimited storage with your Pro Plan",
|
"storageUnlimited": "Unlimited storage with Pro Plan",
|
||||||
"aiCredit": {
|
"aiCredit": {
|
||||||
"title": "Add @:appName AI Credit",
|
"title": "Add @:appName AI Credit",
|
||||||
"price": "5$",
|
"price": "5$",
|
||||||
@ -655,7 +655,7 @@
|
|||||||
"freeInfo": "Perfect for individuals or small teams up to 3 members.",
|
"freeInfo": "Perfect for individuals or small teams up to 3 members.",
|
||||||
"proInfo": "Perfect for small and medium teams up to 10 members.",
|
"proInfo": "Perfect for small and medium teams up to 10 members.",
|
||||||
"teamInfo": "Perfect for all productive and well-organized teams..",
|
"teamInfo": "Perfect for all productive and well-organized teams..",
|
||||||
"upgrade": "Upgrade plan",
|
"upgrade": "Change plan",
|
||||||
"canceledInfo": "Your plan is cancelled, you will be downgraded to the Free plan on {}."
|
"canceledInfo": "Your plan is cancelled, you will be downgraded to the Free plan on {}."
|
||||||
},
|
},
|
||||||
"deal": {
|
"deal": {
|
||||||
@ -681,6 +681,19 @@
|
|||||||
"title": "Payment details",
|
"title": "Payment details",
|
||||||
"methodLabel": "Payment method",
|
"methodLabel": "Payment method",
|
||||||
"methodButtonLabel": "Edit method"
|
"methodButtonLabel": "Edit method"
|
||||||
|
},
|
||||||
|
"addons": {
|
||||||
|
"title": "Add-ons",
|
||||||
|
"aiMax": {
|
||||||
|
"label": "AI Max",
|
||||||
|
"description": "US$8 /user per month billed annually or US$10 billed monthly",
|
||||||
|
"buttonLabel": "Add AI Max"
|
||||||
|
},
|
||||||
|
"aiOnDevice": {
|
||||||
|
"label": "AI On-device",
|
||||||
|
"description": "US$8 /user per month billed annually or US$10 billed monthly",
|
||||||
|
"buttonLabel": "Add AI On-device"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"comparePlanDialog": {
|
"comparePlanDialog": {
|
||||||
@ -695,48 +708,44 @@
|
|||||||
},
|
},
|
||||||
"freePlan": {
|
"freePlan": {
|
||||||
"title": "Free",
|
"title": "Free",
|
||||||
"description": "For organizing every corner of your work & life.",
|
"description": "For individuals and small groups to organize everything",
|
||||||
"price": "$0",
|
"price": "US$0",
|
||||||
"priceInfo": "free forever"
|
"priceInfo": "free forever"
|
||||||
},
|
},
|
||||||
"proPlan": {
|
"proPlan": {
|
||||||
"title": "Professional",
|
"title": "Professional",
|
||||||
"description": "A place for small groups to plan & get organized.",
|
"description": "For small teams to manage projects and team knowledge",
|
||||||
"price": "$10 /month",
|
"price": "US$12.5",
|
||||||
"priceInfo": "billed annually"
|
"priceInfo": "billed monthly"
|
||||||
},
|
},
|
||||||
"planLabels": {
|
"planLabels": {
|
||||||
"itemOne": "Workspaces",
|
"itemOne": "Workspaces",
|
||||||
"itemTwo": "Members",
|
"itemTwo": "Members",
|
||||||
"itemThree": "Guests",
|
"itemThree": "Storage",
|
||||||
"tooltipThree": "Guests have read-only permission to the specifically shared content",
|
"itemFour": "Real-time collaboration",
|
||||||
"itemFour": "Guest collaborators",
|
"itemFive": "Mobile app",
|
||||||
"tooltipFour": "Guest collaborators are billed as one seat",
|
"itemSix": "AI Responses",
|
||||||
"itemFive": "Storage",
|
"tooltipSix": "Lifetime means the number of responses never reset",
|
||||||
"itemSix": "Real-time collaboration",
|
"itemSeven": "Custom namespace",
|
||||||
"itemSeven": "Mobile app",
|
"tooltipSeven": "Allows you to customize part of the URL for your workspace"
|
||||||
"itemEight": "AI Responses",
|
|
||||||
"tooltipEight": "Lifetime means the number of responses never reset"
|
|
||||||
},
|
},
|
||||||
"freeLabels": {
|
"freeLabels": {
|
||||||
"itemOne": "charged per workspace",
|
"itemOne": "charged per workspace",
|
||||||
"itemTwo": "3",
|
"itemTwo": "up to 3",
|
||||||
"itemThree": "",
|
"itemThree": "5 GB",
|
||||||
"itemFour": "0",
|
"itemFour": "yes",
|
||||||
"itemFive": "5 GB",
|
"itemFive": "yes",
|
||||||
"itemSix": "yes",
|
"itemSix": "100 lifetime",
|
||||||
"itemSeven": "yes",
|
"itemSeven": ""
|
||||||
"itemEight": "1,000 lifetime"
|
|
||||||
},
|
},
|
||||||
"proLabels": {
|
"proLabels": {
|
||||||
"itemOne": "charged per workspace",
|
"itemOne": "charged per workspace",
|
||||||
"itemTwo": "up to 10",
|
"itemTwo": "up to 10",
|
||||||
"itemThree": "",
|
"itemThree": "unlimited",
|
||||||
"itemFour": "10 guests billed as one seat",
|
"itemFour": "yes",
|
||||||
"itemFive": "unlimited",
|
"itemFive": "yes",
|
||||||
"itemSix": "yes",
|
"itemSix": "unlimited",
|
||||||
"itemSeven": "yes",
|
"itemSeven": ""
|
||||||
"itemEight": "10,000 monthly"
|
|
||||||
},
|
},
|
||||||
"paymentSuccess": {
|
"paymentSuccess": {
|
||||||
"title": "You are now on the {} plan!",
|
"title": "You are now on the {} plan!",
|
||||||
|
@ -19,9 +19,9 @@ use flowy_server_pub::af_cloud_config::AFCloudConfiguration;
|
|||||||
use flowy_server_pub::AuthenticatorType;
|
use flowy_server_pub::AuthenticatorType;
|
||||||
use flowy_user::entities::{
|
use flowy_user::entities::{
|
||||||
AuthenticatorPB, ChangeWorkspaceIconPB, CloudSettingPB, CreateWorkspacePB, ImportAppFlowyDataPB,
|
AuthenticatorPB, ChangeWorkspaceIconPB, CloudSettingPB, CreateWorkspacePB, ImportAppFlowyDataPB,
|
||||||
OauthSignInPB, RenameWorkspacePB, RepeatedUserWorkspacePB, SignInUrlPB, SignInUrlPayloadPB,
|
OauthSignInPB, RenameWorkspacePB, RepeatedUserWorkspacePB, RepeatedWorkspaceSubscriptionPB,
|
||||||
SignUpPayloadPB, UpdateCloudConfigPB, UpdateUserProfilePayloadPB, UserProfilePB,
|
SignInUrlPB, SignInUrlPayloadPB, SignUpPayloadPB, UpdateCloudConfigPB,
|
||||||
UserWorkspaceIdPB, UserWorkspacePB,
|
UpdateUserProfilePayloadPB, UserProfilePB, UserWorkspaceIdPB, UserWorkspacePB,
|
||||||
};
|
};
|
||||||
use flowy_user::errors::{FlowyError, FlowyResult};
|
use flowy_user::errors::{FlowyError, FlowyResult};
|
||||||
use flowy_user::event_map::UserEvent;
|
use flowy_user::event_map::UserEvent;
|
||||||
@ -315,6 +315,14 @@ impl EventIntegrationTest {
|
|||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_workspace_subscriptions(&self) -> RepeatedWorkspaceSubscriptionPB {
|
||||||
|
EventBuilder::new(self.clone())
|
||||||
|
.event(UserEvent::GetWorkspaceSubscriptions)
|
||||||
|
.async_send()
|
||||||
|
.await
|
||||||
|
.parse::<RepeatedWorkspaceSubscriptionPB>()
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn leave_workspace(&self, workspace_id: &str) {
|
pub async fn leave_workspace(&self, workspace_id: &str) {
|
||||||
let payload = UserWorkspaceIdPB {
|
let payload = UserWorkspaceIdPB {
|
||||||
workspace_id: workspace_id.to_string(),
|
workspace_id: workspace_id.to_string(),
|
||||||
|
@ -240,3 +240,18 @@ async fn af_cloud_different_open_same_workspace_test() {
|
|||||||
assert_eq!(views.len(), 1, "only get: {:?}", views); // Expecting two views.
|
assert_eq!(views.len(), 1, "only get: {:?}", views); // Expecting two views.
|
||||||
assert_eq!(views[0].name, "Getting started");
|
assert_eq!(views[0].name, "Getting started");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn af_cloud_get_workspace_subscriptions_test() {
|
||||||
|
user_localhost_af_cloud().await;
|
||||||
|
|
||||||
|
let test = EventIntegrationTest::new().await;
|
||||||
|
|
||||||
|
let workspaces = test.get_all_workspaces().await.items;
|
||||||
|
let first_workspace_id = workspaces[0].workspace_id.as_str();
|
||||||
|
assert_eq!(workspaces.len(), 1);
|
||||||
|
|
||||||
|
let subscriptions = test.get_workspace_subscriptions().await;
|
||||||
|
assert_eq!(subscriptions.items.len(), 1);
|
||||||
|
assert_eq!(subscriptions.items[0].workspace_id, first_workspace_id);
|
||||||
|
}
|
||||||
|
@ -739,6 +739,7 @@ fn to_workspace_subscription(s: WorkspaceSubscriptionStatus) -> WorkspaceSubscri
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
is_active: matches!(s.subscription_status, SubscriptionStatus::Active),
|
is_active: matches!(s.subscription_status, SubscriptionStatus::Active),
|
||||||
|
has_canceled: s.canceled_at.is_some(),
|
||||||
canceled_at: s.canceled_at,
|
canceled_at: s.canceled_at,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,2 @@
|
|||||||
|
-- This file should undo anything in `up.sql`
|
||||||
|
drop table workspace_subscriptions_table;
|
@ -0,0 +1,11 @@
|
|||||||
|
-- Your SQL goes here
|
||||||
|
CREATE TABLE workspace_subscriptions_table (
|
||||||
|
workspace_id TEXT NOT NULL,
|
||||||
|
subscription_plan INTEGER NOT NULL,
|
||||||
|
recurring_interval INTEGER NOT NULL,
|
||||||
|
is_active BOOLEAN NOT NULL,
|
||||||
|
has_canceled BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
canceled_at TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (workspace_id)
|
||||||
|
);
|
@ -114,6 +114,18 @@ diesel::table! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
workspace_subscriptions_table (workspace_id) {
|
||||||
|
workspace_id -> Text,
|
||||||
|
subscription_plan -> BigInt,
|
||||||
|
recurring_interval -> BigInt,
|
||||||
|
is_active -> Bool,
|
||||||
|
has_canceled -> Bool,
|
||||||
|
canceled_at -> Nullable<BigInt>,
|
||||||
|
updated_at -> Timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
diesel::allow_tables_to_appear_in_same_query!(
|
diesel::allow_tables_to_appear_in_same_query!(
|
||||||
chat_local_setting_table,
|
chat_local_setting_table,
|
||||||
chat_message_table,
|
chat_message_table,
|
||||||
@ -125,4 +137,5 @@ diesel::allow_tables_to_appear_in_same_query!(
|
|||||||
user_table,
|
user_table,
|
||||||
user_workspace_table,
|
user_workspace_table,
|
||||||
workspace_members_table,
|
workspace_members_table,
|
||||||
|
workspace_subscriptions_table,
|
||||||
);
|
);
|
||||||
|
@ -453,22 +453,65 @@ pub struct WorkspaceInvitation {
|
|||||||
pub updated_at: DateTime<Utc>,
|
pub updated_at: DateTime<Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
pub enum RecurringInterval {
|
pub enum RecurringInterval {
|
||||||
Month,
|
Month,
|
||||||
Year,
|
Year,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Into<i64> for RecurringInterval {
|
||||||
|
fn into(self) -> i64 {
|
||||||
|
match self {
|
||||||
|
RecurringInterval::Month => 0,
|
||||||
|
RecurringInterval::Year => 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<i64> for RecurringInterval {
|
||||||
|
fn from(value: i64) -> Self {
|
||||||
|
match value {
|
||||||
|
0 => RecurringInterval::Month,
|
||||||
|
1 => RecurringInterval::Year,
|
||||||
|
_ => RecurringInterval::Month,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
pub enum SubscriptionPlan {
|
pub enum SubscriptionPlan {
|
||||||
None,
|
None,
|
||||||
Pro,
|
Pro,
|
||||||
Team,
|
Team,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Into<i64> for SubscriptionPlan {
|
||||||
|
fn into(self) -> i64 {
|
||||||
|
match self {
|
||||||
|
SubscriptionPlan::None => 0,
|
||||||
|
SubscriptionPlan::Pro => 1,
|
||||||
|
SubscriptionPlan::Team => 2,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<i64> for SubscriptionPlan {
|
||||||
|
fn from(value: i64) -> Self {
|
||||||
|
match value {
|
||||||
|
0 => SubscriptionPlan::None,
|
||||||
|
1 => SubscriptionPlan::Pro,
|
||||||
|
2 => SubscriptionPlan::Team,
|
||||||
|
_ => SubscriptionPlan::None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct WorkspaceSubscription {
|
pub struct WorkspaceSubscription {
|
||||||
pub workspace_id: String,
|
pub workspace_id: String,
|
||||||
pub subscription_plan: SubscriptionPlan,
|
pub subscription_plan: SubscriptionPlan,
|
||||||
pub recurring_interval: RecurringInterval,
|
pub recurring_interval: RecurringInterval,
|
||||||
pub is_active: bool,
|
pub is_active: bool,
|
||||||
|
pub has_canceled: bool,
|
||||||
pub canceled_at: Option<i64>,
|
pub canceled_at: Option<i64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,10 +1,14 @@
|
|||||||
use chrono::{TimeZone, Utc};
|
use chrono::{TimeZone, Utc};
|
||||||
|
use diesel::insert_into;
|
||||||
use diesel::{RunQueryDsl, SqliteConnection};
|
use diesel::{RunQueryDsl, SqliteConnection};
|
||||||
use flowy_error::FlowyError;
|
use flowy_error::{FlowyError, FlowyResult};
|
||||||
use flowy_sqlite::schema::user_workspace_table;
|
use flowy_sqlite::schema::user_workspace_table;
|
||||||
|
use flowy_sqlite::schema::workspace_subscriptions_table;
|
||||||
|
use flowy_sqlite::schema::workspace_subscriptions_table::dsl;
|
||||||
use flowy_sqlite::DBConnection;
|
use flowy_sqlite::DBConnection;
|
||||||
use flowy_sqlite::{query_dsl::*, ExpressionMethods};
|
use flowy_sqlite::{query_dsl::*, ExpressionMethods};
|
||||||
use flowy_user_pub::entities::UserWorkspace;
|
use flowy_user_pub::entities::UserWorkspace;
|
||||||
|
use flowy_user_pub::entities::WorkspaceSubscription;
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
#[derive(Clone, Default, Queryable, Identifiable, Insertable)]
|
#[derive(Clone, Default, Queryable, Identifiable, Insertable)]
|
||||||
@ -18,6 +22,19 @@ pub struct UserWorkspaceTable {
|
|||||||
pub icon: String,
|
pub icon: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Queryable, Insertable, AsChangeset, Debug)]
|
||||||
|
#[diesel(table_name = workspace_subscriptions_table)]
|
||||||
|
#[diesel(primary_key(workspace_id))]
|
||||||
|
pub struct WorkspaceSubscriptionsTable {
|
||||||
|
pub workspace_id: String,
|
||||||
|
pub subscription_plan: i64,
|
||||||
|
pub recurring_interval: i64,
|
||||||
|
pub is_active: bool,
|
||||||
|
pub has_canceled: bool,
|
||||||
|
pub canceled_at: Option<i64>,
|
||||||
|
pub updated_at: chrono::NaiveDateTime,
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_user_workspace_op(workspace_id: &str, mut conn: DBConnection) -> Option<UserWorkspace> {
|
pub fn get_user_workspace_op(workspace_id: &str, mut conn: DBConnection) -> Option<UserWorkspace> {
|
||||||
user_workspace_table::dsl::user_workspace_table
|
user_workspace_table::dsl::user_workspace_table
|
||||||
.filter(user_workspace_table::id.eq(workspace_id))
|
.filter(user_workspace_table::id.eq(workspace_id))
|
||||||
@ -74,6 +91,46 @@ pub fn insert_new_workspaces_op(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn select_workspace_subscription(
|
||||||
|
mut conn: DBConnection,
|
||||||
|
workspace_id: &str,
|
||||||
|
) -> FlowyResult<WorkspaceSubscriptionsTable> {
|
||||||
|
let subscription = dsl::workspace_subscriptions_table
|
||||||
|
.filter(workspace_subscriptions_table::workspace_id.eq(workspace_id))
|
||||||
|
.first::<WorkspaceSubscriptionsTable>(&mut conn)?;
|
||||||
|
|
||||||
|
Ok(subscription)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn upsert_workspace_subscription<T: Into<WorkspaceSubscriptionsTable>>(
|
||||||
|
mut conn: DBConnection,
|
||||||
|
subscription: T,
|
||||||
|
) -> FlowyResult<()> {
|
||||||
|
let subscription = subscription.into();
|
||||||
|
|
||||||
|
insert_into(workspace_subscriptions_table::table)
|
||||||
|
.values(&subscription)
|
||||||
|
.on_conflict((workspace_subscriptions_table::workspace_id,))
|
||||||
|
.do_update()
|
||||||
|
.set(&subscription)
|
||||||
|
.execute(&mut conn)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<WorkspaceSubscriptionsTable> for WorkspaceSubscription {
|
||||||
|
fn from(value: WorkspaceSubscriptionsTable) -> Self {
|
||||||
|
Self {
|
||||||
|
workspace_id: value.workspace_id,
|
||||||
|
subscription_plan: value.subscription_plan.into(),
|
||||||
|
recurring_interval: value.recurring_interval.into(),
|
||||||
|
is_active: value.is_active,
|
||||||
|
has_canceled: value.has_canceled,
|
||||||
|
canceled_at: value.canceled_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl TryFrom<(i64, &UserWorkspace)> for UserWorkspaceTable {
|
impl TryFrom<(i64, &UserWorkspace)> for UserWorkspaceTable {
|
||||||
type Error = FlowyError;
|
type Error = FlowyError;
|
||||||
|
|
||||||
|
@ -11,8 +11,9 @@ use flowy_folder_pub::entities::{AppFlowyData, ImportData};
|
|||||||
use flowy_sqlite::schema::user_workspace_table;
|
use flowy_sqlite::schema::user_workspace_table;
|
||||||
use flowy_sqlite::{query_dsl::*, DBConnection, ExpressionMethods};
|
use flowy_sqlite::{query_dsl::*, DBConnection, ExpressionMethods};
|
||||||
use flowy_user_pub::entities::{
|
use flowy_user_pub::entities::{
|
||||||
Role, UpdateUserProfileParams, UserWorkspace, WorkspaceInvitation, WorkspaceInvitationStatus,
|
RecurringInterval, Role, SubscriptionPlan, UpdateUserProfileParams, UserWorkspace,
|
||||||
WorkspaceMember, WorkspaceSubscription, WorkspaceUsage,
|
WorkspaceInvitation, WorkspaceInvitationStatus, WorkspaceMember, WorkspaceSubscription,
|
||||||
|
WorkspaceUsage,
|
||||||
};
|
};
|
||||||
use lib_dispatch::prelude::af_spawn;
|
use lib_dispatch::prelude::af_spawn;
|
||||||
|
|
||||||
@ -30,7 +31,9 @@ use crate::services::sqlite_sql::member_sql::{
|
|||||||
};
|
};
|
||||||
use crate::services::sqlite_sql::user_sql::UserTableChangeset;
|
use crate::services::sqlite_sql::user_sql::UserTableChangeset;
|
||||||
use crate::services::sqlite_sql::workspace_sql::{
|
use crate::services::sqlite_sql::workspace_sql::{
|
||||||
get_all_user_workspace_op, get_user_workspace_op, insert_new_workspaces_op, UserWorkspaceTable,
|
get_all_user_workspace_op, get_user_workspace_op, insert_new_workspaces_op,
|
||||||
|
select_workspace_subscription, upsert_workspace_subscription, UserWorkspaceTable,
|
||||||
|
WorkspaceSubscriptionsTable,
|
||||||
};
|
};
|
||||||
use crate::user_manager::{upsert_user_profile_change, UserManager};
|
use crate::user_manager::{upsert_user_profile_change, UserManager};
|
||||||
use flowy_user_pub::session::Session;
|
use flowy_user_pub::session::Session;
|
||||||
@ -447,12 +450,62 @@ impl UserManager {
|
|||||||
|
|
||||||
#[instrument(level = "info", skip(self), err)]
|
#[instrument(level = "info", skip(self), err)]
|
||||||
pub async fn get_workspace_subscriptions(&self) -> FlowyResult<Vec<WorkspaceSubscription>> {
|
pub async fn get_workspace_subscriptions(&self) -> FlowyResult<Vec<WorkspaceSubscription>> {
|
||||||
let res = self
|
let session = self.get_session()?;
|
||||||
|
let uid = session.user_id;
|
||||||
|
let workspace_id = session.user_workspace.id.clone();
|
||||||
|
let db = self.authenticate_user.get_sqlite_connection(uid)?;
|
||||||
|
|
||||||
|
// We check if we can use the cache from local sqlite db
|
||||||
|
if let Ok(subscription) = select_workspace_subscription(db, &workspace_id) {
|
||||||
|
if is_older_than_n_minutes(subscription.updated_at, 10) {
|
||||||
|
self.get_workspace_subscriptions_from_remote(uid).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(vec![WorkspaceSubscription {
|
||||||
|
workspace_id: subscription.workspace_id,
|
||||||
|
subscription_plan: subscription.subscription_plan.into(),
|
||||||
|
recurring_interval: subscription.recurring_interval.into(),
|
||||||
|
is_active: subscription.is_active,
|
||||||
|
has_canceled: subscription.has_canceled,
|
||||||
|
canceled_at: subscription.canceled_at,
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let subscriptions = self.get_workspace_subscriptions_from_remote(uid).await?;
|
||||||
|
|
||||||
|
Ok(subscriptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_workspace_subscriptions_from_remote(
|
||||||
|
&self,
|
||||||
|
uid: i64,
|
||||||
|
) -> FlowyResult<Vec<WorkspaceSubscription>> {
|
||||||
|
let subscriptions = self
|
||||||
.cloud_services
|
.cloud_services
|
||||||
.get_user_service()?
|
.get_user_service()?
|
||||||
.get_workspace_subscriptions()
|
.get_workspace_subscriptions()
|
||||||
.await?;
|
.await?;
|
||||||
Ok(res)
|
|
||||||
|
for subscription in &subscriptions {
|
||||||
|
let db = self.authenticate_user.get_sqlite_connection(uid)?;
|
||||||
|
let record = WorkspaceSubscriptionsTable {
|
||||||
|
workspace_id: subscription.workspace_id.clone(),
|
||||||
|
subscription_plan: <SubscriptionPlan as Into<i64>>::into(
|
||||||
|
subscription.subscription_plan.clone(),
|
||||||
|
),
|
||||||
|
recurring_interval: <RecurringInterval as Into<i64>>::into(
|
||||||
|
subscription.recurring_interval.clone(),
|
||||||
|
),
|
||||||
|
is_active: subscription.is_active,
|
||||||
|
has_canceled: subscription.has_canceled,
|
||||||
|
canceled_at: subscription.canceled_at.clone().into(),
|
||||||
|
updated_at: Utc::now().naive_utc(),
|
||||||
|
};
|
||||||
|
|
||||||
|
upsert_workspace_subscription(db, record)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(subscriptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(level = "info", skip(self), err)]
|
#[instrument(level = "info", skip(self), err)]
|
||||||
|
Reference in New Issue
Block a user