mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: refactor and start integrating AI plans
This commit is contained in:
parent
4c18b2bc9d
commit
792e6f1370
@ -95,7 +95,8 @@ class AppFlowyCloudDeepLink {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (_isPaymentSuccessUri(uri)) {
|
if (_isPaymentSuccessUri(uri)) {
|
||||||
return getIt<SubscriptionSuccessListenable>().onPaymentSuccess();
|
final plan = uri.queryParameters['plan'];
|
||||||
|
return getIt<SubscriptionSuccessListenable>().onPaymentSuccess(plan);
|
||||||
}
|
}
|
||||||
|
|
||||||
return _isAuthCallbackDeepLink(uri).fold(
|
return _isAuthCallbackDeepLink(uri).fold(
|
||||||
|
@ -2,6 +2,7 @@ import 'dart:async';
|
|||||||
|
|
||||||
import 'package:appflowy/env/cloud_env.dart';
|
import 'package:appflowy/env/cloud_env.dart';
|
||||||
import 'package:appflowy/startup/startup.dart';
|
import 'package:appflowy/startup/startup.dart';
|
||||||
|
import 'package:appflowy/workspace/application/settings/plan/workspace_subscription_ext.dart';
|
||||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
import 'package:appflowy_backend/dispatch/dispatch.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-folder/workspace.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart';
|
||||||
@ -10,7 +11,10 @@ import 'package:appflowy_result/appflowy_result.dart';
|
|||||||
import 'package:fixnum/fixnum.dart';
|
import 'package:fixnum/fixnum.dart';
|
||||||
|
|
||||||
abstract class IUserBackendService {
|
abstract class IUserBackendService {
|
||||||
Future<FlowyResult<void, FlowyError>> cancelSubscription(String workspaceId);
|
Future<FlowyResult<void, FlowyError>> cancelSubscription(
|
||||||
|
String workspaceId,
|
||||||
|
SubscriptionPlanPB plan,
|
||||||
|
);
|
||||||
Future<FlowyResult<PaymentLinkPB, FlowyError>> createSubscription(
|
Future<FlowyResult<PaymentLinkPB, FlowyError>> createSubscription(
|
||||||
String workspaceId,
|
String workspaceId,
|
||||||
SubscriptionPlanPB plan,
|
SubscriptionPlanPB plan,
|
||||||
@ -228,9 +232,16 @@ class UserBackendService implements IUserBackendService {
|
|||||||
return UserEventLeaveWorkspace(data).send();
|
return UserEventLeaveWorkspace(data).send();
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<FlowyResult<RepeatedWorkspaceSubscriptionPB, FlowyError>>
|
static Future<FlowyResult<WorkspaceSubscriptionInfoPB, FlowyError>>
|
||||||
getWorkspaceSubscriptions() {
|
getWorkspaceSubscriptionInfo(String workspaceId) {
|
||||||
return UserEventGetWorkspaceSubscriptions().send();
|
final params = UserWorkspaceIdPB.create()..workspaceId = workspaceId;
|
||||||
|
return UserEventGetWorkspaceSubscriptionInfo(params).send();
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<FlowyResult<void, FlowyError>>
|
||||||
|
invalidateWorkspaceSubscriptionCache(String workspaceId) {
|
||||||
|
final params = UserWorkspaceIdPB.create()..workspaceId = workspaceId;
|
||||||
|
return UserEventInvalidateWorkspaceSubscriptionInfoCache(params).send();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<FlowyResult<WorkspaceMemberPB, FlowyError>>
|
Future<FlowyResult<WorkspaceMemberPB, FlowyError>>
|
||||||
@ -250,15 +261,19 @@ class UserBackendService implements IUserBackendService {
|
|||||||
..recurringInterval = RecurringIntervalPB.Year
|
..recurringInterval = RecurringIntervalPB.Year
|
||||||
..workspaceSubscriptionPlan = plan
|
..workspaceSubscriptionPlan = plan
|
||||||
..successUrl =
|
..successUrl =
|
||||||
'${getIt<AppFlowyCloudSharedEnv>().appflowyCloudConfig.base_url}/web/payment-success';
|
'${getIt<AppFlowyCloudSharedEnv>().appflowyCloudConfig.base_url}/web/payment-success?plan=${plan.toRecognizable()}';
|
||||||
return UserEventSubscribeWorkspace(request).send();
|
return UserEventSubscribeWorkspace(request).send();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<FlowyResult<void, FlowyError>> cancelSubscription(
|
Future<FlowyResult<void, FlowyError>> cancelSubscription(
|
||||||
String workspaceId,
|
String workspaceId,
|
||||||
|
SubscriptionPlanPB plan,
|
||||||
) {
|
) {
|
||||||
final request = UserWorkspaceIdPB()..workspaceId = workspaceId;
|
final request = CancelWorkspaceSubscriptionPB()
|
||||||
|
..workspaceId = workspaceId
|
||||||
|
..plan = plan;
|
||||||
|
|
||||||
return UserEventCancelWorkspaceSubscription(request).send();
|
return UserEventCancelWorkspaceSubscription(request).send();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,18 +3,19 @@ import 'dart:async';
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
import 'package:appflowy/core/helpers/url_launcher.dart';
|
import 'package:appflowy/core/helpers/url_launcher.dart';
|
||||||
|
import 'package:appflowy/startup/startup.dart';
|
||||||
import 'package:appflowy/user/application/user_service.dart';
|
import 'package:appflowy/user/application/user_service.dart';
|
||||||
|
import 'package:appflowy/workspace/application/subscription_success_listenable/subscription_success_listenable.dart';
|
||||||
import 'package:appflowy/workspace/application/workspace/workspace_service.dart';
|
import 'package:appflowy/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';
|
||||||
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:appflowy_result/appflowy_result.dart';
|
||||||
import 'package:bloc/bloc.dart';
|
import 'package:bloc/bloc.dart';
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:fixnum/fixnum.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';
|
||||||
@ -23,8 +24,12 @@ class SettingsBillingBloc
|
|||||||
extends Bloc<SettingsBillingEvent, SettingsBillingState> {
|
extends Bloc<SettingsBillingEvent, SettingsBillingState> {
|
||||||
SettingsBillingBloc({
|
SettingsBillingBloc({
|
||||||
required this.workspaceId,
|
required this.workspaceId,
|
||||||
|
required Int64 userId,
|
||||||
}) : super(const _Initial()) {
|
}) : super(const _Initial()) {
|
||||||
|
_userService = UserBackendService(userId: userId);
|
||||||
_service = WorkspaceService(workspaceId: workspaceId);
|
_service = WorkspaceService(workspaceId: workspaceId);
|
||||||
|
_successListenable = getIt<SubscriptionSuccessListenable>();
|
||||||
|
_successListenable.addListener(_onPaymentSuccessful);
|
||||||
|
|
||||||
on<SettingsBillingEvent>((event, emit) async {
|
on<SettingsBillingEvent>((event, emit) async {
|
||||||
await event.when(
|
await event.when(
|
||||||
@ -33,31 +38,19 @@ class SettingsBillingBloc
|
|||||||
|
|
||||||
FlowyError? error;
|
FlowyError? error;
|
||||||
|
|
||||||
final subscription =
|
final subscriptionInfo =
|
||||||
(await UserBackendService.getWorkspaceSubscriptions()).fold(
|
(await UserBackendService.getWorkspaceSubscriptionInfo(
|
||||||
(s) =>
|
workspaceId,
|
||||||
s.items.firstWhereOrNull((i) => i.workspaceId == workspaceId) ??
|
))
|
||||||
WorkspaceSubscriptionPB(
|
.fold(
|
||||||
workspaceId: workspaceId,
|
(s) => s,
|
||||||
subscriptionPlan: SubscriptionPlanPB.None,
|
|
||||||
isActive: true,
|
|
||||||
),
|
|
||||||
(e) {
|
(e) {
|
||||||
// Not a Customer yet
|
|
||||||
if (e.code == ErrorCode.InvalidParams) {
|
|
||||||
return WorkspaceSubscriptionPB(
|
|
||||||
workspaceId: workspaceId,
|
|
||||||
subscriptionPlan: SubscriptionPlanPB.None,
|
|
||||||
isActive: true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
error = e;
|
error = e;
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (subscription == null || error != null) {
|
if (subscriptionInfo == null || error != null) {
|
||||||
return emit(SettingsBillingState.error(error: error));
|
return emit(SettingsBillingState.error(error: error));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,6 +59,8 @@ class SettingsBillingBloc
|
|||||||
unawaited(
|
unawaited(
|
||||||
_billingPortalCompleter.future.then(
|
_billingPortalCompleter.future.then(
|
||||||
(result) {
|
(result) {
|
||||||
|
if (isClosed) return;
|
||||||
|
|
||||||
result.fold(
|
result.fold(
|
||||||
(portal) {
|
(portal) {
|
||||||
_billingPortal = portal;
|
_billingPortal = portal;
|
||||||
@ -84,22 +79,20 @@ class SettingsBillingBloc
|
|||||||
|
|
||||||
emit(
|
emit(
|
||||||
SettingsBillingState.ready(
|
SettingsBillingState.ready(
|
||||||
subscription: subscription,
|
subscriptionInfo: subscriptionInfo,
|
||||||
billingPortal: _billingPortal,
|
billingPortal: _billingPortal,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
billingPortalFetched: (billingPortal) {
|
billingPortalFetched: (billingPortal) async => state.maybeWhen(
|
||||||
state.maybeWhen(
|
orElse: () {},
|
||||||
orElse: () {},
|
ready: (subscriptionInfo, _) => emit(
|
||||||
ready: (subscription, _) => emit(
|
SettingsBillingState.ready(
|
||||||
SettingsBillingState.ready(
|
subscriptionInfo: subscriptionInfo,
|
||||||
subscription: subscription,
|
billingPortal: billingPortal,
|
||||||
billingPortal: billingPortal,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
},
|
),
|
||||||
openCustomerPortal: () async {
|
openCustomerPortal: () async {
|
||||||
if (_billingPortalCompleter.isCompleted && _billingPortal != null) {
|
if (_billingPortalCompleter.isCompleted && _billingPortal != null) {
|
||||||
await afLaunchUrlString(_billingPortal!.url);
|
await afLaunchUrlString(_billingPortal!.url);
|
||||||
@ -109,21 +102,58 @@ class SettingsBillingBloc
|
|||||||
await afLaunchUrlString(_billingPortal!.url);
|
await afLaunchUrlString(_billingPortal!.url);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
addSubscription: (plan) async {
|
||||||
|
final result =
|
||||||
|
await _userService.createSubscription(workspaceId, plan);
|
||||||
|
result.fold(
|
||||||
|
(link) => afLaunchUrlString(link.paymentLink),
|
||||||
|
(f) => Log.error(f.msg, f),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
cancelSubscription: (plan) async {
|
||||||
|
await _userService.cancelSubscription(workspaceId, plan);
|
||||||
|
await _onPaymentSuccessful();
|
||||||
|
},
|
||||||
|
paymentSuccessful: () async {
|
||||||
|
final result = await UserBackendService.getWorkspaceSubscriptionInfo(
|
||||||
|
workspaceId,
|
||||||
|
);
|
||||||
|
|
||||||
|
final subscriptionInfo = result.toNullable();
|
||||||
|
|
||||||
|
if (subscriptionInfo != null) {
|
||||||
|
emit(
|
||||||
|
SettingsBillingState.ready(
|
||||||
|
subscriptionInfo: subscriptionInfo,
|
||||||
|
billingPortal: _billingPortal,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
late final String workspaceId;
|
late final String workspaceId;
|
||||||
late final WorkspaceService _service;
|
late final WorkspaceService _service;
|
||||||
|
late final UserBackendService _userService;
|
||||||
final _billingPortalCompleter =
|
final _billingPortalCompleter =
|
||||||
Completer<FlowyResult<BillingPortalPB, FlowyError>>();
|
Completer<FlowyResult<BillingPortalPB, FlowyError>>();
|
||||||
|
|
||||||
BillingPortalPB? _billingPortal;
|
BillingPortalPB? _billingPortal;
|
||||||
|
late final SubscriptionSuccessListenable _successListenable;
|
||||||
|
|
||||||
Future<void> _fetchBillingPortal() async {
|
Future<void> _fetchBillingPortal() async {
|
||||||
final billingPortalResult = await _service.getBillingPortal();
|
final billingPortalResult = await _service.getBillingPortal();
|
||||||
_billingPortalCompleter.complete(billingPortalResult);
|
_billingPortalCompleter.complete(billingPortalResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _onPaymentSuccessful() async {
|
||||||
|
// Invalidate cache for this workspace
|
||||||
|
await UserBackendService.invalidateWorkspaceSubscriptionCache(workspaceId);
|
||||||
|
|
||||||
|
add(const SettingsBillingEvent.paymentSuccessful());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
@ -133,6 +163,12 @@ class SettingsBillingEvent with _$SettingsBillingEvent {
|
|||||||
required BillingPortalPB billingPortal,
|
required BillingPortalPB billingPortal,
|
||||||
}) = _BillingPortalFetched;
|
}) = _BillingPortalFetched;
|
||||||
const factory SettingsBillingEvent.openCustomerPortal() = _OpenCustomerPortal;
|
const factory SettingsBillingEvent.openCustomerPortal() = _OpenCustomerPortal;
|
||||||
|
const factory SettingsBillingEvent.addSubscription(SubscriptionPlanPB plan) =
|
||||||
|
_AddSubscription;
|
||||||
|
const factory SettingsBillingEvent.cancelSubscription(
|
||||||
|
SubscriptionPlanPB plan,
|
||||||
|
) = _CancelSubscription;
|
||||||
|
const factory SettingsBillingEvent.paymentSuccessful() = _PaymentSuccessful;
|
||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
@ -148,7 +184,7 @@ class SettingsBillingState extends Equatable with _$SettingsBillingState {
|
|||||||
}) = _Error;
|
}) = _Error;
|
||||||
|
|
||||||
const factory SettingsBillingState.ready({
|
const factory SettingsBillingState.ready({
|
||||||
required WorkspaceSubscriptionPB subscription,
|
required WorkspaceSubscriptionInfoPB subscriptionInfo,
|
||||||
required BillingPortalPB? billingPortal,
|
required BillingPortalPB? billingPortal,
|
||||||
}) = _Ready;
|
}) = _Ready;
|
||||||
|
|
||||||
|
@ -11,7 +11,6 @@ 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:bloc/bloc.dart';
|
import 'package:bloc/bloc.dart';
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:fixnum/fixnum.dart';
|
import 'package:fixnum/fixnum.dart';
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
@ -34,7 +33,7 @@ class SettingsPlanBloc extends Bloc<SettingsPlanEvent, SettingsPlanState> {
|
|||||||
|
|
||||||
final snapshots = await Future.wait([
|
final snapshots = await Future.wait([
|
||||||
_service.getWorkspaceUsage(),
|
_service.getWorkspaceUsage(),
|
||||||
UserBackendService.getWorkspaceSubscriptions(),
|
UserBackendService.getWorkspaceSubscriptionInfo(workspaceId),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
FlowyError? error;
|
FlowyError? error;
|
||||||
@ -47,30 +46,24 @@ class SettingsPlanBloc extends Bloc<SettingsPlanEvent, SettingsPlanState> {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
final subscription = snapshots[1].fold(
|
final subscriptionInfo = snapshots[1].fold(
|
||||||
(s) =>
|
(s) => s as WorkspaceSubscriptionInfoPB,
|
||||||
(s as RepeatedWorkspaceSubscriptionPB)
|
|
||||||
.items
|
|
||||||
.firstWhereOrNull((i) => i.workspaceId == workspaceId) ??
|
|
||||||
WorkspaceSubscriptionPB(
|
|
||||||
workspaceId: workspaceId,
|
|
||||||
subscriptionPlan: SubscriptionPlanPB.None,
|
|
||||||
isActive: true,
|
|
||||||
),
|
|
||||||
(f) {
|
(f) {
|
||||||
error = f;
|
error = f;
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (usageResult == null || subscription == null || error != null) {
|
if (usageResult == null ||
|
||||||
|
subscriptionInfo == null ||
|
||||||
|
error != null) {
|
||||||
return emit(SettingsPlanState.error(error: error));
|
return emit(SettingsPlanState.error(error: error));
|
||||||
}
|
}
|
||||||
|
|
||||||
emit(
|
emit(
|
||||||
SettingsPlanState.ready(
|
SettingsPlanState.ready(
|
||||||
workspaceUsage: usageResult,
|
workspaceUsage: usageResult,
|
||||||
subscription: subscription,
|
subscriptionInfo: subscriptionInfo,
|
||||||
showSuccessDialog: withShowSuccessful,
|
showSuccessDialog: withShowSuccessful,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -79,7 +72,7 @@ class SettingsPlanBloc extends Bloc<SettingsPlanEvent, SettingsPlanState> {
|
|||||||
emit(
|
emit(
|
||||||
SettingsPlanState.ready(
|
SettingsPlanState.ready(
|
||||||
workspaceUsage: usageResult,
|
workspaceUsage: usageResult,
|
||||||
subscription: subscription,
|
subscriptionInfo: subscriptionInfo,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -100,7 +93,14 @@ class SettingsPlanBloc extends Bloc<SettingsPlanEvent, SettingsPlanState> {
|
|||||||
.mapOrNull(ready: (state) => state)
|
.mapOrNull(ready: (state) => state)
|
||||||
?.copyWith(downgradeProcessing: true);
|
?.copyWith(downgradeProcessing: true);
|
||||||
emit(newState ?? state);
|
emit(newState ?? state);
|
||||||
await _userService.cancelSubscription(workspaceId);
|
|
||||||
|
// We can hardcode the subscription plan here because we cannot cancel addons
|
||||||
|
// on the Plan page
|
||||||
|
await _userService.cancelSubscription(
|
||||||
|
workspaceId,
|
||||||
|
SubscriptionPlanPB.Pro,
|
||||||
|
);
|
||||||
|
|
||||||
add(const SettingsPlanEvent.started());
|
add(const SettingsPlanEvent.started());
|
||||||
},
|
},
|
||||||
paymentSuccessful: () {
|
paymentSuccessful: () {
|
||||||
@ -120,7 +120,10 @@ class SettingsPlanBloc extends Bloc<SettingsPlanEvent, SettingsPlanState> {
|
|||||||
late final IUserBackendService _userService;
|
late final IUserBackendService _userService;
|
||||||
late final SubscriptionSuccessListenable _successListenable;
|
late final SubscriptionSuccessListenable _successListenable;
|
||||||
|
|
||||||
void _onPaymentSuccessful() {
|
Future<void> _onPaymentSuccessful() async {
|
||||||
|
// Invalidate cache for this workspace
|
||||||
|
await UserBackendService.invalidateWorkspaceSubscriptionCache(workspaceId);
|
||||||
|
|
||||||
add(const SettingsPlanEvent.paymentSuccessful());
|
add(const SettingsPlanEvent.paymentSuccessful());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,9 +139,12 @@ class SettingsPlanEvent with _$SettingsPlanEvent {
|
|||||||
const factory SettingsPlanEvent.started({
|
const factory SettingsPlanEvent.started({
|
||||||
@Default(false) bool withShowSuccessful,
|
@Default(false) bool withShowSuccessful,
|
||||||
}) = _Started;
|
}) = _Started;
|
||||||
|
|
||||||
const factory SettingsPlanEvent.addSubscription(SubscriptionPlanPB plan) =
|
const factory SettingsPlanEvent.addSubscription(SubscriptionPlanPB plan) =
|
||||||
_AddSubscription;
|
_AddSubscription;
|
||||||
|
|
||||||
const factory SettingsPlanEvent.cancelSubscription() = _CancelSubscription;
|
const factory SettingsPlanEvent.cancelSubscription() = _CancelSubscription;
|
||||||
|
|
||||||
const factory SettingsPlanEvent.paymentSuccessful() = _PaymentSuccessful;
|
const factory SettingsPlanEvent.paymentSuccessful() = _PaymentSuccessful;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -154,7 +160,7 @@ class SettingsPlanState with _$SettingsPlanState {
|
|||||||
|
|
||||||
const factory SettingsPlanState.ready({
|
const factory SettingsPlanState.ready({
|
||||||
required WorkspaceUsagePB workspaceUsage,
|
required WorkspaceUsagePB workspaceUsage,
|
||||||
required WorkspaceSubscriptionPB subscription,
|
required WorkspaceSubscriptionInfoPB subscriptionInfo,
|
||||||
@Default(false) bool showSuccessDialog,
|
@Default(false) bool showSuccessDialog,
|
||||||
@Default(false) bool downgradeProcessing,
|
@Default(false) bool downgradeProcessing,
|
||||||
}) = _Ready;
|
}) = _Ready;
|
||||||
|
@ -1,26 +1,52 @@
|
|||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.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:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
|
||||||
extension SubscriptionLabels on WorkspaceSubscriptionPB {
|
extension SubscriptionLabels on WorkspaceSubscriptionInfoPB {
|
||||||
String get label => switch (subscriptionPlan) {
|
String get label => switch (plan) {
|
||||||
SubscriptionPlanPB.None =>
|
WorkspacePlanPB.FreePlan =>
|
||||||
LocaleKeys.settings_planPage_planUsage_currentPlan_freeTitle.tr(),
|
LocaleKeys.settings_planPage_planUsage_currentPlan_freeTitle.tr(),
|
||||||
SubscriptionPlanPB.Pro =>
|
WorkspacePlanPB.ProPlan =>
|
||||||
LocaleKeys.settings_planPage_planUsage_currentPlan_proTitle.tr(),
|
LocaleKeys.settings_planPage_planUsage_currentPlan_proTitle.tr(),
|
||||||
SubscriptionPlanPB.Team =>
|
WorkspacePlanPB.TeamPlan =>
|
||||||
LocaleKeys.settings_planPage_planUsage_currentPlan_teamTitle.tr(),
|
LocaleKeys.settings_planPage_planUsage_currentPlan_teamTitle.tr(),
|
||||||
_ => 'N/A',
|
_ => 'N/A',
|
||||||
};
|
};
|
||||||
|
|
||||||
String get info => switch (subscriptionPlan) {
|
String get info => switch (plan) {
|
||||||
SubscriptionPlanPB.None =>
|
WorkspacePlanPB.FreePlan =>
|
||||||
LocaleKeys.settings_planPage_planUsage_currentPlan_freeInfo.tr(),
|
LocaleKeys.settings_planPage_planUsage_currentPlan_freeInfo.tr(),
|
||||||
SubscriptionPlanPB.Pro =>
|
WorkspacePlanPB.ProPlan =>
|
||||||
LocaleKeys.settings_planPage_planUsage_currentPlan_proInfo.tr(),
|
LocaleKeys.settings_planPage_planUsage_currentPlan_proInfo.tr(),
|
||||||
SubscriptionPlanPB.Team =>
|
WorkspacePlanPB.TeamPlan =>
|
||||||
LocaleKeys.settings_planPage_planUsage_currentPlan_teamInfo.tr(),
|
LocaleKeys.settings_planPage_planUsage_currentPlan_teamInfo.tr(),
|
||||||
_ => 'N/A',
|
_ => 'N/A',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension WorkspaceSubscriptionStatusExt on WorkspaceSubscriptionInfoPB {
|
||||||
|
bool get isCanceled =>
|
||||||
|
planSubscription.status == WorkspaceSubscriptionStatusPB.Canceled;
|
||||||
|
}
|
||||||
|
|
||||||
|
extension WorkspaceAddonsExt on WorkspaceSubscriptionInfoPB {
|
||||||
|
bool get hasAIMax =>
|
||||||
|
addOns.any((addon) => addon.type == WorkspaceAddOnPBType.AddOnAiMax);
|
||||||
|
|
||||||
|
bool get hasAIOnDevice =>
|
||||||
|
addOns.any((addon) => addon.type == WorkspaceAddOnPBType.AddOnAiLocal);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// These have to match [SubscriptionSuccessListenable.subscribedPlan] labels
|
||||||
|
extension ToRecognizable on SubscriptionPlanPB {
|
||||||
|
String? toRecognizable() => switch (this) {
|
||||||
|
SubscriptionPlanPB.None => 'free',
|
||||||
|
SubscriptionPlanPB.Pro => 'pro',
|
||||||
|
SubscriptionPlanPB.Team => 'team',
|
||||||
|
SubscriptionPlanPB.AiMax => 'ai_max',
|
||||||
|
SubscriptionPlanPB.AiLocal => 'ai_local',
|
||||||
|
_ => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@ -7,7 +7,7 @@ final _storageNumberFormat = NumberFormat()
|
|||||||
|
|
||||||
extension PresentableUsage on WorkspaceUsagePB {
|
extension PresentableUsage on WorkspaceUsagePB {
|
||||||
String get totalBlobInGb =>
|
String get totalBlobInGb =>
|
||||||
(totalBlobBytesLimit.toInt() / 1024 / 1024 / 1024).round().toString();
|
(storageBytesLimit.toInt() / 1024 / 1024 / 1024).round().toString();
|
||||||
|
|
||||||
/// We use [NumberFormat] to format the current blob in GB.
|
/// We use [NumberFormat] to format the current blob in GB.
|
||||||
///
|
///
|
||||||
@ -16,5 +16,5 @@ extension PresentableUsage on WorkspaceUsagePB {
|
|||||||
/// And [NumberFormat.minimumFractionDigits] is set to 0.
|
/// And [NumberFormat.minimumFractionDigits] is set to 0.
|
||||||
///
|
///
|
||||||
String get currentBlobInGb =>
|
String get currentBlobInGb =>
|
||||||
_storageNumberFormat.format(totalBlobBytes.toInt() / 1024 / 1024 / 1024);
|
_storageNumberFormat.format(storageBytes.toInt() / 1024 / 1024 / 1024);
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,23 @@
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||||
|
|
||||||
class SubscriptionSuccessListenable extends ChangeNotifier {
|
class SubscriptionSuccessListenable extends ChangeNotifier {
|
||||||
SubscriptionSuccessListenable();
|
SubscriptionSuccessListenable();
|
||||||
|
|
||||||
void onPaymentSuccess() => notifyListeners();
|
String? _plan;
|
||||||
|
|
||||||
|
SubscriptionPlanPB? get subscribedPlan => switch (_plan) {
|
||||||
|
'free' => SubscriptionPlanPB.None,
|
||||||
|
'pro' => SubscriptionPlanPB.Pro,
|
||||||
|
'team' => SubscriptionPlanPB.Team,
|
||||||
|
'ai_max' => SubscriptionPlanPB.AiMax,
|
||||||
|
'ai_local' => SubscriptionPlanPB.AiLocal,
|
||||||
|
_ => null,
|
||||||
|
};
|
||||||
|
|
||||||
|
void onPaymentSuccess(String? plan) {
|
||||||
|
_plan = plan;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:appflowy/core/helpers/url_launcher.dart';
|
|
||||||
import 'package:appflowy/workspace/application/settings/billing/settings_billing_bloc.dart';
|
import 'package:appflowy/workspace/application/settings/billing/settings_billing_bloc.dart';
|
||||||
import 'package:appflowy/workspace/application/settings/plan/settings_plan_bloc.dart';
|
import 'package:appflowy/workspace/application/settings/plan/settings_plan_bloc.dart';
|
||||||
import 'package:appflowy/workspace/application/settings/plan/workspace_subscription_ext.dart';
|
import 'package:appflowy/workspace/application/settings/plan/workspace_subscription_ext.dart';
|
||||||
@ -9,6 +8,7 @@ import 'package:appflowy/workspace/presentation/settings/shared/settings_body.da
|
|||||||
import 'package:appflowy/workspace/presentation/settings/shared/settings_category.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/settings/shared/single_setting_action.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:fixnum/fixnum.dart';
|
import 'package:fixnum/fixnum.dart';
|
||||||
import 'package:flowy_infra_ui/widget/error_page.dart';
|
import 'package:flowy_infra_ui/widget/error_page.dart';
|
||||||
@ -16,7 +16,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
|||||||
|
|
||||||
import '../../../../generated/locale_keys.g.dart';
|
import '../../../../generated/locale_keys.g.dart';
|
||||||
|
|
||||||
const _buttonsMinWidth = 116.0;
|
const _buttonsMinWidth = 100.0;
|
||||||
|
|
||||||
class SettingsBillingView extends StatelessWidget {
|
class SettingsBillingView extends StatelessWidget {
|
||||||
const SettingsBillingView({
|
const SettingsBillingView({
|
||||||
@ -31,8 +31,10 @@ class SettingsBillingView extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider<SettingsBillingBloc>(
|
return BlocProvider<SettingsBillingBloc>(
|
||||||
create: (context) => SettingsBillingBloc(workspaceId: workspaceId)
|
create: (_) => SettingsBillingBloc(
|
||||||
..add(const SettingsBillingEvent.started()),
|
workspaceId: workspaceId,
|
||||||
|
userId: user.id,
|
||||||
|
)..add(const SettingsBillingEvent.started()),
|
||||||
child: BlocBuilder<SettingsBillingBloc, SettingsBillingState>(
|
child: BlocBuilder<SettingsBillingBloc, SettingsBillingState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return state.map(
|
return state.map(
|
||||||
@ -59,8 +61,7 @@ class SettingsBillingView extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
ready: (state) {
|
ready: (state) {
|
||||||
final billingPortalEnabled =
|
final billingPortalEnabled =
|
||||||
state.subscription.subscriptionPlan !=
|
state.subscriptionInfo.plan != WorkspacePlanPB.FreePlan;
|
||||||
SubscriptionPlanPB.None;
|
|
||||||
|
|
||||||
return SettingsBody(
|
return SettingsBody(
|
||||||
title: LocaleKeys.settings_billingPage_title.tr(),
|
title: LocaleKeys.settings_billingPage_title.tr(),
|
||||||
@ -73,10 +74,10 @@ class SettingsBillingView extends StatelessWidget {
|
|||||||
context,
|
context,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
user.id,
|
user.id,
|
||||||
state.subscription,
|
state.subscriptionInfo,
|
||||||
),
|
),
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
label: state.subscription.label,
|
label: state.subscriptionInfo.label,
|
||||||
buttonLabel: LocaleKeys
|
buttonLabel: LocaleKeys
|
||||||
.settings_billingPage_plan_planButtonLabel
|
.settings_billingPage_plan_planButtonLabel
|
||||||
.tr(),
|
.tr(),
|
||||||
@ -127,33 +128,31 @@ class SettingsBillingView extends StatelessWidget {
|
|||||||
SettingsCategory(
|
SettingsCategory(
|
||||||
title: LocaleKeys.settings_billingPage_addons_title.tr(),
|
title: LocaleKeys.settings_billingPage_addons_title.tr(),
|
||||||
children: [
|
children: [
|
||||||
SingleSettingAction(
|
_AITile(
|
||||||
buttonType: SingleSettingsButtonType.highlight,
|
plan: SubscriptionPlanPB.AiMax,
|
||||||
label: LocaleKeys
|
label: LocaleKeys
|
||||||
.settings_billingPage_addons_aiMax_label
|
.settings_billingPage_addons_aiMax_label
|
||||||
.tr(),
|
.tr(),
|
||||||
description: LocaleKeys
|
description: LocaleKeys
|
||||||
.settings_billingPage_addons_aiMax_description
|
.settings_billingPage_addons_aiMax_description
|
||||||
.tr(),
|
.tr(),
|
||||||
buttonLabel: LocaleKeys
|
subscriptionInfo:
|
||||||
.settings_billingPage_addons_aiMax_buttonLabel
|
state.subscriptionInfo.addOns.firstWhereOrNull(
|
||||||
.tr(),
|
(a) => a.type == WorkspaceAddOnPBType.AddOnAiMax,
|
||||||
fontWeight: FontWeight.w500,
|
),
|
||||||
minWidth: _buttonsMinWidth,
|
|
||||||
),
|
),
|
||||||
SingleSettingAction(
|
_AITile(
|
||||||
buttonType: SingleSettingsButtonType.highlight,
|
plan: SubscriptionPlanPB.AiLocal,
|
||||||
label: LocaleKeys
|
label: LocaleKeys
|
||||||
.settings_billingPage_addons_aiOnDevice_label
|
.settings_billingPage_addons_aiOnDevice_label
|
||||||
.tr(),
|
.tr(),
|
||||||
description: LocaleKeys
|
description: LocaleKeys
|
||||||
.settings_billingPage_addons_aiOnDevice_description
|
.settings_billingPage_addons_aiOnDevice_description
|
||||||
.tr(),
|
.tr(),
|
||||||
buttonLabel: LocaleKeys
|
subscriptionInfo:
|
||||||
.settings_billingPage_addons_aiOnDevice_buttonLabel
|
state.subscriptionInfo.addOns.firstWhereOrNull(
|
||||||
.tr(),
|
(a) => a.type == WorkspaceAddOnPBType.AddOnAiLocal,
|
||||||
fontWeight: FontWeight.w500,
|
),
|
||||||
minWidth: _buttonsMinWidth,
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -170,7 +169,7 @@ class SettingsBillingView extends StatelessWidget {
|
|||||||
BuildContext context,
|
BuildContext context,
|
||||||
String workspaceId,
|
String workspaceId,
|
||||||
Int64 userId,
|
Int64 userId,
|
||||||
WorkspaceSubscriptionPB subscription,
|
WorkspaceSubscriptionInfoPB subscriptionInfo,
|
||||||
) =>
|
) =>
|
||||||
showDialog<bool?>(
|
showDialog<bool?>(
|
||||||
context: context,
|
context: context,
|
||||||
@ -180,7 +179,7 @@ class SettingsBillingView extends StatelessWidget {
|
|||||||
..add(const SettingsPlanEvent.started()),
|
..add(const SettingsPlanEvent.started()),
|
||||||
child: SettingsPlanComparisonDialog(
|
child: SettingsPlanComparisonDialog(
|
||||||
workspaceId: workspaceId,
|
workspaceId: workspaceId,
|
||||||
subscription: subscription,
|
subscriptionInfo: subscriptionInfo,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
).then((didChangePlan) {
|
).then((didChangePlan) {
|
||||||
@ -191,3 +190,48 @@ class SettingsBillingView extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _AITile extends StatelessWidget {
|
||||||
|
const _AITile({
|
||||||
|
required this.label,
|
||||||
|
required this.description,
|
||||||
|
required this.plan,
|
||||||
|
this.subscriptionInfo,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String label;
|
||||||
|
final String description;
|
||||||
|
final SubscriptionPlanPB plan;
|
||||||
|
final WorkspaceAddOnPB? subscriptionInfo;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final isCanceled = subscriptionInfo?.addOnSubscription.status ==
|
||||||
|
WorkspaceSubscriptionStatusPB.Canceled;
|
||||||
|
|
||||||
|
return SingleSettingAction(
|
||||||
|
label: label,
|
||||||
|
description: description,
|
||||||
|
buttonLabel: subscriptionInfo != null
|
||||||
|
? isCanceled
|
||||||
|
? LocaleKeys.settings_billingPage_addons_renewLabel.tr()
|
||||||
|
: LocaleKeys.settings_billingPage_addons_removeLabel.tr()
|
||||||
|
: LocaleKeys.settings_billingPage_addons_addLabel.tr(),
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
minWidth: _buttonsMinWidth,
|
||||||
|
onPressed: () {
|
||||||
|
if (subscriptionInfo != null && !isCanceled) {
|
||||||
|
// Cancel the addon
|
||||||
|
context
|
||||||
|
.read<SettingsBillingBloc>()
|
||||||
|
.add(SettingsBillingEvent.cancelSubscription(plan));
|
||||||
|
} else {
|
||||||
|
// Add/renew the addon
|
||||||
|
context
|
||||||
|
.read<SettingsBillingBloc>()
|
||||||
|
.add(SettingsBillingEvent.addSubscription(plan));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,28 +1,29 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
|
||||||
import 'package:appflowy/util/theme_extension.dart';
|
import 'package:appflowy/util/theme_extension.dart';
|
||||||
import 'package:appflowy/workspace/application/settings/plan/settings_plan_bloc.dart';
|
import 'package:appflowy/workspace/application/settings/plan/settings_plan_bloc.dart';
|
||||||
import 'package:appflowy/workspace/application/settings/plan/workspace_subscription_ext.dart';
|
import 'package:appflowy/workspace/application/settings/plan/workspace_subscription_ext.dart';
|
||||||
import 'package:appflowy/workspace/presentation/settings/shared/settings_alert_dialog.dart';
|
import 'package:appflowy/workspace/presentation/settings/shared/settings_alert_dialog.dart';
|
||||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra/theme_extension.dart';
|
import 'package:flowy_infra/theme_extension.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.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/flowy_tooltip.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
|
import '../../../../generated/locale_keys.g.dart';
|
||||||
|
|
||||||
class SettingsPlanComparisonDialog extends StatefulWidget {
|
class SettingsPlanComparisonDialog extends StatefulWidget {
|
||||||
const SettingsPlanComparisonDialog({
|
const SettingsPlanComparisonDialog({
|
||||||
super.key,
|
super.key,
|
||||||
required this.workspaceId,
|
required this.workspaceId,
|
||||||
required this.subscription,
|
required this.subscriptionInfo,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String workspaceId;
|
final String workspaceId;
|
||||||
final WorkspaceSubscriptionPB subscription;
|
final WorkspaceSubscriptionInfoPB subscriptionInfo;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<SettingsPlanComparisonDialog> createState() =>
|
State<SettingsPlanComparisonDialog> createState() =>
|
||||||
@ -34,7 +35,7 @@ class _SettingsPlanComparisonDialogState
|
|||||||
final horizontalController = ScrollController();
|
final horizontalController = ScrollController();
|
||||||
final verticalController = ScrollController();
|
final verticalController = ScrollController();
|
||||||
|
|
||||||
late WorkspaceSubscriptionPB currentSubscription = widget.subscription;
|
late WorkspaceSubscriptionInfoPB currentInfo = widget.subscriptionInfo;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
@ -58,19 +59,17 @@ class _SettingsPlanComparisonDialogState
|
|||||||
if (readyState.showSuccessDialog) {
|
if (readyState.showSuccessDialog) {
|
||||||
SettingsAlertDialog(
|
SettingsAlertDialog(
|
||||||
title: LocaleKeys.settings_comparePlanDialog_paymentSuccess_title
|
title: LocaleKeys.settings_comparePlanDialog_paymentSuccess_title
|
||||||
.tr(args: [readyState.subscription.label]),
|
.tr(args: [readyState.subscriptionInfo.label]),
|
||||||
subtitle: LocaleKeys
|
subtitle: LocaleKeys
|
||||||
.settings_comparePlanDialog_paymentSuccess_description
|
.settings_comparePlanDialog_paymentSuccess_description
|
||||||
.tr(args: [readyState.subscription.label]),
|
.tr(args: [readyState.subscriptionInfo.label]),
|
||||||
hideCancelButton: true,
|
hideCancelButton: true,
|
||||||
confirm: Navigator.of(context).pop,
|
confirm: Navigator.of(context).pop,
|
||||||
confirmLabel: LocaleKeys.button_close.tr(),
|
confirmLabel: LocaleKeys.button_close.tr(),
|
||||||
).show(context);
|
).show(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(() {
|
setState(() => currentInfo = readyState.subscriptionInfo);
|
||||||
currentSubscription = readyState.subscription;
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
builder: (context, state) => FlowyDialog(
|
builder: (context, state) => FlowyDialog(
|
||||||
constraints: const BoxConstraints(maxWidth: 784, minWidth: 674),
|
constraints: const BoxConstraints(maxWidth: 784, minWidth: 674),
|
||||||
@ -90,8 +89,7 @@ class _SettingsPlanComparisonDialogState
|
|||||||
const Spacer(),
|
const Spacer(),
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () => Navigator.of(context).pop(
|
onTap: () => Navigator.of(context).pop(
|
||||||
currentSubscription.subscriptionPlan !=
|
currentInfo.plan != widget.subscriptionInfo.plan,
|
||||||
widget.subscription.subscriptionPlan,
|
|
||||||
),
|
),
|
||||||
child: MouseRegion(
|
child: MouseRegion(
|
||||||
cursor: SystemMouseCursors.click,
|
cursor: SystemMouseCursors.click,
|
||||||
@ -170,32 +168,30 @@ class _SettingsPlanComparisonDialogState
|
|||||||
.settings_comparePlanDialog_freePlan_priceInfo
|
.settings_comparePlanDialog_freePlan_priceInfo
|
||||||
.tr(),
|
.tr(),
|
||||||
cells: _freeLabels,
|
cells: _freeLabels,
|
||||||
isCurrent: currentSubscription.subscriptionPlan ==
|
isCurrent:
|
||||||
SubscriptionPlanPB.None,
|
currentInfo.plan == WorkspacePlanPB.FreePlan,
|
||||||
canDowngrade:
|
canDowngrade:
|
||||||
currentSubscription.subscriptionPlan !=
|
currentInfo.plan != WorkspacePlanPB.FreePlan,
|
||||||
SubscriptionPlanPB.None,
|
currentCanceled: currentInfo.isCanceled ||
|
||||||
currentCanceled: currentSubscription.hasCanceled ||
|
|
||||||
(context
|
(context
|
||||||
.watch<SettingsPlanBloc>()
|
.watch<SettingsPlanBloc>()
|
||||||
.state
|
.state
|
||||||
.mapOrNull(
|
.mapOrNull(
|
||||||
loading: (_) => true,
|
loading: (_) => true,
|
||||||
ready: (state) =>
|
ready: (s) => s.downgradeProcessing,
|
||||||
state.downgradeProcessing,
|
|
||||||
) ??
|
) ??
|
||||||
false),
|
false),
|
||||||
onSelected: () async {
|
onSelected: () async {
|
||||||
if (currentSubscription.subscriptionPlan ==
|
if (currentInfo.plan ==
|
||||||
SubscriptionPlanPB.None ||
|
WorkspacePlanPB.FreePlan ||
|
||||||
currentSubscription.hasCanceled) {
|
currentInfo.isCanceled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await SettingsAlertDialog(
|
await SettingsAlertDialog(
|
||||||
title: LocaleKeys
|
title: LocaleKeys
|
||||||
.settings_comparePlanDialog_downgradeDialog_title
|
.settings_comparePlanDialog_downgradeDialog_title
|
||||||
.tr(args: [currentSubscription.label]),
|
.tr(args: [currentInfo.label]),
|
||||||
subtitle: LocaleKeys
|
subtitle: LocaleKeys
|
||||||
.settings_comparePlanDialog_downgradeDialog_description
|
.settings_comparePlanDialog_downgradeDialog_description
|
||||||
.tr(),
|
.tr(),
|
||||||
@ -228,11 +224,11 @@ class _SettingsPlanComparisonDialogState
|
|||||||
.settings_comparePlanDialog_proPlan_priceInfo
|
.settings_comparePlanDialog_proPlan_priceInfo
|
||||||
.tr(),
|
.tr(),
|
||||||
cells: _proLabels,
|
cells: _proLabels,
|
||||||
isCurrent: currentSubscription.subscriptionPlan ==
|
isCurrent:
|
||||||
SubscriptionPlanPB.Pro,
|
currentInfo.plan == WorkspacePlanPB.ProPlan,
|
||||||
canUpgrade: currentSubscription.subscriptionPlan ==
|
canUpgrade:
|
||||||
SubscriptionPlanPB.None,
|
currentInfo.plan == WorkspacePlanPB.FreePlan,
|
||||||
currentCanceled: currentSubscription.hasCanceled,
|
currentCanceled: currentInfo.isCanceled,
|
||||||
onSelected: () =>
|
onSelected: () =>
|
||||||
context.read<SettingsPlanBloc>().add(
|
context.read<SettingsPlanBloc>().add(
|
||||||
const SettingsPlanEvent.addSubscription(
|
const SettingsPlanEvent.addSubscription(
|
||||||
|
@ -68,40 +68,74 @@ class SettingsPlanView extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
_PlanUsageSummary(
|
_PlanUsageSummary(
|
||||||
usage: state.workspaceUsage,
|
usage: state.workspaceUsage,
|
||||||
subscription: state.subscription,
|
subscriptionInfo: state.subscriptionInfo,
|
||||||
),
|
),
|
||||||
const VSpace(16),
|
const VSpace(16),
|
||||||
_CurrentPlanBox(subscription: state.subscription),
|
_CurrentPlanBox(subscriptionInfo: state.subscriptionInfo),
|
||||||
const VSpace(16),
|
const VSpace(16),
|
||||||
// TODO(Mathias): Localize and add business logic
|
// TODO(Mathias): Localize and add business logic
|
||||||
FlowyText(
|
FlowyText(
|
||||||
'Add-ons',
|
LocaleKeys.settings_planPage_planUsage_addons_title.tr(),
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
color: AFThemeExtension.of(context).strongText,
|
color: AFThemeExtension.of(context).strongText,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
const VSpace(8),
|
const VSpace(8),
|
||||||
const Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Flexible(
|
Flexible(
|
||||||
child: _AddOnBox(
|
child: _AddOnBox(
|
||||||
title: "AI Max",
|
title: LocaleKeys
|
||||||
description:
|
.settings_planPage_planUsage_addons_aiMax_title
|
||||||
"Unlimited AI models and access to advanced models",
|
.tr(),
|
||||||
price: "US\$8",
|
description: LocaleKeys
|
||||||
priceInfo: "billed annually or \$10 billed monthly",
|
.settings_planPage_planUsage_addons_aiMax_description
|
||||||
buttonText: "Add AI Max",
|
.tr(),
|
||||||
|
price: LocaleKeys
|
||||||
|
.settings_planPage_planUsage_addons_aiMax_price
|
||||||
|
.tr(args: ['\$8']),
|
||||||
|
priceInfo: LocaleKeys
|
||||||
|
.settings_planPage_planUsage_addons_aiMax_priceInfo
|
||||||
|
.tr(),
|
||||||
|
billingInfo: LocaleKeys
|
||||||
|
.settings_planPage_planUsage_addons_aiMax_billingInfo
|
||||||
|
.tr(args: ['\$10']),
|
||||||
|
buttonText: state.subscriptionInfo.hasAIMax
|
||||||
|
? LocaleKeys
|
||||||
|
.settings_planPage_planUsage_addons_activeLabel
|
||||||
|
.tr()
|
||||||
|
: LocaleKeys
|
||||||
|
.settings_planPage_planUsage_addons_addLabel
|
||||||
|
.tr(),
|
||||||
|
isActive: state.subscriptionInfo.hasAIMax,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
HSpace(8),
|
const HSpace(8),
|
||||||
Flexible(
|
Flexible(
|
||||||
child: _AddOnBox(
|
child: _AddOnBox(
|
||||||
title: "AI Offline",
|
title: LocaleKeys
|
||||||
description:
|
.settings_planPage_planUsage_addons_aiOnDevice_title
|
||||||
"Local AI on your own hardware for ultimate privacy",
|
.tr(),
|
||||||
price: "US\$8",
|
description: LocaleKeys
|
||||||
priceInfo: "billed annually or \$10 billed monthly",
|
.settings_planPage_planUsage_addons_aiOnDevice_description
|
||||||
buttonText: "Add AI Offline",
|
.tr(),
|
||||||
|
price: LocaleKeys
|
||||||
|
.settings_planPage_planUsage_addons_aiOnDevice_price
|
||||||
|
.tr(args: ['\$8']),
|
||||||
|
priceInfo: LocaleKeys
|
||||||
|
.settings_planPage_planUsage_addons_aiOnDevice_priceInfo
|
||||||
|
.tr(),
|
||||||
|
billingInfo: LocaleKeys
|
||||||
|
.settings_planPage_planUsage_addons_aiOnDevice_billingInfo
|
||||||
|
.tr(args: ['\$10']),
|
||||||
|
buttonText: state.subscriptionInfo.hasAIOnDevice
|
||||||
|
? LocaleKeys
|
||||||
|
.settings_planPage_planUsage_addons_activeLabel
|
||||||
|
.tr()
|
||||||
|
: LocaleKeys
|
||||||
|
.settings_planPage_planUsage_addons_addLabel
|
||||||
|
.tr(),
|
||||||
|
isActive: state.subscriptionInfo.hasAIOnDevice,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -116,9 +150,9 @@ class SettingsPlanView extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _CurrentPlanBox extends StatefulWidget {
|
class _CurrentPlanBox extends StatefulWidget {
|
||||||
const _CurrentPlanBox({required this.subscription});
|
const _CurrentPlanBox({required this.subscriptionInfo});
|
||||||
|
|
||||||
final WorkspaceSubscriptionPB subscription;
|
final WorkspaceSubscriptionInfoPB subscriptionInfo;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_CurrentPlanBox> createState() => _CurrentPlanBoxState();
|
State<_CurrentPlanBox> createState() => _CurrentPlanBoxState();
|
||||||
@ -161,13 +195,13 @@ class _CurrentPlanBoxState extends State<_CurrentPlanBox> {
|
|||||||
children: [
|
children: [
|
||||||
const VSpace(4),
|
const VSpace(4),
|
||||||
FlowyText.semibold(
|
FlowyText.semibold(
|
||||||
widget.subscription.label,
|
widget.subscriptionInfo.label,
|
||||||
fontSize: 24,
|
fontSize: 24,
|
||||||
color: AFThemeExtension.of(context).strongText,
|
color: AFThemeExtension.of(context).strongText,
|
||||||
),
|
),
|
||||||
const VSpace(8),
|
const VSpace(8),
|
||||||
FlowyText.regular(
|
FlowyText.regular(
|
||||||
widget.subscription.info,
|
widget.subscriptionInfo.info,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: AFThemeExtension.of(context).strongText,
|
color: AFThemeExtension.of(context).strongText,
|
||||||
maxLines: 3,
|
maxLines: 3,
|
||||||
@ -189,7 +223,7 @@ class _CurrentPlanBoxState extends State<_CurrentPlanBox> {
|
|||||||
onPressed: () => _openPricingDialog(
|
onPressed: () => _openPricingDialog(
|
||||||
context,
|
context,
|
||||||
context.read<SettingsPlanBloc>().workspaceId,
|
context.read<SettingsPlanBloc>().workspaceId,
|
||||||
widget.subscription,
|
widget.subscriptionInfo,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -198,7 +232,7 @@ class _CurrentPlanBoxState extends State<_CurrentPlanBox> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (widget.subscription.hasCanceled) ...[
|
if (widget.subscriptionInfo.isCanceled) ...[
|
||||||
const VSpace(12),
|
const VSpace(12),
|
||||||
FlowyText(
|
FlowyText(
|
||||||
LocaleKeys
|
LocaleKeys
|
||||||
@ -245,7 +279,7 @@ class _CurrentPlanBoxState extends State<_CurrentPlanBox> {
|
|||||||
String _canceledDate(BuildContext context) {
|
String _canceledDate(BuildContext context) {
|
||||||
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.subscriptionInfo.planSubscription.endDate.toDateTime(),
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -253,7 +287,7 @@ class _CurrentPlanBoxState extends State<_CurrentPlanBox> {
|
|||||||
void _openPricingDialog(
|
void _openPricingDialog(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
String workspaceId,
|
String workspaceId,
|
||||||
WorkspaceSubscriptionPB subscription,
|
WorkspaceSubscriptionInfoPB subscriptionInfo,
|
||||||
) =>
|
) =>
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
@ -261,7 +295,7 @@ class _CurrentPlanBoxState extends State<_CurrentPlanBox> {
|
|||||||
value: planBloc,
|
value: planBloc,
|
||||||
child: SettingsPlanComparisonDialog(
|
child: SettingsPlanComparisonDialog(
|
||||||
workspaceId: workspaceId,
|
workspaceId: workspaceId,
|
||||||
subscription: subscription,
|
subscriptionInfo: subscriptionInfo,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -270,11 +304,11 @@ class _CurrentPlanBoxState extends State<_CurrentPlanBox> {
|
|||||||
class _PlanUsageSummary extends StatelessWidget {
|
class _PlanUsageSummary extends StatelessWidget {
|
||||||
const _PlanUsageSummary({
|
const _PlanUsageSummary({
|
||||||
required this.usage,
|
required this.usage,
|
||||||
required this.subscription,
|
required this.subscriptionInfo,
|
||||||
});
|
});
|
||||||
|
|
||||||
final WorkspaceUsagePB usage;
|
final WorkspaceUsagePB usage;
|
||||||
final WorkspaceSubscriptionPB subscription;
|
final WorkspaceSubscriptionInfoPB subscriptionInfo;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -294,8 +328,8 @@ class _PlanUsageSummary extends StatelessWidget {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: _UsageBox(
|
child: _UsageBox(
|
||||||
title: LocaleKeys.settings_planPage_planUsage_storageLabel.tr(),
|
title: LocaleKeys.settings_planPage_planUsage_storageLabel.tr(),
|
||||||
replacementText: subscription.subscriptionPlan ==
|
replacementText: subscriptionInfo.plan ==
|
||||||
SubscriptionPlanPB.Pro
|
WorkspacePlanPB.ProPlan
|
||||||
? LocaleKeys.settings_planPage_planUsage_storageUnlimited
|
? LocaleKeys.settings_planPage_planUsage_storageUnlimited
|
||||||
.tr()
|
.tr()
|
||||||
: null,
|
: null,
|
||||||
@ -305,8 +339,8 @@ class _PlanUsageSummary extends StatelessWidget {
|
|||||||
usage.totalBlobInGb,
|
usage.totalBlobInGb,
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
value: usage.totalBlobBytes.toInt() /
|
value: usage.storageBytes.toInt() /
|
||||||
usage.totalBlobBytesLimit.toInt(),
|
usage.storageBytesLimit.toInt(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
@ -330,12 +364,11 @@ class _PlanUsageSummary extends StatelessWidget {
|
|||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (subscription.subscriptionPlan == SubscriptionPlanPB.None) ...[
|
if (subscriptionInfo.plan == WorkspacePlanPB.FreePlan) ...[
|
||||||
_ToggleMore(
|
_ToggleMore(
|
||||||
value: false,
|
value: false,
|
||||||
label:
|
label:
|
||||||
LocaleKeys.settings_planPage_planUsage_memberProToggle.tr(),
|
LocaleKeys.settings_planPage_planUsage_memberProToggle.tr(),
|
||||||
subscription: subscription,
|
|
||||||
badgeLabel:
|
badgeLabel:
|
||||||
LocaleKeys.settings_planPage_planUsage_proBadge.tr(),
|
LocaleKeys.settings_planPage_planUsage_proBadge.tr(),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
@ -407,14 +440,12 @@ class _ToggleMore extends StatefulWidget {
|
|||||||
const _ToggleMore({
|
const _ToggleMore({
|
||||||
required this.value,
|
required this.value,
|
||||||
required this.label,
|
required this.label,
|
||||||
required this.subscription,
|
|
||||||
this.badgeLabel,
|
this.badgeLabel,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
});
|
});
|
||||||
|
|
||||||
final bool value;
|
final bool value;
|
||||||
final String label;
|
final String label;
|
||||||
final WorkspaceSubscriptionPB subscription;
|
|
||||||
final String? badgeLabel;
|
final String? badgeLabel;
|
||||||
final Future<void> Function()? onTap;
|
final Future<void> Function()? onTap;
|
||||||
|
|
||||||
@ -535,14 +566,18 @@ class _AddOnBox extends StatelessWidget {
|
|||||||
required this.description,
|
required this.description,
|
||||||
required this.price,
|
required this.price,
|
||||||
required this.priceInfo,
|
required this.priceInfo,
|
||||||
|
required this.billingInfo,
|
||||||
required this.buttonText,
|
required this.buttonText,
|
||||||
|
required this.isActive,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String title;
|
final String title;
|
||||||
final String description;
|
final String description;
|
||||||
final String price;
|
final String price;
|
||||||
final String priceInfo;
|
final String priceInfo;
|
||||||
|
final String billingInfo;
|
||||||
final String buttonText;
|
final String buttonText;
|
||||||
|
final bool isActive;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -553,7 +588,12 @@ class _AddOnBox extends StatelessWidget {
|
|||||||
vertical: 12,
|
vertical: 12,
|
||||||
),
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border.all(color: const Color(0xFFBDBDBD)),
|
border: Border.all(
|
||||||
|
color: isActive ? const Color(0xFF9C00FB) : const Color(0xFFBDBDBD),
|
||||||
|
),
|
||||||
|
color: isActive
|
||||||
|
? const Color(0xFFF7F8FC).withOpacity(0.05)
|
||||||
|
: Colors.transparent,
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
@ -578,7 +618,7 @@ class _AddOnBox extends StatelessWidget {
|
|||||||
color: AFThemeExtension.of(context).strongText,
|
color: AFThemeExtension.of(context).strongText,
|
||||||
),
|
),
|
||||||
FlowyText(
|
FlowyText(
|
||||||
'/user per month',
|
priceInfo,
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
color: AFThemeExtension.of(context).strongText,
|
color: AFThemeExtension.of(context).strongText,
|
||||||
),
|
),
|
||||||
@ -587,7 +627,7 @@ class _AddOnBox extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: FlowyText(
|
child: FlowyText(
|
||||||
priceInfo,
|
billingInfo,
|
||||||
color: AFThemeExtension.of(context).secondaryTextColor,
|
color: AFThemeExtension.of(context).secondaryTextColor,
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
@ -598,19 +638,28 @@ class _AddOnBox extends StatelessWidget {
|
|||||||
const Spacer(),
|
const Spacer(),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
FlowyTextButton(
|
Expanded(
|
||||||
buttonText,
|
child: FlowyTextButton(
|
||||||
padding:
|
buttonText,
|
||||||
const EdgeInsets.symmetric(horizontal: 16, vertical: 7),
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
fillColor: Colors.transparent,
|
padding:
|
||||||
constraints: const BoxConstraints(minWidth: 115),
|
const EdgeInsets.symmetric(horizontal: 16, vertical: 7),
|
||||||
radius: Corners.s16Border,
|
fillColor:
|
||||||
hoverColor: const Color(0xFF5C3699),
|
isActive ? const Color(0xFFE8E2EE) : Colors.transparent,
|
||||||
fontColor: const Color(0xFF5C3699),
|
constraints: const BoxConstraints(minWidth: 115),
|
||||||
fontHoverColor: Colors.white,
|
radius: Corners.s16Border,
|
||||||
borderColor: const Color(0xFF5C3699),
|
hoverColor: isActive
|
||||||
fontSize: 12,
|
? const Color(0xFFE8E2EE)
|
||||||
onPressed: () {},
|
: const Color(0xFF5C3699),
|
||||||
|
fontColor: const Color(0xFF5C3699),
|
||||||
|
fontHoverColor:
|
||||||
|
isActive ? const Color(0xFF5C3699) : Colors.white,
|
||||||
|
borderColor: isActive
|
||||||
|
? const Color(0xFFE8E2EE)
|
||||||
|
: const Color(0xFF5C3699),
|
||||||
|
fontSize: 12,
|
||||||
|
onPressed: isActive ? null : () {},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -213,7 +213,7 @@ class FlowyTextButton extends StatelessWidget {
|
|||||||
child = ConstrainedBox(
|
child = ConstrainedBox(
|
||||||
constraints: constraints,
|
constraints: constraints,
|
||||||
child: TextButton(
|
child: TextButton(
|
||||||
onPressed: onPressed ?? () {},
|
onPressed: onPressed,
|
||||||
focusNode: FocusNode(skipTraversal: onPressed == null),
|
focusNode: FocusNode(skipTraversal: onPressed == null),
|
||||||
style: ButtonStyle(
|
style: ButtonStyle(
|
||||||
overlayColor: const WidgetStatePropertyAll(Colors.transparent),
|
overlayColor: const WidgetStatePropertyAll(Colors.transparent),
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M7.31665 11.5668L12.0166 6.86683L11.0833 5.9335L7.31665 9.70016L5.41665 7.80016L4.48331 8.7335L7.31665 11.5668ZM8.24998 15.1668C7.32776 15.1668 6.46109 14.9918 5.64998 14.6418C4.83887 14.2918 4.13331 13.8168 3.53331 13.2168C2.93331 12.6168 2.45831 11.9113 2.10831 11.1002C1.75831 10.2891 1.58331 9.42238 1.58331 8.50016C1.58331 7.57794 1.75831 6.71127 2.10831 5.90016C2.45831 5.08905 2.93331 4.3835 3.53331 3.7835C4.13331 3.1835 4.83887 2.7085 5.64998 2.3585C6.46109 2.0085 7.32776 1.8335 8.24998 1.8335C9.1722 1.8335 10.0389 2.0085 10.85 2.3585C11.6611 2.7085 12.3666 3.1835 12.9666 3.7835C13.5666 4.3835 14.0416 5.08905 14.3916 5.90016C14.7416 6.71127 14.9166 7.57794 14.9166 8.50016C14.9166 9.42238 14.7416 10.2891 14.3916 11.1002C14.0416 11.9113 13.5666 12.6168 12.9666 13.2168C12.3666 13.8168 11.6611 14.2918 10.85 14.6418C10.0389 14.9918 9.1722 15.1668 8.24998 15.1668ZM8.24998 13.8335C9.73887 13.8335 11 13.3168 12.0333 12.2835C13.0666 11.2502 13.5833 9.98905 13.5833 8.50016C13.5833 7.01127 13.0666 5.75016 12.0333 4.71683C11 3.6835 9.73887 3.16683 8.24998 3.16683C6.76109 3.16683 5.49998 3.6835 4.46665 4.71683C3.43331 5.75016 2.91665 7.01127 2.91665 8.50016C2.91665 9.98905 3.43331 11.2502 4.46665 12.2835C5.49998 13.3168 6.76109 13.8335 8.24998 13.8335Z" fill="#653E8C"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
@ -666,6 +666,25 @@
|
|||||||
"upgrade": "Change 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 {}."
|
||||||
},
|
},
|
||||||
|
"addons": {
|
||||||
|
"title": "Add-ons",
|
||||||
|
"addLabel": "Add",
|
||||||
|
"activeLabel": "Added",
|
||||||
|
"aiMax": {
|
||||||
|
"title": "AI Max",
|
||||||
|
"description": "Unlock unlimited AI",
|
||||||
|
"price": "US{}",
|
||||||
|
"priceInfo": "/user per month",
|
||||||
|
"billingInfo": "billed annually or US{} biled monthly"
|
||||||
|
},
|
||||||
|
"aiOnDevice": {
|
||||||
|
"title": "AI On-device",
|
||||||
|
"description": "AI offline on your device",
|
||||||
|
"price": "US{}",
|
||||||
|
"priceInfo": "/user per month",
|
||||||
|
"billingInfo": "billed annually or US{} biled monthly"
|
||||||
|
}
|
||||||
|
},
|
||||||
"deal": {
|
"deal": {
|
||||||
"bannerLabel": "New year deal!",
|
"bannerLabel": "New year deal!",
|
||||||
"title": "Grow your team!",
|
"title": "Grow your team!",
|
||||||
@ -692,15 +711,16 @@
|
|||||||
},
|
},
|
||||||
"addons": {
|
"addons": {
|
||||||
"title": "Add-ons",
|
"title": "Add-ons",
|
||||||
|
"addLabel": "Add",
|
||||||
|
"removeLabel": "Remove",
|
||||||
|
"renewLabel": "Renew",
|
||||||
"aiMax": {
|
"aiMax": {
|
||||||
"label": "AI Max",
|
"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"
|
||||||
"buttonLabel": "Add AI Max"
|
|
||||||
},
|
},
|
||||||
"aiOnDevice": {
|
"aiOnDevice": {
|
||||||
"label": "AI On-device",
|
"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"
|
||||||
"buttonLabel": "Add AI On-device"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -2033,7 +2053,6 @@
|
|||||||
"upgrade": "Update",
|
"upgrade": "Update",
|
||||||
"upgradeYourSpace": "Create multiple Spaces",
|
"upgradeYourSpace": "Create multiple Spaces",
|
||||||
"quicklySwitch": "Quickly switch to the next space",
|
"quicklySwitch": "Quickly switch to the next space",
|
||||||
|
|
||||||
"duplicate": "Duplicate Space",
|
"duplicate": "Duplicate Space",
|
||||||
"movePageToSpace": "Move page to space",
|
"movePageToSpace": "Move page to space",
|
||||||
"switchSpace": "Switch space"
|
"switchSpace": "Switch space"
|
||||||
|
@ -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, RepeatedWorkspaceSubscriptionPB,
|
OauthSignInPB, RenameWorkspacePB, RepeatedUserWorkspacePB, SignInUrlPB, SignInUrlPayloadPB,
|
||||||
SignInUrlPB, SignInUrlPayloadPB, SignUpPayloadPB, UpdateCloudConfigPB,
|
SignUpPayloadPB, UpdateCloudConfigPB, UpdateUserProfilePayloadPB, UserProfilePB,
|
||||||
UpdateUserProfilePayloadPB, UserProfilePB, UserWorkspaceIdPB, UserWorkspacePB,
|
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,14 +315,6 @@ 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,18 +240,3 @@ 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);
|
|
||||||
}
|
|
||||||
|
@ -2,10 +2,9 @@
|
|||||||
CREATE TABLE workspace_subscriptions_table (
|
CREATE TABLE workspace_subscriptions_table (
|
||||||
workspace_id TEXT NOT NULL,
|
workspace_id TEXT NOT NULL,
|
||||||
subscription_plan INTEGER NOT NULL,
|
subscription_plan INTEGER NOT NULL,
|
||||||
recurring_interval INTEGER NOT NULL,
|
workspace_status INTEGER NOT NULL,
|
||||||
is_active BOOLEAN NOT NULL,
|
end_date TIMESTAMP,
|
||||||
has_canceled BOOLEAN NOT NULL DEFAULT FALSE,
|
addons TEXT NOT NULL,
|
||||||
canceled_at TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
PRIMARY KEY (workspace_id)
|
PRIMARY KEY (workspace_id)
|
||||||
);
|
);
|
@ -118,10 +118,9 @@ diesel::table! {
|
|||||||
workspace_subscriptions_table (workspace_id) {
|
workspace_subscriptions_table (workspace_id) {
|
||||||
workspace_id -> Text,
|
workspace_id -> Text,
|
||||||
subscription_plan -> BigInt,
|
subscription_plan -> BigInt,
|
||||||
recurring_interval -> BigInt,
|
workspace_status -> BigInt,
|
||||||
is_active -> Bool,
|
end_date -> BigInt,
|
||||||
has_canceled -> Bool,
|
addons -> Text,
|
||||||
canceled_at -> Nullable<BigInt>,
|
|
||||||
updated_at -> Timestamp,
|
updated_at -> Timestamp,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
|
use chrono::Utc;
|
||||||
use client_api::entity::billing_dto::{
|
use client_api::entity::billing_dto::{
|
||||||
RecurringInterval, SubscriptionPlan, SubscriptionStatus, WorkspaceSubscriptionStatus,
|
RecurringInterval, SubscriptionPlan, SubscriptionStatus, WorkspaceSubscriptionStatus,
|
||||||
};
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use validator::Validate;
|
use validator::Validate;
|
||||||
|
|
||||||
@ -9,6 +11,8 @@ use flowy_user_pub::cloud::{AFWorkspaceSettings, AFWorkspaceSettingsChange};
|
|||||||
use flowy_user_pub::entities::{Role, WorkspaceInvitation, WorkspaceMember, WorkspaceSubscription};
|
use flowy_user_pub::entities::{Role, WorkspaceInvitation, WorkspaceMember, WorkspaceSubscription};
|
||||||
use lib_infra::validator_fn::required_not_empty_str;
|
use lib_infra::validator_fn::required_not_empty_str;
|
||||||
|
|
||||||
|
use crate::services::sqlite_sql::workspace_sql::WorkspaceSubscriptionsTable;
|
||||||
|
|
||||||
#[derive(ProtoBuf, Default, Clone)]
|
#[derive(ProtoBuf, Default, Clone)]
|
||||||
pub struct WorkspaceMemberPB {
|
pub struct WorkspaceMemberPB {
|
||||||
#[pb(index = 1)]
|
#[pb(index = 1)]
|
||||||
@ -265,17 +269,28 @@ impl From<RecurringInterval> for RecurringIntervalPB {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(ProtoBuf_Enum, Clone, Default, Debug)]
|
#[derive(ProtoBuf_Enum, Clone, Default, Debug, Serialize, Deserialize)]
|
||||||
pub enum SubscriptionPlanPB {
|
pub enum SubscriptionPlanPB {
|
||||||
#[default]
|
#[default]
|
||||||
None = 0,
|
None = 0,
|
||||||
Pro = 1,
|
Pro = 1,
|
||||||
Team = 2,
|
Team = 2,
|
||||||
|
|
||||||
|
// Add-ons
|
||||||
AiMax = 3,
|
AiMax = 3,
|
||||||
AiLocal = 4,
|
AiLocal = 4,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<WorkspacePlanPB> for SubscriptionPlanPB {
|
||||||
|
fn from(value: WorkspacePlanPB) -> Self {
|
||||||
|
match value {
|
||||||
|
WorkspacePlanPB::FreePlan => SubscriptionPlanPB::None,
|
||||||
|
WorkspacePlanPB::ProPlan => SubscriptionPlanPB::Pro,
|
||||||
|
WorkspacePlanPB::TeamPlan => SubscriptionPlanPB::Team,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<SubscriptionPlanPB> for SubscriptionPlan {
|
impl From<SubscriptionPlanPB> for SubscriptionPlan {
|
||||||
fn from(value: SubscriptionPlanPB) -> Self {
|
fn from(value: SubscriptionPlanPB) -> Self {
|
||||||
match value {
|
match value {
|
||||||
@ -467,30 +482,55 @@ pub struct WorkspaceSubscriptionInfoPB {
|
|||||||
pub add_ons: Vec<WorkspaceAddOnPB>,
|
pub add_ons: Vec<WorkspaceAddOnPB>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl WorkspaceSubscriptionInfoPB {
|
||||||
|
pub fn default_from_workspace_id(workspace_id: String) -> Self {
|
||||||
|
Self {
|
||||||
|
plan: WorkspacePlanPB::FreePlan,
|
||||||
|
plan_subscription: WorkspaceSubscriptionV2PB {
|
||||||
|
workspace_id,
|
||||||
|
subscription_plan: SubscriptionPlanPB::None,
|
||||||
|
status: WorkspaceSubscriptionStatusPB::Active,
|
||||||
|
end_date: 0,
|
||||||
|
},
|
||||||
|
add_ons: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<Vec<WorkspaceSubscriptionStatus>> for WorkspaceSubscriptionInfoPB {
|
impl From<Vec<WorkspaceSubscriptionStatus>> for WorkspaceSubscriptionInfoPB {
|
||||||
fn from(subs: Vec<WorkspaceSubscriptionStatus>) -> Self {
|
fn from(subs: Vec<WorkspaceSubscriptionStatus>) -> Self {
|
||||||
let mut plan = WorkspacePlanPB::WorkspacePlanFree;
|
let mut plan = WorkspacePlanPB::FreePlan;
|
||||||
let mut plan_subscription = WorkspaceSubscriptionV2PB::default();
|
let mut plan_subscription = WorkspaceSubscriptionV2PB::default();
|
||||||
let mut add_ons = Vec::new();
|
let mut add_ons = Vec::new();
|
||||||
for sub in subs {
|
for sub in subs {
|
||||||
match sub.workspace_plan {
|
match sub.workspace_plan {
|
||||||
SubscriptionPlan::Free => {
|
SubscriptionPlan::Free => {
|
||||||
plan = WorkspacePlanPB::WorkspacePlanFree;
|
plan = WorkspacePlanPB::FreePlan;
|
||||||
},
|
},
|
||||||
SubscriptionPlan::Pro => {
|
SubscriptionPlan::Pro => {
|
||||||
plan = WorkspacePlanPB::WorkspacePlanPro;
|
plan = WorkspacePlanPB::ProPlan;
|
||||||
plan_subscription = sub.into();
|
plan_subscription = sub.into();
|
||||||
},
|
},
|
||||||
SubscriptionPlan::Team => {
|
SubscriptionPlan::Team => {
|
||||||
plan = WorkspacePlanPB::WorkspacePlanTeam;
|
plan = WorkspacePlanPB::TeamPlan;
|
||||||
},
|
},
|
||||||
SubscriptionPlan::AiMax => {
|
SubscriptionPlan::AiMax => {
|
||||||
|
if plan_subscription.workspace_id.is_empty() {
|
||||||
|
plan_subscription =
|
||||||
|
WorkspaceSubscriptionV2PB::default_with_workspace_id(sub.workspace_id.clone());
|
||||||
|
}
|
||||||
|
|
||||||
add_ons.push(WorkspaceAddOnPB {
|
add_ons.push(WorkspaceAddOnPB {
|
||||||
type_: WorkspaceAddOnPBType::AddOnAiMax,
|
type_: WorkspaceAddOnPBType::AddOnAiMax,
|
||||||
add_on_subscription: sub.into(),
|
add_on_subscription: sub.into(),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
SubscriptionPlan::AiLocal => {
|
SubscriptionPlan::AiLocal => {
|
||||||
|
if plan_subscription.workspace_id.is_empty() {
|
||||||
|
plan_subscription =
|
||||||
|
WorkspaceSubscriptionV2PB::default_with_workspace_id(sub.workspace_id.clone());
|
||||||
|
}
|
||||||
|
|
||||||
add_ons.push(WorkspaceAddOnPB {
|
add_ons.push(WorkspaceAddOnPB {
|
||||||
type_: WorkspaceAddOnPBType::AddOnAiLocal,
|
type_: WorkspaceAddOnPBType::AddOnAiLocal,
|
||||||
add_on_subscription: sub.into(),
|
add_on_subscription: sub.into(),
|
||||||
@ -498,6 +538,7 @@ impl From<Vec<WorkspaceSubscriptionStatus>> for WorkspaceSubscriptionInfoPB {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
WorkspaceSubscriptionInfoPB {
|
WorkspaceSubscriptionInfoPB {
|
||||||
plan,
|
plan,
|
||||||
plan_subscription,
|
plan_subscription,
|
||||||
@ -506,15 +547,45 @@ impl From<Vec<WorkspaceSubscriptionStatus>> for WorkspaceSubscriptionInfoPB {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<WorkspaceSubscriptionInfoPB> for WorkspaceSubscriptionsTable {
|
||||||
|
fn from(value: WorkspaceSubscriptionInfoPB) -> Self {
|
||||||
|
WorkspaceSubscriptionsTable {
|
||||||
|
workspace_id: value.plan_subscription.workspace_id,
|
||||||
|
subscription_plan: value.plan.into(),
|
||||||
|
workspace_status: value.plan_subscription.status.into(),
|
||||||
|
end_date: value.plan_subscription.end_date,
|
||||||
|
addons: serde_json::to_string(&value.add_ons).unwrap_or_default(),
|
||||||
|
updated_at: Utc::now().naive_utc(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(ProtoBuf_Enum, Debug, Clone, Eq, PartialEq, Default)]
|
#[derive(ProtoBuf_Enum, Debug, Clone, Eq, PartialEq, Default)]
|
||||||
pub enum WorkspacePlanPB {
|
pub enum WorkspacePlanPB {
|
||||||
#[default]
|
#[default]
|
||||||
WorkspacePlanFree = 0,
|
FreePlan = 0,
|
||||||
WorkspacePlanPro = 1,
|
ProPlan = 1,
|
||||||
WorkspacePlanTeam = 2,
|
TeamPlan = 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, ProtoBuf, Default, Clone)]
|
impl Into<i64> for WorkspacePlanPB {
|
||||||
|
fn into(self) -> i64 {
|
||||||
|
self as i64
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<i64> for WorkspacePlanPB {
|
||||||
|
fn from(value: i64) -> Self {
|
||||||
|
match value {
|
||||||
|
0 => WorkspacePlanPB::FreePlan,
|
||||||
|
1 => WorkspacePlanPB::ProPlan,
|
||||||
|
2 => WorkspacePlanPB::TeamPlan,
|
||||||
|
_ => WorkspacePlanPB::FreePlan,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, ProtoBuf, Default, Clone, Serialize, Deserialize)]
|
||||||
pub struct WorkspaceAddOnPB {
|
pub struct WorkspaceAddOnPB {
|
||||||
#[pb(index = 1)]
|
#[pb(index = 1)]
|
||||||
type_: WorkspaceAddOnPBType,
|
type_: WorkspaceAddOnPBType,
|
||||||
@ -522,14 +593,14 @@ pub struct WorkspaceAddOnPB {
|
|||||||
add_on_subscription: WorkspaceSubscriptionV2PB,
|
add_on_subscription: WorkspaceSubscriptionV2PB,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(ProtoBuf_Enum, Debug, Clone, Eq, PartialEq, Default)]
|
#[derive(ProtoBuf_Enum, Debug, Clone, Eq, PartialEq, Default, Serialize, Deserialize)]
|
||||||
pub enum WorkspaceAddOnPBType {
|
pub enum WorkspaceAddOnPBType {
|
||||||
#[default]
|
#[default]
|
||||||
AddOnAiLocal = 0,
|
AddOnAiLocal = 0,
|
||||||
AddOnAiMax = 1,
|
AddOnAiMax = 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, ProtoBuf, Default, Clone)]
|
#[derive(Debug, ProtoBuf, Default, Clone, Serialize, Deserialize)]
|
||||||
pub struct WorkspaceSubscriptionV2PB {
|
pub struct WorkspaceSubscriptionV2PB {
|
||||||
#[pb(index = 1)]
|
#[pb(index = 1)]
|
||||||
pub workspace_id: String,
|
pub workspace_id: String,
|
||||||
@ -544,6 +615,17 @@ pub struct WorkspaceSubscriptionV2PB {
|
|||||||
pub end_date: i64,
|
pub end_date: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl WorkspaceSubscriptionV2PB {
|
||||||
|
pub fn default_with_workspace_id(workspace_id: String) -> Self {
|
||||||
|
Self {
|
||||||
|
workspace_id,
|
||||||
|
subscription_plan: SubscriptionPlanPB::None,
|
||||||
|
status: WorkspaceSubscriptionStatusPB::Active,
|
||||||
|
end_date: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<WorkspaceSubscriptionStatus> for WorkspaceSubscriptionV2PB {
|
impl From<WorkspaceSubscriptionStatus> for WorkspaceSubscriptionV2PB {
|
||||||
fn from(sub: WorkspaceSubscriptionStatus) -> Self {
|
fn from(sub: WorkspaceSubscriptionStatus) -> Self {
|
||||||
Self {
|
Self {
|
||||||
@ -555,13 +637,28 @@ impl From<WorkspaceSubscriptionStatus> for WorkspaceSubscriptionV2PB {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(ProtoBuf_Enum, Debug, Clone, Eq, PartialEq, Default)]
|
#[derive(ProtoBuf_Enum, Debug, Clone, Eq, PartialEq, Default, Serialize, Deserialize)]
|
||||||
pub enum WorkspaceSubscriptionStatusPB {
|
pub enum WorkspaceSubscriptionStatusPB {
|
||||||
#[default]
|
#[default]
|
||||||
Active = 0,
|
Active = 0,
|
||||||
Canceled = 1,
|
Canceled = 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Into<i64> for WorkspaceSubscriptionStatusPB {
|
||||||
|
fn into(self) -> i64 {
|
||||||
|
self as i64
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<i64> for WorkspaceSubscriptionStatusPB {
|
||||||
|
fn from(value: i64) -> Self {
|
||||||
|
match value {
|
||||||
|
0 => WorkspaceSubscriptionStatusPB::Active,
|
||||||
|
_ => WorkspaceSubscriptionStatusPB::Canceled,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<SubscriptionStatus> for WorkspaceSubscriptionStatusPB {
|
impl From<SubscriptionStatus> for WorkspaceSubscriptionStatusPB {
|
||||||
fn from(status: SubscriptionStatus) -> Self {
|
fn from(status: SubscriptionStatus) -> Self {
|
||||||
match status {
|
match status {
|
||||||
|
@ -773,20 +773,6 @@ pub async fn subscribe_workspace_handler(
|
|||||||
data_result_ok(PaymentLinkPB { payment_link })
|
data_result_ok(PaymentLinkPB { payment_link })
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(level = "debug", skip_all, err)]
|
|
||||||
pub async fn get_workspace_subscriptions_handler(
|
|
||||||
manager: AFPluginState<Weak<UserManager>>,
|
|
||||||
) -> DataResult<RepeatedWorkspaceSubscriptionPB, FlowyError> {
|
|
||||||
let manager = upgrade_manager(manager)?;
|
|
||||||
let subs = manager
|
|
||||||
.get_workspace_subscriptions()
|
|
||||||
.await?
|
|
||||||
.into_iter()
|
|
||||||
.map(WorkspaceSubscriptionPB::from)
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
data_result_ok(RepeatedWorkspaceSubscriptionPB { items: subs })
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tracing::instrument(level = "debug", skip_all, err)]
|
#[tracing::instrument(level = "debug", skip_all, err)]
|
||||||
pub async fn get_workspace_subscription_info_handler(
|
pub async fn get_workspace_subscription_info_handler(
|
||||||
params: AFPluginData<UserWorkspaceIdPB>,
|
params: AFPluginData<UserWorkspaceIdPB>,
|
||||||
@ -843,6 +829,18 @@ pub async fn get_billing_portal_handler(
|
|||||||
data_result_ok(BillingPortalPB { url })
|
data_result_ok(BillingPortalPB { url })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(level = "debug", skip_all, err)]
|
||||||
|
pub async fn invalidate_workspace_subscription_info_cache_handler(
|
||||||
|
params: AFPluginData<UserWorkspaceIdPB>,
|
||||||
|
manager: AFPluginState<Weak<UserManager>>,
|
||||||
|
) -> FlowyResult<()> {
|
||||||
|
let params = params.try_into_inner().unwrap();
|
||||||
|
let manager = upgrade_manager(manager).unwrap();
|
||||||
|
manager
|
||||||
|
.invalidate_workspace_subscription_info_cache(params.workspace_id)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
#[tracing::instrument(level = "debug", skip_all, err)]
|
#[tracing::instrument(level = "debug", skip_all, err)]
|
||||||
pub async fn get_workspace_member_info(
|
pub async fn get_workspace_member_info(
|
||||||
param: AFPluginData<WorkspaceMemberIdPB>,
|
param: AFPluginData<WorkspaceMemberIdPB>,
|
||||||
|
@ -70,15 +70,14 @@ pub fn init(user_manager: Weak<UserManager>) -> AFPlugin {
|
|||||||
.event(UserEvent::AcceptWorkspaceInvitation, accept_workspace_invitations_handler)
|
.event(UserEvent::AcceptWorkspaceInvitation, accept_workspace_invitations_handler)
|
||||||
// Billing
|
// Billing
|
||||||
.event(UserEvent::SubscribeWorkspace, subscribe_workspace_handler)
|
.event(UserEvent::SubscribeWorkspace, subscribe_workspace_handler)
|
||||||
.event(UserEvent::GetWorkspaceSubscriptions, get_workspace_subscriptions_handler)
|
.event(UserEvent::GetWorkspaceSubscriptionInfo, get_workspace_subscription_info_handler)
|
||||||
.event(UserEvent::GetWorkspaceSubscriptionInfo, get_workspace_subscription_info_handler)
|
|
||||||
.event(UserEvent::CancelWorkspaceSubscription, cancel_workspace_subscription_handler)
|
.event(UserEvent::CancelWorkspaceSubscription, cancel_workspace_subscription_handler)
|
||||||
.event(UserEvent::GetWorkspaceUsage, get_workspace_usage_handler)
|
.event(UserEvent::GetWorkspaceUsage, get_workspace_usage_handler)
|
||||||
.event(UserEvent::GetBillingPortal, get_billing_portal_handler)
|
.event(UserEvent::GetBillingPortal, get_billing_portal_handler)
|
||||||
|
.event(UserEvent::InvalidateWorkspaceSubscriptionInfoCache, invalidate_workspace_subscription_info_cache_handler)
|
||||||
// Workspace Setting
|
// Workspace Setting
|
||||||
.event(UserEvent::UpdateWorkspaceSetting, update_workspace_setting)
|
.event(UserEvent::UpdateWorkspaceSetting, update_workspace_setting)
|
||||||
.event(UserEvent::GetWorkspaceSetting, get_workspace_setting)
|
.event(UserEvent::GetWorkspaceSetting, get_workspace_setting)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)]
|
#[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)]
|
||||||
@ -243,10 +242,7 @@ pub enum UserEvent {
|
|||||||
#[event(input = "SubscribeWorkspacePB", output = "PaymentLinkPB")]
|
#[event(input = "SubscribeWorkspacePB", output = "PaymentLinkPB")]
|
||||||
SubscribeWorkspace = 51,
|
SubscribeWorkspace = 51,
|
||||||
|
|
||||||
#[event(output = "RepeatedWorkspaceSubscriptionPB")]
|
#[event(input = "CancelWorkspaceSubscriptionPB")]
|
||||||
GetWorkspaceSubscriptions = 52,
|
|
||||||
|
|
||||||
#[event(input = "UserWorkspaceIdPB")]
|
|
||||||
CancelWorkspaceSubscription = 53,
|
CancelWorkspaceSubscription = 53,
|
||||||
|
|
||||||
#[event(input = "UserWorkspaceIdPB", output = "WorkspaceUsagePB")]
|
#[event(input = "UserWorkspaceIdPB", output = "WorkspaceUsagePB")]
|
||||||
@ -266,6 +262,9 @@ pub enum UserEvent {
|
|||||||
|
|
||||||
#[event(input = "UserWorkspaceIdPB", output = "WorkspaceSubscriptionInfoPB")]
|
#[event(input = "UserWorkspaceIdPB", output = "WorkspaceSubscriptionInfoPB")]
|
||||||
GetWorkspaceSubscriptionInfo = 59,
|
GetWorkspaceSubscriptionInfo = 59,
|
||||||
|
|
||||||
|
#[event(input = "UserWorkspaceIdPB")]
|
||||||
|
InvalidateWorkspaceSubscriptionInfoCache = 60,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait UserStatusCallback: Send + Sync + 'static {
|
pub trait UserStatusCallback: Send + Sync + 'static {
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
use chrono::{TimeZone, Utc};
|
use chrono::{TimeZone, Utc};
|
||||||
use client_api::entity::billing_dto::{RecurringInterval, SubscriptionPlan};
|
use diesel::{delete, insert_into};
|
||||||
use diesel::insert_into;
|
|
||||||
use diesel::{RunQueryDsl, SqliteConnection};
|
use diesel::{RunQueryDsl, SqliteConnection};
|
||||||
use flowy_error::{FlowyError, FlowyResult};
|
use flowy_error::{FlowyError, FlowyResult};
|
||||||
use flowy_sqlite::schema::user_workspace_table;
|
use flowy_sqlite::schema::user_workspace_table;
|
||||||
@ -8,9 +7,13 @@ use flowy_sqlite::schema::workspace_subscriptions_table;
|
|||||||
use flowy_sqlite::schema::workspace_subscriptions_table::dsl;
|
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, WorkspaceSubscription};
|
use flowy_user_pub::entities::UserWorkspace;
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
use crate::entities::{
|
||||||
|
SubscriptionPlanPB, WorkspacePlanPB, WorkspaceSubscriptionInfoPB, WorkspaceSubscriptionV2PB,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Clone, Default, Queryable, Identifiable, Insertable)]
|
#[derive(Clone, Default, Queryable, Identifiable, Insertable)]
|
||||||
#[diesel(table_name = user_workspace_table)]
|
#[diesel(table_name = user_workspace_table)]
|
||||||
pub struct UserWorkspaceTable {
|
pub struct UserWorkspaceTable {
|
||||||
@ -28,10 +31,9 @@ pub struct UserWorkspaceTable {
|
|||||||
pub struct WorkspaceSubscriptionsTable {
|
pub struct WorkspaceSubscriptionsTable {
|
||||||
pub workspace_id: String,
|
pub workspace_id: String,
|
||||||
pub subscription_plan: i64,
|
pub subscription_plan: i64,
|
||||||
pub recurring_interval: i64,
|
pub workspace_status: i64,
|
||||||
pub is_active: bool,
|
pub end_date: i64,
|
||||||
pub has_canceled: bool,
|
pub addons: String,
|
||||||
pub canceled_at: Option<i64>,
|
|
||||||
pub updated_at: chrono::NaiveDateTime,
|
pub updated_at: chrono::NaiveDateTime,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,17 +120,33 @@ pub fn upsert_workspace_subscription<T: Into<WorkspaceSubscriptionsTable>>(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<WorkspaceSubscriptionsTable> for WorkspaceSubscription {
|
pub fn delete_workspace_subscription_from_cache(
|
||||||
type Error = FlowyError;
|
mut conn: DBConnection,
|
||||||
fn try_from(value: WorkspaceSubscriptionsTable) -> Result<Self, Self::Error> {
|
workspace_id: &str,
|
||||||
Ok(Self {
|
) -> FlowyResult<()> {
|
||||||
workspace_id: value.workspace_id,
|
let delete = delete(
|
||||||
subscription_plan: SubscriptionPlan::try_from(value.subscription_plan as i16)?,
|
dsl::workspace_subscriptions_table
|
||||||
recurring_interval: RecurringInterval::try_from(value.recurring_interval as i16)?,
|
.filter(workspace_subscriptions_table::workspace_id.eq(workspace_id)),
|
||||||
is_active: value.is_active,
|
);
|
||||||
has_canceled: value.has_canceled,
|
|
||||||
canceled_at: value.canceled_at,
|
delete.execute(&mut conn)?;
|
||||||
})
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Into<WorkspaceSubscriptionInfoPB> for WorkspaceSubscriptionsTable {
|
||||||
|
fn into(self) -> WorkspaceSubscriptionInfoPB {
|
||||||
|
WorkspaceSubscriptionInfoPB {
|
||||||
|
plan: self.subscription_plan.into(),
|
||||||
|
plan_subscription: WorkspaceSubscriptionV2PB {
|
||||||
|
workspace_id: self.workspace_id,
|
||||||
|
subscription_plan: SubscriptionPlanPB::from(WorkspacePlanPB::from(self.subscription_plan)),
|
||||||
|
status: self.workspace_status.into(),
|
||||||
|
end_date: self.end_date,
|
||||||
|
},
|
||||||
|
// Deserialize
|
||||||
|
add_ons: serde_json::from_str(&self.addons).unwrap_or_default(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
use chrono::{Duration, NaiveDateTime, Utc};
|
use chrono::{Duration, NaiveDateTime, Utc};
|
||||||
use client_api::entity::billing_dto::{
|
use client_api::entity::billing_dto::{SubscriptionPlan, WorkspaceUsageAndLimit};
|
||||||
RecurringInterval, SubscriptionPlan, WorkspaceSubscriptionStatus, WorkspaceUsageAndLimit,
|
|
||||||
};
|
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
@ -15,13 +14,13 @@ 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,
|
Role, UpdateUserProfileParams, UserWorkspace, WorkspaceInvitation, WorkspaceInvitationStatus,
|
||||||
WorkspaceMember, WorkspaceSubscription,
|
WorkspaceMember,
|
||||||
};
|
};
|
||||||
use lib_dispatch::prelude::af_spawn;
|
use lib_dispatch::prelude::af_spawn;
|
||||||
|
|
||||||
use crate::entities::{
|
use crate::entities::{
|
||||||
RepeatedUserWorkspacePB, ResetWorkspacePB, SubscribeWorkspacePB, UpdateUserWorkspaceSettingPB,
|
RepeatedUserWorkspacePB, ResetWorkspacePB, SubscribeWorkspacePB, UpdateUserWorkspaceSettingPB,
|
||||||
UseAISettingPB, UserWorkspacePB,
|
UseAISettingPB, UserWorkspacePB, WorkspaceSubscriptionInfoPB,
|
||||||
};
|
};
|
||||||
use crate::migrations::AnonUser;
|
use crate::migrations::AnonUser;
|
||||||
use crate::notification::{send_notification, UserNotification};
|
use crate::notification::{send_notification, UserNotification};
|
||||||
@ -33,9 +32,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,
|
delete_workspace_subscription_from_cache, get_all_user_workspace_op, get_user_workspace_op,
|
||||||
select_workspace_subscription, upsert_workspace_subscription, UserWorkspaceTable,
|
insert_new_workspaces_op, select_workspace_subscription, upsert_workspace_subscription,
|
||||||
WorkspaceSubscriptionsTable,
|
UserWorkspaceTable,
|
||||||
};
|
};
|
||||||
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;
|
||||||
@ -450,75 +449,54 @@ impl UserManager {
|
|||||||
Ok(payment_link)
|
Ok(payment_link)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(level = "info", skip(self), err)]
|
|
||||||
pub async fn get_workspace_subscriptions(&self) -> FlowyResult<Vec<WorkspaceSubscription>> {
|
|
||||||
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_plan: SubscriptionPlan::try_from(subscription.subscription_plan as i16)?,
|
|
||||||
recurring_interval: RecurringInterval::try_from(subscription.recurring_interval as i16)?,
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[instrument(level = "info", skip(self), err)]
|
#[instrument(level = "info", skip(self), err)]
|
||||||
pub async fn get_workspace_subscription_info(
|
pub async fn get_workspace_subscription_info(
|
||||||
&self,
|
&self,
|
||||||
workspace_id: String,
|
workspace_id: String,
|
||||||
) -> FlowyResult<Vec<WorkspaceSubscriptionStatus>> {
|
) -> FlowyResult<WorkspaceSubscriptionInfoPB> {
|
||||||
self
|
let session = self.get_session()?;
|
||||||
.cloud_services
|
let uid = session.user_id;
|
||||||
.get_user_service()?
|
let db = self.authenticate_user.get_sqlite_connection(uid)?;
|
||||||
.get_workspace_subscriptions()
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_workspace_subscriptions_from_remote(
|
if let Ok(subscription) = select_workspace_subscription(db, &workspace_id) {
|
||||||
&self,
|
if is_older_than_n_minutes(subscription.updated_at, 10) {
|
||||||
uid: i64,
|
self
|
||||||
) -> FlowyResult<Vec<WorkspaceSubscription>> {
|
.get_workspace_subscription_info_from_remote(uid, workspace_id)
|
||||||
let subscriptions: Vec<WorkspaceSubscription> = self
|
.await?;
|
||||||
.cloud_services
|
}
|
||||||
.get_user_service()?
|
|
||||||
.get_workspace_subscriptions()
|
|
||||||
.await?
|
|
||||||
.into_iter()
|
|
||||||
.map(WorkspaceSubscription::from)
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
for subscription in &subscriptions {
|
return Ok(subscription.into());
|
||||||
let db = self.authenticate_user.get_sqlite_connection(uid)?;
|
|
||||||
let record = WorkspaceSubscriptionsTable {
|
|
||||||
workspace_id: subscription.workspace_id.clone().into(),
|
|
||||||
subscription_plan: subscription.subscription_plan.clone() as i64,
|
|
||||||
recurring_interval: subscription.recurring_interval.clone() as i64,
|
|
||||||
is_active: subscription.canceled_at.is_none(),
|
|
||||||
has_canceled: subscription.canceled_at.is_some(),
|
|
||||||
canceled_at: subscription.canceled_at.into(),
|
|
||||||
updated_at: Utc::now().naive_utc(),
|
|
||||||
};
|
|
||||||
|
|
||||||
upsert_workspace_subscription(db, record)?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(subscriptions)
|
let info = self
|
||||||
|
.get_workspace_subscription_info_from_remote(uid, workspace_id)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(info)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_workspace_subscription_info_from_remote(
|
||||||
|
&self,
|
||||||
|
uid: i64,
|
||||||
|
workspace_id: String,
|
||||||
|
) -> FlowyResult<WorkspaceSubscriptionInfoPB> {
|
||||||
|
let subscriptions = self
|
||||||
|
.cloud_services
|
||||||
|
.get_user_service()?
|
||||||
|
.get_workspace_subscription_one(workspace_id.clone())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let info = WorkspaceSubscriptionInfoPB::from(subscriptions);
|
||||||
|
let record = if info.plan_subscription.workspace_id.is_empty() {
|
||||||
|
WorkspaceSubscriptionInfoPB::default_from_workspace_id(workspace_id)
|
||||||
|
} else {
|
||||||
|
info.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
let db = self.authenticate_user.get_sqlite_connection(uid)?;
|
||||||
|
upsert_workspace_subscription(db, record)?;
|
||||||
|
|
||||||
|
Ok(info)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(level = "info", skip(self), err)]
|
#[instrument(level = "info", skip(self), err)]
|
||||||
@ -535,6 +513,16 @@ impl UserManager {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "info", skip(self), err)]
|
||||||
|
pub async fn invalidate_workspace_subscription_info_cache(
|
||||||
|
&self,
|
||||||
|
workspace_id: String,
|
||||||
|
) -> FlowyResult<()> {
|
||||||
|
let uid = self.user_id()?;
|
||||||
|
let db = self.authenticate_user.get_sqlite_connection(uid)?;
|
||||||
|
delete_workspace_subscription_from_cache(db, &workspace_id)
|
||||||
|
}
|
||||||
|
|
||||||
#[instrument(level = "info", skip(self), err)]
|
#[instrument(level = "info", skip(self), err)]
|
||||||
pub async fn get_workspace_usage(
|
pub async fn get_workspace_usage(
|
||||||
&self,
|
&self,
|
||||||
|
Loading…
Reference in New Issue
Block a user