2023-12-08 20:01:54 +07:00
|
|
|
import 'dart:async';
|
2023-11-25 01:18:31 -08:00
|
|
|
import 'dart:io';
|
|
|
|
|
2023-12-08 20:01:54 +07:00
|
|
|
import 'package:app_links/app_links.dart';
|
2023-11-25 01:18:31 -08:00
|
|
|
import 'package:appflowy/env/cloud_env.dart';
|
2023-10-12 20:25:00 +08:00
|
|
|
import 'package:appflowy/startup/startup.dart';
|
2023-11-29 12:55:13 -08:00
|
|
|
import 'package:appflowy/startup/tasks/app_widget.dart';
|
2023-11-25 01:18:31 -08:00
|
|
|
import 'package:appflowy/startup/tasks/supabase_task.dart';
|
2023-11-29 12:55:13 -08:00
|
|
|
import 'package:appflowy/user/application/auth/auth_error.dart';
|
2023-12-08 20:01:54 +07:00
|
|
|
import 'package:appflowy/user/application/auth/auth_service.dart';
|
2023-11-29 12:55:13 -08:00
|
|
|
import 'package:appflowy/user/application/auth/device_id.dart';
|
2023-10-12 20:25:00 +08:00
|
|
|
import 'package:appflowy/user/application/user_auth_listener.dart';
|
2023-11-29 12:55:13 -08:00
|
|
|
import 'package:appflowy/workspace/presentation/home/toast.dart';
|
|
|
|
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
2023-12-08 20:01:54 +07:00
|
|
|
import 'package:appflowy_backend/log.dart';
|
2023-12-21 08:12:40 +08:00
|
|
|
import 'package:appflowy_backend/protobuf/flowy-error/code.pb.dart';
|
2023-11-29 12:55:13 -08:00
|
|
|
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
|
|
|
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
|
|
|
import 'package:dartz/dartz.dart';
|
2023-12-08 20:01:54 +07:00
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import 'package:url_protocol/url_protocol.dart';
|
2023-11-29 12:55:13 -08:00
|
|
|
|
|
|
|
class AppFlowyCloudDeepLink {
|
|
|
|
final _appLinks = AppLinks();
|
2023-12-21 08:12:40 +08:00
|
|
|
// The AppLinks is a singleton, so we need to cancel the previous subscription
|
|
|
|
// before creating a new one.
|
|
|
|
static StreamSubscription<Uri?>? _deeplinkSubscription;
|
|
|
|
ValueNotifier<DeepLinkResult?>? _stateNotifier = ValueNotifier(null);
|
2023-11-29 12:55:13 -08:00
|
|
|
Completer<Either<FlowyError, UserProfilePB>>? _completer;
|
|
|
|
|
|
|
|
AppFlowyCloudDeepLink() {
|
2023-12-21 08:12:40 +08:00
|
|
|
if (_deeplinkSubscription == null) {
|
|
|
|
_deeplinkSubscription = _appLinks.uriLinkStream.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();
|
|
|
|
_deeplinkSubscription = null;
|
|
|
|
},
|
|
|
|
);
|
|
|
|
if (Platform.isWindows) {
|
|
|
|
// register deep link for Windows
|
|
|
|
registerProtocolHandler(appflowyDeepLinkSchema);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
_deeplinkSubscription?.resume();
|
2023-11-29 12:55:13 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> dispose() async {
|
2023-12-21 08:12:40 +08:00
|
|
|
_deeplinkSubscription?.pause();
|
|
|
|
_stateNotifier?.dispose();
|
|
|
|
_stateNotifier = null;
|
2023-11-29 12:55:13 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
void resigerCompleter(
|
|
|
|
Completer<Either<FlowyError, UserProfilePB>> completer,
|
|
|
|
) {
|
|
|
|
_completer = completer;
|
|
|
|
}
|
|
|
|
|
|
|
|
VoidCallback subscribeDeepLinkLoadingState(
|
|
|
|
ValueChanged<DeepLinkResult> listener,
|
|
|
|
) {
|
2023-12-08 20:01:54 +07:00
|
|
|
void listenerFn() {
|
2023-12-21 08:12:40 +08:00
|
|
|
if (_stateNotifier?.value != null) {
|
|
|
|
listener(_stateNotifier!.value!);
|
2023-11-29 12:55:13 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-12-21 08:12:40 +08:00
|
|
|
_stateNotifier?.addListener(listenerFn);
|
2023-11-29 12:55:13 -08:00
|
|
|
return listenerFn;
|
|
|
|
}
|
|
|
|
|
2023-12-21 08:12:40 +08:00
|
|
|
void unsubscribeDeepLinkLoadingState(VoidCallback listener) =>
|
|
|
|
_stateNotifier?.removeListener(listener);
|
2023-11-29 12:55:13 -08:00
|
|
|
|
|
|
|
Future<void> _handleUri(
|
|
|
|
Uri? uri,
|
|
|
|
) async {
|
2023-12-21 08:12:40 +08:00
|
|
|
_stateNotifier?.value = DeepLinkResult(state: DeepLinkState.none);
|
2023-11-29 12:55:13 -08:00
|
|
|
if (uri != null) {
|
2023-12-21 08:12:40 +08:00
|
|
|
_isAuthCallbackDeeplink(uri).fold(
|
|
|
|
(_) async {
|
|
|
|
final deviceId = await getDeviceId();
|
|
|
|
final payload = OauthSignInPB(
|
2023-12-27 11:42:39 +08:00
|
|
|
authenticator: AuthenticatorPB.AppFlowyCloud,
|
2023-12-21 08:12:40 +08:00
|
|
|
map: {
|
|
|
|
AuthServiceMapKeys.signInURL: uri.toString(),
|
|
|
|
AuthServiceMapKeys.deviceId: deviceId,
|
2023-11-29 12:55:13 -08:00
|
|
|
},
|
|
|
|
);
|
2023-12-21 08:12:40 +08:00
|
|
|
_stateNotifier?.value = DeepLinkResult(state: DeepLinkState.loading);
|
|
|
|
final result = await UserEventOauthSignIn(payload)
|
|
|
|
.send()
|
|
|
|
.then((value) => value.swap());
|
|
|
|
|
|
|
|
_stateNotifier?.value = DeepLinkResult(
|
|
|
|
state: DeepLinkState.finish,
|
|
|
|
result: result,
|
|
|
|
);
|
|
|
|
// If there is no completer, runAppFlowy() will be called.
|
|
|
|
if (_completer == null) {
|
|
|
|
result.fold(
|
|
|
|
(err) {
|
|
|
|
Log.error(err);
|
|
|
|
final context = AppGlobals.rootNavKey.currentState?.context;
|
|
|
|
if (context != null) {
|
|
|
|
showSnackBarMessage(
|
|
|
|
context,
|
|
|
|
err.msg,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
(_) async {
|
|
|
|
await runAppFlowy();
|
|
|
|
},
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
_completer?.complete(result);
|
|
|
|
_completer = null;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
(err) {
|
|
|
|
Log.error('onDeepLinkError: Unexpect deep link: $err');
|
|
|
|
if (_completer == null) {
|
|
|
|
final context = AppGlobals.rootNavKey.currentState?.context;
|
|
|
|
if (context != null) {
|
|
|
|
showSnackBarMessage(
|
|
|
|
context,
|
|
|
|
err.msg,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
_completer?.complete(left(err));
|
|
|
|
_completer = null;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
);
|
2023-11-29 12:55:13 -08:00
|
|
|
} else {
|
|
|
|
Log.error('onDeepLinkError: Unexpect empty deep link callback');
|
|
|
|
_completer?.complete(left(AuthError.emptyDeeplink));
|
|
|
|
_completer = null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-12-21 08:12:40 +08:00
|
|
|
Either<(), FlowyError> _isAuthCallbackDeeplink(Uri uri) {
|
|
|
|
if (uri.fragment.contains('access_token')) {
|
|
|
|
return left(());
|
|
|
|
}
|
|
|
|
|
|
|
|
return right(
|
|
|
|
FlowyError.create()
|
|
|
|
..code = ErrorCode.MissingAuthField
|
|
|
|
..msg = uri.path,
|
|
|
|
);
|
2023-11-29 12:55:13 -08:00
|
|
|
}
|
|
|
|
}
|
2023-10-12 20:25:00 +08:00
|
|
|
|
|
|
|
class InitAppFlowyCloudTask extends LaunchTask {
|
2023-10-24 23:13:51 +08:00
|
|
|
UserAuthStateListener? _authStateListener;
|
2023-10-12 20:25:00 +08:00
|
|
|
bool isLoggingOut = false;
|
|
|
|
|
|
|
|
@override
|
|
|
|
Future<void> initialize(LaunchContext context) async {
|
|
|
|
if (!isAppFlowyCloudEnabled) {
|
|
|
|
return;
|
|
|
|
}
|
2023-10-24 23:13:51 +08:00
|
|
|
_authStateListener = UserAuthStateListener();
|
2023-10-12 20:25:00 +08:00
|
|
|
|
2023-10-24 23:13:51 +08:00
|
|
|
_authStateListener?.start(
|
2023-10-12 20:25:00 +08:00
|
|
|
didSignIn: () {
|
|
|
|
isLoggingOut = false;
|
|
|
|
},
|
|
|
|
onInvalidAuth: (message) async {
|
2023-10-24 20:11:06 +08:00
|
|
|
Log.error(message);
|
2023-10-12 20:25:00 +08:00
|
|
|
if (!isLoggingOut) {
|
|
|
|
await runAppFlowy();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
2023-10-24 23:13:51 +08:00
|
|
|
|
|
|
|
@override
|
|
|
|
Future<void> dispose() async {
|
|
|
|
await _authStateListener?.stop();
|
|
|
|
_authStateListener = null;
|
|
|
|
}
|
2023-10-12 20:25:00 +08:00
|
|
|
}
|
2023-11-29 12:55:13 -08:00
|
|
|
|
|
|
|
class DeepLinkResult {
|
|
|
|
final DeepLinkState state;
|
|
|
|
final Either<FlowyError, UserProfilePB>? result;
|
|
|
|
|
|
|
|
DeepLinkResult({required this.state, this.result});
|
|
|
|
}
|
|
|
|
|
|
|
|
enum DeepLinkState {
|
|
|
|
none,
|
|
|
|
loading,
|
|
|
|
finish,
|
|
|
|
}
|