Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: ai billing #5741

Merged
merged 43 commits into from
Jul 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
81acba4
feat: start on AI plan+billing UI
Xazin Jul 5, 2024
dd5bd23
chore: enable plan and billing
Xazin Jul 5, 2024
4e1be1c
feat: cache workspace subscription + minor fixes (#5705)
Xazin Jul 8, 2024
5961e8b
feat: update api from billing
speed2exe Jul 9, 2024
f2fc35f
feat: add api for workspace subscription info (#5717)
speed2exe Jul 10, 2024
c04eb18
feat: refactor and start integrating AI plans
Xazin Jul 11, 2024
5cbfbd5
feat: refine UI and add business logic for AI
Xazin Jul 11, 2024
c9827e7
feat: complete UIUX for AI and limits
Xazin Jul 11, 2024
f60a4a7
chore: remove resolved todo
Xazin Jul 11, 2024
5268820
chore: localize remove addon dialog
Xazin Jul 11, 2024
58f5ed0
chore: fix spacing issue for usage
Xazin Jul 11, 2024
9957c56
fix: interpret subscription + usage on action
Xazin Jul 14, 2024
67e49e7
chore: update api for billing (#5735)
speed2exe Jul 15, 2024
8daf140
chore: update revisions
Xazin Jul 15, 2024
58eeee6
fix: remove subscription cache
Xazin Jul 15, 2024
d8f5ddf
fix: copy improvements + use consistent dialog
Xazin Jul 15, 2024
28a78a3
Merge branch 'main' into feat/ai-billing
speed2exe Jul 16, 2024
b54de20
chore: update to the latest client api
speed2exe Jul 16, 2024
9851dbf
feat: support updating billing period
Xazin Jul 16, 2024
4ca2790
Merge branch 'main' into feat/ai-billing
Xazin Jul 16, 2024
cd7c277
Feat/ai billing cancel reason (#5752)
speed2exe Jul 17, 2024
37d8342
chore: merge with main
speed2exe Jul 19, 2024
34bdd1a
chore: half merge
speed2exe Jul 19, 2024
268218a
chore: fix conflict
appflowy Jul 19, 2024
81820c1
Merge branch 'main' into feat/ai-billing-dart-conflict
appflowy Jul 19, 2024
c0efa0b
chore: observer error
appflowy Jul 19, 2024
0e9d4d6
chore: remove unneeded protobuf and remove unwrap
speed2exe Jul 19, 2024
2427f3e
feat: added subscription plan details
speed2exe Jul 20, 2024
0a04556
chore: check error code and update sidebar toast
appflowy Jul 21, 2024
be32deb
chore: periodically check billing state
appflowy Jul 21, 2024
5f1744b
chore: editor ai error
appflowy Jul 21, 2024
67114e7
chore: return file upload error
appflowy Jul 21, 2024
4749cb7
chore: fmt
appflowy Jul 22, 2024
a084b54
chore: clippy
appflowy Jul 22, 2024
3b36461
chore: disable upload image when exceed storage limitation
appflowy Jul 22, 2024
9b50477
chore: remove todo
appflowy Jul 22, 2024
a14cbb5
chore: remove openai i18n
appflowy Jul 22, 2024
fe4616b
chore: update log
appflowy Jul 22, 2024
3d3926d
chore: update client-api to fix stream error
appflowy Jul 22, 2024
7a38abb
Merge branch 'main' into feat/ai-billing
appflowy Jul 22, 2024
e9b5030
chore: clippy
appflowy Jul 22, 2024
a08c0b8
chore: fix language file
appflowy Jul 22, 2024
a32ce94
chore: disable billing UI
appflowy Jul 22, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ class DocumentService {
}) async {
final workspace = await FolderEventReadCurrentWorkspace().send();
return workspace.fold((l) async {
final payload = UploadedFilePB(
final payload = DownloadFilePB(
url: url,
);
final result = await DocumentEventDownloadFile(payload).send();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/util/file_extension.dart';
import 'package:appflowy/workspace/application/settings/application_data_storage.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-error/code.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:path/path.dart' as p;
Expand Down Expand Up @@ -63,6 +64,12 @@ Future<(String? path, String? errorMessage)> saveImageToCloudStorage(
);
return (s.url, null);
},
(e) => (null, e.msg),
(err) {
if (err.code == ErrorCode.FileStorageLimitExceeded) {
return (null, LocaleKeys.sideBar_storageLimitDialogTitle.tr());
} else {
return (null, err.msg);
}
},
);
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,26 @@
import 'package:freezed_annotation/freezed_annotation.dart';

part 'error.freezed.dart';
part 'error.g.dart';

@freezed
class AIError with _$AIError {
const factory AIError({
String? code,
required String message,
@Default(AIErrorCode.other) AIErrorCode code,
}) = _AIError;

factory AIError.fromJson(Map<String, Object?> json) =>
_$AIErrorFromJson(json);
}

enum AIErrorCode {
@JsonValue('AIResponseLimitExceeded')
aiResponseLimitExceeded,
@JsonValue('Other')
other,
}

extension AIErrorExtension on AIError {
bool get isLimitExceeded => code == AIErrorCode.aiResponseLimitExceeded;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';

class _AILimitDialog extends StatelessWidget {
const _AILimitDialog({
required this.message,
required this.onOkPressed,
});
final VoidCallback onOkPressed;
final String message;

@override
Widget build(BuildContext context) {
return NavigatorOkCancelDialog(
message: message,
okTitle: LocaleKeys.button_ok.tr(),
onOkPressed: onOkPressed,
titleUpperCase: false,
);
}
}

void showAILimitDialog(BuildContext context, String message) {
showDialog(
context: context,
barrierDismissible: false,
useRootNavigator: false,
builder: (dialogContext) => _AILimitDialog(
message: message,
onOkPressed: () {},
),
);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/build_context_extension.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/text_robot.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/error.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/discard_dialog.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart';
import 'package:appflowy/user/application/ai_service.dart';
Expand All @@ -17,6 +18,8 @@ import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import 'ai_limit_dialog.dart';

class AutoCompletionBlockKeys {
const AutoCompletionBlockKeys._();

Expand Down Expand Up @@ -225,11 +228,15 @@ class _AutoCompletionBlockComponentState
onError: (error) async {
barrierDialog?.dismiss();
if (mounted) {
showSnackBarMessage(
context,
error.message,
showCancel: true,
);
if (error.isLimitExceeded) {
showAILimitDialog(context, error.message);
} else {
showSnackBarMessage(
context,
error.message,
showCancel: true,
);
}
}
},
);
Expand Down Expand Up @@ -304,11 +311,15 @@ class _AutoCompletionBlockComponentState
onEnd: () async {},
onError: (error) async {
if (mounted) {
showSnackBarMessage(
context,
error.message,
showCancel: true,
);
if (error.isLimitExceeded) {
showAILimitDialog(context, error.message);
} else {
showSnackBarMessage(
context,
error.message,
showCancel: true,
);
}
}
},
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'dart:async';

import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/ai_client.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/error.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/discard_dialog.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_action.dart';
import 'package:appflowy/startup/startup.dart';
Expand All @@ -16,6 +17,8 @@ import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:provider/provider.dart';

import 'ai_limit_dialog.dart';

class SmartEditBlockKeys {
const SmartEditBlockKeys._();

Expand Down Expand Up @@ -426,11 +429,15 @@ class _SmartEditInputWidgetState extends State<SmartEditInputWidget> {
});
},
onError: (error) async {
showSnackBarMessage(
context,
error.message,
showCancel: true,
);
if (error.isLimitExceeded) {
showAILimitDialog(context, error.message);
} else {
showSnackBarMessage(
context,
error.message,
showCancel: true,
);
}
await _onExit();
},
);
Expand Down
3 changes: 2 additions & 1 deletion frontend/appflowy_flutter/lib/shared/feature_flags.dart
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ enum FeatureFlag {

bool get isOn {
if ([
FeatureFlag.planBilling,
// release this feature in version 0.6.1
FeatureFlag.spaceDesign,
// release this feature in version 0.5.9
Expand All @@ -110,14 +111,14 @@ enum FeatureFlag {
}

switch (this) {
case FeatureFlag.planBilling:
case FeatureFlag.search:
case FeatureFlag.syncDocument:
case FeatureFlag.syncDatabase:
case FeatureFlag.spaceDesign:
return true;
case FeatureFlag.collaborativeWorkspace:
case FeatureFlag.membersSettings:
case FeatureFlag.planBilling:
case FeatureFlag.unknown:
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,9 @@ class AppFlowyCloudDeepLink {
}

if (_isPaymentSuccessUri(uri)) {
return getIt<SubscriptionSuccessListenable>().onPaymentSuccess();
Log.debug("Payment success deep link: ${uri.toString()}");
final plan = uri.queryParameters['plan'];
return getIt<SubscriptionSuccessListenable>().onPaymentSuccess(plan);
}

return _isAuthCallbackDeepLink(uri).fold(
Expand Down
12 changes: 12 additions & 0 deletions frontend/appflowy_flutter/lib/user/application/ai_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ import 'dart:async';
import 'dart:ffi';
import 'dart:isolate';

import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/ai_client.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/error.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/text_completion.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_action.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:fixnum/fixnum.dart' as fixnum;

class AppFlowyAIService implements AIRepository {
Expand Down Expand Up @@ -85,6 +87,15 @@ class CompletionStream {
_port.handler = _controller.add;
_subscription = _controller.stream.listen(
(event) async {
if (event == "AI_RESPONSE_LIMIT") {
onError(
AIError(
message: LocaleKeys.sideBar_aiResponseLitmit.tr(),
code: AIErrorCode.aiResponseLimitExceeded,
),
);
}

if (event.startsWith("start:")) {
await onStart();
}
Expand All @@ -96,6 +107,7 @@ class CompletionStream {
if (event.startsWith("finish:")) {
await onEnd();
}

if (event.startsWith("error:")) {
onError(AIError(message: event.substring(6)));
}
Expand Down
34 changes: 28 additions & 6 deletions frontend/appflowy_flutter/lib/user/application/user_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'dart:async';

import 'package:appflowy/env/cloud_env.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/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart';
Expand All @@ -10,7 +11,10 @@ import 'package:appflowy_result/appflowy_result.dart';
import 'package:fixnum/fixnum.dart';

abstract class IUserBackendService {
Future<FlowyResult<void, FlowyError>> cancelSubscription(String workspaceId);
Future<FlowyResult<void, FlowyError>> cancelSubscription(
String workspaceId,
SubscriptionPlanPB plan,
);
Future<FlowyResult<PaymentLinkPB, FlowyError>> createSubscription(
String workspaceId,
SubscriptionPlanPB plan,
Expand Down Expand Up @@ -228,9 +232,10 @@ class UserBackendService implements IUserBackendService {
return UserEventLeaveWorkspace(data).send();
}

static Future<FlowyResult<RepeatedWorkspaceSubscriptionPB, FlowyError>>
getWorkspaceSubscriptions() {
return UserEventGetWorkspaceSubscriptions().send();
static Future<FlowyResult<WorkspaceSubscriptionInfoPB, FlowyError>>
getWorkspaceSubscriptionInfo(String workspaceId) {
final params = UserWorkspaceIdPB.create()..workspaceId = workspaceId;
return UserEventGetWorkspaceSubscriptionInfo(params).send();
}

Future<FlowyResult<WorkspaceMemberPB, FlowyError>>
Expand All @@ -250,15 +255,32 @@ class UserBackendService implements IUserBackendService {
..recurringInterval = RecurringIntervalPB.Year
..workspaceSubscriptionPlan = plan
..successUrl =
'${getIt<AppFlowyCloudSharedEnv>().appflowyCloudConfig.base_url}/web/payment-success';
'${getIt<AppFlowyCloudSharedEnv>().appflowyCloudConfig.base_url}/web/payment-success?plan=${plan.toRecognizable()}';
return UserEventSubscribeWorkspace(request).send();
}

@override
Future<FlowyResult<void, FlowyError>> cancelSubscription(
String workspaceId,
SubscriptionPlanPB plan,
) {
final request = UserWorkspaceIdPB()..workspaceId = workspaceId;
final request = CancelWorkspaceSubscriptionPB()
..workspaceId = workspaceId
..plan = plan;

return UserEventCancelWorkspaceSubscription(request).send();
}

Future<FlowyResult<void, FlowyError>> updateSubscriptionPeriod(
String workspaceId,
SubscriptionPlanPB plan,
RecurringIntervalPB interval,
) {
final request = UpdateWorkspaceSubscriptionPaymentPeriodPB()
..workspaceId = workspaceId
..plan = plan
..recurringInterval = interval;

return UserEventUpdateWorkspaceSubscriptionPaymentPeriod(request).send();
}
}
Loading
Loading