mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: plan+billing (#5518)
* feat: billing client * feat: subscribe workspace default impl * feat: added create subscription * feat: add get workspace subs * feat: added subscription cancellation * feat: add workspace limits api * fix: update client api * feat: user billing portal * feat: billing UI (#5455) * feat: plan ui * feat: billing ui * feat: settings plan comparison dialog * feat: complete plan+billing ui * feat: backend integration * chore: cleaning * chore: fixes after merge * fix: dependency issue * feat: added subscription plan cancellation information * feat: subscription callback + canceled date * feat: put behind feature flag * feat: downgrade/upgrade dialogs * feat: update limit error codes * fix: billing refresh + downgrade dialog * fix: some minor improvements to settings * chore: use patch for client-api in tauri * fix: add shared-entity to patch * fix: compile * ci: try to add back maximize build space step * test: increase timeout in failing test --------- Co-authored-by: Zack Fu Zi Xiang <speed2exe@live.com.sg>
This commit is contained in:
@ -1,7 +1,10 @@
|
||||
use validator::Validate;
|
||||
|
||||
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
|
||||
use flowy_user_pub::entities::{Role, WorkspaceInvitation, WorkspaceMember};
|
||||
use flowy_user_pub::entities::{
|
||||
RecurringInterval, Role, SubscriptionPlan, WorkspaceInvitation, WorkspaceMember,
|
||||
WorkspaceSubscription,
|
||||
};
|
||||
use lib_infra::validator_fn::required_not_empty_str;
|
||||
|
||||
#[derive(ProtoBuf, Default, Clone)]
|
||||
@ -201,3 +204,136 @@ pub struct ChangeWorkspaceIconPB {
|
||||
#[pb(index = 2)]
|
||||
pub new_icon: String,
|
||||
}
|
||||
|
||||
#[derive(ProtoBuf, Default, Clone, Validate, Debug)]
|
||||
pub struct SubscribeWorkspacePB {
|
||||
#[pb(index = 1)]
|
||||
#[validate(custom = "required_not_empty_str")]
|
||||
pub workspace_id: String,
|
||||
|
||||
#[pb(index = 2)]
|
||||
pub recurring_interval: RecurringIntervalPB,
|
||||
|
||||
#[pb(index = 3)]
|
||||
pub workspace_subscription_plan: SubscriptionPlanPB,
|
||||
|
||||
#[pb(index = 4)]
|
||||
pub success_url: String,
|
||||
}
|
||||
|
||||
#[derive(ProtoBuf_Enum, Clone, Default, Debug)]
|
||||
pub enum RecurringIntervalPB {
|
||||
#[default]
|
||||
Month = 0,
|
||||
Year = 1,
|
||||
}
|
||||
|
||||
impl From<RecurringIntervalPB> for RecurringInterval {
|
||||
fn from(r: RecurringIntervalPB) -> Self {
|
||||
match r {
|
||||
RecurringIntervalPB::Month => RecurringInterval::Month,
|
||||
RecurringIntervalPB::Year => RecurringInterval::Year,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RecurringInterval> for RecurringIntervalPB {
|
||||
fn from(r: RecurringInterval) -> Self {
|
||||
match r {
|
||||
RecurringInterval::Month => RecurringIntervalPB::Month,
|
||||
RecurringInterval::Year => RecurringIntervalPB::Year,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(ProtoBuf_Enum, Clone, Default, Debug)]
|
||||
pub enum SubscriptionPlanPB {
|
||||
#[default]
|
||||
None = 0,
|
||||
Pro = 1,
|
||||
Team = 2,
|
||||
}
|
||||
|
||||
impl From<SubscriptionPlanPB> for SubscriptionPlan {
|
||||
fn from(value: SubscriptionPlanPB) -> Self {
|
||||
match value {
|
||||
SubscriptionPlanPB::Pro => SubscriptionPlan::Pro,
|
||||
SubscriptionPlanPB::Team => SubscriptionPlan::Team,
|
||||
SubscriptionPlanPB::None => SubscriptionPlan::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SubscriptionPlan> for SubscriptionPlanPB {
|
||||
fn from(value: SubscriptionPlan) -> Self {
|
||||
match value {
|
||||
SubscriptionPlan::Pro => SubscriptionPlanPB::Pro,
|
||||
SubscriptionPlan::Team => SubscriptionPlanPB::Team,
|
||||
SubscriptionPlan::None => SubscriptionPlanPB::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, ProtoBuf, Default, Clone)]
|
||||
pub struct PaymentLinkPB {
|
||||
#[pb(index = 1)]
|
||||
pub payment_link: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, ProtoBuf, Default, Clone)]
|
||||
pub struct RepeatedWorkspaceSubscriptionPB {
|
||||
#[pb(index = 1)]
|
||||
pub items: Vec<WorkspaceSubscriptionPB>,
|
||||
}
|
||||
|
||||
#[derive(Debug, ProtoBuf, Default, Clone)]
|
||||
pub struct WorkspaceSubscriptionPB {
|
||||
#[pb(index = 1)]
|
||||
pub workspace_id: String,
|
||||
|
||||
#[pb(index = 2)]
|
||||
pub subscription_plan: SubscriptionPlanPB,
|
||||
|
||||
#[pb(index = 3)]
|
||||
pub recurring_interval: RecurringIntervalPB,
|
||||
|
||||
#[pb(index = 4)]
|
||||
pub is_active: bool,
|
||||
|
||||
#[pb(index = 5)]
|
||||
pub has_canceled: bool,
|
||||
|
||||
#[pb(index = 6)]
|
||||
pub canceled_at: i64, // value is valid only if has_canceled is true
|
||||
}
|
||||
|
||||
impl From<WorkspaceSubscription> for WorkspaceSubscriptionPB {
|
||||
fn from(s: WorkspaceSubscription) -> Self {
|
||||
Self {
|
||||
workspace_id: s.workspace_id,
|
||||
subscription_plan: s.subscription_plan.into(),
|
||||
recurring_interval: s.recurring_interval.into(),
|
||||
is_active: s.is_active,
|
||||
has_canceled: s.canceled_at.is_some(),
|
||||
canceled_at: s.canceled_at.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, ProtoBuf, Default, Clone)]
|
||||
pub struct WorkspaceUsagePB {
|
||||
#[pb(index = 1)]
|
||||
pub member_count: u64,
|
||||
#[pb(index = 2)]
|
||||
pub member_count_limit: u64,
|
||||
#[pb(index = 3)]
|
||||
pub total_blob_bytes: u64,
|
||||
#[pb(index = 4)]
|
||||
pub total_blob_bytes_limit: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, ProtoBuf, Default, Clone)]
|
||||
pub struct BillingPortalPB {
|
||||
#[pb(index = 1)]
|
||||
pub url: String,
|
||||
}
|
||||
|
@ -774,3 +774,64 @@ pub async fn leave_workspace_handler(
|
||||
manager.leave_workspace(&workspace_id).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip_all, err)]
|
||||
pub async fn subscribe_workspace_handler(
|
||||
params: AFPluginData<SubscribeWorkspacePB>,
|
||||
manager: AFPluginState<Weak<UserManager>>,
|
||||
) -> DataResult<PaymentLinkPB, FlowyError> {
|
||||
let params = params.try_into_inner()?;
|
||||
let manager = upgrade_manager(manager)?;
|
||||
let payment_link = manager.subscribe_workspace(params).await?;
|
||||
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)]
|
||||
pub async fn cancel_workspace_subscription_handler(
|
||||
param: AFPluginData<UserWorkspaceIdPB>,
|
||||
manager: AFPluginState<Weak<UserManager>>,
|
||||
) -> Result<(), FlowyError> {
|
||||
let workspace_id = param.into_inner().workspace_id;
|
||||
let manager = upgrade_manager(manager)?;
|
||||
manager.cancel_workspace_subscription(workspace_id).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip_all, err)]
|
||||
pub async fn get_workspace_usage_handler(
|
||||
param: AFPluginData<UserWorkspaceIdPB>,
|
||||
manager: AFPluginState<Weak<UserManager>>,
|
||||
) -> DataResult<WorkspaceUsagePB, FlowyError> {
|
||||
let workspace_id = param.into_inner().workspace_id;
|
||||
let manager = upgrade_manager(manager)?;
|
||||
let workspace_usage = manager.get_workspace_usage(workspace_id).await?;
|
||||
data_result_ok(WorkspaceUsagePB {
|
||||
member_count: workspace_usage.member_count as u64,
|
||||
member_count_limit: workspace_usage.member_count_limit as u64,
|
||||
total_blob_bytes: workspace_usage.total_blob_bytes as u64,
|
||||
total_blob_bytes_limit: workspace_usage.total_blob_bytes_limit as u64,
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip_all, err)]
|
||||
pub async fn get_billing_portal_handler(
|
||||
manager: AFPluginState<Weak<UserManager>>,
|
||||
) -> DataResult<BillingPortalPB, FlowyError> {
|
||||
let manager = upgrade_manager(manager)?;
|
||||
let url = manager.get_billing_portal_url().await?;
|
||||
data_result_ok(BillingPortalPB { url })
|
||||
}
|
||||
|
@ -71,6 +71,12 @@ pub fn init(user_manager: Weak<UserManager>) -> AFPlugin {
|
||||
.event(UserEvent::InviteWorkspaceMember, invite_workspace_member_handler)
|
||||
.event(UserEvent::ListWorkspaceInvitations, list_workspace_invitations_handler)
|
||||
.event(UserEvent::AcceptWorkspaceInvitation, accept_workspace_invitations_handler)
|
||||
// Billing
|
||||
.event(UserEvent::SubscribeWorkspace, subscribe_workspace_handler)
|
||||
.event(UserEvent::GetWorkspaceSubscriptions, get_workspace_subscriptions_handler)
|
||||
.event(UserEvent::CancelWorkspaceSubscription, cancel_workspace_subscription_handler)
|
||||
.event(UserEvent::GetWorkspaceUsage, get_workspace_usage_handler)
|
||||
.event(UserEvent::GetBillingPortal, get_billing_portal_handler)
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)]
|
||||
@ -230,6 +236,21 @@ pub enum UserEvent {
|
||||
|
||||
#[event(input = "MagicLinkSignInPB", output = "UserProfilePB")]
|
||||
MagicLinkSignIn = 50,
|
||||
|
||||
#[event(input = "SubscribeWorkspacePB", output = "PaymentLinkPB")]
|
||||
SubscribeWorkspace = 51,
|
||||
|
||||
#[event(output = "RepeatedWorkspaceSubscriptionPB")]
|
||||
GetWorkspaceSubscriptions = 52,
|
||||
|
||||
#[event(input = "UserWorkspaceIdPB")]
|
||||
CancelWorkspaceSubscription = 53,
|
||||
|
||||
#[event(input = "UserWorkspaceIdPB", output = "WorkspaceUsagePB")]
|
||||
GetWorkspaceUsage = 54,
|
||||
|
||||
#[event(output = "BillingPortalPB")]
|
||||
GetBillingPortal = 55,
|
||||
}
|
||||
|
||||
pub trait UserStatusCallback: Send + Sync + 'static {
|
||||
|
@ -11,10 +11,13 @@ use flowy_sqlite::schema::user_workspace_table;
|
||||
use flowy_sqlite::{query_dsl::*, DBConnection, ExpressionMethods};
|
||||
use flowy_user_pub::entities::{
|
||||
Role, UserWorkspace, WorkspaceInvitation, WorkspaceInvitationStatus, WorkspaceMember,
|
||||
WorkspaceSubscription, WorkspaceUsage,
|
||||
};
|
||||
use lib_dispatch::prelude::af_spawn;
|
||||
|
||||
use crate::entities::{RepeatedUserWorkspacePB, ResetWorkspacePB, UserWorkspacePB};
|
||||
use crate::entities::{
|
||||
RepeatedUserWorkspacePB, ResetWorkspacePB, SubscribeWorkspacePB, UserWorkspacePB,
|
||||
};
|
||||
use crate::migrations::AnonUser;
|
||||
use crate::notification::{send_notification, UserNotification};
|
||||
use crate::services::data_import::{
|
||||
@ -417,6 +420,65 @@ impl UserManager {
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(level = "info", skip(self), err)]
|
||||
pub async fn subscribe_workspace(
|
||||
&self,
|
||||
workspace_subscription: SubscribeWorkspacePB,
|
||||
) -> FlowyResult<String> {
|
||||
let payment_link = self
|
||||
.cloud_services
|
||||
.get_user_service()?
|
||||
.subscribe_workspace(
|
||||
workspace_subscription.workspace_id,
|
||||
workspace_subscription.recurring_interval.into(),
|
||||
workspace_subscription.workspace_subscription_plan.into(),
|
||||
workspace_subscription.success_url,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(payment_link)
|
||||
}
|
||||
|
||||
#[instrument(level = "info", skip(self), err)]
|
||||
pub async fn get_workspace_subscriptions(&self) -> FlowyResult<Vec<WorkspaceSubscription>> {
|
||||
let res = self
|
||||
.cloud_services
|
||||
.get_user_service()?
|
||||
.get_workspace_subscriptions()
|
||||
.await?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
#[instrument(level = "info", skip(self), err)]
|
||||
pub async fn cancel_workspace_subscription(&self, workspace_id: String) -> FlowyResult<()> {
|
||||
self
|
||||
.cloud_services
|
||||
.get_user_service()?
|
||||
.cancel_workspace_subscription(workspace_id)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(level = "info", skip(self), err)]
|
||||
pub async fn get_workspace_usage(&self, workspace_id: String) -> FlowyResult<WorkspaceUsage> {
|
||||
let workspace_usage = self
|
||||
.cloud_services
|
||||
.get_user_service()?
|
||||
.get_workspace_usage(workspace_id)
|
||||
.await?;
|
||||
Ok(workspace_usage)
|
||||
}
|
||||
|
||||
#[instrument(level = "info", skip(self), err)]
|
||||
pub async fn get_billing_portal_url(&self) -> FlowyResult<String> {
|
||||
let url = self
|
||||
.cloud_services
|
||||
.get_user_service()?
|
||||
.get_billing_portal_url()
|
||||
.await?;
|
||||
Ok(url)
|
||||
}
|
||||
}
|
||||
|
||||
/// This method is used to save one user workspace to the SQLite database
|
||||
|
Reference in New Issue
Block a user