fix: interpret subscription + usage on action

This commit is contained in:
Mathias Mogensen 2024-07-14 23:11:07 +02:00
parent 687091e2f2
commit 9668ab20f9
3 changed files with 133 additions and 5 deletions

View File

@ -5,6 +5,7 @@ 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/startup/startup.dart';
import 'package:appflowy/user/application/user_service.dart'; import 'package:appflowy/user/application/user_service.dart';
import 'package:appflowy/workspace/application/settings/plan/workspace_subscription_ext.dart';
import 'package:appflowy/workspace/application/subscription_success_listenable/subscription_success_listenable.dart'; import 'package:appflowy/workspace/application/subscription_success_listenable/subscription_success_listenable.dart';
import 'package:appflowy/workspace/application/workspace/workspace_service.dart'; import 'package:appflowy/workspace/application/workspace/workspace_service.dart';
import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/log.dart';
@ -17,6 +18,7 @@ import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.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';
import 'package:protobuf/protobuf.dart';
part 'settings_billing_bloc.freezed.dart'; part 'settings_billing_bloc.freezed.dart';
@ -113,8 +115,61 @@ class SettingsBillingBloc
); );
}, },
cancelSubscription: (plan) async { cancelSubscription: (plan) async {
await _userService.cancelSubscription(workspaceId, plan); final result =
await _onPaymentSuccessful(); await _userService.cancelSubscription(workspaceId, plan);
final successOrNull = result.fold(
(_) => true,
(f) {
Log.error(
'Failed to cancel subscription of ${plan.label}: ${f.msg}',
f,
);
return null;
},
);
if (successOrNull != true) {
return;
}
// Invalidate cache for this workspace
await UserBackendService.invalidateWorkspaceSubscriptionCache(
workspaceId,
);
final subscriptionInfo = state.mapOrNull(
ready: (s) => s.subscriptionInfo,
);
// This is impossible, but for good measure
if (subscriptionInfo == null) {
return;
}
subscriptionInfo.freeze();
final newInfo = subscriptionInfo.rebuild((value) {
if (plan.isAddOn) {
value.addOns.removeWhere(
(addon) => addon.addOnSubscription.subscriptionPlan == plan,
);
}
if (value.plan == WorkspacePlanPB.ProPlan) {
value.plan = WorkspacePlanPB.FreePlan;
value.planSubscription.freeze();
value.planSubscription = value.planSubscription.rebuild((sub) {
sub.status = WorkspaceSubscriptionStatusPB.Active;
sub.subscriptionPlan = SubscriptionPlanPB.None;
});
}
});
emit(
SettingsBillingState.ready(
subscriptionInfo: newInfo,
billingPortal: _billingPortal,
),
);
}, },
paymentSuccessful: (plan) async { paymentSuccessful: (plan) async {
final result = await UserBackendService.getWorkspaceSubscriptionInfo( final result = await UserBackendService.getWorkspaceSubscriptionInfo(
@ -205,6 +260,7 @@ class SettingsBillingState extends Equatable with _$SettingsBillingState {
subscription, subscription,
billingPortal, billingPortal,
plan, plan,
...subscription.addOns,
], ],
); );
} }

View File

@ -3,6 +3,7 @@ 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/startup/startup.dart';
import 'package:appflowy/user/application/user_service.dart'; import 'package:appflowy/user/application/user_service.dart';
import 'package:appflowy/workspace/application/settings/plan/workspace_subscription_ext.dart';
import 'package:appflowy/workspace/application/subscription_success_listenable/subscription_success_listenable.dart'; import 'package:appflowy/workspace/application/subscription_success_listenable/subscription_success_listenable.dart';
import 'package:appflowy/workspace/application/workspace/workspace_service.dart'; import 'package:appflowy/workspace/application/workspace/workspace_service.dart';
import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/log.dart';
@ -13,6 +14,7 @@ import 'package:appflowy_backend/protobuf/flowy-user/workspace.pbserver.dart';
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.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';
import 'package:protobuf/protobuf.dart';
part 'settings_plan_bloc.freezed.dart'; part 'settings_plan_bloc.freezed.dart';
@ -87,7 +89,10 @@ class SettingsPlanBloc extends Bloc<SettingsPlanEvent, SettingsPlanState> {
result.fold( result.fold(
(pl) => afLaunchUrlString(pl.paymentLink), (pl) => afLaunchUrlString(pl.paymentLink),
(f) => Log.error(f.msg, f), (f) => Log.error(
'Failed to fetch paymentlink for $plan: ${f.msg}',
f,
),
); );
}, },
cancelSubscription: () async { cancelSubscription: () async {
@ -98,12 +103,68 @@ class SettingsPlanBloc extends Bloc<SettingsPlanEvent, SettingsPlanState> {
// We can hardcode the subscription plan here because we cannot cancel addons // We can hardcode the subscription plan here because we cannot cancel addons
// on the Plan page // on the Plan page
await _userService.cancelSubscription( final result = await _userService.cancelSubscription(
workspaceId, workspaceId,
SubscriptionPlanPB.Pro, SubscriptionPlanPB.Pro,
); );
add(const SettingsPlanEvent.started()); final successOrNull = result.fold(
(_) => true,
(f) {
Log.error('Failed to cancel subscription of Pro: ${f.msg}', f);
return null;
},
);
if (successOrNull != true) {
return;
}
// Invalidate the cache
await UserBackendService.invalidateWorkspaceSubscriptionCache(
workspaceId,
);
final subscriptionInfo = state.mapOrNull(
ready: (s) => s.subscriptionInfo,
);
// This is impossible, but for good measure
if (subscriptionInfo == null) {
return;
}
// We assume their new plan is Free, since we only have Pro plan
// at the moment.
subscriptionInfo.freeze();
final newInfo = subscriptionInfo.rebuild((value) {
value.plan = WorkspacePlanPB.FreePlan;
value.planSubscription.freeze();
value.planSubscription = value.planSubscription.rebuild((sub) {
sub.status = WorkspaceSubscriptionStatusPB.Active;
sub.subscriptionPlan = SubscriptionPlanPB.None;
});
});
// We need to remove unlimited indicator for storage and
// AI usage, if they don't have an addon that changes this behavior.
final usage = state.mapOrNull(ready: (s) => s.workspaceUsage)!;
usage.freeze();
final newUsage = usage.rebuild((value) {
if (!newInfo.hasAIMax && !newInfo.hasAIOnDevice) {
value.aiResponsesUnlimited = false;
}
value.storageBytesUnlimited = false;
});
emit(
SettingsPlanState.ready(
subscriptionInfo: newInfo,
workspaceUsage: newUsage,
),
);
}, },
paymentSuccessful: (plan) { paymentSuccessful: (plan) {
final readyState = state.mapOrNull(ready: (state) => state); final readyState = state.mapOrNull(ready: (state) => state);

View File

@ -66,3 +66,14 @@ extension ToRecognizable on SubscriptionPlanPB {
_ => null, _ => null,
}; };
} }
extension IsAddOn on SubscriptionPlanPB {
/// Returns true if the plan is an add-on and not
/// a workspace plan.
///
bool get isAddOn => switch (this) {
SubscriptionPlanPB.AiMax => true,
SubscriptionPlanPB.AiLocal => true,
_ => false,
};
}