AppFlowy/frontend/appflowy_flutter/lib/startup/tasks/appflowy_cloud_task.dart

256 lines
7.2 KiB
Dart
Raw Normal View History

import 'dart:async';
import 'dart:io';
import 'package:app_links/app_links.dart';
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/startup/tasks/app_widget.dart';
import 'package:appflowy/user/application/auth/auth_error.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/user/application/auth/device_id.dart';
import 'package:appflowy/user/application/user_auth_listener.dart';
import 'package:appflowy/workspace/application/subscription_success_listenable/subscription_success_listenable.dart';
import 'package:appflowy/workspace/presentation/home/toast.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-error/code.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:flutter/material.dart';
import 'package:url_protocol/url_protocol.dart';
const appflowyDeepLinkSchema = 'appflowy-flutter';
class AppFlowyCloudDeepLink {
AppFlowyCloudDeepLink() {
_deepLinkSubscription = _AppLinkWrapper.instance.listen(
(Uri? uri) async {
Log.info('onDeepLink: ${uri.toString()}');
await _handleUri(uri);
},
onError: (Object err, StackTrace stackTrace) {
Log.error('on DeepLink stream error: ${err.toString()}', stackTrace);
_deepLinkSubscription.cancel();
},
);
if (Platform.isWindows) {
// register deep link for Windows
registerProtocolHandler(appflowyDeepLinkSchema);
}
}
ValueNotifier<DeepLinkResult?>? _stateNotifier = ValueNotifier(null);
Completer<FlowyResult<UserProfilePB, FlowyError>>? _completer;
set completer(Completer<FlowyResult<UserProfilePB, FlowyError>>? value) {
Log.debug('AppFlowyCloudDeepLink: $hashCode completer');
_completer = value;
}
late final StreamSubscription<Uri?> _deepLinkSubscription;
Future<void> dispose() async {
Log.debug('AppFlowyCloudDeepLink: $hashCode dispose');
await _deepLinkSubscription.cancel();
_stateNotifier?.dispose();
_stateNotifier = null;
completer = null;
}
void registerCompleter(
Completer<FlowyResult<UserProfilePB, FlowyError>> completer,
) {
this.completer = completer;
}
VoidCallback subscribeDeepLinkLoadingState(
ValueChanged<DeepLinkResult> listener,
) {
void listenerFn() {
if (_stateNotifier?.value != null) {
listener(_stateNotifier!.value!);
}
}
_stateNotifier?.addListener(listenerFn);
return listenerFn;
}
void unsubscribeDeepLinkLoadingState(VoidCallback listener) =>
_stateNotifier?.removeListener(listener);
Future<void> _handleUri(
Uri? uri,
) async {
_stateNotifier?.value = DeepLinkResult(state: DeepLinkState.none);
if (uri == null) {
Log.error('onDeepLinkError: Unexpected empty deep link callback');
_completer?.complete(FlowyResult.failure(AuthError.emptyDeepLink));
completer = null;
2024-03-14 09:17:59 +08:00
return;
}
2024-03-14 09:17:59 +08:00
if (_isPaymentSuccessUri(uri)) {
feat: ai billing (#5741) * feat: start on AI plan+billing UI * chore: enable plan and billing * feat: cache workspace subscription + minor fixes (#5705) * feat: update api from billing * feat: add api for workspace subscription info (#5717) * feat: refactor and start integrating AI plans * feat: refine UI and add business logic for AI * feat: complete UIUX for AI and limits * chore: remove resolved todo * chore: localize remove addon dialog * chore: fix spacing issue for usage * fix: interpret subscription + usage on action * chore: update api for billing (#5735) * chore: update revisions * fix: remove subscription cache * fix: copy improvements + use consistent dialog * chore: update to the latest client api * feat: support updating billing period * Feat/ai billing cancel reason (#5752) * chore: add cancellation reason field * fix: ci add one retry for concurrent sign up * chore: merge with main * chore: half merge * chore: fix conflict * chore: observer error * chore: remove unneeded protobuf and remove unwrap * feat: added subscription plan details * chore: check error code and update sidebar toast * chore: periodically check billing state * chore: editor ai error * chore: return file upload error * chore: fmt * chore: clippy * chore: disable upload image when exceed storage limitation * chore: remove todo * chore: remove openai i18n * chore: update log * chore: update client-api to fix stream error * chore: clippy * chore: fix language file * chore: disable billing UI --------- Co-authored-by: Zack Fu Zi Xiang <speed2exe@live.com.sg> Co-authored-by: nathan <nathan@appflowy.io>
2024-07-22 09:43:48 +02:00
Log.debug("Payment success deep link: ${uri.toString()}");
final plan = uri.queryParameters['plan'];
return getIt<SubscriptionSuccessListenable>().onPaymentSuccess(plan);
}
2024-03-14 09:17:59 +08:00
return _isAuthCallbackDeepLink(uri).fold(
(_) async {
final deviceId = await getDeviceId();
final payload = OauthSignInPB(
authenticator: AuthenticatorPB.AppFlowyCloud,
map: {
AuthServiceMapKeys.signInURL: uri.toString(),
AuthServiceMapKeys.deviceId: deviceId,
},
);
_stateNotifier?.value = DeepLinkResult(state: DeepLinkState.loading);
2024-03-14 09:17:59 +08:00
final result = await UserEventOauthSignIn(payload).send();
_stateNotifier?.value = DeepLinkResult(
state: DeepLinkState.finish,
result: result,
);
// If there is no completer, runAppFlowy() will be called.
if (_completer == null) {
await result.fold(
(_) async {
await runAppFlowy();
},
(err) {
Log.error(err);
final context = AppGlobals.rootNavKey.currentState?.context;
if (context != null) {
showToastNotification(
message: err.msg,
);
}
},
);
} else {
_completer?.complete(result);
completer = null;
}
},
(err) {
Log.error('onDeepLinkError: Unexpected deep link: $err');
if (_completer == null) {
final context = AppGlobals.rootNavKey.currentState?.context;
if (context != null) {
showSnackBarMessage(
context,
err.msg,
);
}
} else {
_completer?.complete(FlowyResult.failure(err));
completer = null;
}
},
);
}
FlowyResult<void, FlowyError> _isAuthCallbackDeepLink(Uri uri) {
if (uri.fragment.contains('access_token')) {
return FlowyResult.success(null);
}
return FlowyResult.failure(
FlowyError.create()
..code = ErrorCode.MissingAuthField
..msg = uri.path,
);
}
bool _isPaymentSuccessUri(Uri uri) {
return uri.host == 'payment-success';
}
}
class InitAppFlowyCloudTask extends LaunchTask {
UserAuthStateListener? _authStateListener;
bool isLoggingOut = false;
@override
Future<void> initialize(LaunchContext context) async {
if (!isAppFlowyCloudEnabled) {
return;
}
_authStateListener = UserAuthStateListener();
_authStateListener?.start(
didSignIn: () {
isLoggingOut = false;
},
onInvalidAuth: (message) async {
Log.error(message);
if (!isLoggingOut) {
await runAppFlowy();
}
},
);
}
@override
Future<void> dispose() async {
await _authStateListener?.stop();
_authStateListener = null;
}
}
class DeepLinkResult {
DeepLinkResult({
required this.state,
this.result,
});
final DeepLinkState state;
final FlowyResult<UserProfilePB, FlowyError>? result;
}
enum DeepLinkState {
none,
loading,
finish,
}
// wrapper for AppLinks to support multiple listeners
class _AppLinkWrapper {
_AppLinkWrapper._() {
_appLinkSubscription = _appLinks.uriLinkStream.listen((event) {
_streamSubscription.sink.add(event);
});
}
static final _AppLinkWrapper instance = _AppLinkWrapper._();
final AppLinks _appLinks = AppLinks();
final _streamSubscription = StreamController<Uri?>.broadcast();
late final StreamSubscription<Uri?> _appLinkSubscription;
StreamSubscription<Uri?> listen(
void Function(Uri?) listener, {
Function? onError,
bool? cancelOnError,
}) {
return _streamSubscription.stream.listen(
listener,
onError: onError,
cancelOnError: cancelOnError,
);
}
void dispose() {
_streamSubscription.close();
_appLinkSubscription.cancel();
}
}