From 5a2106293cb5859bade6dbb119b49167e152bd4a Mon Sep 17 00:00:00 2001 From: phuoc Date: Thu, 24 Oct 2024 10:57:44 +0700 Subject: [PATCH 01/57] support passkey, unsupport didKey and tezos Signed-off-by: phuoc --- .../autonomy_flutter/BackupDartPlugin.kt | 121 +++++- ios/Runner/AppDelegate.swift | 15 + ios/Runner/Constant.swift | 2 + ios/Runner/SystemChannelHandler.swift | 67 +++ lib/common/injector.dart | 13 +- .../entity/draft_customer_support.g.dart | 2 +- lib/gateway/iap_api.dart | 5 + lib/gateway/user_api.dart | 37 ++ lib/gateway/user_api.g.dart | 216 ++++++++++ .../account_settings/cloud_manager.dart | 27 +- lib/model/connection_supports.g.dart | 2 +- lib/model/credential_request_option.dart | 99 +++++ lib/model/customer_support.g.dart | 8 +- lib/model/ether_gas.g.dart | 12 +- lib/model/ff_artwork.g.dart | 4 +- lib/model/jwt.dart | 7 +- lib/model/merchandise_order.g.dart | 2 +- lib/model/passkey_creation_option.dart | 104 +++++ lib/screen/bloc/accounts/accounts_bloc.dart | 2 +- lib/service/account_service.dart | 137 +++---- lib/service/address_service.dart | 17 +- lib/service/auth_service.dart | 24 +- lib/service/backup_service.dart | 174 -------- lib/service/configuration_service.dart | 13 + lib/service/passkey_service.dart | 119 ++++++ lib/service/settings_data_service.dart | 56 +-- lib/util/dio_interceptors.dart | 3 +- ...channel.dart => user_account_channel.dart} | 58 ++- lib/util/wallet_address_ext.dart | 2 +- pubspec.lock | 72 +++- pubspec.yaml | 3 +- .../mock_configuration_service.mocks.dart | 381 +++++++++--------- 32 files changed, 1229 insertions(+), 575 deletions(-) create mode 100644 lib/gateway/user_api.dart create mode 100644 lib/gateway/user_api.g.dart create mode 100644 lib/model/credential_request_option.dart create mode 100644 lib/model/passkey_creation_option.dart delete mode 100644 lib/service/backup_service.dart create mode 100644 lib/service/passkey_service.dart rename lib/util/{primary_address_channel.dart => user_account_channel.dart} (60%) diff --git a/android/app/src/main/kotlin/com/bitmark/autonomy_flutter/BackupDartPlugin.kt b/android/app/src/main/kotlin/com/bitmark/autonomy_flutter/BackupDartPlugin.kt index 71885e16c..03602d0e3 100644 --- a/android/app/src/main/kotlin/com/bitmark/autonomy_flutter/BackupDartPlugin.kt +++ b/android/app/src/main/kotlin/com/bitmark/autonomy_flutter/BackupDartPlugin.kt @@ -32,7 +32,9 @@ class BackupDartPlugin : MethodChannel.MethodCallHandler { private lateinit var context: Context private lateinit var disposables: CompositeDisposable private lateinit var client: BlockstoreClient - private final val primaryAddressStoreKey = "primary_address" + private val primaryAddressStoreKey = "primary_address" + private val userIdStoreKey = "user_id" + private val didRegisterPasskeys = "did_register_passkeys" fun createChannels(@NonNull flutterEngine: FlutterEngine, @NonNull context: Context) { this.context = context @@ -50,6 +52,11 @@ class BackupDartPlugin : MethodChannel.MethodCallHandler { "setPrimaryAddress" -> setPrimaryAddress(call, result) "getPrimaryAddress" -> getPrimaryAddress(call, result) "clearPrimaryAddress" -> clearPrimaryAddress(call, result) + "setUserId" -> setUserId(call, result) + "getUserId" -> getUserId(call, result) + "clearUserId" -> clearUserId(call, result) + "setDidRegisterPasskey" -> setDidRegisterPasskey(call, result) + "didRegisterPasskey" -> didRegisterPasskey(call, result) "deleteKeys" -> deleteKeys(call, result) else -> { result.notImplemented() @@ -259,6 +266,118 @@ class BackupDartPlugin : MethodChannel.MethodCallHandler { } } + private fun setUserId(call: MethodCall, result: MethodChannel.Result) { + val data: String = call.argument("data") ?: return + + val storeBytesBuilder = StoreBytesData.Builder() + .setKey(userIdStoreKey) + .setBytes(data.toByteArray(Charsets.UTF_8)) + + client.storeBytes(storeBytesBuilder.build()) + .addOnSuccessListener { + + Log.e("setUserId", "user id set successfully"); + result.success("") + } + .addOnFailureListener { e -> + Log.e("setUserId", e.message ?: "") + result.error("setUserId error", e.message, e) + } + } + + private fun getUserId(call: MethodCall, result: MethodChannel.Result) { + val request = RetrieveBytesRequest.Builder() + .setKeys(listOf(userIdStoreKey)) // Specify the key + .build() + client.retrieveBytes(request) + .addOnSuccessListener { + try { // Retrieve bytes using the key + val dataMap = it.blockstoreDataMap[userIdStoreKey] + if (dataMap != null) { + val bytes = dataMap.bytes + val idString = bytes.toString(Charsets.UTF_8) + Log.d("getUserId", idString) + + + result.success(idString) + } else { + Log.e("getUserId", "No data found for the key") + result.success(null) + } + } catch (e: Exception) { + Log.e("getUserId", e.message ?: "Error decoding data") + //No primary address found + result.success("") + } + } + .addOnFailureListener { + //Block store not available + result.error("getUserId Block store error", it.message, it) + } + } + + private fun clearUserId(call: MethodCall, result: MethodChannel.Result) { + val retrieveRequest = DeleteBytesRequest.Builder() + .setKeys(listOf(userIdStoreKey)) + .build() + client.deleteBytes(retrieveRequest) + .addOnSuccessListener { + result.success(it) + } + .addOnFailureListener { + result.error("deleteUserId error", it.message, it) + } + } + + private fun setDidRegisterPasskey(call: MethodCall, result: MethodChannel.Result) { + val data: Boolean = call.argument("data") ?: false + + val storeBytesBuilder = StoreBytesData.Builder() + .setKey(didRegisterPasskeys) + .setBytes(data.toString().toByteArray(Charsets.UTF_8)) + + client.storeBytes(storeBytesBuilder.build()) + .addOnSuccessListener { + + Log.e("setDidRegisterPasskey", data.toString()); + result.success("") + } + .addOnFailureListener { e -> + Log.e("setDidRegisterPasskey", e.message ?: "") + result.error("setDidRegisterPasskey error", e.message, e) + } + } + + private fun didRegisterPasskey(call: MethodCall, result: MethodChannel.Result) { + val request = RetrieveBytesRequest.Builder() + .setKeys(listOf(didRegisterPasskeys)) // Specify the key + .build() + client.retrieveBytes(request) + .addOnSuccessListener { + try { // Retrieve bytes using the key + val dataMap = it.blockstoreDataMap[userIdStoreKey] + if (dataMap != null) { + val bytes = dataMap.bytes + val resultString = bytes.toString(Charsets.UTF_8) + Log.d("didRegisterPasskey", resultString) + + + result.success(resultString.toBoolean()) + } else { + Log.e("didRegisterPasskey", "No data found for the key") + result.success(false) + } + } catch (e: Exception) { + Log.e("didRegisterPasskey", e.message ?: "Error decoding data") + //No primary address found + result.success(false) + } + } + .addOnFailureListener { + //Block store not available + result.error("didRegisterPasskey Block store error", it.message, it) + } + } private fun deleteKeys(call: MethodCall, result: MethodChannel.Result) { val deleteRequestBuilder = DeleteBytesRequest.Builder() diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index efacc71d2..be1af8722 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -192,6 +192,21 @@ import Logging case "clearPrimaryAddress": SystemChannelHandler.shared.clearPrimaryAddress(call: call) + + case "getUserId": + SystemChannelHandler.shared.getUserId(call: call, result: result) + + case "setUserId": + SystemChannelHandler.shared.setUserId(call: call, result: result) + + case "clearUserId": + SystemChannelHandler.shared.clearUserId(call: call) + + case "didRegisterPasskey": + SystemChannelHandler.shared.didRegisterPasskey(call: call, result: result) + + case "setDidRegisterPasskey": + SystemChannelHandler.shared.setDidRegisterPasskey(call: call, result: result) default: result(FlutterMethodNotImplemented) diff --git a/ios/Runner/Constant.swift b/ios/Runner/Constant.swift index af90bb875..1b0c6676e 100644 --- a/ios/Runner/Constant.swift +++ b/ios/Runner/Constant.swift @@ -36,4 +36,6 @@ struct Constant { return bundleIdentifier.contains("inhouse") } static let primaryAddressKey: String = "primary_address_key" + static let userIdKey: String = "user_id_key" + static let didRegisterPasskeys = "did_register_passkeys" } diff --git a/ios/Runner/SystemChannelHandler.swift b/ios/Runner/SystemChannelHandler.swift index 0c56ff4fe..c323e4089 100644 --- a/ios/Runner/SystemChannelHandler.swift +++ b/ios/Runner/SystemChannelHandler.swift @@ -175,4 +175,71 @@ class SystemChannelHandler: NSObject { } + func setUserId(call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let args = call.arguments as? [String: Any], + let data = args["data"] as? String else { + result(false) + return + } + let keychain = Keychain() + if keychain.set(data.data(using: .utf8)!, forKey: Constant.userIdKey) { + result(true) + } else { + result(false) + } + } + + func getUserId(call: FlutterMethodCall, result: @escaping FlutterResult) { + let keychain = Keychain() + + guard let data = keychain.getData(Constant.userIdKey, isSync: true), + let userId = String(data: data, encoding: .utf8) else { + result("") + return + } + + result(userId) + } + + func clearUserId(call: FlutterMethodCall) { + let keychain = Keychain() + + keychain.remove(key: Constant.userIdKey, isSync: true) + return + } + + func setDidRegisterPasskey(call: FlutterMethodCall, result: @escaping FlutterResult) { + // Safely extract the arguments and handle cases where "data" is nil or invalid, default to false + let args = call.arguments as? [String: Any] + let data = (args?["data"] as? Bool) ?? false + + let keychain = Keychain() + + // Encode Bool to Data + let boolData = Data([data ? 1 : 0]) + + // Safely store the Bool data in Keychain + if keychain.set(boolData, forKey: Constant.didRegisterPasskeys) { + result(true) + } else { + result(false) + } + } + + func didRegisterPasskey(call: FlutterMethodCall, result: @escaping FlutterResult) { + let keychain = Keychain() + + // Safely retrieve data from Keychain + guard let data = keychain.getData(Constant.didRegisterPasskeys, isSync: true) else { + result(false) + return + } + + // Decode the data back to a Bool + let didRegisterPasskeys = data.first == 1 + + // Return the Bool value + result(didRegisterPasskeys) + } + } diff --git a/lib/common/injector.dart b/lib/common/injector.dart index 0aad3e4f4..a7af5625c 100644 --- a/lib/common/injector.dart +++ b/lib/common/injector.dart @@ -23,6 +23,7 @@ import 'package:autonomy_flutter/gateway/pubdoc_api.dart'; import 'package:autonomy_flutter/gateway/remote_config_api.dart'; import 'package:autonomy_flutter/gateway/source_exhibition_api.dart'; import 'package:autonomy_flutter/gateway/tv_cast_api.dart'; +import 'package:autonomy_flutter/gateway/user_api.dart'; import 'package:autonomy_flutter/graphql/account_settings/account_settings_client.dart'; import 'package:autonomy_flutter/graphql/account_settings/cloud_manager.dart'; import 'package:autonomy_flutter/model/canvas_device_info.dart'; @@ -45,7 +46,6 @@ import 'package:autonomy_flutter/service/address_service.dart'; import 'package:autonomy_flutter/service/announcement/announcement_service.dart'; import 'package:autonomy_flutter/service/announcement/announcement_store.dart'; import 'package:autonomy_flutter/service/auth_service.dart'; -import 'package:autonomy_flutter/service/backup_service.dart'; import 'package:autonomy_flutter/service/canvas_client_service_v2.dart'; import 'package:autonomy_flutter/service/chat_auth_service.dart'; import 'package:autonomy_flutter/service/chat_service.dart'; @@ -83,7 +83,7 @@ import 'package:autonomy_flutter/util/au_file_service.dart'; import 'package:autonomy_flutter/util/dio_interceptors.dart'; import 'package:autonomy_flutter/util/dio_util.dart'; import 'package:autonomy_flutter/util/log.dart'; -import 'package:autonomy_flutter/util/primary_address_channel.dart'; +import 'package:autonomy_flutter/util/user_account_channel.dart'; import 'package:dio/dio.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:get_it/get_it.dart'; @@ -196,11 +196,10 @@ Future setupInjector() async { injector(), injector(), injector(), - injector(), )); - injector.registerLazySingleton( - () => PrimaryAddressChannel()); + injector + .registerLazySingleton(() => UserAccountChannel()); injector.registerLazySingleton( () => AddressService(injector(), injector())); @@ -212,6 +211,8 @@ Future setupInjector() async { injector.registerLazySingleton(() => ChatAuthService(injector())); injector.registerLazySingleton( () => IAPApi(authenticatedDio, baseUrl: Environment.autonomyAuthURL)); + injector.registerLazySingleton( + () => UserApi(dio, baseUrl: Environment.autonomyAuthURL)); final tzktUrl = Environment.appTestnetConfig ? Environment.tzktTestnetURL @@ -228,7 +229,6 @@ Future setupInjector() async { RemoteConfigApi(dio, baseUrl: Environment.remoteConfigURL))); injector.registerLazySingleton( () => AuthService(injector(), injector(), injector())); - injector.registerLazySingleton(() => BackupService(injector())); injector .registerLazySingleton(() => TezosBeaconService(injector(), injector())); @@ -248,7 +248,6 @@ Future setupInjector() async { .registerLazySingleton(() => SettingsDataServiceImpl( injector(), injector(), - injector(), )); injector.registerLazySingleton( diff --git a/lib/database/entity/draft_customer_support.g.dart b/lib/database/entity/draft_customer_support.g.dart index c1256d87b..ea0899cf5 100644 --- a/lib/database/entity/draft_customer_support.g.dart +++ b/lib/database/entity/draft_customer_support.g.dart @@ -15,7 +15,7 @@ DraftCustomerSupportData _$DraftCustomerSupportDataFromJson( .toList(), title: json['title'] as String?, artworkReportID: json['artworkReportID'] as String?, - rating: json['rating'] as int? ?? 0, + rating: (json['rating'] as num?)?.toInt() ?? 0, announcementContentId: json['announcementContentId'] as String?, ); diff --git a/lib/gateway/iap_api.dart b/lib/gateway/iap_api.dart index 75d27efde..4a54991f9 100644 --- a/lib/gateway/iap_api.dart +++ b/lib/gateway/iap_api.dart @@ -21,6 +21,11 @@ abstract class IAPApi { static const addressAuthenticationPath = '/apis/v2/addresses/auth'; static const registerPrimaryAddressPath = '/apis/v2/addresses/primary'; + static const shouldIgnoreAuthorizationPaths = [ + addressAuthenticationPath, + registerPrimaryAddressPath, + ]; + factory IAPApi(Dio dio, {String baseUrl}) = _IAPApi; @POST(addressAuthenticationPath) diff --git a/lib/gateway/user_api.dart b/lib/gateway/user_api.dart new file mode 100644 index 000000000..8de00d0e5 --- /dev/null +++ b/lib/gateway/user_api.dart @@ -0,0 +1,37 @@ +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// Copyright © 2022 Bitmark. All rights reserved. +// Use of this source code is governed by the BSD-2-Clause Plus Patent License +// that can be found in the LICENSE file. +// + +import 'package:autonomy_flutter/model/credential_request_option.dart'; +import 'package:autonomy_flutter/model/jwt.dart'; +import 'package:autonomy_flutter/model/passkey_creation_option.dart'; +import 'package:dio/dio.dart'; +import 'package:retrofit/retrofit.dart'; + +part 'user_api.g.dart'; + +@RestApi(baseUrl: '') +abstract class UserApi { + factory UserApi(Dio dio, {String baseUrl}) = _UserApi; + + @POST('/apis/users/passkeys/registration/initialize') + Future registerInitialize(); + + @POST('/apis/users/passkeys/registration/finalize') + Future registerFinalize(@Body() Map body); + + @POST('/apis/users/passkeys/login/initialize') + Future logInInitialize(); + + @POST('/apis/users/passkeys/login/finalize') + Future logInFinalize(@Body() Map body); + + @POST('/apis/users/addresses/authenticate') + Future authenticateAddress(@Body() Map body); + + @POST('/apis/users/jwt/refresh') + Future refreshJWT(@Body() Map body); +} diff --git a/lib/gateway/user_api.g.dart b/lib/gateway/user_api.g.dart new file mode 100644 index 000000000..ab203bdbc --- /dev/null +++ b/lib/gateway/user_api.g.dart @@ -0,0 +1,216 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'user_api.dart'; + +// ************************************************************************** +// RetrofitGenerator +// ************************************************************************** + +// ignore_for_file: unnecessary_brace_in_string_interps,no_leading_underscores_for_local_identifiers + +class _UserApi implements UserApi { + _UserApi( + this._dio, { + this.baseUrl, + }); + + final Dio _dio; + + String? baseUrl; + + @override + Future registerInitialize() async { + const _extra = {}; + final queryParameters = {}; + final _headers = {}; + final Map? _data = null; + final _result = await _dio.fetch>( + _setStreamType(Options( + method: 'POST', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/apis/users/passkeys/registration/initialize', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + )))); + final value = CredentialCreationOption.fromJson(_result.data!); + return value; + } + + @override + Future registerFinalize(Map body) async { + const _extra = {}; + final queryParameters = {}; + final _headers = {}; + final _data = {}; + _data.addAll(body); + final _result = + await _dio.fetch>(_setStreamType(Options( + method: 'POST', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/apis/users/passkeys/registration/finalize', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + )))); + final value = JWT.fromJson(_result.data!); + return value; + } + + @override + Future logInInitialize() async { + const _extra = {}; + final queryParameters = {}; + final _headers = {}; + final Map? _data = null; + final _result = await _dio.fetch>( + _setStreamType(Options( + method: 'POST', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/apis/users/passkeys/login/initialize', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + )))); + final value = CredentialRequestOption.fromJson(_result.data!); + return value; + } + + @override + Future logInFinalize(Map body) async { + const _extra = {}; + final queryParameters = {}; + final _headers = {}; + final _data = {}; + _data.addAll(body); + final _result = + await _dio.fetch>(_setStreamType(Options( + method: 'POST', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/apis/users/passkeys/login/finalize', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + )))); + final value = JWT.fromJson(_result.data!); + return value; + } + + @override + Future authenticateAddress(Map body) async { + const _extra = {}; + final queryParameters = {}; + final _headers = {}; + final _data = {}; + _data.addAll(body); + final _result = + await _dio.fetch>(_setStreamType(Options( + method: 'POST', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/apis/users/addresses/authenticate', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + )))); + final value = JWT.fromJson(_result.data!); + return value; + } + + @override + Future refreshJWT(Map body) async { + const _extra = {}; + final queryParameters = {}; + final _headers = {}; + final _data = {}; + _data.addAll(body); + final _result = + await _dio.fetch>(_setStreamType(Options( + method: 'POST', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/apis/users/jwt/refresh', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + )))); + final value = JWT.fromJson(_result.data!); + return value; + } + + RequestOptions _setStreamType(RequestOptions requestOptions) { + if (T != dynamic && + !(requestOptions.responseType == ResponseType.bytes || + requestOptions.responseType == ResponseType.stream)) { + if (T == String) { + requestOptions.responseType = ResponseType.plain; + } else { + requestOptions.responseType = ResponseType.json; + } + } + return requestOptions; + } + + String _combineBaseUrls( + String dioBaseUrl, + String? baseUrl, + ) { + if (baseUrl == null || baseUrl.trim().isEmpty) { + return dioBaseUrl; + } + + final url = Uri.parse(baseUrl); + + if (url.isAbsolute) { + return url.toString(); + } + + return Uri.parse(dioBaseUrl).resolveUri(url).toString(); + } +} diff --git a/lib/graphql/account_settings/cloud_manager.dart b/lib/graphql/account_settings/cloud_manager.dart index 3538ec7de..f0e2ea50d 100644 --- a/lib/graphql/account_settings/cloud_manager.dart +++ b/lib/graphql/account_settings/cloud_manager.dart @@ -130,17 +130,16 @@ class CloudManager { } Future copyDataFrom(CloudDatabase source) async { - await source.addressDao.getAllAddresses().then((addresses) async { - final data = addresses.map((e) => e.toKeyValue).toList(); - await _walletAddressObject.db.write(data); - }); - - await source.connectionDao.getConnections().then((connections) async { - final data = connections.map((e) => e.toKeyValue).toList(); - await _connectionObject.db.write(data); - }); - try { + await source.addressDao.getAllAddresses().then((addresses) async { + final data = addresses.map((e) => e.toKeyValue).toList(); + await _walletAddressObject.db.write(data); + }); + + await source.connectionDao.getConnections().then((connections) async { + final data = connections.map((e) => e.toKeyValue).toList(); + await _connectionObject.db.write(data); + }); await injector().deleteAllProfiles(_requester); } catch (_) {} } @@ -169,12 +168,4 @@ class CloudManager { Future deleteAll() async { await injector().delete(vars: {'search': ''}); } - - Future uploadCurrentCache() async { - await _walletAddressObject.db.uploadCurrentCache(); - await _connectionObject.db.uploadCurrentCache(); - await _deviceSettingsDB.uploadCurrentCache(); - await _userSettingsDB.uploadCurrentCache(); - await _playlistCloudObject.db.uploadCurrentCache(); - } } diff --git a/lib/model/connection_supports.g.dart b/lib/model/connection_supports.g.dart index d468efb22..f4d219459 100644 --- a/lib/model/connection_supports.g.dart +++ b/lib/model/connection_supports.g.dart @@ -10,7 +10,7 @@ BeaconConnectConnection _$BeaconConnectConnectionFromJson( Map json) => BeaconConnectConnection( personaUuid: json['personaUuid'] as String, - index: json['index'] as int? ?? 0, + index: (json['index'] as num?)?.toInt() ?? 0, peer: P2PPeer.fromJson(json['peer'] as Map), ); diff --git a/lib/model/credential_request_option.dart b/lib/model/credential_request_option.dart new file mode 100644 index 000000000..c5406b15c --- /dev/null +++ b/lib/model/credential_request_option.dart @@ -0,0 +1,99 @@ +import 'package:passkeys/types.dart'; + +// Main model class for CredentialRequestOption +class CredentialRequestOption { + final PublicKeyCredentialRequestOptions publicKey; + final MediationType mediation; + + CredentialRequestOption({ + required this.publicKey, + required this.mediation, + }); + + factory CredentialRequestOption.fromJson(Map json) => + CredentialRequestOption( + publicKey: + PublicKeyCredentialRequestOptions.fromJson(json['publicKey']), + mediation: getMediationTypeFromString(json['mediation']), + ); +} + +// Model for CredProps (within ClientExtensionResults) +class CredProps { + final bool? rk; + + CredProps({this.rk}); + + factory CredProps.fromJson(Map json) => + CredProps(rk: json['rk']); +} + +// Model for Extensions (appid, appidExclude, and credProps) +class Extensions { + final bool? appid; + final bool? appidExclude; + final CredProps? credProps; + + Extensions({ + this.appid, + this.appidExclude, + this.credProps, + }); + + factory Extensions.fromJson(Map json) => Extensions( + appid: json['appid'], + appidExclude: json['appidExclude'], + credProps: json['credProps'] != null + ? CredProps.fromJson(json['credProps']) + : null, + ); +} + +// Model for PublicKeyCredentialRequestOptions +class PublicKeyCredentialRequestOptions { + final String challenge; + final int? timeout; + final String? rpId; + final List? allowCredentials; + final String? userVerification; + final Extensions? extensions; + + PublicKeyCredentialRequestOptions({ + required this.challenge, + this.timeout, + this.rpId, + this.allowCredentials, + this.userVerification, + this.extensions, + }); + + factory PublicKeyCredentialRequestOptions.fromJson( + Map json) => + PublicKeyCredentialRequestOptions( + challenge: json['challenge'], + timeout: json['timeout'], + rpId: json['rpId'], + allowCredentials: json['allowCredentials'] != null + ? (json['allowCredentials'] as List) + .map((cred) => CredentialType.fromJson(cred)) + .toList() + : null, + userVerification: json['userVerification'], + extensions: json['extensions'] != null + ? Extensions.fromJson(json['extensions']) + : null, + ); +} + +MediationType getMediationTypeFromString(String mediation) { + switch (mediation.toLowerCase()) { + case 'silent': + return MediationType.Silent; + case 'optional': + return MediationType.Optional; + case 'required': + return MediationType.Required; + default: + throw Exception('Unknown mediation type: $mediation'); + } +} diff --git a/lib/model/customer_support.g.dart b/lib/model/customer_support.g.dart index 44a164252..56052f33c 100644 --- a/lib/model/customer_support.g.dart +++ b/lib/model/customer_support.g.dart @@ -12,15 +12,15 @@ Issue _$IssueFromJson(Map json) => Issue( title: json['title'] as String, tags: (json['tags'] as List).map((e) => e as String).toList(), timestamp: DateTime.parse(json['timestamp'] as String), - total: json['total'] as int, - unread: json['unread'] as int, + total: (json['total'] as num).toInt(), + unread: (json['unread'] as num).toInt(), lastMessage: json['last_message'] == null ? null : Message.fromJson(json['last_message'] as Map), firstMessage: json['first_message'] == null ? null : Message.fromJson(json['first_message'] as Map), - rating: json['rating'] as int, + rating: (json['rating'] as num).toInt(), announcementContentId: json['announcement_content_id'] as String?, ); @@ -65,7 +65,7 @@ Map _$ReceiveAttachmentToJson(ReceiveAttachment instance) => }; Message _$MessageFromJson(Map json) => Message( - id: json['id'] as int, + id: (json['id'] as num).toInt(), read: json['read'] as bool, from: json['from'] as String, message: json['message'] as String, diff --git a/lib/model/ether_gas.g.dart b/lib/model/ether_gas.g.dart index e7f977e3f..5726abf71 100644 --- a/lib/model/ether_gas.g.dart +++ b/lib/model/ether_gas.g.dart @@ -7,7 +7,7 @@ part of 'ether_gas.dart'; // ************************************************************************** EtherGas _$EtherGasFromJson(Map json) => EtherGas( - code: json['code'] as int, + code: (json['code'] as num).toInt(), data: EtherGasData.fromJson(json['data'] as Map), ); @@ -17,11 +17,11 @@ Map _$EtherGasToJson(EtherGas instance) => { }; EtherGasData _$EtherGasDataFromJson(Map json) => EtherGasData( - rapid: json['rapid'] as int?, - fast: json['fast'] as int?, - standard: json['standard'] as int?, - slow: json['slow'] as int?, - timestamp: json['timestamp'] as int?, + rapid: (json['rapid'] as num?)?.toInt(), + fast: (json['fast'] as num?)?.toInt(), + standard: (json['standard'] as num?)?.toInt(), + slow: (json['slow'] as num?)?.toInt(), + timestamp: (json['timestamp'] as num?)?.toInt(), priceUSD: (json['priceUSD'] as num?)?.toDouble(), ); diff --git a/lib/model/ff_artwork.g.dart b/lib/model/ff_artwork.g.dart index c806c6dbb..4e950c89a 100644 --- a/lib/model/ff_artwork.g.dart +++ b/lib/model/ff_artwork.g.dart @@ -19,7 +19,7 @@ Map _$ArtworkResponseToJson(ArtworkResponse instance) => Artwork _$ArtworkFromJson(Map json) => Artwork( json['id'] as String, json['seriesID'] as String, - json['index'] as int, + (json['index'] as num).toInt(), json['name'] as String, json['category'] as String?, json['ownerAddress'] as String?, @@ -79,7 +79,7 @@ ArtworkAttribute _$ArtworkAttributeFromJson(Map json) => id: json['id'] as String, artworkID: json['artworkID'] as String, seriesID: json['seriesID'] as String, - index: json['index'] as int, + index: (json['index'] as num).toInt(), percentage: (json['percentage'] as num).toDouble(), traitType: json['traitType'] as String, value: json['value'] as String, diff --git a/lib/model/jwt.dart b/lib/model/jwt.dart index 03b58b1ab..248a25c1a 100644 --- a/lib/model/jwt.dart +++ b/lib/model/jwt.dart @@ -51,16 +51,19 @@ enum MembershipType { class JWT { int? expireIn; String jwtToken; + String accessToken; - JWT({required this.jwtToken, this.expireIn}); + JWT({required this.jwtToken, this.expireIn, this.accessToken = ''}); JWT.fromJson(Map json) : expireIn = double.tryParse(json['expire_in'].toString())?.toInt(), - jwtToken = json['jwt_token']; + jwtToken = json['jwt_token'], + accessToken = json['access_token'] ?? ''; Map toJson() => { 'expire_in': expireIn, 'jwt_token': jwtToken, + 'access_token': accessToken, }; bool _isValid() { diff --git a/lib/model/merchandise_order.g.dart b/lib/model/merchandise_order.g.dart index d19e0e0d0..d512c12a8 100644 --- a/lib/model/merchandise_order.g.dart +++ b/lib/model/merchandise_order.g.dart @@ -42,7 +42,7 @@ Map _$OrderDataToJson(OrderData instance) => { Item _$ItemFromJson(Map json) => Item( variant: Variant.fromJson(json['variant'] as Map), - quantity: json['quantity'] as int, + quantity: (json['quantity'] as num).toInt(), ); Map _$ItemToJson(Item instance) => { diff --git a/lib/model/passkey_creation_option.dart b/lib/model/passkey_creation_option.dart new file mode 100644 index 000000000..80eff447b --- /dev/null +++ b/lib/model/passkey_creation_option.dart @@ -0,0 +1,104 @@ +import 'package:passkeys/types.dart'; + +class CredentialCreationOption { + final PublicKey publicKey; + + CredentialCreationOption({ + required this.publicKey, + }); + + factory CredentialCreationOption.fromJson(Map json) => + CredentialCreationOption( + publicKey: PublicKey.fromJson(json['publicKey']), + ); + + Map toJson() => { + 'publicKey': publicKey.toJson(), + }; +} + +// Main model class for CredentialCreationOption +class PublicKey { + final RelyingPartyType rp; + final UserType user; + final String challenge; + final List pubKeyCredParams; + final int? timeout; + final List? excludeCredentials; + final AuthenticatorSelectionType? authenticatorSelection; + final String? attestation; + final Map? extensions; + + PublicKey({ + required this.rp, + required this.user, + required this.challenge, + required this.pubKeyCredParams, + this.timeout, + this.excludeCredentials, + this.authenticatorSelection, + this.attestation, + this.extensions, + }); + + factory PublicKey.fromJson(Map json) => PublicKey( + rp: RelyingPartyType.fromJson(json['rp']), + user: UserType.fromJson(json['user']), + challenge: json['challenge'], + pubKeyCredParams: (json['pubKeyCredParams'] as List) + .map((param) => PubKeyCredParamType.fromJson(param)) + .toList(), + timeout: json['timeout'], + excludeCredentials: json['excludeCredentials'] != null + ? (json['excludeCredentials'] as List) + .map((cred) => CredentialType.fromJson(cred)) + .toList() + : null, + authenticatorSelection: json['authenticatorSelection'] != null + ? AuthenticatorSelectionType.fromJson( + json['authenticatorSelection']) + : null, + attestation: json['attestation'], + extensions: json['extensions'], + ); + + Map toJson() => { + 'rp': rp.toJson(), + 'user': user.toJson(), + 'challenge': challenge, + 'pubKeyCredParams': + pubKeyCredParams.map((param) => param.toJson()).toList(), + 'timeout': timeout, + 'excludeCredentials': + excludeCredentials?.map((cred) => cred.toJson()).toList(), + 'authenticatorSelection': authenticatorSelection?.toJson(), + 'attestation': attestation, + 'extensions': extensions, + }; +} + +// Model for authentication-selection-entity (AuthenticatorSelectionCriteria) +class AuthenticatorSelectionEntity { + final String? authenticatorAttachment; + final bool? requireResidentKey; + final String? userVerification; + + AuthenticatorSelectionEntity({ + this.authenticatorAttachment, + this.requireResidentKey, + this.userVerification, + }); + + factory AuthenticatorSelectionEntity.fromJson(Map json) => + AuthenticatorSelectionEntity( + authenticatorAttachment: json['authenticatorAttachment'], + requireResidentKey: json['requireResidentKey'], + userVerification: json['userVerification'], + ); + + Map toJson() => { + 'authenticatorAttachment': authenticatorAttachment, + 'requireResidentKey': requireResidentKey, + 'userVerification': userVerification, + }; +} diff --git a/lib/screen/bloc/accounts/accounts_bloc.dart b/lib/screen/bloc/accounts/accounts_bloc.dart index 719152873..42e6cdff3 100644 --- a/lib/screen/bloc/accounts/accounts_bloc.dart +++ b/lib/screen/bloc/accounts/accounts_bloc.dart @@ -16,7 +16,7 @@ import 'package:autonomy_flutter/model/network.dart'; import 'package:autonomy_flutter/service/account_service.dart'; import 'package:autonomy_flutter/service/address_service.dart'; import 'package:autonomy_flutter/util/constants.dart'; -import 'package:autonomy_flutter/util/primary_address_channel.dart'; +import 'package:autonomy_flutter/util/user_account_channel.dart'; import 'package:autonomy_flutter/util/wallet_utils.dart'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; diff --git a/lib/service/account_service.dart b/lib/service/account_service.dart index 98469a951..3cc43f700 100644 --- a/lib/service/account_service.dart +++ b/lib/service/account_service.dart @@ -18,10 +18,8 @@ import 'package:autonomy_flutter/model/shared_postcard.dart'; import 'package:autonomy_flutter/model/wc2_request.dart'; import 'package:autonomy_flutter/screen/bloc/scan_wallet/scan_wallet_state.dart'; import 'package:autonomy_flutter/service/address_service.dart'; -import 'package:autonomy_flutter/service/backup_service.dart'; import 'package:autonomy_flutter/service/configuration_service.dart'; import 'package:autonomy_flutter/service/keychain_service.dart'; -import 'package:autonomy_flutter/service/settings_data_service.dart'; import 'package:autonomy_flutter/service/tezos_beacon_service.dart'; import 'package:autonomy_flutter/util/android_backup_channel.dart'; import 'package:autonomy_flutter/util/constants.dart'; @@ -29,7 +27,7 @@ import 'package:autonomy_flutter/util/device.dart'; import 'package:autonomy_flutter/util/exception.dart'; import 'package:autonomy_flutter/util/ios_backup_channel.dart'; import 'package:autonomy_flutter/util/log.dart'; -import 'package:autonomy_flutter/util/primary_address_channel.dart' +import 'package:autonomy_flutter/util/user_account_channel.dart' as primary_address_channel; import 'package:autonomy_flutter/util/string_ext.dart'; import 'package:autonomy_flutter/util/wallet_address_ext.dart'; @@ -64,7 +62,7 @@ abstract class AccountService { Future isAndroidEndToEndEncryptionAvailable(); - Future> createNewWallet( + Future createNewWallet( {String name = '', String passphrase = ''}); Future importWords(String words, String passphrase, @@ -120,7 +118,6 @@ class AccountServiceImpl extends AccountService { final ConfigurationService _configurationService; final AndroidBackupChannel _androidBackupChannel = AndroidBackupChannel(); final IOSBackupChannel _iosBackupChannel = IOSBackupChannel(); - final BackupService _backupService; final nft.AddressService _nftCollectionAddressService; final AddressService _addressService; final CloudManager _cloudObject; @@ -128,31 +125,19 @@ class AccountServiceImpl extends AccountService { AccountServiceImpl( this._tezosBeaconService, this._configurationService, - this._backupService, this._nftCollectionAddressService, this._addressService, this._cloudObject, ); @override - Future> createNewWallet( + Future createNewWallet( {String name = '', String passphrase = ''}) async { final uuid = const Uuid().v4(); final walletStorage = LibAukDart.getWallet(uuid); await walletStorage.createKey(passphrase, name); log.fine('[AccountService] Created persona $uuid}'); - - await _addressService.registerPrimaryAddress( - info: primary_address_channel.AddressInfo( - uuid: uuid, - chain: 'ethereum', - index: 0, - )); - - final wallets = await insertNextAddressFromUuid(uuid, WalletType.MultiChain, - name: name); - await androidBackupKeys(); - return wallets; + return walletStorage; } @override @@ -220,14 +205,10 @@ class AccountServiceImpl extends AccountService { Future _getDefaultWallet() async { /// we can improve this by checking if the wallet is exist in the server - String? uuid; - if (Platform.isIOS) { - final uuids = await _iosBackupChannel.getUUIDsFromKeychain(); - uuid = uuids.firstOrNull; - } else { - final accounts = await _androidBackupChannel.restoreKeys(); - uuid = accounts.firstOrNull?.uuid; - } + + final uuids = await _getUuidsFromLocal(); + + String? uuid = uuids.firstOrNull; if (uuid == null) { return null; @@ -235,6 +216,15 @@ class AccountServiceImpl extends AccountService { return LibAukDart.getWallet(uuid); } + Future> _getUuidsFromLocal() async { + if (Platform.isIOS) { + return await _iosBackupChannel.getUUIDsFromKeychain(); + } else { + final accounts = await _androidBackupChannel.restoreKeys(); + return accounts.map((e) => e.uuid).toList(); + } + } + Future getPrimaryWallet() async { final primaryAddress = await injector().getPrimaryAddressInfo(); @@ -636,6 +626,11 @@ class AccountServiceImpl extends AccountService { await _cloudObject.addressObject.updateAddresses([walletAddress]); } + Future _createInitialJwt() async { + // this might change to a callback for login finalized + // + } + @override Future migrateAccount() async { log.info('[AccountService] migrateAccount'); @@ -662,25 +657,46 @@ class AccountServiceImpl extends AccountService { /// that is their default account (using to save cloud data base) /// but if user has no primary address, but have backup version, /// we will take the first uuid as default account + /// + /// Migrate passkeys note: + /// To simplify this migration on mobile, we will not support restoring + /// backed-up data for users who uninstall and then reinstall apps that + /// still use didKey and the Tezos primary address. + /// The data that will not be restored includes playlists, settings, + /// and derived addresses. However, we will automatically derive a pair + /// of addresses for each wallet that users own. // case 1: complete new user, no primary address, no backup keychain // nothing to do other than create new wallet + /// create passkeys, no need to migrate if (defaultWallet == null) { log.info('[AccountService] migrateAccount: case 1 complete new user'); - await createNewWallet(); + final wallet = await createNewWallet(); + await _addressService.registerPrimaryAddress( + info: primary_address_channel.AddressInfo( + uuid: wallet.uuid, + chain: 'ethereum', + index: 0, + )); + await _createInitialJwt(); + await insertNextAddressFromUuid(wallet.uuid, WalletType.MultiChain, + name: ''); + await androidBackupKeys(); unawaited(_cloudObject.setMigrated()); log.info('[AccountService] migrateAccount: case 1 finished'); return; } // case 2: update app from old version using did key - if (addressInfo == null && isDoneOnboarding) { + // case 3: restore app from old version using did key + // we won't restore data, just derive addresses automatically + if (addressInfo == null) { log.info('[AccountService] migrateAccount: ' - 'case 2 update app from old version using did key'); + 'case 2/3 update/restore app from old version using did key'); await _addressService.registerPrimaryAddress( info: primary_address_channel.AddressInfo( uuid: defaultWallet.uuid, chain: 'ethereum', index: 0), - withDidKey: true, ); + await _createInitialJwt(); await _cloudObject.copyDataFrom(cloudDb); unawaited(cloudDb.removeAll()); @@ -691,31 +707,14 @@ class AccountServiceImpl extends AccountService { return; } - // case 3: restore app from old version using did key - // we register first uuid as primary address (with didKey = true) - // then restore - if (addressInfo == null && !isDoneOnboarding) { - log.info('[AccountService] migrateAccount: ' - 'case 3 restore app from old version using did key'); - await _addressService.registerPrimaryAddress( - info: primary_address_channel.AddressInfo( - uuid: defaultWallet.uuid, chain: 'ethereum', index: 0), - withDidKey: true, - ); - - await injector() - .restoreSettingsData(fromProfileData: true); - await _backupService.restoreCloudDatabase(); - - // ensure that we have addresses; - unawaited(_ensureHavingWalletAddress()); - log.info('[AccountService] migrateAccount: case 3 finished'); - return; - } - // from case 4, user has primary address, // we need to check if user has migrate to account-settings; + if (!addressInfo.isEthereum) { + await _addressService.migrateToEthereumAddress(addressInfo); + } + await _createInitialJwt(); + // we don't care for user use tezos primary address. // this is to reduce loading time bool didMigrate = _configurationService.didMigrateToAccountSetting(); if (!didMigrate) { @@ -738,41 +737,21 @@ class AccountServiceImpl extends AccountService { // if user has not migrated, there are 2 cases: // update app and restore app - // we need to check if user using ethereum or tezos for each case to migrate + // case 5/6: update/restore app from old version using primary address - // case 5: update app from old version using primary address if (isDoneOnboarding) { + // case 3: restore app from old version using primary address log.info('[AccountService] migrateAccount: ' 'case 5 update app from old version using primary address'); // migrate to ethereum first, then upload to account-settings - if (!addressInfo!.isEthereum) { - await _addressService.migrateToEthereumAddress(addressInfo); - } + await _cloudObject.copyDataFrom(cloudDb); - unawaited(_cloudObject - .setMigrated() - .then((_) => unawaited(cloudDb.removeAll()))); + unawaited(_cloudObject.setMigrated()); log.info('[AccountService] migrateAccount: case 5 finished'); } - // case 6: restore app from old version using primary address - else { - log.info('[AccountService] migrateAccount: ' - 'case 6 restore app from old version using primary address'); - await injector() - .restoreSettingsData(fromProfileData: true); - await _backupService.restoreCloudDatabase(); - // now all data are in _cloudObject cache - if (!addressInfo!.isEthereum) { - // migrate to tezos - await _addressService.migrateToEthereumAddress(addressInfo); - - await _cloudObject.uploadCurrentCache(); - } - - unawaited(_cloudObject.setMigrated()); - log.info('[AccountService] migrateAccount: case 6 finished'); - } + unawaited(cloudDb.removeAll()); + unawaited(_cloudObject.setMigrated()); // ensure that we have addresses; unawaited(_ensureHavingWalletAddress()); diff --git a/lib/service/address_service.dart b/lib/service/address_service.dart index 44f237c72..494f35671 100644 --- a/lib/service/address_service.dart +++ b/lib/service/address_service.dart @@ -12,17 +12,16 @@ import 'package:autonomy_flutter/common/injector.dart'; import 'package:autonomy_flutter/database/entity/wallet_address.dart'; import 'package:autonomy_flutter/graphql/account_settings/cloud_manager.dart'; import 'package:autonomy_flutter/service/auth_service.dart'; -import 'package:autonomy_flutter/service/metric_client_service.dart'; import 'package:autonomy_flutter/util/constants.dart'; import 'package:autonomy_flutter/util/log.dart'; -import 'package:autonomy_flutter/util/primary_address_channel.dart'; +import 'package:autonomy_flutter/util/user_account_channel.dart'; import 'package:autonomy_flutter/util/wallet_storage_ext.dart'; import 'package:libauk_dart/libauk_dart.dart'; import 'package:sentry/sentry.dart'; import 'package:tezart/src/crypto/crypto.dart' as crypto; class AddressService { - final PrimaryAddressChannel _primaryAddressChannel; + final UserAccountChannel _primaryAddressChannel; final CloudManager _cloudObject; AddressService(this._primaryAddressChannel, this._cloudObject); @@ -58,19 +57,9 @@ class AddressService { return true; } - Future registerPrimaryAddress( - {required AddressInfo info, bool withDidKey = false}) async { + Future registerPrimaryAddress({required AddressInfo info}) async { log.info('[AddressService] Registering primary address: ${info.toJson()}'); - await injector().registerPrimaryAddress( - primaryAddressInfo: info, withDidKey: withDidKey); - log.info('[AddressService] Primary address registered: ${info.toJson()}'); final res = await setPrimaryAddressInfo(info: info); - // when register primary address, we need to update the auth token - log.info( - '[AddressService] Getting auth token after primary address registered'); - await injector().getAuthToken(forceRefresh: true); - // we also need to identity the metric client - await injector().identity(); return res; } diff --git a/lib/service/auth_service.dart b/lib/service/auth_service.dart index 0ee769b84..38694c2e3 100644 --- a/lib/service/auth_service.dart +++ b/lib/service/auth_service.dart @@ -18,12 +18,9 @@ import 'package:autonomy_flutter/screen/settings/subscription/upgrade_state.dart import 'package:autonomy_flutter/service/account_service.dart'; import 'package:autonomy_flutter/service/address_service.dart'; import 'package:autonomy_flutter/service/configuration_service.dart'; -import 'package:autonomy_flutter/util/dio_exception_ext.dart'; import 'package:autonomy_flutter/util/log.dart'; import 'package:autonomy_flutter/util/notification_util.dart'; -import 'package:autonomy_flutter/util/primary_address_channel.dart'; -import 'package:dio/dio.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:autonomy_flutter/util/user_account_channel.dart'; class AuthService { final IAPApi _authApi; @@ -104,23 +101,8 @@ class AuthService { } Future _getAuthAddress(Map payload, - {AddressInfo? primaryAddress}) async { - try { - var newJwt = await _authApi.authAddress(payload); - return newJwt; - } on DioException catch (e) { - if (e.ffErrorCode == 998 && primaryAddress != null) { - log.warning('Primary address not registered, retrying'); - unawaited(Sentry.captureMessage('Primary address not registered, ' - 'registerPrimaryAddress ${primaryAddress.uuid}')); - await injector() - .registerPrimaryAddress(info: primaryAddress); - return await _authApi.authAddress(payload); - } else { - rethrow; - } - } - } + {AddressInfo? primaryAddress}) async => + await _authApi.authAddress(payload); Future getAuthToken({ String? messageToSign, diff --git a/lib/service/backup_service.dart b/lib/service/backup_service.dart deleted file mode 100644 index 4de049388..000000000 --- a/lib/service/backup_service.dart +++ /dev/null @@ -1,174 +0,0 @@ -// -// SPDX-License-Identifier: BSD-2-Clause-Patent -// Copyright © 2022 Bitmark. All rights reserved. -// Use of this source code is governed by the BSD-2-Clause Plus Patent License -// that can be found in the LICENSE file. -// - -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; - -import 'package:autonomy_flutter/common/environment.dart'; -import 'package:autonomy_flutter/common/injector.dart'; -import 'package:autonomy_flutter/database/cloud_database.dart'; -import 'package:autonomy_flutter/graphql/account_settings/cloud_manager.dart'; -import 'package:autonomy_flutter/model/backup_versions.dart'; -import 'package:autonomy_flutter/service/account_service.dart'; -import 'package:autonomy_flutter/service/address_service.dart'; -import 'package:autonomy_flutter/service/auth_service.dart'; -import 'package:autonomy_flutter/util/helpers.dart'; -import 'package:autonomy_flutter/util/log.dart'; -import 'package:floor/floor.dart'; -import 'package:http/http.dart' as http; -import 'package:libauk_dart/libauk_dart.dart'; -import 'package:package_info_plus/package_info_plus.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; -import 'package:sqflite/sqflite.dart' as sqflite; -import 'package:sqflite/sqflite.dart'; - -class BackupService { - static const _dbFileName = 'cloud_database.db'; - static const _dbEncryptedFileName = 'cloud_database.db.encrypted'; - - final CloudManager _cloudObjects; - - BackupService(this._cloudObjects); - - Future getBackupVersion(String deviceId) async { - PackageInfo packageInfo = await PackageInfo.fromPlatform(); - String version = packageInfo.version; - final authToken = await injector().getAuthToken(); - - if (authToken == null) { - return ''; - } - - final endpoint = Environment.autonomyAuthURL; - - http.Response? response; - - for (String filename in [_dbEncryptedFileName, _dbFileName]) { - try { - response = await http.get( - Uri.parse( - '$endpoint/apis/v1/premium/profile-data/versions?filename=$filename'), - headers: { - 'requester': deviceId, - 'Authorization': 'Bearer ${authToken.jwtToken}' - }); - if (response.statusCode == 200) { - break; - } - } catch (e) { - log.warning('[BackupService] failed fetch $filename $e'); - } - } - - if (response == null || response.statusCode != 200) { - log.warning('[BackupService] failed fetchBackupVersion'); - return ''; - } - - final result = BackupVersions.fromJson(json.decode(response.body)); - - var versions = result.versions..sort((a, b) => compareVersion(b, a)); - - String backupVersion = ''; - for (String element in versions) { - if (compareVersion(element, version) <= 0) { - backupVersion = element; - break; - } - } - - return backupVersion; - } - - Future deleteAllProfiles(WalletStorage account) async { - log.info('[BackupService][start] deleteAllProfiles'); - String? deviceId = await getBackupId(); - final endpoint = Environment.autonomyAuthURL; - final authToken = await injector().getAuthToken(); - - await http.delete(Uri.parse('$endpoint/apis/v1/premium/profile-data'), - headers: { - 'requester': deviceId, - 'Authorization': 'Bearer ${authToken?.jwtToken}' - }); - } - - Future restoreCloudDatabase({String dbName = 'cloud_database.db'}) async { - log.info('[BackupService] start database restore'); - - String? deviceId = await getBackupId(); - final version = await getBackupVersion(deviceId); - final authToken = await injector().getAuthToken(); - final primaryAddressInfo = - await injector().getPrimaryAddressInfo(); - final account = LibAukDart.getWallet(primaryAddressInfo!.uuid); - final endpoint = Environment.autonomyAuthURL; - final resp = await http.get( - Uri.parse( - '$endpoint/apis/v1/premium/profile-data?filename=$_dbEncryptedFileName&appVersion=$version'), - headers: { - 'requester': deviceId, - 'Authorization': 'Bearer ${authToken?.jwtToken}', - }, - ); - if (resp.statusCode == 200) { - log.info('[BackupService] got response'); - try { - final version = await injector().database.getVersion(); - log.info('[BackupService] Cloud database local version is $version'); - final tempFilePath = - '${(await getTemporaryDirectory()).path}/$_dbEncryptedFileName'; - final tempFile = File(tempFilePath); - await tempFile.writeAsBytes(resp.bodyBytes, flush: true); - const String tempDbName = 'temp_cloud_database.db'; - final dbFilePath = - await sqfliteDatabaseFactory.getDatabasePath(tempDbName); - - try { - await account.decryptFile( - inputPath: tempFilePath, - outputPath: dbFilePath, - ); - } catch (e) { - log.warning('[BackupService] Cloud database decrypted failed,' - ' fallback to legacy method'); - unawaited(Sentry.captureException( - '[BackupService] Cloud database decrypted failed, ' - 'fallback to legacy method, $e')); - await account.decryptFile( - inputPath: tempFilePath, - outputPath: dbFilePath, - usingLegacy: true, - ); - } - - final tempDb = - await $FloorCloudDatabase.databaseBuilder(tempDbName).build(); - await _cloudObjects.copyDataFrom(tempDb); - await tempFile.delete(); - await File(dbFilePath).delete(); - log.info('[BackupService] Cloud database is restored $version'); - return; - } catch (e) { - log.info('[BackupService] Failed to restore Cloud Database $e'); - unawaited(Sentry.captureException(e, stackTrace: StackTrace.current)); - } - } - await _cloudObjects.setMigrated(); - - log.info('[BackupService] done database restore'); - } - - Future getBackupId() async { - PackageInfo packageInfo = await PackageInfo.fromPlatform(); - String? deviceId = await injector().getBackupDeviceID(); - - return '${deviceId}_${packageInfo.packageName}'; - } -} diff --git a/lib/service/configuration_service.dart b/lib/service/configuration_service.dart index 25ce3ba78..799864394 100644 --- a/lib/service/configuration_service.dart +++ b/lib/service/configuration_service.dart @@ -24,6 +24,10 @@ import 'package:uuid/uuid.dart'; //ignore_for_file: constant_identifier_names abstract class ConfigurationService { + String getAccessToken(); + + Future setAccessToken(String value); + bool didMigrateToAccountSetting(); Future setMigrateToAccountSetting(bool value); @@ -206,6 +210,7 @@ abstract class ConfigurationService { } class ConfigurationServiceImpl implements ConfigurationService { + static const String keyAccessToken = 'access_token'; static const String keyDidMigrateToAccountSetting = 'did_migrate_to_account_setting'; static const String keyDidShowLiveWithArt = 'did_show_live_with_art'; @@ -929,6 +934,14 @@ class ConfigurationServiceImpl implements ConfigurationService { await _preferences.setString( KEY_ANNOUNCEMENT_TO_ISSUE_MAP, jsonEncode(mapJson)); } + + @override + String getAccessToken() => _preferences.getString(keyAccessToken) ?? ''; + + @override + Future setAccessToken(String value) async { + await _preferences.setString(keyAccessToken, value); + } } enum ConflictAction { diff --git a/lib/service/passkey_service.dart b/lib/service/passkey_service.dart new file mode 100644 index 000000000..973e863e2 --- /dev/null +++ b/lib/service/passkey_service.dart @@ -0,0 +1,119 @@ +import 'package:autonomy_flutter/gateway/user_api.dart'; +import 'package:autonomy_flutter/model/jwt.dart'; +import 'package:passkeys/authenticator.dart'; +import 'package:passkeys/types.dart'; + +abstract class PasskeyService { + Future isPassKeyAvailable(); + + Future logInInitiate(); + + Future logInRequest(); + + Future logInFinalize(); + + Future registerInitiate(); + + Future registerRequest(); + + Future registerFinalize(); +} + +class PasskeyServiceImpl implements PasskeyService { + final _passkeyAuthenticator = PasskeyAuthenticator(); + + RegisterRequestType? _registerRequest; + RegisterResponseType? _registerResponse; + + AuthenticateRequestType? _loginRequest; + AuthenticateResponseType? _loginResponse; + + final UserApi _userApi; + + PasskeyServiceImpl(this._userApi); + + static final AuthenticatorSelectionType _defaultAuthenticatorSelection = + AuthenticatorSelectionType( + requireResidentKey: false, + residentKey: 'discouraged', + userVerification: 'preferred', + ); + + static const _defaultRelayingPartyId = 'www.feralfile.com'; + static const _preferImmediatelyAvailableCredentials = true; + + @override + Future isPassKeyAvailable() async => + await _passkeyAuthenticator.canAuthenticate(); + + @override + Future logInInitiate() async { + final response = await _userApi.logInInitialize(); + final pubKey = response.publicKey; + _loginRequest = AuthenticateRequestType( + challenge: pubKey.challenge, + allowCredentials: pubKey.allowCredentials ?? [], + relyingPartyId: pubKey.rpId ?? _defaultRelayingPartyId, + mediation: response.mediation, + preferImmediatelyAvailableCredentials: + _preferImmediatelyAvailableCredentials, + ); + } + + @override + Future logInRequest() async { + if (_loginResponse != null) { + return _loginResponse!; + } + _loginResponse = await _passkeyAuthenticator.authenticate(_loginRequest!); + return _loginResponse!; + } + + @override + Future logInFinalize() async { + final response = await _userApi.logInFinalize({ + 'public_key_credential': _loginResponse!.toJson(), + }); + return response; + } + + @override + Future registerInitiate() async { + final response = await _userApi.registerInitialize(); + final pubKey = response.publicKey; + _registerRequest = RegisterRequestType( + challenge: pubKey.challenge, + relyingParty: pubKey.rp, + user: pubKey.user, + authSelectionType: + pubKey.authenticatorSelection ?? _defaultAuthenticatorSelection, + excludeCredentials: pubKey.excludeCredentials ?? [], + ); + } + + @override + Future registerRequest() async { + if (_registerResponse != null) { + return _registerResponse!; + } + _registerResponse = await _passkeyAuthenticator.register(_registerRequest!); + return _registerResponse!; + } + + @override + Future registerFinalize() async { + final response = await _userApi.registerFinalize({ + 'public_key_credential': _registerResponse!.toJson(), + }); + return response; + } +} + +extension RegisterResponseTypeExt on RegisterResponseType { + Map toJson() => { + 'id': id, + 'rawId': rawId, + 'clientDataJSON': clientDataJSON, + 'attestationObject': attestationObject, + }; +} diff --git a/lib/service/settings_data_service.dart b/lib/service/settings_data_service.dart index 5dc0f3667..8e3f6e12b 100644 --- a/lib/service/settings_data_service.dart +++ b/lib/service/settings_data_service.dart @@ -9,7 +9,6 @@ import 'dart:convert'; import 'package:autonomy_flutter/common/injector.dart'; import 'package:autonomy_flutter/database/entity/connection.dart'; -import 'package:autonomy_flutter/gateway/iap_api.dart'; import 'package:autonomy_flutter/graphql/account_settings/cloud_manager.dart'; import 'package:autonomy_flutter/model/play_list_model.dart'; import 'package:autonomy_flutter/screen/settings/preferences/preferences_bloc.dart'; @@ -19,7 +18,7 @@ import 'package:collection/collection.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; abstract class SettingsDataService { - Future restoreSettingsData({bool fromProfileData = false}); + Future restoreSettingsData(); Future backupDeviceSettings(); @@ -28,20 +27,13 @@ abstract class SettingsDataService { class SettingsDataServiceImpl implements SettingsDataService { final ConfigurationService _configurationService; - final IAPApi _iapApi; final CloudManager _cloudObject; SettingsDataServiceImpl( this._configurationService, - this._iapApi, this._cloudObject, ); - final _requester = - 'requester'; // server ignore this when putting jwt, so just put something - final _filename = 'settings_data_backup.json'; - final _version = '1'; - // legacy settings, they were store in device settings static const _keyPlaylists = 'playlists'; static const _keyHiddenLinkedAccountsFromGallery = @@ -67,42 +59,26 @@ class SettingsDataServiceImpl implements SettingsDataService { ]; @override - Future restoreSettingsData({bool fromProfileData = false}) async { + Future restoreSettingsData() async { if (PreferencesBloc.isOnChanging) { log.info('[SettingsDataService] skip restore: on changing preference'); return; } log.info('[SettingsDataService][Start] restoreSettingsData'); - if (!fromProfileData) { - log.info('[SettingsDataService] from account setting db'); - await Future.wait([ - _cloudObject.deviceSettingsDB.download(keys: _deviceSettingsKeys), - _cloudObject.userSettingsDB.download(keys: _userSettingsKeys), - ]); - final Map data = {} - ..addAll(_cloudObject.deviceSettingsDB.allInstance - .map((key, value) => MapEntry(key, jsonDecode(value)))) - ..addAll(_cloudObject.userSettingsDB.allInstance - .map((key, value) => MapEntry(key, jsonDecode(value)))); - - log.info('[SettingsDataService] restore $data'); - - await _saveSettingToConfig(data); - } else { - log.info('[SettingsDataService] migrate from old server'); - try { - final response = - await _iapApi.getProfileData(_requester, _filename, _version); - final data = json.decode(response); - - await _saveSettingToConfig(data); - - log.info('[SettingsDataService][Done] restoreSettingsData'); - } catch (exception, stacktrace) { - await Sentry.captureException(exception, stackTrace: stacktrace); - return; - } - } + + await Future.wait([ + _cloudObject.deviceSettingsDB.download(keys: _deviceSettingsKeys), + _cloudObject.userSettingsDB.download(keys: _userSettingsKeys), + ]); + final Map data = {} + ..addAll(_cloudObject.deviceSettingsDB.allInstance + .map((key, value) => MapEntry(key, jsonDecode(value)))) + ..addAll(_cloudObject.userSettingsDB.allInstance + .map((key, value) => MapEntry(key, jsonDecode(value)))); + + log.info('[SettingsDataService] restore $data'); + + await _saveSettingToConfig(data); } Future _saveSettingToConfig(Map data) async { diff --git a/lib/util/dio_interceptors.dart b/lib/util/dio_interceptors.dart index 281b77154..17e3d50f0 100644 --- a/lib/util/dio_interceptors.dart +++ b/lib/util/dio_interceptors.dart @@ -149,8 +149,7 @@ class AutonomyAuthInterceptor extends Interceptor { Future onRequest( RequestOptions options, RequestInterceptorHandler handler) async { final shouldIgnoreAuthorizationPath = [ - IAPApi.addressAuthenticationPath, - IAPApi.registerPrimaryAddressPath, + ...IAPApi.shouldIgnoreAuthorizationPaths ]; if (!shouldIgnoreAuthorizationPath.contains(options.path)) { final jwt = await injector().getAuthToken(); diff --git a/lib/util/primary_address_channel.dart b/lib/util/user_account_channel.dart similarity index 60% rename from lib/util/primary_address_channel.dart rename to lib/util/user_account_channel.dart index 989b1e7b4..2329988c9 100644 --- a/lib/util/primary_address_channel.dart +++ b/lib/util/user_account_channel.dart @@ -6,46 +6,94 @@ import 'package:autonomy_flutter/util/log.dart'; import 'package:autonomy_flutter/util/wallet_utils.dart'; import 'package:flutter/services.dart'; -class PrimaryAddressChannel { +class UserAccountChannel { final MethodChannel _channel; - PrimaryAddressChannel() + UserAccountChannel() : _channel = Platform.isIOS ? const MethodChannel('migration_util') : const MethodChannel('backup'); + String? _userId; + AddressInfo? _primaryAddress; + Future setPrimaryAddress(AddressInfo info) async { try { await _channel .invokeMethod('setPrimaryAddress', {'data': info.toString()}); + _primaryAddress = info; } catch (e) { log.info('setPrimaryAddress error: $e'); } } Future getPrimaryAddress() async { + if (_primaryAddress != null) { + return _primaryAddress; + } try { final String data = await _channel.invokeMethod('getPrimaryAddress', {}); if (data.isEmpty) { - return null; + _primaryAddress = null; } final primaryAddressInfo = json.decode(data); - return AddressInfo.fromJson(primaryAddressInfo); + _primaryAddress = AddressInfo.fromJson(primaryAddressInfo); } catch (e) { log.info('getPrimaryAddress error: $e'); - return null; + _primaryAddress = null; } + return _primaryAddress; } Future clearPrimaryAddress() async { try { final result = await _channel.invokeMethod('clearPrimaryAddress', {}); + _primaryAddress = null; return result; } catch (e) { log.info('clearPrimaryAddress error', e); return false; } } + + Future setUserId(String userId, {bool isPasskeys = false }) async { + try { + await _channel.invokeMethod('setUserId', {'data': userId}); + _userId = userId; + return true; + } catch (e) { + log.info('setUserId error', e); + return false; + } + } + + Future getUserId() async { + if (_userId != null) { + return _userId!; + } + try { + final userId = await _channel.invokeMethod('getUserId', {}); + _userId = userId ?? ''; + return _userId!; + } catch (e) { + log.info('getUserId error', e); + return ''; + } + } + + Future clearUserId() async { + try { + await _channel.invokeMethod('clearUserId', {}); + _userId = null; + } catch (e) { + log.info('clearUserId error', e); + } + } + + Future didMigrateUser() async { + final userId = await getUserId(); + return userId.isNotEmpty; + } } class AddressInfo { diff --git a/lib/util/wallet_address_ext.dart b/lib/util/wallet_address_ext.dart index 313542cba..d7224b31f 100644 --- a/lib/util/wallet_address_ext.dart +++ b/lib/util/wallet_address_ext.dart @@ -1,5 +1,5 @@ import 'package:autonomy_flutter/database/entity/wallet_address.dart'; -import 'package:autonomy_flutter/util/primary_address_channel.dart'; +import 'package:autonomy_flutter/util/user_account_channel.dart'; import 'package:libauk_dart/libauk_dart.dart'; extension WalletAddressExt on WalletAddress { diff --git a/pubspec.lock b/pubspec.lock index 393be1ab5..208358fd0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -385,6 +385,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.1" + corbado_frontend_api_client: + dependency: transitive + description: + name: corbado_frontend_api_client + sha256: a6d65fc0da88f2e6a6e95251de0b67735556128c5d96c9b609e7b18010a6f6c1 + url: "https://pub.dev" + source: hosted + version: "1.1.0" coverage: dependency: transitive description: @@ -1595,10 +1603,10 @@ packages: dependency: "direct main" description: name: json_annotation - sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" url: "https://pub.dev" source: hosted - version: "4.8.1" + version: "4.9.0" json_rpc_2: dependency: transitive description: @@ -1611,10 +1619,10 @@ packages: dependency: "direct dev" description: name: json_serializable - sha256: aa1f5a8912615733e0fdc7a02af03308933c93235bdc8d50d0b0c8a8ccb0b969 + sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b url: "https://pub.dev" source: hosted - version: "6.7.1" + version: "6.8.0" just_audio: dependency: "direct main" description: @@ -1929,6 +1937,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.2" + openapi_generator_annotations: + dependency: transitive + description: + name: openapi_generator_annotations + sha256: "46f1fb675029d78e19ce9143e70ce414d738b0f6c45c49c004b4b3afdb405a5c" + url: "https://pub.dev" + source: hosted + version: "4.13.1" overlay_support: dependency: "direct main" description: @@ -1969,6 +1985,46 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.9" + passkeys: + dependency: "direct main" + description: + name: passkeys + sha256: "59e50b21746aff90cbc56145174caa3b99523f449e42f7d8aa2199ec09c511cd" + url: "https://pub.dev" + source: hosted + version: "2.0.8" + passkeys_android: + dependency: transitive + description: + name: passkeys_android + sha256: e7f6aa1131b0c0e954bb50d0b59e9bbf6fb8eb0a6a1c282d6af982df5d746b51 + url: "https://pub.dev" + source: hosted + version: "2.1.0" + passkeys_ios: + dependency: transitive + description: + name: passkeys_ios + sha256: "1e23e40efd12867725443923a72914c11f926dce8623757b45ed2d7a16e95731" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + passkeys_platform_interface: + dependency: transitive + description: + name: passkeys_platform_interface + sha256: e32fbac05561a1c3ab439a504e7c19daf96b8339fd6bcbdac40e2ed9281add57 + url: "https://pub.dev" + source: hosted + version: "2.1.0" + passkeys_web: + dependency: transitive + description: + name: passkeys_web + sha256: "1c7815020332b9be1af4df67686826a91b6dd29fea53be947d6082654abd6280" + url: "https://pub.dev" + source: hosted + version: "2.0.1" path: dependency: "direct main" description: @@ -2711,6 +2767,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.2" + ua_client_hints: + dependency: transitive + description: + name: ua_client_hints + sha256: dfea54a1b4d259c057d0f33f198094cf4e09e1a21d347baadbe6dbd3d820c0d4 + url: "https://pub.dev" + source: hosted + version: "1.4.0" undo: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index d26371970..b761a2c0e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -103,7 +103,7 @@ dependencies: uuid: web3dart: ^2.5.1 web_socket_channel: - json_annotation: ^4.6.0 + json_annotation: ^4.9.0 iso_duration_parser: ^1.1.1 feralfile_app_theme: git: @@ -166,6 +166,7 @@ dependencies: just_audio: ^0.9.29 xml: ^6.1.0 vector_graphics_compiler: ^1.1.7 + passkeys: ^2.0.8 dependency_overrides: diff --git a/test/generate_mock/service/mock_configuration_service.mocks.dart b/test/generate_mock/service/mock_configuration_service.mocks.dart index 1e3f2dd0c..ca684bb41 100644 --- a/test/generate_mock/service/mock_configuration_service.mocks.dart +++ b/test/generate_mock/service/mock_configuration_service.mocks.dart @@ -3,11 +3,11 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i5; +import 'dart:async' as _i6; -import 'package:autonomy_flutter/model/jwt.dart' as _i6; -import 'package:autonomy_flutter/model/network.dart' as _i7; -import 'package:autonomy_flutter/model/sent_artwork.dart' as _i8; +import 'package:autonomy_flutter/model/jwt.dart' as _i7; +import 'package:autonomy_flutter/model/network.dart' as _i8; +import 'package:autonomy_flutter/model/sent_artwork.dart' as _i9; import 'package:autonomy_flutter/model/shared_postcard.dart' as _i10; import 'package:autonomy_flutter/screen/chat/chat_thread_page.dart' as _i3; import 'package:autonomy_flutter/screen/interactive_postcard/postcard_detail_page.dart' @@ -17,7 +17,7 @@ import 'package:autonomy_flutter/screen/interactive_postcard/stamp_preview.dart' import 'package:autonomy_flutter/service/configuration_service.dart' as _i4; import 'package:flutter/material.dart' as _i2; import 'package:mockito/mockito.dart' as _i1; -import 'package:mockito/src/dummies.dart' as _i9; +import 'package:mockito/src/dummies.dart' as _i5; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -72,6 +72,29 @@ class MockConfigurationService extends _i1.Mock ), ) as _i2.ValueNotifier); @override + String getAccessToken() => (super.noSuchMethod( + Invocation.method( + #getAccessToken, + [], + ), + returnValue: _i5.dummyValue( + this, + Invocation.method( + #getAccessToken, + [], + ), + ), + ) as String); + @override + _i6.Future setAccessToken(String? value) => (super.noSuchMethod( + Invocation.method( + #setAccessToken, + [value], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + @override bool didMigrateToAccountSetting() => (super.noSuchMethod( Invocation.method( #didMigrateToAccountSetting, @@ -80,24 +103,24 @@ class MockConfigurationService extends _i1.Mock returnValue: false, ) as bool); @override - _i5.Future setMigrateToAccountSetting(bool? value) => + _i6.Future setMigrateToAccountSetting(bool? value) => (super.noSuchMethod( Invocation.method( #setMigrateToAccountSetting, [value], ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); @override - _i5.Future setDidShowLiveWithArt(bool? value) => (super.noSuchMethod( + _i6.Future setDidShowLiveWithArt(bool? value) => (super.noSuchMethod( Invocation.method( #setDidShowLiveWithArt, [value], ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); @override bool didShowLiveWithArt() => (super.noSuchMethod( Invocation.method( @@ -107,15 +130,15 @@ class MockConfigurationService extends _i1.Mock returnValue: false, ) as bool); @override - _i5.Future setLastPullAnnouncementTime(int? lastPullTime) => + _i6.Future setLastPullAnnouncementTime(int? lastPullTime) => (super.noSuchMethod( Invocation.method( #setLastPullAnnouncementTime, [lastPullTime], ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); @override int getLastPullAnnouncementTime() => (super.noSuchMethod( Invocation.method( @@ -125,29 +148,7 @@ class MockConfigurationService extends _i1.Mock returnValue: 0, ) as int); @override - _i5.Future setRecordOwners( - List? owners, { - bool? override = false, - }) => - (super.noSuchMethod( - Invocation.method( - #setRecordOwners, - [owners], - {#override: override}, - ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); - @override - List getRecordOwners() => (super.noSuchMethod( - Invocation.method( - #getRecordOwners, - [], - ), - returnValue: [], - ) as List); - @override - _i5.Future setHasMerchandiseSupport( + _i6.Future setHasMerchandiseSupport( String? indexId, { bool? value = true, bool? isOverride = false, @@ -161,9 +162,9 @@ class MockConfigurationService extends _i1.Mock #isOverride: isOverride, }, ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); @override bool hasMerchandiseSupport(String? indexId) => (super.noSuchMethod( Invocation.method( @@ -173,15 +174,15 @@ class MockConfigurationService extends _i1.Mock returnValue: false, ) as bool); @override - _i5.Future setPostcardChatConfig(_i3.PostcardChatConfig? config) => + _i6.Future setPostcardChatConfig(_i3.PostcardChatConfig? config) => (super.noSuchMethod( Invocation.method( #setPostcardChatConfig, [config], ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); @override _i3.PostcardChatConfig getPostcardChatConfig({ required String? address, @@ -209,14 +210,14 @@ class MockConfigurationService extends _i1.Mock ), ) as _i3.PostcardChatConfig); @override - _i5.Future setDidMigrateAddress(bool? value) => (super.noSuchMethod( + _i6.Future setDidMigrateAddress(bool? value) => (super.noSuchMethod( Invocation.method( #setDidMigrateAddress, [value], ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); @override bool getDidMigrateAddress() => (super.noSuchMethod( Invocation.method( @@ -226,24 +227,24 @@ class MockConfigurationService extends _i1.Mock returnValue: false, ) as bool); @override - _i5.Future setAnnouncementLastPullTime(int? lastPullTime) => + _i6.Future setAnnouncementLastPullTime(int? lastPullTime) => (super.noSuchMethod( Invocation.method( #setAnnouncementLastPullTime, [lastPullTime], ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); @override - _i5.Future setOldUser() => (super.noSuchMethod( + _i6.Future setOldUser() => (super.noSuchMethod( Invocation.method( #setOldUser, [], ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); @override bool getIsOldUser() => (super.noSuchMethod( Invocation.method( @@ -253,32 +254,32 @@ class MockConfigurationService extends _i1.Mock returnValue: false, ) as bool); @override - _i5.Future setIAPReceipt(String? value) => (super.noSuchMethod( + _i6.Future setIAPReceipt(String? value) => (super.noSuchMethod( Invocation.method( #setIAPReceipt, [value], ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); @override - _i5.Future setIAPJWT(_i6.JWT? value) => (super.noSuchMethod( + _i6.Future setIAPJWT(_i7.JWT? value) => (super.noSuchMethod( Invocation.method( #setIAPJWT, [value], ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); @override - _i5.Future setDevicePasscodeEnabled(bool? value) => (super.noSuchMethod( + _i6.Future setDevicePasscodeEnabled(bool? value) => (super.noSuchMethod( Invocation.method( #setDevicePasscodeEnabled, [value], ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); @override bool isDevicePasscodeEnabled() => (super.noSuchMethod( Invocation.method( @@ -288,14 +289,14 @@ class MockConfigurationService extends _i1.Mock returnValue: false, ) as bool); @override - _i5.Future setNotificationEnabled(bool? value) => (super.noSuchMethod( + _i6.Future setNotificationEnabled(bool? value) => (super.noSuchMethod( Invocation.method( #setNotificationEnabled, [value], ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); @override bool isNotificationEnabled() => (super.noSuchMethod( Invocation.method( @@ -305,14 +306,14 @@ class MockConfigurationService extends _i1.Mock returnValue: false, ) as bool); @override - _i5.Future setAnalyticEnabled(bool? value) => (super.noSuchMethod( + _i6.Future setAnalyticEnabled(bool? value) => (super.noSuchMethod( Invocation.method( #setAnalyticEnabled, [value], ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); @override bool isAnalyticsEnabled() => (super.noSuchMethod( Invocation.method( @@ -322,14 +323,14 @@ class MockConfigurationService extends _i1.Mock returnValue: false, ) as bool); @override - _i5.Future setDoneOnboarding(bool? value) => (super.noSuchMethod( + _i6.Future setDoneOnboarding(bool? value) => (super.noSuchMethod( Invocation.method( #setDoneOnboarding, [value], ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); @override bool isDoneOnboarding() => (super.noSuchMethod( Invocation.method( @@ -339,16 +340,16 @@ class MockConfigurationService extends _i1.Mock returnValue: false, ) as bool); @override - _i5.Future setLastTimeAskForSubscription(DateTime? date) => + _i6.Future setLastTimeAskForSubscription(DateTime? date) => (super.noSuchMethod( Invocation.method( #setLastTimeAskForSubscription, [date], ), - returnValue: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i6.Future.value(), + ) as _i6.Future); @override - List getTempStorageHiddenTokenIDs({_i7.Network? network}) => + List getTempStorageHiddenTokenIDs({_i8.Network? network}) => (super.noSuchMethod( Invocation.method( #getTempStorageHiddenTokenIDs, @@ -358,10 +359,10 @@ class MockConfigurationService extends _i1.Mock returnValue: [], ) as List); @override - _i5.Future updateTempStorageHiddenTokenIDs( + _i6.Future updateTempStorageHiddenTokenIDs( List? tokenIDs, bool? isAdd, { - _i7.Network? network, + _i8.Network? network, bool? override = false, }) => (super.noSuchMethod( @@ -376,19 +377,19 @@ class MockConfigurationService extends _i1.Mock #override: override, }, ), - returnValue: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i6.Future.value(), + ) as _i6.Future); @override - List<_i8.SentArtwork> getRecentlySentToken() => (super.noSuchMethod( + List<_i9.SentArtwork> getRecentlySentToken() => (super.noSuchMethod( Invocation.method( #getRecentlySentToken, [], ), - returnValue: <_i8.SentArtwork>[], - ) as List<_i8.SentArtwork>); + returnValue: <_i9.SentArtwork>[], + ) as List<_i9.SentArtwork>); @override - _i5.Future updateRecentlySentToken( - List<_i8.SentArtwork>? sentArtwork, { + _i6.Future updateRecentlySentToken( + List<_i9.SentArtwork>? sentArtwork, { bool? override = false, }) => (super.noSuchMethod( @@ -397,60 +398,60 @@ class MockConfigurationService extends _i1.Mock [sentArtwork], {#override: override}, ), - returnValue: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i6.Future.value(), + ) as _i6.Future); @override - _i5.Future setReadReleaseNotesInVersion(String? version) => + _i6.Future setReadReleaseNotesInVersion(String? version) => (super.noSuchMethod( Invocation.method( #setReadReleaseNotesInVersion, [version], ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); @override - _i5.Future setPreviousBuildNumber(String? value) => (super.noSuchMethod( + _i6.Future setPreviousBuildNumber(String? value) => (super.noSuchMethod( Invocation.method( #setPreviousBuildNumber, [value], ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); @override - _i5.Future getAccountHMACSecret() => (super.noSuchMethod( + _i6.Future getAccountHMACSecret() => (super.noSuchMethod( Invocation.method( #getAccountHMACSecret, [], ), - returnValue: _i5.Future.value(_i9.dummyValue( + returnValue: _i6.Future.value(_i5.dummyValue( this, Invocation.method( #getAccountHMACSecret, [], ), )), - ) as _i5.Future); + ) as _i6.Future); @override - _i5.Future setLastRemindReviewDate(String? value) => + _i6.Future setLastRemindReviewDate(String? value) => (super.noSuchMethod( Invocation.method( #setLastRemindReviewDate, [value], ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); @override - _i5.Future setCountOpenApp(int? value) => (super.noSuchMethod( + _i6.Future setCountOpenApp(int? value) => (super.noSuchMethod( Invocation.method( #setCountOpenApp, [value], ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); @override bool showTokenDebugInfo() => (super.noSuchMethod( Invocation.method( @@ -460,58 +461,58 @@ class MockConfigurationService extends _i1.Mock returnValue: false, ) as bool); @override - _i5.Future setShowTokenDebugInfo(bool? show) => (super.noSuchMethod( + _i6.Future setShowTokenDebugInfo(bool? show) => (super.noSuchMethod( Invocation.method( #setShowTokenDebugInfo, [show], ), - returnValue: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i6.Future.value(), + ) as _i6.Future); @override - _i5.Future setDoneOnboardingTime(DateTime? time) => + _i6.Future setDoneOnboardingTime(DateTime? time) => (super.noSuchMethod( Invocation.method( #setDoneOnboardingTime, [time], ), - returnValue: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i6.Future.value(), + ) as _i6.Future); @override - _i5.Future setSubscriptionTime(DateTime? time) => + _i6.Future setSubscriptionTime(DateTime? time) => (super.noSuchMethod( Invocation.method( #setSubscriptionTime, [time], ), - returnValue: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i6.Future.value(), + ) as _i6.Future); @override - _i5.Future setSentTezosArtworkMetric(int? hashedAddresses) => + _i6.Future setSentTezosArtworkMetric(int? hashedAddresses) => (super.noSuchMethod( Invocation.method( #setSentTezosArtworkMetric, [hashedAddresses], ), - returnValue: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i6.Future.value(), + ) as _i6.Future); @override - _i5.Future reload() => (super.noSuchMethod( + _i6.Future reload() => (super.noSuchMethod( Invocation.method( #reload, [], ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); @override - _i5.Future removeAll() => (super.noSuchMethod( + _i6.Future removeAll() => (super.noSuchMethod( Invocation.method( #removeAll, [], ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); @override List<_i10.SharedPostcard> getSharedPostcard() => (super.noSuchMethod( Invocation.method( @@ -521,7 +522,7 @@ class MockConfigurationService extends _i1.Mock returnValue: <_i10.SharedPostcard>[], ) as List<_i10.SharedPostcard>); @override - _i5.Future updateSharedPostcard( + _i6.Future updateSharedPostcard( List<_i10.SharedPostcard>? sharedPostcards, { bool? override = false, bool? isRemoved = false, @@ -535,20 +536,20 @@ class MockConfigurationService extends _i1.Mock #isRemoved: isRemoved, }, ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); @override - _i5.Future removeSharedPostcardWhere( + _i6.Future removeSharedPostcardWhere( bool Function(_i10.SharedPostcard)? test) => (super.noSuchMethod( Invocation.method( #removeSharedPostcardWhere, [test], ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); @override List getListPostcardMint() => (super.noSuchMethod( Invocation.method( @@ -558,7 +559,7 @@ class MockConfigurationService extends _i1.Mock returnValue: [], ) as List); @override - _i5.Future setListPostcardMint( + _i6.Future setListPostcardMint( List? tokenID, { bool? override = false, bool? isRemoved = false, @@ -572,9 +573,9 @@ class MockConfigurationService extends _i1.Mock #isRemoved: isRemoved, }, ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); @override List<_i11.StampingPostcard> getStampingPostcard() => (super.noSuchMethod( Invocation.method( @@ -584,7 +585,7 @@ class MockConfigurationService extends _i1.Mock returnValue: <_i11.StampingPostcard>[], ) as List<_i11.StampingPostcard>); @override - _i5.Future updateStampingPostcard( + _i6.Future updateStampingPostcard( List<_i11.StampingPostcard>? values, { bool? override = false, bool? isRemove = false, @@ -598,11 +599,11 @@ class MockConfigurationService extends _i1.Mock #isRemove: isRemove, }, ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); @override - _i5.Future setProcessingStampPostcard( + _i6.Future setProcessingStampPostcard( List<_i11.ProcessingStampPostcard>? values, { bool? override = false, bool? isRemove = false, @@ -616,9 +617,9 @@ class MockConfigurationService extends _i1.Mock #isRemove: isRemove, }, ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); @override List<_i11.ProcessingStampPostcard> getProcessingStampPostcard() => (super.noSuchMethod( @@ -629,14 +630,14 @@ class MockConfigurationService extends _i1.Mock returnValue: <_i11.ProcessingStampPostcard>[], ) as List<_i11.ProcessingStampPostcard>); @override - _i5.Future setAutoShowPostcard(bool? value) => (super.noSuchMethod( + _i6.Future setAutoShowPostcard(bool? value) => (super.noSuchMethod( Invocation.method( #setAutoShowPostcard, [value], ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); @override bool isAutoShowPostcard() => (super.noSuchMethod( Invocation.method( @@ -655,7 +656,7 @@ class MockConfigurationService extends _i1.Mock returnValue: <_i12.PostcardIdentity>[], ) as List<_i12.PostcardIdentity>); @override - _i5.Future setListPostcardAlreadyShowYouDidIt( + _i6.Future setListPostcardAlreadyShowYouDidIt( List<_i12.PostcardIdentity>? value, { bool? override = false, }) => @@ -665,11 +666,11 @@ class MockConfigurationService extends _i1.Mock [value], {#override: override}, ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); @override - _i5.Future setAlreadyShowPostcardUpdates( + _i6.Future setAlreadyShowPostcardUpdates( List<_i12.PostcardIdentity>? value, { bool? override = false, }) => @@ -679,9 +680,9 @@ class MockConfigurationService extends _i1.Mock [value], {#override: override}, ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); @override List<_i12.PostcardIdentity> getAlreadyShowPostcardUpdates() => (super.noSuchMethod( @@ -697,7 +698,7 @@ class MockConfigurationService extends _i1.Mock #getVersionInfo, [], ), - returnValue: _i9.dummyValue( + returnValue: _i5.dummyValue( this, Invocation.method( #getVersionInfo, @@ -706,14 +707,14 @@ class MockConfigurationService extends _i1.Mock ), ) as String); @override - _i5.Future setVersionInfo(String? version) => (super.noSuchMethod( + _i6.Future setVersionInfo(String? version) => (super.noSuchMethod( Invocation.method( #setVersionInfo, [version], ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); @override List getHiddenOrSentTokenIDs() => (super.noSuchMethod( Invocation.method( @@ -723,14 +724,14 @@ class MockConfigurationService extends _i1.Mock returnValue: [], ) as List); @override - _i5.Future setShowPostcardBanner(bool? bool) => (super.noSuchMethod( + _i6.Future setShowPostcardBanner(bool? bool) => (super.noSuchMethod( Invocation.method( #setShowPostcardBanner, [bool], ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); @override bool getShowPostcardBanner() => (super.noSuchMethod( Invocation.method( @@ -740,14 +741,14 @@ class MockConfigurationService extends _i1.Mock returnValue: false, ) as bool); @override - _i5.Future setShowAddAddressBanner(bool? bool) => (super.noSuchMethod( + _i6.Future setShowAddAddressBanner(bool? bool) => (super.noSuchMethod( Invocation.method( #setShowAddAddressBanner, [bool], ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); @override bool getShowAddAddressBanner() => (super.noSuchMethod( Invocation.method( @@ -757,7 +758,7 @@ class MockConfigurationService extends _i1.Mock returnValue: false, ) as bool); @override - _i5.Future setMerchandiseOrderIds( + _i6.Future setMerchandiseOrderIds( List? ids, { bool? override = false, }) => @@ -767,9 +768,9 @@ class MockConfigurationService extends _i1.Mock [ids], {#override: override}, ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); @override List getMerchandiseOrderIds() => (super.noSuchMethod( Invocation.method( @@ -779,12 +780,12 @@ class MockConfigurationService extends _i1.Mock returnValue: [], ) as List); @override - _i5.Future setReferralCode(String? referralCode) => (super.noSuchMethod( + _i6.Future setReferralCode(String? referralCode) => (super.noSuchMethod( Invocation.method( #setReferralCode, [referralCode], ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); } From ada0b2e3025a9659c74b42cf4f2ec75bb21bec8e Mon Sep 17 00:00:00 2001 From: phuoc Date: Thu, 24 Oct 2024 11:24:12 +0700 Subject: [PATCH 02/57] didRegisterPasskey config Signed-off-by: phuoc --- .../bitmark/autonomy_flutter/BackupDartPlugin.kt | 4 ++-- lib/util/user_account_channel.dart | 16 ++++++++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/android/app/src/main/kotlin/com/bitmark/autonomy_flutter/BackupDartPlugin.kt b/android/app/src/main/kotlin/com/bitmark/autonomy_flutter/BackupDartPlugin.kt index 03602d0e3..e97466828 100644 --- a/android/app/src/main/kotlin/com/bitmark/autonomy_flutter/BackupDartPlugin.kt +++ b/android/app/src/main/kotlin/com/bitmark/autonomy_flutter/BackupDartPlugin.kt @@ -340,11 +340,11 @@ class BackupDartPlugin : MethodChannel.MethodCallHandler { .addOnSuccessListener { Log.e("setDidRegisterPasskey", data.toString()); - result.success("") + result.success(true) } .addOnFailureListener { e -> Log.e("setDidRegisterPasskey", e.message ?: "") - result.error("setDidRegisterPasskey error", e.message, e) + result.success(false) } } diff --git a/lib/util/user_account_channel.dart b/lib/util/user_account_channel.dart index 2329988c9..1e05bfb10 100644 --- a/lib/util/user_account_channel.dart +++ b/lib/util/user_account_channel.dart @@ -56,7 +56,7 @@ class UserAccountChannel { } } - Future setUserId(String userId, {bool isPasskeys = false }) async { + Future setUserId(String userId, {bool isPasskeys = false}) async { try { await _channel.invokeMethod('setUserId', {'data': userId}); _userId = userId; @@ -90,10 +90,22 @@ class UserAccountChannel { } } - Future didMigrateUser() async { + Future didCreateUser() async { final userId = await getUserId(); return userId.isNotEmpty; } + + Future didRegisterPasskey() async { + final didRegister = await _channel.invokeMethod('didRegisterPasskey', {}); + return didRegister; + } + + Future setDidRegisterPasskey(bool value) async { + final didRegister = await _channel.invokeMethod('setDidRegisterPasskey', { + 'data': value, + }); + return didRegister; + } } class AddressInfo { From 53044b844dde958ccb985023cadd44b3b3237c17 Mon Sep 17 00:00:00 2001 From: phuoc Date: Thu, 24 Oct 2024 13:48:17 +0700 Subject: [PATCH 03/57] refactor authService Signed-off-by: phuoc --- .../autonomy_flutter/BackupDartPlugin.kt | 4 +- lib/gateway/iap_api.dart | 32 ----- lib/gateway/iap_api.g.dart | 135 ------------------ lib/gateway/user_api.dart | 4 +- lib/gateway/user_api.g.dart | 4 +- lib/model/jwt.dart | 8 +- lib/screen/irl_screen/webview_irl_screen.dart | 4 +- lib/screen/onboarding_page.dart | 2 +- lib/service/account_service.dart | 27 ++-- lib/service/address_service.dart | 30 ---- lib/service/auth_service.dart | 115 +++------------ lib/service/configuration_service.dart | 12 +- lib/service/iap_service.dart | 3 +- lib/service/passkey_service.dart | 10 +- lib/util/dio_interceptors.dart | 16 +-- lib/util/gift_handler.dart | 5 +- lib/util/user_account_channel.dart | 8 +- .../gateway/mock_iap_api.mocks.dart | 96 ++----------- .../service/mock_account_service.mocks.dart | 17 ++- .../mock_configuration_service.mocks.dart | 10 +- 20 files changed, 101 insertions(+), 441 deletions(-) diff --git a/android/app/src/main/kotlin/com/bitmark/autonomy_flutter/BackupDartPlugin.kt b/android/app/src/main/kotlin/com/bitmark/autonomy_flutter/BackupDartPlugin.kt index e97466828..636fcb011 100644 --- a/android/app/src/main/kotlin/com/bitmark/autonomy_flutter/BackupDartPlugin.kt +++ b/android/app/src/main/kotlin/com/bitmark/autonomy_flutter/BackupDartPlugin.kt @@ -277,7 +277,7 @@ class BackupDartPlugin : MethodChannel.MethodCallHandler { .addOnSuccessListener { Log.e("setUserId", "user id set successfully"); - result.success("") + result.success(true) } .addOnFailureListener { e -> Log.e("setUserId", e.message ?: "") @@ -307,7 +307,7 @@ class BackupDartPlugin : MethodChannel.MethodCallHandler { } catch (e: Exception) { Log.e("getUserId", e.message ?: "Error decoding data") //No primary address found - result.success("") + result.success(null) } } .addOnFailureListener { diff --git a/lib/gateway/iap_api.dart b/lib/gateway/iap_api.dart index 4a54991f9..b68e4d8a0 100644 --- a/lib/gateway/iap_api.dart +++ b/lib/gateway/iap_api.dart @@ -5,8 +5,6 @@ // that can be found in the LICENSE file. // -import 'dart:io'; - import 'package:autonomy_flutter/model/announcement/announcement.dart'; import 'package:autonomy_flutter/model/announcement/announcement_request.dart'; import 'package:autonomy_flutter/model/jwt.dart'; @@ -18,38 +16,8 @@ part 'iap_api.g.dart'; @RestApi(baseUrl: '') abstract class IAPApi { - static const addressAuthenticationPath = '/apis/v2/addresses/auth'; - static const registerPrimaryAddressPath = '/apis/v2/addresses/primary'; - - static const shouldIgnoreAuthorizationPaths = [ - addressAuthenticationPath, - registerPrimaryAddressPath, - ]; - factory IAPApi(Dio dio, {String baseUrl}) = _IAPApi; - @POST(addressAuthenticationPath) - Future authAddress(@Body() Map body); - - @POST(registerPrimaryAddressPath) - Future registerPrimaryAddress(@Body() Map body); - - @MultiPart() - @POST('/apis/v1/premium/profile-data') - Future uploadProfile( - @Header('requester') String requester, - @Part(name: 'filename') String filename, - @Part(name: 'appVersion') String appVersion, - @Part(name: 'data') File data, - ); - - @GET('/apis/v1/premium/profile-data') - Future getProfileData( - @Header('requester') String requester, - @Query('filename') String filename, - @Query('appVersion') String version, - ); - @DELETE('/apis/v1/premium/profile-data') Future deleteAllProfiles( @Header('requester') String requester, diff --git a/lib/gateway/iap_api.g.dart b/lib/gateway/iap_api.g.dart index 0610ad20c..7bbbc0514 100644 --- a/lib/gateway/iap_api.g.dart +++ b/lib/gateway/iap_api.g.dart @@ -18,141 +18,6 @@ class _IAPApi implements IAPApi { String? baseUrl; - @override - Future authAddress(Map body) async { - const _extra = {}; - final queryParameters = {}; - final _headers = {}; - final _data = {}; - _data.addAll(body); - final _result = - await _dio.fetch>(_setStreamType(Options( - method: 'POST', - headers: _headers, - extra: _extra, - ) - .compose( - _dio.options, - '/apis/v2/addresses/auth', - queryParameters: queryParameters, - data: _data, - ) - .copyWith( - baseUrl: _combineBaseUrls( - _dio.options.baseUrl, - baseUrl, - )))); - final value = JWT.fromJson(_result.data!); - return value; - } - - @override - Future registerPrimaryAddress(Map body) async { - const _extra = {}; - final queryParameters = {}; - final _headers = {}; - final _data = {}; - _data.addAll(body); - await _dio.fetch(_setStreamType(Options( - method: 'POST', - headers: _headers, - extra: _extra, - ) - .compose( - _dio.options, - '/apis/v2/addresses/primary', - queryParameters: queryParameters, - data: _data, - ) - .copyWith( - baseUrl: _combineBaseUrls( - _dio.options.baseUrl, - baseUrl, - )))); - } - - @override - Future uploadProfile( - String requester, - String filename, - String appVersion, - File data, - ) async { - const _extra = {}; - final queryParameters = {}; - final _headers = {r'requester': requester}; - _headers.removeWhere((k, v) => v == null); - final _data = FormData(); - _data.fields.add(MapEntry( - 'filename', - filename, - )); - _data.fields.add(MapEntry( - 'appVersion', - appVersion, - )); - _data.files.add(MapEntry( - 'data', - MultipartFile.fromFileSync( - data.path, - filename: data.path.split(Platform.pathSeparator).last, - ), - )); - final _result = await _dio.fetch(_setStreamType(Options( - method: 'POST', - headers: _headers, - extra: _extra, - contentType: 'multipart/form-data', - ) - .compose( - _dio.options, - '/apis/v1/premium/profile-data', - queryParameters: queryParameters, - data: _data, - ) - .copyWith( - baseUrl: _combineBaseUrls( - _dio.options.baseUrl, - baseUrl, - )))); - final value = _result.data; - return value; - } - - @override - Future getProfileData( - String requester, - String filename, - String version, - ) async { - const _extra = {}; - final queryParameters = { - r'filename': filename, - r'appVersion': version, - }; - final _headers = {r'requester': requester}; - _headers.removeWhere((k, v) => v == null); - final Map? _data = null; - final _result = await _dio.fetch(_setStreamType(Options( - method: 'GET', - headers: _headers, - extra: _extra, - ) - .compose( - _dio.options, - '/apis/v1/premium/profile-data', - queryParameters: queryParameters, - data: _data, - ) - .copyWith( - baseUrl: _combineBaseUrls( - _dio.options.baseUrl, - baseUrl, - )))); - final value = _result.data; - return value; - } - @override Future deleteAllProfiles(String requester) async { const _extra = {}; diff --git a/lib/gateway/user_api.dart b/lib/gateway/user_api.dart index 8de00d0e5..329f5169b 100644 --- a/lib/gateway/user_api.dart +++ b/lib/gateway/user_api.dart @@ -23,8 +23,8 @@ abstract class UserApi { @POST('/apis/users/passkeys/registration/finalize') Future registerFinalize(@Body() Map body); - @POST('/apis/users/passkeys/login/initialize') - Future logInInitialize(); + @POST('/apis/users/{id}/passkeys/login/initialize') + Future logInInitialize(@Path('id') String id); @POST('/apis/users/passkeys/login/finalize') Future logInFinalize(@Body() Map body); diff --git a/lib/gateway/user_api.g.dart b/lib/gateway/user_api.g.dart index ab203bdbc..8bf32b4da 100644 --- a/lib/gateway/user_api.g.dart +++ b/lib/gateway/user_api.g.dart @@ -74,7 +74,7 @@ class _UserApi implements UserApi { } @override - Future logInInitialize() async { + Future logInInitialize(String id) async { const _extra = {}; final queryParameters = {}; final _headers = {}; @@ -87,7 +87,7 @@ class _UserApi implements UserApi { ) .compose( _dio.options, - '/apis/users/passkeys/login/initialize', + '/apis/users/${id}/passkeys/login/initialize', queryParameters: queryParameters, data: _data, ) diff --git a/lib/model/jwt.dart b/lib/model/jwt.dart index 248a25c1a..c7b4eb54b 100644 --- a/lib/model/jwt.dart +++ b/lib/model/jwt.dart @@ -51,19 +51,19 @@ enum MembershipType { class JWT { int? expireIn; String jwtToken; - String accessToken; + String refreshToken; - JWT({required this.jwtToken, this.expireIn, this.accessToken = ''}); + JWT({required this.jwtToken, this.expireIn, this.refreshToken = ''}); JWT.fromJson(Map json) : expireIn = double.tryParse(json['expire_in'].toString())?.toInt(), jwtToken = json['jwt_token'], - accessToken = json['access_token'] ?? ''; + refreshToken = json['refresh_token'] ?? ''; Map toJson() => { 'expire_in': expireIn, 'jwt_token': jwtToken, - 'access_token': accessToken, + 'refresh_token': refreshToken, }; bool _isValid() { diff --git a/lib/screen/irl_screen/webview_irl_screen.dart b/lib/screen/irl_screen/webview_irl_screen.dart index a7ef631e9..24c9376bc 100644 --- a/lib/screen/irl_screen/webview_irl_screen.dart +++ b/lib/screen/irl_screen/webview_irl_screen.dart @@ -657,7 +657,7 @@ class IRLHandler { static Future refreshJWT(List arguments) async { final authService = injector.get(); - final newJWT = await authService.getAuthToken(forceRefresh: true); - return JSResult.result(newJWT?.isPremiumValid() ?? false); + final newJWT = await authService.refreshJWT(); + return JSResult.result(newJWT.isPremiumValid()); } } diff --git a/lib/screen/onboarding_page.dart b/lib/screen/onboarding_page.dart index 21af35971..2a46748e7 100644 --- a/lib/screen/onboarding_page.dart +++ b/lib/screen/onboarding_page.dart @@ -149,7 +149,7 @@ class _OnboardingPageState extends State '[_createAccountOrRestoreIfNeeded] Loading more than 10s')); }); log.info('[_fetchRuntimeCache] start'); - await injector().migrateAccount(); + await injector().migrateAccount(() {}); unawaited(_registerPushNotifications()); unawaited(injector().setup()); log.info('[_fetchRuntimeCache] end'); diff --git a/lib/service/account_service.dart b/lib/service/account_service.dart index 3cc43f700..3ccaa9516 100644 --- a/lib/service/account_service.dart +++ b/lib/service/account_service.dart @@ -27,9 +27,9 @@ import 'package:autonomy_flutter/util/device.dart'; import 'package:autonomy_flutter/util/exception.dart'; import 'package:autonomy_flutter/util/ios_backup_channel.dart'; import 'package:autonomy_flutter/util/log.dart'; +import 'package:autonomy_flutter/util/string_ext.dart'; import 'package:autonomy_flutter/util/user_account_channel.dart' as primary_address_channel; -import 'package:autonomy_flutter/util/string_ext.dart'; import 'package:autonomy_flutter/util/wallet_address_ext.dart'; import 'package:autonomy_flutter/util/wallet_storage_ext.dart'; import 'package:autonomy_flutter/util/wallet_utils.dart'; @@ -43,7 +43,7 @@ import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:uuid/uuid.dart'; abstract class AccountService { - Future migrateAccount(); + Future migrateAccount(Function() createLoginJwt); List getWalletsAddress(CryptoType cryptoType); @@ -626,13 +626,8 @@ class AccountServiceImpl extends AccountService { await _cloudObject.addressObject.updateAddresses([walletAddress]); } - Future _createInitialJwt() async { - // this might change to a callback for login finalized - // - } - @override - Future migrateAccount() async { + Future migrateAccount(Function() createLoginJwt) async { log.info('[AccountService] migrateAccount'); final cloudDb = injector(); final isDoneOnboarding = _configurationService.isDoneOnboarding(); @@ -653,11 +648,6 @@ class AccountServiceImpl extends AccountService { 'isDoneOnboarding: $isDoneOnboarding, ' 'defaultWallet: ${defaultWallet?.uuid}'); - /// in previous version, we assume that if user has primary address, - /// that is their default account (using to save cloud data base) - /// but if user has no primary address, but have backup version, - /// we will take the first uuid as default account - /// /// Migrate passkeys note: /// To simplify this migration on mobile, we will not support restoring /// backed-up data for users who uninstall and then reinstall apps that @@ -677,7 +667,7 @@ class AccountServiceImpl extends AccountService { chain: 'ethereum', index: 0, )); - await _createInitialJwt(); + await createLoginJwt(); await insertNextAddressFromUuid(wallet.uuid, WalletType.MultiChain, name: ''); await androidBackupKeys(); @@ -694,9 +684,12 @@ class AccountServiceImpl extends AccountService { 'case 2/3 update/restore app from old version using did key'); await _addressService.registerPrimaryAddress( info: primary_address_channel.AddressInfo( - uuid: defaultWallet.uuid, chain: 'ethereum', index: 0), + uuid: defaultWallet.uuid, + chain: 'ethereum', + index: 0, + ), ); - await _createInitialJwt(); + await createLoginJwt(); await _cloudObject.copyDataFrom(cloudDb); unawaited(cloudDb.removeAll()); @@ -712,7 +705,7 @@ class AccountServiceImpl extends AccountService { if (!addressInfo.isEthereum) { await _addressService.migrateToEthereumAddress(addressInfo); } - await _createInitialJwt(); + await createLoginJwt(); // we don't care for user use tezos primary address. // this is to reduce loading time diff --git a/lib/service/address_service.dart b/lib/service/address_service.dart index 494f35671..2bb5255e2 100644 --- a/lib/service/address_service.dart +++ b/lib/service/address_service.dart @@ -18,7 +18,6 @@ import 'package:autonomy_flutter/util/user_account_channel.dart'; import 'package:autonomy_flutter/util/wallet_storage_ext.dart'; import 'package:libauk_dart/libauk_dart.dart'; import 'package:sentry/sentry.dart'; -import 'package:tezart/src/crypto/crypto.dart' as crypto; class AddressService { final UserAccountChannel _primaryAddressChannel; @@ -115,11 +114,6 @@ class AddressService { signature = await walletStorage.ethSignPersonalMessage( utf8.encode(message), index: addressInfo.index); - case 'tezos': - final signatureUInt8List = await walletStorage - .tezosSignMessage(utf8.encode(message), index: addressInfo.index); - signature = crypto.encodeWithPrefix( - prefix: crypto.Prefixes.edsig, bytes: signatureUInt8List); default: throw UnsupportedError('Unsupported chain: $chain'); } @@ -134,30 +128,6 @@ class AddressService { return getAddressSignature(addressInfo: addressInfo, message: message); } - Future getAddressPublicKey({required AddressInfo addressInfo}) async { - final walletStorage = WalletStorage(addressInfo.uuid); - final chain = addressInfo.chain; - String publicKey; - switch (chain) { - case 'ethereum': - publicKey = ''; - case 'tezos': - publicKey = - await walletStorage.getTezosPublicKey(index: addressInfo.index); - default: - throw UnsupportedError('Unsupported chain: $chain'); - } - return publicKey; - } - - Future getPrimaryAddressPublicKey() async { - final addressInfo = await getPrimaryAddressInfo(); - if (addressInfo == null) { - throw UnsupportedError('Primary address not found'); - } - return getAddressPublicKey(addressInfo: addressInfo); - } - String getFeralfileAccountMessage( {required String address, required String timestamp}) => 'feralfile-account: {"requester":"$address","timestamp":"$timestamp"}'; diff --git a/lib/service/auth_service.dart b/lib/service/auth_service.dart index 38694c2e3..2b4796f1e 100644 --- a/lib/service/auth_service.dart +++ b/lib/service/auth_service.dart @@ -10,27 +10,26 @@ import 'dart:io'; import 'package:autonomy_flutter/common/injector.dart'; import 'package:autonomy_flutter/gateway/iap_api.dart'; +import 'package:autonomy_flutter/gateway/user_api.dart'; import 'package:autonomy_flutter/model/jwt.dart'; import 'package:autonomy_flutter/screen/bloc/subscription/subscription_bloc.dart'; import 'package:autonomy_flutter/screen/bloc/subscription/subscription_state.dart'; import 'package:autonomy_flutter/screen/settings/subscription/upgrade_bloc.dart'; import 'package:autonomy_flutter/screen/settings/subscription/upgrade_state.dart'; -import 'package:autonomy_flutter/service/account_service.dart'; -import 'package:autonomy_flutter/service/address_service.dart'; import 'package:autonomy_flutter/service/configuration_service.dart'; +import 'package:autonomy_flutter/util/exception.dart'; import 'package:autonomy_flutter/util/log.dart'; -import 'package:autonomy_flutter/util/notification_util.dart'; -import 'package:autonomy_flutter/util/user_account_channel.dart'; +import 'package:easy_localization/easy_localization.dart'; class AuthService { final IAPApi _authApi; - final AddressService _addressService; + final UserApi _userApi; final ConfigurationService _configurationService; JWT? _jwt; AuthService( this._authApi, - this._addressService, + this._userApi, this._configurationService, ); @@ -38,32 +37,12 @@ class AuthService { _jwt = null; } - Future _getPrimaryAddressAuthToken({String? receiptData}) async { - final primaryAddressInfo = await _addressService.getPrimaryAddressInfo(); - if (primaryAddressInfo == null) { - log.warning('Primary address not set'); - return null; + Future refreshJWT({String? receiptData}) async { + final refreshToken = _jwt?.refreshToken; + if (refreshToken == null || refreshToken.isEmpty) { + throw JwtException(message: 'refresh_token_empty'.tr()); } - final address = await _addressService.getPrimaryAddress(); - final timeStamp = DateTime.now().millisecondsSinceEpoch.toString(); - - final message = _addressService.getFeralfileAccountMessage( - address: address!, - timestamp: timeStamp, - ); - - final signature = - await _addressService.getPrimaryAddressSignature(message: message); - final publicKey = await _addressService.getPrimaryAddressPublicKey(); - - Map payload = { - 'requester': address, - 'type': primaryAddressInfo.chain, - 'publicKey': publicKey, - 'timestamp': timeStamp, - 'signature': signature, - }; - + final Map payload = {'refresh_token': refreshToken}; if (receiptData != null) { // add the receipt data if available final String platform; @@ -76,83 +55,35 @@ class AuthService { 'receipt': {'platform': platform, 'receipt_data': receiptData} }); } - - var newJwt = await _getAuthAddress( - payload, - primaryAddress: primaryAddressInfo, - ); + final newJwt = await _userApi.refreshJWT(payload); _jwt = newJwt; + _refreshSubscriptionStatus(newJwt, receiptData: receiptData); + return newJwt; + } - if (newJwt.isValid(withSubscription: true)) { + void _refreshSubscriptionStatus(JWT jwt, {String? receiptData}) { + if (jwt.isValid(withSubscription: true)) { log.info('jwt with valid subscription'); unawaited(_configurationService .setIAPReceipt(receiptData ?? _configurationService.getIAPReceipt())); - unawaited(_configurationService.setIAPJWT(newJwt)); + unawaited(_configurationService.setIAPJWT(jwt)); } else { log.info('jwt with invalid subscription'); unawaited(_configurationService.setIAPReceipt(null)); unawaited(_configurationService.setIAPJWT(null)); } - - // after get new jwt, update subscription status injector().add(GetSubscriptionEvent()); injector().add(UpgradeQueryInfoEvent()); - return newJwt; } - Future _getAuthAddress(Map payload, - {AddressInfo? primaryAddress}) async => - await _authApi.authAddress(payload); - - Future getAuthToken({ - String? messageToSign, - String? receiptData, - bool forceRefresh = false, - }) async { - if (!forceRefresh && _jwt != null && _jwt!.isValid()) { - return _jwt!; + Future getAuthToken() async { + if (_jwt == null) { + return null; } - final primaryAddressAuthToken = - await _getPrimaryAddressAuthToken(receiptData: receiptData); - return primaryAddressAuthToken; - } - - Future registerPrimaryAddress( - {required AddressInfo primaryAddressInfo, - bool withDidKey = false}) async { - final address = await _addressService.getAddress(info: primaryAddressInfo); - final timestamp = DateTime.now().millisecondsSinceEpoch.toString(); - final messageForAddress = _addressService.getFeralfileAccountMessage( - address: address, - timestamp: timestamp, - ); - final signatureForAddress = await _addressService.getAddressSignature( - addressInfo: primaryAddressInfo, - message: messageForAddress, - ); - - Map payload = { - 'requester': address, - 'type': primaryAddressInfo.chain, - 'signature': signatureForAddress, - 'timestamp': timestamp, - }; - if (withDidKey) { - final defaultAccount = - await injector().getDefaultAccount(); - final didKey = await defaultAccount.getAccountDID(); - final messageForDidKey = _addressService.getFeralfileAccountMessage( - address: didKey, - timestamp: timestamp, - ); - log.info('setting external user by did: $didKey'); - unawaited(OneSignalHelper.setExternalUserId(userId: didKey)); - final signatureForDidKey = - await defaultAccount.getAccountDIDSignature(messageForDidKey); - payload['did'] = didKey; - payload['didSignature'] = signatureForDidKey; + if (!_jwt!.isValid()) { + await refreshJWT(); } - await _authApi.registerPrimaryAddress(payload); + return _jwt; } Future redeemGiftCode(String giftCode) async { diff --git a/lib/service/configuration_service.dart b/lib/service/configuration_service.dart index 799864394..379c420a4 100644 --- a/lib/service/configuration_service.dart +++ b/lib/service/configuration_service.dart @@ -24,9 +24,9 @@ import 'package:uuid/uuid.dart'; //ignore_for_file: constant_identifier_names abstract class ConfigurationService { - String getAccessToken(); + String getRefreshToken(); - Future setAccessToken(String value); + Future setRefreshToken(String value); bool didMigrateToAccountSetting(); @@ -210,7 +210,7 @@ abstract class ConfigurationService { } class ConfigurationServiceImpl implements ConfigurationService { - static const String keyAccessToken = 'access_token'; + static const String keyRefreshToken = 'refresh_token'; static const String keyDidMigrateToAccountSetting = 'did_migrate_to_account_setting'; static const String keyDidShowLiveWithArt = 'did_show_live_with_art'; @@ -936,11 +936,11 @@ class ConfigurationServiceImpl implements ConfigurationService { } @override - String getAccessToken() => _preferences.getString(keyAccessToken) ?? ''; + String getRefreshToken() => _preferences.getString(keyRefreshToken) ?? ''; @override - Future setAccessToken(String value) async { - await _preferences.setString(keyAccessToken, value); + Future setRefreshToken(String value) async { + await _preferences.setString(keyRefreshToken, value); } } diff --git a/lib/service/iap_service.dart b/lib/service/iap_service.dart index f77600646..6237a7d6e 100644 --- a/lib/service/iap_service.dart +++ b/lib/service/iap_service.dart @@ -246,8 +246,7 @@ class IAPServiceImpl implements IAPService { Future _verifyPurchase(String receiptData) async { try { log.info('[IAPService] get authToken with receipt'); - final jwt = await _authService.getAuthToken( - receiptData: receiptData, forceRefresh: true); + final jwt = await _authService.refreshJWT(receiptData: receiptData); return jwt; } catch (error) { log.info('[IAPService] error when verifying receipt', error); diff --git a/lib/service/passkey_service.dart b/lib/service/passkey_service.dart index 973e863e2..ee0498d83 100644 --- a/lib/service/passkey_service.dart +++ b/lib/service/passkey_service.dart @@ -1,5 +1,6 @@ import 'package:autonomy_flutter/gateway/user_api.dart'; import 'package:autonomy_flutter/model/jwt.dart'; +import 'package:autonomy_flutter/util/user_account_channel.dart'; import 'package:passkeys/authenticator.dart'; import 'package:passkeys/types.dart'; @@ -29,8 +30,9 @@ class PasskeyServiceImpl implements PasskeyService { AuthenticateResponseType? _loginResponse; final UserApi _userApi; + final UserAccountChannel _userAccountChannel; - PasskeyServiceImpl(this._userApi); + PasskeyServiceImpl(this._userApi, this._userAccountChannel); static final AuthenticatorSelectionType _defaultAuthenticatorSelection = AuthenticatorSelectionType( @@ -48,7 +50,11 @@ class PasskeyServiceImpl implements PasskeyService { @override Future logInInitiate() async { - final response = await _userApi.logInInitialize(); + final userId = await _userAccountChannel.getUserId(); + if (userId == null) { + throw Exception('User ID is not set'); + } + final response = await _userApi.logInInitialize(userId); final pubKey = response.publicKey; _loginRequest = AuthenticateRequestType( challenge: pubKey.challenge, diff --git a/lib/util/dio_interceptors.dart b/lib/util/dio_interceptors.dart index 17e3d50f0..35fab6efa 100644 --- a/lib/util/dio_interceptors.dart +++ b/lib/util/dio_interceptors.dart @@ -10,7 +10,6 @@ import 'dart:convert'; import 'package:autonomy_flutter/common/environment.dart'; import 'package:autonomy_flutter/common/injector.dart'; -import 'package:autonomy_flutter/gateway/iap_api.dart'; import 'package:autonomy_flutter/model/ff_account.dart'; import 'package:autonomy_flutter/service/auth_service.dart'; import 'package:autonomy_flutter/service/network_issue_manager.dart'; @@ -148,17 +147,12 @@ class AutonomyAuthInterceptor extends Interceptor { @override Future onRequest( RequestOptions options, RequestInterceptorHandler handler) async { - final shouldIgnoreAuthorizationPath = [ - ...IAPApi.shouldIgnoreAuthorizationPaths - ]; - if (!shouldIgnoreAuthorizationPath.contains(options.path)) { - final jwt = await injector().getAuthToken(); - if (jwt == null) { - unawaited(Sentry.captureMessage('JWT is null')); - throw JwtException(message: 'can_not_authenticate_desc'.tr()); - } - options.headers['Authorization'] = 'Bearer ${jwt.jwtToken}'; + final jwt = await injector().getAuthToken(); + if (jwt == null) { + unawaited(Sentry.captureMessage('JWT is null')); + throw JwtException(message: 'can_not_authenticate_desc'.tr()); } + options.headers['Authorization'] = 'Bearer ${jwt.jwtToken}'; return handler.next(options); } diff --git a/lib/util/gift_handler.dart b/lib/util/gift_handler.dart index 74c437cfb..acb0d7269 100644 --- a/lib/util/gift_handler.dart +++ b/lib/util/gift_handler.dart @@ -1,6 +1,4 @@ import 'package:autonomy_flutter/common/injector.dart'; -import 'package:autonomy_flutter/screen/bloc/subscription/subscription_bloc.dart'; -import 'package:autonomy_flutter/screen/bloc/subscription/subscription_state.dart'; import 'package:autonomy_flutter/service/auth_service.dart'; import 'package:autonomy_flutter/service/iap_service.dart'; import 'package:autonomy_flutter/service/navigation_service.dart'; @@ -23,8 +21,7 @@ class GiftHandler { try { final isSuccess = await authService.redeemGiftCode(giftCode); if (isSuccess) { - await authService.getAuthToken(forceRefresh: true); - injector().add(GetSubscriptionEvent()); + await authService.refreshJWT(); await navigationService.showRedeemMembershipSuccess(); return; } diff --git a/lib/util/user_account_channel.dart b/lib/util/user_account_channel.dart index 1e05bfb10..01c715a45 100644 --- a/lib/util/user_account_channel.dart +++ b/lib/util/user_account_channel.dart @@ -67,17 +67,17 @@ class UserAccountChannel { } } - Future getUserId() async { + Future getUserId() async { if (_userId != null) { return _userId!; } try { final userId = await _channel.invokeMethod('getUserId', {}); - _userId = userId ?? ''; + _userId = userId; return _userId!; } catch (e) { log.info('getUserId error', e); - return ''; + return null; } } @@ -92,7 +92,7 @@ class UserAccountChannel { Future didCreateUser() async { final userId = await getUserId(); - return userId.isNotEmpty; + return userId != null; } Future didRegisterPasskey() async { diff --git a/test/generate_mock/gateway/mock_iap_api.mocks.dart b/test/generate_mock/gateway/mock_iap_api.mocks.dart index 4f4fee864..81eef6b76 100644 --- a/test/generate_mock/gateway/mock_iap_api.mocks.dart +++ b/test/generate_mock/gateway/mock_iap_api.mocks.dart @@ -4,12 +4,11 @@ // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i5; -import 'dart:io' as _i6; import 'package:autonomy_flutter/gateway/iap_api.dart' as _i4; -import 'package:autonomy_flutter/model/announcement/announcement.dart' as _i7; +import 'package:autonomy_flutter/model/announcement/announcement.dart' as _i6; import 'package:autonomy_flutter/model/announcement/announcement_request.dart' - as _i8; + as _i7; import 'package:autonomy_flutter/model/jwt.dart' as _i2; import 'package:autonomy_flutter/model/ok_response.dart' as _i3; import 'package:mockito/mockito.dart' as _i1; @@ -27,19 +26,9 @@ import 'package:mockito/mockito.dart' as _i1; // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class -class _FakeJWT_0 extends _i1.SmartFake implements _i2.JWT { - _FakeJWT_0( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeOnesignalIdentityHash_1 extends _i1.SmartFake +class _FakeOnesignalIdentityHash_0 extends _i1.SmartFake implements _i2.OnesignalIdentityHash { - _FakeOnesignalIdentityHash_1( + _FakeOnesignalIdentityHash_0( Object parent, Invocation parentInvocation, ) : super( @@ -48,8 +37,8 @@ class _FakeOnesignalIdentityHash_1 extends _i1.SmartFake ); } -class _FakeOkResponse_2 extends _i1.SmartFake implements _i3.OkResponse { - _FakeOkResponse_2( +class _FakeOkResponse_1 extends _i1.SmartFake implements _i3.OkResponse { + _FakeOkResponse_1( Object parent, Invocation parentInvocation, ) : super( @@ -66,67 +55,6 @@ class MockIAPApi extends _i1.Mock implements _i4.IAPApi { _i1.throwOnMissingStub(this); } - @override - _i5.Future<_i2.JWT> authAddress(Map? body) => - (super.noSuchMethod( - Invocation.method( - #authAddress, - [body], - ), - returnValue: _i5.Future<_i2.JWT>.value(_FakeJWT_0( - this, - Invocation.method( - #authAddress, - [body], - ), - )), - ) as _i5.Future<_i2.JWT>); - @override - _i5.Future registerPrimaryAddress(Map? body) => - (super.noSuchMethod( - Invocation.method( - #registerPrimaryAddress, - [body], - ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); - @override - _i5.Future uploadProfile( - String? requester, - String? filename, - String? appVersion, - _i6.File? data, - ) => - (super.noSuchMethod( - Invocation.method( - #uploadProfile, - [ - requester, - filename, - appVersion, - data, - ], - ), - returnValue: _i5.Future.value(), - ) as _i5.Future); - @override - _i5.Future getProfileData( - String? requester, - String? filename, - String? version, - ) => - (super.noSuchMethod( - Invocation.method( - #getProfileData, - [ - requester, - filename, - version, - ], - ), - returnValue: _i5.Future.value(), - ) as _i5.Future); @override _i5.Future deleteAllProfiles(String? requester) => (super.noSuchMethod( @@ -153,7 +81,7 @@ class MockIAPApi extends _i1.Mock implements _i4.IAPApi { [body], ), returnValue: _i5.Future<_i2.OnesignalIdentityHash>.value( - _FakeOnesignalIdentityHash_1( + _FakeOnesignalIdentityHash_0( this, Invocation.method( #generateIdentityHash, @@ -162,23 +90,23 @@ class MockIAPApi extends _i1.Mock implements _i4.IAPApi { )), ) as _i5.Future<_i2.OnesignalIdentityHash>); @override - _i5.Future> getAnnouncements( - _i8.AnnouncementRequest? body) => + _i5.Future> getAnnouncements( + _i7.AnnouncementRequest? body) => (super.noSuchMethod( Invocation.method( #getAnnouncements, [body], ), returnValue: - _i5.Future>.value(<_i7.Announcement>[]), - ) as _i5.Future>); + _i5.Future>.value(<_i6.Announcement>[]), + ) as _i5.Future>); @override _i5.Future<_i3.OkResponse> redeemGiftCode(String? id) => (super.noSuchMethod( Invocation.method( #redeemGiftCode, [id], ), - returnValue: _i5.Future<_i3.OkResponse>.value(_FakeOkResponse_2( + returnValue: _i5.Future<_i3.OkResponse>.value(_FakeOkResponse_1( this, Invocation.method( #redeemGiftCode, diff --git a/test/generate_mock/service/mock_account_service.mocks.dart b/test/generate_mock/service/mock_account_service.mocks.dart index 7a1a45bbc..f6ec01660 100644 --- a/test/generate_mock/service/mock_account_service.mocks.dart +++ b/test/generate_mock/service/mock_account_service.mocks.dart @@ -162,7 +162,7 @@ class MockAccountService extends _i1.Mock implements _i5.AccountService { returnValue: _i6.Future.value(), ) as _i6.Future); @override - _i6.Future> createNewWallet({ + _i6.Future<_i2.WalletStorage> createNewWallet({ String? name = r'', String? passphrase = r'', }) => @@ -175,9 +175,18 @@ class MockAccountService extends _i1.Mock implements _i5.AccountService { #passphrase: passphrase, }, ), - returnValue: - _i6.Future>.value(<_i7.WalletAddress>[]), - ) as _i6.Future>); + returnValue: _i6.Future<_i2.WalletStorage>.value(_FakeWalletStorage_0( + this, + Invocation.method( + #createNewWallet, + [], + { + #name: name, + #passphrase: passphrase, + }, + ), + )), + ) as _i6.Future<_i2.WalletStorage>); @override _i6.Future<_i2.WalletStorage> importWords( String? words, diff --git a/test/generate_mock/service/mock_configuration_service.mocks.dart b/test/generate_mock/service/mock_configuration_service.mocks.dart index ca684bb41..b0e30f2a5 100644 --- a/test/generate_mock/service/mock_configuration_service.mocks.dart +++ b/test/generate_mock/service/mock_configuration_service.mocks.dart @@ -72,23 +72,23 @@ class MockConfigurationService extends _i1.Mock ), ) as _i2.ValueNotifier); @override - String getAccessToken() => (super.noSuchMethod( + String getRefreshToken() => (super.noSuchMethod( Invocation.method( - #getAccessToken, + #getRefreshToken, [], ), returnValue: _i5.dummyValue( this, Invocation.method( - #getAccessToken, + #getRefreshToken, [], ), ), ) as String); @override - _i6.Future setAccessToken(String? value) => (super.noSuchMethod( + _i6.Future setRefreshToken(String? value) => (super.noSuchMethod( Invocation.method( - #setAccessToken, + #setRefreshToken, [value], ), returnValue: _i6.Future.value(), From 171bdcad054d7f7cac94a36b49bfbac46b87f0a5 Mon Sep 17 00:00:00 2001 From: phuoc Date: Fri, 25 Oct 2024 12:07:04 +0700 Subject: [PATCH 04/57] redefine flow Signed-off-by: phuoc --- lib/common/injector.dart | 9 ++++++- lib/gateway/user_api.dart | 2 +- lib/gateway/user_api.g.dart | 6 ++--- lib/model/passkey_creation_option.dart | 16 ++++++++++++ lib/screen/onboarding_page.dart | 36 +++++++++++++++++++++++++- lib/service/account_service.dart | 4 +-- lib/service/address_service.dart | 30 ++++++++++++++------- lib/service/auth_service.dart | 11 ++++++++ lib/service/configuration_service.dart | 14 ++++++++++ lib/service/passkey_service.dart | 34 +++++++++++++++++++----- lib/util/user_account_channel.dart | 9 +++++++ 11 files changed, 147 insertions(+), 24 deletions(-) diff --git a/lib/common/injector.dart b/lib/common/injector.dart index a7af5625c..410aaf77f 100644 --- a/lib/common/injector.dart +++ b/lib/common/injector.dart @@ -70,6 +70,7 @@ import 'package:autonomy_flutter/service/navigation_service.dart'; import 'package:autonomy_flutter/service/network_issue_manager.dart'; import 'package:autonomy_flutter/service/network_service.dart'; import 'package:autonomy_flutter/service/notification_service.dart'; +import 'package:autonomy_flutter/service/passkey_service.dart'; import 'package:autonomy_flutter/service/pending_token_service.dart'; import 'package:autonomy_flutter/service/playlist_service.dart'; import 'package:autonomy_flutter/service/postcard_service.dart'; @@ -228,10 +229,16 @@ Future setupInjector() async { RemoteConfigServiceImpl( RemoteConfigApi(dio, baseUrl: Environment.remoteConfigURL))); injector.registerLazySingleton( - () => AuthService(injector(), injector(), injector())); + () => AuthService(injector(), injector(), injector(), injector())); injector .registerLazySingleton(() => TezosBeaconService(injector(), injector())); + injector.registerLazySingleton(() => PasskeyServiceImpl( + injector(), + injector(), + injector(), + )); + injector.registerFactoryParam( (p1, p2) => NftCollectionBloc( injector(), diff --git a/lib/gateway/user_api.dart b/lib/gateway/user_api.dart index 329f5169b..52661a127 100644 --- a/lib/gateway/user_api.dart +++ b/lib/gateway/user_api.dart @@ -18,7 +18,7 @@ abstract class UserApi { factory UserApi(Dio dio, {String baseUrl}) = _UserApi; @POST('/apis/users/passkeys/registration/initialize') - Future registerInitialize(); + Future registerInitialize(); @POST('/apis/users/passkeys/registration/finalize') Future registerFinalize(@Body() Map body); diff --git a/lib/gateway/user_api.g.dart b/lib/gateway/user_api.g.dart index 8bf32b4da..e582cdcf4 100644 --- a/lib/gateway/user_api.g.dart +++ b/lib/gateway/user_api.g.dart @@ -19,13 +19,13 @@ class _UserApi implements UserApi { String? baseUrl; @override - Future registerInitialize() async { + Future registerInitialize() async { const _extra = {}; final queryParameters = {}; final _headers = {}; final Map? _data = null; final _result = await _dio.fetch>( - _setStreamType(Options( + _setStreamType(Options( method: 'POST', headers: _headers, extra: _extra, @@ -41,7 +41,7 @@ class _UserApi implements UserApi { _dio.options.baseUrl, baseUrl, )))); - final value = CredentialCreationOption.fromJson(_result.data!); + final value = CredentialCreationOptionResponse.fromJson(_result.data!); return value; } diff --git a/lib/model/passkey_creation_option.dart b/lib/model/passkey_creation_option.dart index 80eff447b..6d4fa376f 100644 --- a/lib/model/passkey_creation_option.dart +++ b/lib/model/passkey_creation_option.dart @@ -1,5 +1,21 @@ import 'package:passkeys/types.dart'; +class CredentialCreationOptionResponse { + final CredentialCreationOption credentialCreationOption; + final String passkeyUserID; + CredentialCreationOptionResponse({ + required this.credentialCreationOption, + required this.passkeyUserID, + }); + + factory CredentialCreationOptionResponse.fromJson(Map json) => + CredentialCreationOptionResponse( + credentialCreationOption: + CredentialCreationOption.fromJson(json['credential']), + passkeyUserID: json['passkeyUserID'], + ); +} + class CredentialCreationOption { final PublicKey publicKey; diff --git a/lib/screen/onboarding_page.dart b/lib/screen/onboarding_page.dart index 2a46748e7..e74a34371 100644 --- a/lib/screen/onboarding_page.dart +++ b/lib/screen/onboarding_page.dart @@ -12,11 +12,13 @@ import 'package:autonomy_flutter/common/environment.dart'; import 'package:autonomy_flutter/common/injector.dart'; import 'package:autonomy_flutter/screen/app_router.dart'; import 'package:autonomy_flutter/service/account_service.dart'; +import 'package:autonomy_flutter/service/auth_service.dart'; import 'package:autonomy_flutter/service/configuration_service.dart'; import 'package:autonomy_flutter/service/deeplink_service.dart'; import 'package:autonomy_flutter/service/device_info_service.dart'; import 'package:autonomy_flutter/service/metric_client_service.dart'; import 'package:autonomy_flutter/service/notification_service.dart'; +import 'package:autonomy_flutter/service/passkey_service.dart'; import 'package:autonomy_flutter/service/remote_config_service.dart'; import 'package:autonomy_flutter/util/dailies_helper.dart'; import 'package:autonomy_flutter/util/john_gerrard_helper.dart'; @@ -24,6 +26,7 @@ import 'package:autonomy_flutter/util/log.dart'; import 'package:autonomy_flutter/util/metric_helper.dart'; import 'package:autonomy_flutter/util/notification_util.dart'; import 'package:autonomy_flutter/util/style.dart'; +import 'package:autonomy_flutter/util/user_account_channel.dart'; import 'package:autonomy_flutter/view/back_appbar.dart'; import 'package:autonomy_flutter/view/primary_button.dart'; import 'package:autonomy_flutter/view/responsive.dart'; @@ -50,6 +53,10 @@ class _OnboardingPageState extends State final deepLinkService = injector.get(); Timer? _timer; + final _passkeyService = injector.get(); + final _authService = injector.get(); + final _userAccountChannel = injector.get(); + final _onboardingLogo = Semantics( label: 'onboarding_logo', child: Center( @@ -149,7 +156,7 @@ class _OnboardingPageState extends State '[_createAccountOrRestoreIfNeeded] Loading more than 10s')); }); log.info('[_fetchRuntimeCache] start'); - await injector().migrateAccount(() {}); + await _loginProcess(); unawaited(_registerPushNotifications()); unawaited(injector().setup()); log.info('[_fetchRuntimeCache] end'); @@ -165,6 +172,33 @@ class _OnboardingPageState extends State await _goToTargetScreen(context); } + Future _loginProcess() async { + final isSupportPasskey = await _passkeyService.isPassKeyAvailable(); + if (!isSupportPasskey) { + log.info('Passkey is not supported. Login with address'); + await injector().migrateAccount(() async { + await _authService.authenticateAddress(); + }); + } else { + log.info('Passkey is supported. Login with passkey'); + final didRegisterPasskey = await _userAccountChannel.didRegisterPasskey(); + if (didRegisterPasskey) { + await _loginWithPasskey(); + } else { + await _registerPasskey(); + } + } + await injector().migrateAccount(() async {}); + } + + Future _loginWithPasskey() async { + + } + + Future _registerPasskey() async { + + } + @override Widget build(BuildContext context) => Scaffold( appBar: getDarkEmptyAppBar(Colors.transparent), diff --git a/lib/service/account_service.dart b/lib/service/account_service.dart index 3ccaa9516..4f0a79c35 100644 --- a/lib/service/account_service.dart +++ b/lib/service/account_service.dart @@ -43,7 +43,7 @@ import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:uuid/uuid.dart'; abstract class AccountService { - Future migrateAccount(Function() createLoginJwt); + Future migrateAccount(Future Function() createLoginJwt); List getWalletsAddress(CryptoType cryptoType); @@ -627,7 +627,7 @@ class AccountServiceImpl extends AccountService { } @override - Future migrateAccount(Function() createLoginJwt) async { + Future migrateAccount(Future Function() createLoginJwt) async { log.info('[AccountService] migrateAccount'); final cloudDb = injector(); final isDoneOnboarding = _configurationService.isDoneOnboarding(); diff --git a/lib/service/address_service.dart b/lib/service/address_service.dart index 2bb5255e2..7b8f618e1 100644 --- a/lib/service/address_service.dart +++ b/lib/service/address_service.dart @@ -89,8 +89,6 @@ class AddressService { await walletStorage.getETHEip55Address(index: info.index); final checksumAddress = address.getETHEip55Address(); return checksumAddress; - case 'tezos': - return walletStorage.getTezosAddress(index: info.index); default: throw UnsupportedError('Unsupported chain: $chain'); } @@ -104,7 +102,7 @@ class AddressService { return getAddress(info: addressInfo); } - Future getAddressSignature( + Future _getAddressSignature( {required AddressInfo addressInfo, required String message}) async { final walletStorage = WalletStorage(addressInfo.uuid); final chain = addressInfo.chain; @@ -120,18 +118,30 @@ class AddressService { return signature; } - Future getPrimaryAddressSignature({required String message}) async { + String _getFeralfileAccountMessage( + {required String address, required String timestamp}) => + 'feralfile-account: {"requester":"$address","timestamp":"$timestamp"}'; + + Future> getAddressAuthenticationMap() async { final addressInfo = await getPrimaryAddressInfo(); if (addressInfo == null) { - return null; + throw Exception( + 'No primary address found during get address authentication'); } - return getAddressSignature(addressInfo: addressInfo, message: message); + final address = await getAddress(info: addressInfo); + final timestamp = DateTime.now().millisecondsSinceEpoch.toString(); + final message = + _getFeralfileAccountMessage(address: address, timestamp: timestamp); + final signature = + await _getAddressSignature(addressInfo: addressInfo, message: message); + return { + 'requester': address, + 'timestamp': timestamp, + 'signature': signature, + 'type': 'ethereum', + }; } - String getFeralfileAccountMessage( - {required String address, required String timestamp}) => - 'feralfile-account: {"requester":"$address","timestamp":"$timestamp"}'; - Future> getAllAddress() async { final addresses = _cloudObject.addressObject.getAllAddresses(); return addresses; diff --git a/lib/service/auth_service.dart b/lib/service/auth_service.dart index 2b4796f1e..016664698 100644 --- a/lib/service/auth_service.dart +++ b/lib/service/auth_service.dart @@ -16,6 +16,7 @@ import 'package:autonomy_flutter/screen/bloc/subscription/subscription_bloc.dart import 'package:autonomy_flutter/screen/bloc/subscription/subscription_state.dart'; import 'package:autonomy_flutter/screen/settings/subscription/upgrade_bloc.dart'; import 'package:autonomy_flutter/screen/settings/subscription/upgrade_state.dart'; +import 'package:autonomy_flutter/service/address_service.dart'; import 'package:autonomy_flutter/service/configuration_service.dart'; import 'package:autonomy_flutter/util/exception.dart'; import 'package:autonomy_flutter/util/log.dart'; @@ -25,12 +26,14 @@ class AuthService { final IAPApi _authApi; final UserApi _userApi; final ConfigurationService _configurationService; + final AddressService _addressService; JWT? _jwt; AuthService( this._authApi, this._userApi, this._configurationService, + this._addressService, ); void reset() { @@ -61,6 +64,14 @@ class AuthService { return newJwt; } + Future authenticateAddress() async { + final payload = await _addressService.getAddressAuthenticationMap(); + final jwt = await _userApi.authenticateAddress(payload); + _jwt = jwt; + _refreshSubscriptionStatus(jwt); + return jwt; + } + void _refreshSubscriptionStatus(JWT jwt, {String? receiptData}) { if (jwt.isValid(withSubscription: true)) { log.info('jwt with valid subscription'); diff --git a/lib/service/configuration_service.dart b/lib/service/configuration_service.dart index 379c420a4..65538d286 100644 --- a/lib/service/configuration_service.dart +++ b/lib/service/configuration_service.dart @@ -24,6 +24,10 @@ import 'package:uuid/uuid.dart'; //ignore_for_file: constant_identifier_names abstract class ConfigurationService { + bool didRegisterPasskey(); + + Future setRegisterPasskey(bool value); + String getRefreshToken(); Future setRefreshToken(String value); @@ -210,6 +214,7 @@ abstract class ConfigurationService { } class ConfigurationServiceImpl implements ConfigurationService { + static const String keyDidRegisterPasskey = 'did_register_passkey'; static const String keyRefreshToken = 'refresh_token'; static const String keyDidMigrateToAccountSetting = 'did_migrate_to_account_setting'; @@ -942,6 +947,15 @@ class ConfigurationServiceImpl implements ConfigurationService { Future setRefreshToken(String value) async { await _preferences.setString(keyRefreshToken, value); } + + @override + bool didRegisterPasskey() => + _preferences.getBool(keyDidRegisterPasskey) ?? false; + + @override + Future setRegisterPasskey(bool value) async { + await _preferences.setBool(keyDidRegisterPasskey, value); + } } enum ConflictAction { diff --git a/lib/service/passkey_service.dart b/lib/service/passkey_service.dart index ee0498d83..558534729 100644 --- a/lib/service/passkey_service.dart +++ b/lib/service/passkey_service.dart @@ -1,5 +1,6 @@ import 'package:autonomy_flutter/gateway/user_api.dart'; import 'package:autonomy_flutter/model/jwt.dart'; +import 'package:autonomy_flutter/service/address_service.dart'; import 'package:autonomy_flutter/util/user_account_channel.dart'; import 'package:passkeys/authenticator.dart'; import 'package:passkeys/types.dart'; @@ -25,15 +26,22 @@ class PasskeyServiceImpl implements PasskeyService { RegisterRequestType? _registerRequest; RegisterResponseType? _registerResponse; + String? _passkeyUserId; AuthenticateRequestType? _loginRequest; AuthenticateResponseType? _loginResponse; final UserApi _userApi; final UserAccountChannel _userAccountChannel; + final AddressService _addressService; - PasskeyServiceImpl(this._userApi, this._userAccountChannel); + PasskeyServiceImpl( + this._userApi, + this._userAccountChannel, + this._addressService, + ); + /* static final AuthenticatorSelectionType _defaultAuthenticatorSelection = AuthenticatorSelectionType( requireResidentKey: false, @@ -42,7 +50,10 @@ class PasskeyServiceImpl implements PasskeyService { ); static const _defaultRelayingPartyId = 'www.feralfile.com'; - static const _preferImmediatelyAvailableCredentials = true; + + */ + + static const _preferImmediatelyAvailableCredentials = false; @override Future isPassKeyAvailable() async => @@ -56,10 +67,14 @@ class PasskeyServiceImpl implements PasskeyService { } final response = await _userApi.logInInitialize(userId); final pubKey = response.publicKey; + + if (pubKey.rpId == null) { + throw Exception('RP ID is not set'); + } _loginRequest = AuthenticateRequestType( challenge: pubKey.challenge, allowCredentials: pubKey.allowCredentials ?? [], - relyingPartyId: pubKey.rpId ?? _defaultRelayingPartyId, + relyingPartyId: pubKey.rpId!, mediation: response.mediation, preferImmediatelyAvailableCredentials: _preferImmediatelyAvailableCredentials, @@ -86,13 +101,16 @@ class PasskeyServiceImpl implements PasskeyService { @override Future registerInitiate() async { final response = await _userApi.registerInitialize(); - final pubKey = response.publicKey; + final pubKey = response.credentialCreationOption.publicKey; + if (pubKey.authenticatorSelection == null) { + throw Exception('Authenticator selection is not set'); + } + _passkeyUserId = response.passkeyUserID; _registerRequest = RegisterRequestType( challenge: pubKey.challenge, relyingParty: pubKey.rp, user: pubKey.user, - authSelectionType: - pubKey.authenticatorSelection ?? _defaultAuthenticatorSelection, + authSelectionType: pubKey.authenticatorSelection!, excludeCredentials: pubKey.excludeCredentials ?? [], ); } @@ -108,7 +126,11 @@ class PasskeyServiceImpl implements PasskeyService { @override Future registerFinalize() async { + final addressAuthentication = + await _addressService.getAddressAuthenticationMap(); final response = await _userApi.registerFinalize({ + 'addressAuthentication': addressAuthentication, + 'passkeyUserId': _passkeyUserId, 'public_key_credential': _registerResponse!.toJson(), }); return response; diff --git a/lib/util/user_account_channel.dart b/lib/util/user_account_channel.dart index 01c715a45..8eda131a1 100644 --- a/lib/util/user_account_channel.dart +++ b/lib/util/user_account_channel.dart @@ -2,6 +2,8 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'package:autonomy_flutter/common/injector.dart'; +import 'package:autonomy_flutter/service/configuration_service.dart'; import 'package:autonomy_flutter/util/log.dart'; import 'package:autonomy_flutter/util/wallet_utils.dart'; import 'package:flutter/services.dart'; @@ -96,11 +98,18 @@ class UserAccountChannel { } Future didRegisterPasskey() async { + if (Platform.isAndroid) { + return injector().didRegisterPasskey(); + } final didRegister = await _channel.invokeMethod('didRegisterPasskey', {}); return didRegister; } Future setDidRegisterPasskey(bool value) async { + if (Platform.isAndroid) { + await injector().setRegisterPasskey(value); + return true; + } final didRegister = await _channel.invokeMethod('setDidRegisterPasskey', { 'data': value, }); From ac45cb33c28f34f443118e3bd7154e1e9f0bc361 Mon Sep 17 00:00:00 2001 From: phuoc Date: Fri, 25 Oct 2024 14:39:28 +0700 Subject: [PATCH 05/57] login ui Signed-off-by: phuoc --- lib/screen/onboarding_page.dart | 22 +-- lib/service/passkey_service.dart | 2 + lib/util/ui_helper.dart | 40 ++++++ lib/view/passkey/passkey_login_view.dart | 105 ++++++++++++++ lib/view/passkey/passkey_register_view.dart | 144 ++++++++++++++++++++ 5 files changed, 302 insertions(+), 11 deletions(-) create mode 100644 lib/view/passkey/passkey_login_view.dart create mode 100644 lib/view/passkey/passkey_register_view.dart diff --git a/lib/screen/onboarding_page.dart b/lib/screen/onboarding_page.dart index e74a34371..d0713ea72 100644 --- a/lib/screen/onboarding_page.dart +++ b/lib/screen/onboarding_page.dart @@ -26,6 +26,7 @@ import 'package:autonomy_flutter/util/log.dart'; import 'package:autonomy_flutter/util/metric_helper.dart'; import 'package:autonomy_flutter/util/notification_util.dart'; import 'package:autonomy_flutter/util/style.dart'; +import 'package:autonomy_flutter/util/ui_helper.dart'; import 'package:autonomy_flutter/util/user_account_channel.dart'; import 'package:autonomy_flutter/view/back_appbar.dart'; import 'package:autonomy_flutter/view/primary_button.dart'; @@ -182,22 +183,21 @@ class _OnboardingPageState extends State } else { log.info('Passkey is supported. Login with passkey'); final didRegisterPasskey = await _userAccountChannel.didRegisterPasskey(); - if (didRegisterPasskey) { - await _loginWithPasskey(); - } else { - await _registerPasskey(); + + final didLoginSuccess = didRegisterPasskey + ? await _loginWithPasskey() + : await _registerPasskey(); + if (didLoginSuccess != true) { + throw Exception('Failed to login with passkey'); } } - await injector().migrateAccount(() async {}); } - Future _loginWithPasskey() async { - - } + Future _loginWithPasskey() async => + await UIHelper.showPasskeyLoginDialog(context); - Future _registerPasskey() async { - - } + Future _registerPasskey() async => + await UIHelper.showPasskeyRegisterDialog(context); @override Widget build(BuildContext context) => Scaffold( diff --git a/lib/service/passkey_service.dart b/lib/service/passkey_service.dart index 558534729..32771c465 100644 --- a/lib/service/passkey_service.dart +++ b/lib/service/passkey_service.dart @@ -133,6 +133,8 @@ class PasskeyServiceImpl implements PasskeyService { 'passkeyUserId': _passkeyUserId, 'public_key_credential': _registerResponse!.toJson(), }); + await _userAccountChannel.setDidRegisterPasskey(true); + await _userAccountChannel.setUserId(addressAuthentication['requester']); return response; } } diff --git a/lib/util/ui_helper.dart b/lib/util/ui_helper.dart index e3cf71855..2e3653516 100644 --- a/lib/util/ui_helper.dart +++ b/lib/util/ui_helper.dart @@ -33,6 +33,8 @@ import 'package:autonomy_flutter/view/artwork_common_widget.dart'; import 'package:autonomy_flutter/view/au_button_clipper.dart'; import 'package:autonomy_flutter/view/back_appbar.dart'; import 'package:autonomy_flutter/view/confetti.dart'; +import 'package:autonomy_flutter/view/passkey/passkey_login_view.dart'; +import 'package:autonomy_flutter/view/passkey/passkey_register_view.dart'; import 'package:autonomy_flutter/view/postcard_button.dart'; import 'package:autonomy_flutter/view/postcard_common_widget.dart'; import 'package:autonomy_flutter/view/primary_button.dart'; @@ -1039,6 +1041,44 @@ class UIHelper { ); } + static Future showPasskeyRegisterDialog(BuildContext context) async => + await showRawCenterSheet( + context, + content: const PasskeyRegisterView(), + ); + + static Future showPasskeyLoginDialog(BuildContext context) async { + await showRawCenterSheet( + context, + content: const PasskeyLoginView(), + ); + } + + static Future showRawCenterSheet(BuildContext context, + {required Widget content, + double horizontalPadding = 20, + Color backgroundColor = AppColor.white}) async { + UIHelper.hideInfoDialog(context); + return await showCupertinoModalPopup( + context: context, + builder: (context) => Scaffold( + backgroundColor: Colors.transparent, + body: Center( + child: Container( + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: BorderRadius.circular(5), + ), + constraints: BoxConstraints( + maxHeight: MediaQuery.sizeOf(context).height - 256, + ), + padding: const EdgeInsets.symmetric(vertical: 20), + child: Flexible(child: content), + ), + ), + )); + } + static Future showCenterSheet(BuildContext context, {required Widget content, String? actionButton, diff --git a/lib/view/passkey/passkey_login_view.dart b/lib/view/passkey/passkey_login_view.dart new file mode 100644 index 000000000..56c420ff9 --- /dev/null +++ b/lib/view/passkey/passkey_login_view.dart @@ -0,0 +1,105 @@ +import 'package:autonomy_flutter/common/injector.dart'; +import 'package:autonomy_flutter/service/account_service.dart'; +import 'package:autonomy_flutter/service/passkey_service.dart'; +import 'package:autonomy_flutter/view/primary_button.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:feralfile_app_theme/extensions/theme_extension.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class PasskeyLoginView extends StatefulWidget { + const PasskeyLoginView({super.key}); + + @override + State createState() => _PasskeyLoginViewState(); +} + +class _PasskeyLoginViewState extends State { + final _passkeyService = injector.get(); + final _accountService = injector.get(); + + bool _isError = false; + bool _isLogging = false; + + @override + Widget build(BuildContext context) => SingleChildScrollView( + child: Column( + children: [ + _getTitle(context), + const SizedBox(height: 20), + _getDesc(context), + const SizedBox(height: 20), + _getIcon(), + const SizedBox(height: 20), + _getAction(context), + const SizedBox(height: 20), + _havingTrouble(context) + ], + ), + ); + + Widget _getTitle(BuildContext context) => Text( + 'login_title'.tr(), + style: Theme.of(context).textTheme.ppMori700Black16, + ); + + Widget _getDesc(BuildContext context) { + final theme = Theme.of(context); + final style = theme.textTheme.ppMori400Black14; + return Text( + 'login_desc'.tr(), + style: style, + ); + } + + Widget _getIcon() => SvgPicture.asset( + 'assets/images/passkey_icon.svg', + ); + + Widget _getAction(BuildContext context) => PrimaryAsyncButton( + enabled: !_isError, + onTap: () async { + if (_isLogging) { + return; + } + setState(() { + _isLogging = true; + }); + try { + await _passkeyService.logInInitiate(); + await _passkeyService.logInRequest(); + await _accountService.migrateAccount(() async { + await _passkeyService.registerFinalize(); + }); + if (context.mounted) { + Navigator.of(context).pop(true); + } + } catch (e) { + setState(() { + _isError = true; + }); + } + }, + text: 'login_button'.tr(), + ); + + Widget _havingTrouble(BuildContext context) { + if (!_isError && !_isLogging) { + return const SizedBox(); + } + return Padding( + padding: const EdgeInsets.only(bottom: 20), + child: GestureDetector( + onTap: () { + //Navigator.of(context).pop(); + }, + child: Text( + 'having_trouble'.tr(), + style: Theme.of(context).textTheme.ppMori400Grey14.copyWith( + decoration: TextDecoration.underline, + ), + ), + ), + ); + } +} diff --git a/lib/view/passkey/passkey_register_view.dart b/lib/view/passkey/passkey_register_view.dart new file mode 100644 index 000000000..0f6a0ea6e --- /dev/null +++ b/lib/view/passkey/passkey_register_view.dart @@ -0,0 +1,144 @@ +import 'package:autonomy_flutter/common/injector.dart'; +import 'package:autonomy_flutter/service/account_service.dart'; +import 'package:autonomy_flutter/service/passkey_service.dart'; +import 'package:autonomy_flutter/view/primary_button.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:feralfile_app_theme/extensions/theme_extension.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class PasskeyRegisterView extends StatefulWidget { + const PasskeyRegisterView({super.key}); + + @override + State createState() => _PasskeyRegisterViewState(); +} + +class _PasskeyRegisterViewState extends State { + final _passkeyService = injector.get(); + final _accountService = injector.get(); + + bool _isError = false; + bool _registering = false; + bool _didSuccess = false; + + @override + Widget build(BuildContext context) => SingleChildScrollView( + child: Column( + children: [ + _getTitle(context), + const SizedBox(height: 20), + _getDesc(context), + const SizedBox(height: 20), + _getIcon(), + const SizedBox(height: 20), + _getAction(context), + const SizedBox(height: 20), + _havingTrouble(context) + ], + ), + ); + + Widget _getTitle(BuildContext context) => Text( + _didSuccess ? 'passkey_created'.tr() : 'introducing_passkey'.tr(), + style: Theme.of(context).textTheme.ppMori700Black16, + ); + + Widget _getDesc(BuildContext context) { + final theme = Theme.of(context); + final style = theme.textTheme.ppMori400Black14; + if (_didSuccess) { + return Column( + children: [ + Text( + 'passkey_created_desc'.tr(), + style: style, + ), + ], + ); + } + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'introducing_passkey_desc_1'.tr(), + style: style, + ), + const SizedBox(height: 10), + Text( + 'introducing_passkey_desc_2'.tr(), + style: style, + ), + ], + ); + } + + Widget _getIcon() { + if (_didSuccess) { + return SvgPicture.asset( + 'assets/images/selected_round.svg', + ); + } + return SvgPicture.asset( + 'assets/images/passkey_icon.svg', + ); + } + + Widget _getAction(BuildContext context) { + if (_didSuccess) { + return PrimaryButton( + text: 'continue'.tr(), + onTap: () { + Navigator.of(context).pop(true); + }, + ); + } + return PrimaryAsyncButton( + enabled: !_isError, + onTap: () async { + if (_registering) { + return; + } + setState(() { + _registering = true; + }); + try { + await _passkeyService.registerInitiate(); + await _passkeyService.logInRequest(); + await _accountService.migrateAccount(() async { + await _passkeyService.logInFinalize(); + }); + setState(() { + _didSuccess = true; + }); + } catch (e) { + setState(() { + _isError = true; + }); + } + }, + text: 'get_started'.tr(), + processingText: 'creating_passkey'.tr(), + ); + } + + Widget _havingTrouble(BuildContext context) { + if (_didSuccess || (!_isError && !_registering)) { + return const SizedBox(); + } + return Padding( + padding: const EdgeInsets.only(bottom: 20), + child: GestureDetector( + onTap: () { + //Navigator.of(context).pop(); + }, + child: Text( + 'having_trouble'.tr(), + style: Theme.of(context).textTheme.ppMori400Grey14.copyWith( + decoration: TextDecoration.underline, + ), + ), + ), + ); + } +} From 6b04cfaf403d7f38b16686123d55ee964f8cc840 Mon Sep 17 00:00:00 2001 From: phuoc Date: Fri, 25 Oct 2024 15:28:33 +0700 Subject: [PATCH 06/57] upgrade kotlin version Signed-off-by: phuoc --- android/app/build.gradle | 2 +- android/settings.gradle | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 9cd2375f0..8e2c75953 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -4,7 +4,7 @@ buildscript { maven { url 'https://plugins.gradle.org/m2/' } // Gradle Plugin Portal google() // Google's Maven repository } - ext.kotlin_version = '1.7.20' + ext.kotlin_version = '1.9.0' dependencies { // ... // OneSignal-Gradle-Plugin diff --git a/android/settings.gradle b/android/settings.gradle index fcdb69d34..57a13b892 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -7,7 +7,7 @@ pluginManagement { return flutterSdkPath } settings.ext.flutterSdkPath = flutterSdkPath() - ext.kotlin_version = '1.7.20' + ext.kotlin_version = '1.9.0' includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") @@ -21,7 +21,7 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" id "com.android.application" version '7.4.2' apply false - id "org.jetbrains.kotlin.android" version "1.7.20" apply false + id "org.jetbrains.kotlin.android" version "1.9.0" apply false id "org.jetbrains.kotlin.plugin.serialization" version "1.7.20" apply false id "com.google.gms.google-services" version "4.3.14" apply false } From b46d7a5cd3dfae876e0fde7861166b72cdbcc9fb Mon Sep 17 00:00:00 2001 From: phuoc Date: Fri, 25 Oct 2024 15:55:47 +0700 Subject: [PATCH 07/57] update ui, model Signed-off-by: phuoc --- lib/model/passkey_creation_option.dart | 6 +- lib/screen/onboarding_page.dart | 2 +- lib/service/passkey_service.dart | 2 +- lib/util/ui_helper.dart | 33 +++++---- lib/view/passkey/passkey_login_view.dart | 78 +++++++++++---------- lib/view/passkey/passkey_register_view.dart | 38 +++++----- lib/view/primary_button.dart | 2 +- 7 files changed, 91 insertions(+), 70 deletions(-) diff --git a/lib/model/passkey_creation_option.dart b/lib/model/passkey_creation_option.dart index 6d4fa376f..f4b543422 100644 --- a/lib/model/passkey_creation_option.dart +++ b/lib/model/passkey_creation_option.dart @@ -3,15 +3,17 @@ import 'package:passkeys/types.dart'; class CredentialCreationOptionResponse { final CredentialCreationOption credentialCreationOption; final String passkeyUserID; + CredentialCreationOptionResponse({ required this.credentialCreationOption, required this.passkeyUserID, }); - factory CredentialCreationOptionResponse.fromJson(Map json) => + factory CredentialCreationOptionResponse.fromJson( + Map json) => CredentialCreationOptionResponse( credentialCreationOption: - CredentialCreationOption.fromJson(json['credential']), + CredentialCreationOption.fromJson(json['credentialCreation']), passkeyUserID: json['passkeyUserID'], ); } diff --git a/lib/screen/onboarding_page.dart b/lib/screen/onboarding_page.dart index d0713ea72..515018599 100644 --- a/lib/screen/onboarding_page.dart +++ b/lib/screen/onboarding_page.dart @@ -181,7 +181,7 @@ class _OnboardingPageState extends State await _authService.authenticateAddress(); }); } else { - log.info('Passkey is supported. Login with passkey'); + log.info('Passkey is supported. Authenticate with passkey'); final didRegisterPasskey = await _userAccountChannel.didRegisterPasskey(); final didLoginSuccess = didRegisterPasskey diff --git a/lib/service/passkey_service.dart b/lib/service/passkey_service.dart index 32771c465..0d1f78480 100644 --- a/lib/service/passkey_service.dart +++ b/lib/service/passkey_service.dart @@ -49,7 +49,7 @@ class PasskeyServiceImpl implements PasskeyService { userVerification: 'preferred', ); - static const _defaultRelayingPartyId = 'www.feralfile.com'; + static const _defaultRelayingPartyId = 'accounts.dev.feralfile.com'; */ diff --git a/lib/util/ui_helper.dart b/lib/util/ui_helper.dart index 2e3653516..dd229e07d 100644 --- a/lib/util/ui_helper.dart +++ b/lib/util/ui_helper.dart @@ -1041,7 +1041,8 @@ class UIHelper { ); } - static Future showPasskeyRegisterDialog(BuildContext context) async => + static Future showPasskeyRegisterDialog( + BuildContext context) async => await showRawCenterSheet( context, content: const PasskeyRegisterView(), @@ -1061,19 +1062,25 @@ class UIHelper { UIHelper.hideInfoDialog(context); return await showCupertinoModalPopup( context: context, - builder: (context) => Scaffold( - backgroundColor: Colors.transparent, - body: Center( - child: Container( - decoration: BoxDecoration( - color: backgroundColor, - borderRadius: BorderRadius.circular(5), - ), - constraints: BoxConstraints( - maxHeight: MediaQuery.sizeOf(context).height - 256, + builder: (context) => Padding( + padding: EdgeInsets.symmetric(horizontal: horizontalPadding), + child: Scaffold( + backgroundColor: Colors.transparent, + body: Center( + child: Container( + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: BorderRadius.circular(5), + ), + padding: const EdgeInsets.symmetric( + vertical: 20, horizontal: 20), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + content, + ], + ), ), - padding: const EdgeInsets.symmetric(vertical: 20), - child: Flexible(child: content), ), ), )); diff --git a/lib/view/passkey/passkey_login_view.dart b/lib/view/passkey/passkey_login_view.dart index 56c420ff9..76147c5a5 100644 --- a/lib/view/passkey/passkey_login_view.dart +++ b/lib/view/passkey/passkey_login_view.dart @@ -1,6 +1,7 @@ import 'package:autonomy_flutter/common/injector.dart'; import 'package:autonomy_flutter/service/account_service.dart'; import 'package:autonomy_flutter/service/passkey_service.dart'; +import 'package:autonomy_flutter/util/log.dart'; import 'package:autonomy_flutter/view/primary_button.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:feralfile_app_theme/extensions/theme_extension.dart'; @@ -22,20 +23,23 @@ class _PasskeyLoginViewState extends State { bool _isLogging = false; @override - Widget build(BuildContext context) => SingleChildScrollView( - child: Column( - children: [ - _getTitle(context), - const SizedBox(height: 20), - _getDesc(context), - const SizedBox(height: 20), - _getIcon(), - const SizedBox(height: 20), - _getAction(context), - const SizedBox(height: 20), - _havingTrouble(context) - ], - ), + Widget build(BuildContext context) => Column( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _getTitle(context), + const SizedBox(height: 20), + _getDesc(context), + ], + ), + const SizedBox(height: 20), + _getIcon(), + const SizedBox(height: 20), + _getAction(context), + const SizedBox(height: 20), + _havingTrouble(context) + ], ); Widget _getTitle(BuildContext context) => Text( @@ -57,31 +61,33 @@ class _PasskeyLoginViewState extends State { ); Widget _getAction(BuildContext context) => PrimaryAsyncButton( - enabled: !_isError, - onTap: () async { - if (_isLogging) { - return; - } - setState(() { - _isLogging = true; - }); - try { - await _passkeyService.logInInitiate(); - await _passkeyService.logInRequest(); - await _accountService.migrateAccount(() async { - await _passkeyService.registerFinalize(); - }); - if (context.mounted) { - Navigator.of(context).pop(true); + key: const Key('login_button'), + enabled: !_isError, + onTap: () async { + if (_isLogging) { + return; } - } catch (e) { setState(() { - _isError = true; + _isLogging = true; }); - } - }, - text: 'login_button'.tr(), - ); + try { + await _passkeyService.logInInitiate(); + await _passkeyService.logInRequest(); + await _accountService.migrateAccount(() async { + await _passkeyService.registerFinalize(); + }); + if (context.mounted) { + Navigator.of(context).pop(true); + } + } catch (e) { + log.info('Failed to login with passkey: $e'); + setState(() { + _isError = true; + }); + } + }, + text: 'login_button'.tr(), + ); Widget _havingTrouble(BuildContext context) { if (!_isError && !_isLogging) { diff --git a/lib/view/passkey/passkey_register_view.dart b/lib/view/passkey/passkey_register_view.dart index 0f6a0ea6e..cda5f485d 100644 --- a/lib/view/passkey/passkey_register_view.dart +++ b/lib/view/passkey/passkey_register_view.dart @@ -1,6 +1,7 @@ import 'package:autonomy_flutter/common/injector.dart'; import 'package:autonomy_flutter/service/account_service.dart'; import 'package:autonomy_flutter/service/passkey_service.dart'; +import 'package:autonomy_flutter/util/log.dart'; import 'package:autonomy_flutter/view/primary_button.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:feralfile_app_theme/extensions/theme_extension.dart'; @@ -23,21 +24,24 @@ class _PasskeyRegisterViewState extends State { bool _didSuccess = false; @override - Widget build(BuildContext context) => SingleChildScrollView( - child: Column( - children: [ - _getTitle(context), - const SizedBox(height: 20), - _getDesc(context), - const SizedBox(height: 20), - _getIcon(), - const SizedBox(height: 20), - _getAction(context), - const SizedBox(height: 20), - _havingTrouble(context) - ], - ), - ); + Widget build(BuildContext context) => Column( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _getTitle(context), + const SizedBox(height: 20), + _getDesc(context), + ], + ), + const SizedBox(height: 20), + _getIcon(), + const SizedBox(height: 20), + _getAction(context), + const SizedBox(height: 20), + _havingTrouble(context) + ], + ); Widget _getTitle(BuildContext context) => Text( _didSuccess ? 'passkey_created'.tr() : 'introducing_passkey'.tr(), @@ -94,6 +98,7 @@ class _PasskeyRegisterViewState extends State { ); } return PrimaryAsyncButton( + key: const Key('register_button'), enabled: !_isError, onTap: () async { if (_registering) { @@ -104,7 +109,7 @@ class _PasskeyRegisterViewState extends State { }); try { await _passkeyService.registerInitiate(); - await _passkeyService.logInRequest(); + await _passkeyService.registerRequest(); await _accountService.migrateAccount(() async { await _passkeyService.logInFinalize(); }); @@ -112,6 +117,7 @@ class _PasskeyRegisterViewState extends State { _didSuccess = true; }); } catch (e) { + log.info('Failed to register passkey: $e'); setState(() { _isError = true; }); diff --git a/lib/view/primary_button.dart b/lib/view/primary_button.dart index 3a75c21a6..84e6c0201 100644 --- a/lib/view/primary_button.dart +++ b/lib/view/primary_button.dart @@ -41,7 +41,7 @@ class PrimaryButton extends StatelessWidget { child: ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: - enabled ? color ?? AppColor.feralFileHighlight : disabledColor, + enabled ? color ?? AppColor.feralFileLightBlue : disabledColor, padding: elevatedPadding, shadowColor: Colors.transparent, disabledForegroundColor: disabledColor, From 2b0c1feac70fe4db9c712eec1be3c46397887c60 Mon Sep 17 00:00:00 2001 From: phuoc Date: Fri, 25 Oct 2024 16:16:38 +0700 Subject: [PATCH 08/57] remove onboarding timeout Signed-off-by: phuoc --- lib/screen/onboarding_page.dart | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/lib/screen/onboarding_page.dart b/lib/screen/onboarding_page.dart index 515018599..c92581f95 100644 --- a/lib/screen/onboarding_page.dart +++ b/lib/screen/onboarding_page.dart @@ -52,7 +52,6 @@ class _OnboardingPageState extends State with TickerProviderStateMixin, AfterLayoutMixin { final metricClient = injector.get(); final deepLinkService = injector.get(); - Timer? _timer; final _passkeyService = injector.get(); final _authService = injector.get(); @@ -69,11 +68,6 @@ class _OnboardingPageState extends State @override void afterFirstLayout(BuildContext context) { - _timer = Timer(const Duration(seconds: 10), () { - log.info('OnboardingPage loading more than 10s'); - unawaited(Sentry.captureMessage('OnboardingPage loading more than 10s')); - // unawaited(injector().showAppLoadError()); - }); unawaited(setup(context).then((_) => _fetchRuntimeCache())); } @@ -142,28 +136,17 @@ class _OnboardingPageState extends State Future _goToTargetScreen(BuildContext context) async { log.info('[_goToTargetScreen] start'); - if (_timer?.isActive ?? false) { - _timer?.cancel(); - } unawaited(Navigator.of(context) .pushReplacementNamed(AppRouter.homePageNoTransition)); await injector().setDoneOnboarding(true); } Future _fetchRuntimeCache() async { - final timer = Timer(const Duration(seconds: 10), () { - log.info('[_createAccountOrRestoreIfNeeded] Loading more than 10s'); - unawaited(Sentry.captureMessage( - '[_createAccountOrRestoreIfNeeded] Loading more than 10s')); - }); log.info('[_fetchRuntimeCache] start'); await _loginProcess(); unawaited(_registerPushNotifications()); unawaited(injector().setup()); log.info('[_fetchRuntimeCache] end'); - if (timer.isActive) { - timer.cancel(); - } unawaited(metricClient.identity()); // count open app unawaited(metricClient.addEvent(MetricEventName.openApp)); From e6623ce21e80d842ec37295c3331a1639b9f9dae Mon Sep 17 00:00:00 2001 From: phuoc Date: Fri, 25 Oct 2024 16:25:22 +0700 Subject: [PATCH 09/57] fix finalize Signed-off-by: phuoc --- lib/view/passkey/passkey_login_view.dart | 2 +- lib/view/passkey/passkey_register_view.dart | 2 +- pubspec.lock | 42 ++++++++++----------- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/lib/view/passkey/passkey_login_view.dart b/lib/view/passkey/passkey_login_view.dart index 76147c5a5..a4e79e844 100644 --- a/lib/view/passkey/passkey_login_view.dart +++ b/lib/view/passkey/passkey_login_view.dart @@ -74,7 +74,7 @@ class _PasskeyLoginViewState extends State { await _passkeyService.logInInitiate(); await _passkeyService.logInRequest(); await _accountService.migrateAccount(() async { - await _passkeyService.registerFinalize(); + await _passkeyService.logInFinalize(); }); if (context.mounted) { Navigator.of(context).pop(true); diff --git a/lib/view/passkey/passkey_register_view.dart b/lib/view/passkey/passkey_register_view.dart index cda5f485d..f34e4a524 100644 --- a/lib/view/passkey/passkey_register_view.dart +++ b/lib/view/passkey/passkey_register_view.dart @@ -111,7 +111,7 @@ class _PasskeyRegisterViewState extends State { await _passkeyService.registerInitiate(); await _passkeyService.registerRequest(); await _accountService.migrateAccount(() async { - await _passkeyService.logInFinalize(); + await _passkeyService.registerFinalize(); }); setState(() { _didSuccess = true; diff --git a/pubspec.lock b/pubspec.lock index 208358fd0..a596692a7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -349,10 +349,10 @@ packages: dependency: "direct main" description: name: collection - sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a url: "https://pub.dev" source: hosted - version: "1.19.0" + version: "1.18.0" confetti: dependency: "direct main" description: @@ -1651,18 +1651,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" url: "https://pub.dev" source: hosted - version: "10.0.7" + version: "10.0.5" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" url: "https://pub.dev" source: hosted - version: "3.0.8" + version: "3.0.5" leak_tracker_testing: dependency: transitive description: @@ -2517,7 +2517,7 @@ packages: dependency: transitive description: flutter source: sdk - version: "0.0.0" + version: "0.0.99" slidable_button: dependency: "direct main" description: @@ -2634,10 +2634,10 @@ packages: dependency: transitive description: name: stack_trace - sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.11.1" stream_channel: dependency: transitive description: @@ -2658,10 +2658,10 @@ packages: dependency: transitive description: name: string_scanner - sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.2.0" strings: dependency: transitive description: @@ -2714,26 +2714,26 @@ packages: dependency: "direct dev" description: name: test - sha256: "713a8789d62f3233c46b4a90b174737b2c04cb6ae4500f2aa8b1be8f03f5e67f" + sha256: "7ee44229615f8f642b68120165ae4c2a75fe77ae2065b1e55ae4711f6cf0899e" url: "https://pub.dev" source: hosted - version: "1.25.8" + version: "1.25.7" test_api: dependency: transitive description: name: test_api - sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" url: "https://pub.dev" source: hosted - version: "0.7.3" + version: "0.7.2" test_core: dependency: transitive description: name: test_core - sha256: "12391302411737c176b0b5d6491f466b0dd56d4763e347b6714efbaa74d7953d" + sha256: "55ea5a652e38a1dfb32943a7973f3681a60f872f8c3a05a14664ad54ef9c6696" url: "https://pub.dev" source: hosted - version: "0.6.5" + version: "0.6.4" tezart: dependency: "direct main" description: @@ -2987,10 +2987,10 @@ packages: dependency: transitive description: name: vm_service - sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" url: "https://pub.dev" source: hosted - version: "14.3.0" + version: "14.2.5" wakelock_plus: dependency: "direct main" description: @@ -3059,10 +3059,10 @@ packages: dependency: transitive description: name: webdriver - sha256: "3d773670966f02a646319410766d3b5e1037efb7f07cc68f844d5e06cd4d61c8" + sha256: "003d7da9519e1e5f329422b36c4dcdf18d7d2978d1ba099ea4e45ba490ed845e" url: "https://pub.dev" source: hosted - version: "3.0.4" + version: "3.0.3" webkit_inspection_protocol: dependency: transitive description: From 789fe1d17419896eac621ca240c3bb7d593ad4af Mon Sep 17 00:00:00 2001 From: phuoc Date: Fri, 25 Oct 2024 17:05:36 +0700 Subject: [PATCH 10/57] lint Signed-off-by: phuoc --- lib/view/passkey/passkey_register_view.dart | 28 ++++++++++----------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/lib/view/passkey/passkey_register_view.dart b/lib/view/passkey/passkey_register_view.dart index f34e4a524..49da7b4bb 100644 --- a/lib/view/passkey/passkey_register_view.dart +++ b/lib/view/passkey/passkey_register_view.dart @@ -25,23 +25,23 @@ class _PasskeyRegisterViewState extends State { @override Widget build(BuildContext context) => Column( - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, children: [ - _getTitle(context), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _getTitle(context), + const SizedBox(height: 20), + _getDesc(context), + ], + ), + const SizedBox(height: 20), + _getIcon(), + const SizedBox(height: 20), + _getAction(context), const SizedBox(height: 20), - _getDesc(context), + _havingTrouble(context) ], - ), - const SizedBox(height: 20), - _getIcon(), - const SizedBox(height: 20), - _getAction(context), - const SizedBox(height: 20), - _havingTrouble(context) - ], - ); + ); Widget _getTitle(BuildContext context) => Text( _didSuccess ? 'passkey_created'.tr() : 'introducing_passkey'.tr(), From 8915c514654916abeaa95ad4ce12941bbf0e774c Mon Sep 17 00:00:00 2001 From: phuoc Date: Fri, 25 Oct 2024 17:07:05 +0700 Subject: [PATCH 11/57] update assets Signed-off-by: phuoc --- assets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets b/assets index d60117ec5..8c6e957e3 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit d60117ec5ab6c32b6233c736c6e63ecf0935fc89 +Subproject commit 8c6e957e34ade297e7b99705db1934a449a5f640 From c4c43e3f07765816e043f6a8ffe139732ff80feb Mon Sep 17 00:00:00 2001 From: phuoc Date: Fri, 25 Oct 2024 17:19:52 +0700 Subject: [PATCH 12/57] add sentry Signed-off-by: phuoc --- lib/view/passkey/passkey_login_view.dart | 6 +++++- lib/view/passkey/passkey_register_view.dart | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/view/passkey/passkey_login_view.dart b/lib/view/passkey/passkey_login_view.dart index a4e79e844..ff24e69d9 100644 --- a/lib/view/passkey/passkey_login_view.dart +++ b/lib/view/passkey/passkey_login_view.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:autonomy_flutter/common/injector.dart'; import 'package:autonomy_flutter/service/account_service.dart'; import 'package:autonomy_flutter/service/passkey_service.dart'; @@ -7,6 +9,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:feralfile_app_theme/extensions/theme_extension.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; class PasskeyLoginView extends StatefulWidget { const PasskeyLoginView({super.key}); @@ -79,8 +82,9 @@ class _PasskeyLoginViewState extends State { if (context.mounted) { Navigator.of(context).pop(true); } - } catch (e) { + } catch (e, stackTrace) { log.info('Failed to login with passkey: $e'); + unawaited(Sentry.captureException(e, stackTrace: stackTrace)); setState(() { _isError = true; }); diff --git a/lib/view/passkey/passkey_register_view.dart b/lib/view/passkey/passkey_register_view.dart index 49da7b4bb..ac5e74955 100644 --- a/lib/view/passkey/passkey_register_view.dart +++ b/lib/view/passkey/passkey_register_view.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:autonomy_flutter/common/injector.dart'; import 'package:autonomy_flutter/service/account_service.dart'; import 'package:autonomy_flutter/service/passkey_service.dart'; @@ -7,6 +9,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:feralfile_app_theme/extensions/theme_extension.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; class PasskeyRegisterView extends StatefulWidget { const PasskeyRegisterView({super.key}); @@ -116,8 +119,9 @@ class _PasskeyRegisterViewState extends State { setState(() { _didSuccess = true; }); - } catch (e) { + } catch (e, stackTrace) { log.info('Failed to register passkey: $e'); + unawaited(Sentry.captureException(e, stackTrace: stackTrace)); setState(() { _isError = true; }); From a04e423b712aae1ea096cd30409c422543596304 Mon Sep 17 00:00:00 2001 From: phuoc Date: Fri, 25 Oct 2024 17:35:03 +0700 Subject: [PATCH 13/57] add ios associated domains Signed-off-by: phuoc --- ios/Runner/Runner InhouseDebug.entitlements | 2 ++ ios/Runner/Runner-Inhouse.entitlements | 2 ++ ios/Runner/Runner.entitlements | 2 ++ ios/Runner/RunnerDebug.entitlements | 2 ++ 4 files changed, 8 insertions(+) diff --git a/ios/Runner/Runner InhouseDebug.entitlements b/ios/Runner/Runner InhouseDebug.entitlements index f642a9d0f..aab5d568d 100644 --- a/ios/Runner/Runner InhouseDebug.entitlements +++ b/ios/Runner/Runner InhouseDebug.entitlements @@ -16,6 +16,8 @@ applinks:app.feralfile.com feralfile-app.test-app.link feralfile-app-alternate.test-app.link + accounts.dev.feralfile.com + accounts.feralfile.com com.apple.developer.icloud-container-identifiers diff --git a/ios/Runner/Runner-Inhouse.entitlements b/ios/Runner/Runner-Inhouse.entitlements index 3eab97749..ae9f6ba26 100644 --- a/ios/Runner/Runner-Inhouse.entitlements +++ b/ios/Runner/Runner-Inhouse.entitlements @@ -16,6 +16,8 @@ applinks:app.feralfile.com feralfile-app.test-app.link feralfile-app-alternate.test-app.link + accounts.dev.feralfile.com + accounts.feralfile.com com.apple.developer.icloud-container-identifiers diff --git a/ios/Runner/Runner.entitlements b/ios/Runner/Runner.entitlements index cea993611..7d9967ba4 100644 --- a/ios/Runner/Runner.entitlements +++ b/ios/Runner/Runner.entitlements @@ -16,6 +16,8 @@ applinks:app.feralfile.com feralfile-app.test-app.link feralfile-app-alternate.test-app.link + accounts.dev.feralfile.com + accounts.feralfile.com com.apple.developer.icloud-container-identifiers diff --git a/ios/Runner/RunnerDebug.entitlements b/ios/Runner/RunnerDebug.entitlements index 6aacc2872..6018d940a 100644 --- a/ios/Runner/RunnerDebug.entitlements +++ b/ios/Runner/RunnerDebug.entitlements @@ -16,6 +16,8 @@ applinks:app.feralfile.com feralfile-app.test-app.link feralfile-app-alternate.test-app.link + accounts.dev.feralfile.com + accounts.feralfile.com com.apple.developer.icloud-container-identifiers From 6a45088c41b538c23447004dbc5628ddb5eb65db Mon Sep 17 00:00:00 2001 From: phuoc Date: Mon, 28 Oct 2024 09:16:02 +0700 Subject: [PATCH 14/57] bring back setup timeout, update assets Signed-off-by: phuoc --- assets | 2 +- lib/screen/onboarding_page.dart | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/assets b/assets index 8c6e957e3..1fa76d549 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 8c6e957e34ade297e7b99705db1934a449a5f640 +Subproject commit 1fa76d549e4c054d5322ffd0dd473262c4f6cc4d diff --git a/lib/screen/onboarding_page.dart b/lib/screen/onboarding_page.dart index c92581f95..d78d456be 100644 --- a/lib/screen/onboarding_page.dart +++ b/lib/screen/onboarding_page.dart @@ -52,6 +52,7 @@ class _OnboardingPageState extends State with TickerProviderStateMixin, AfterLayoutMixin { final metricClient = injector.get(); final deepLinkService = injector.get(); + Timer? _timer; final _passkeyService = injector.get(); final _authService = injector.get(); @@ -68,6 +69,12 @@ class _OnboardingPageState extends State @override void afterFirstLayout(BuildContext context) { + _timer = Timer(const Duration(seconds: 10), () { + log.info('OnboardingPage loading more than 10s'); + unawaited(Sentry.captureMessage('OnboardingPage loading more than 10s')); + // unawaited(injector().showAppLoadError()); + }); + unawaited(setup(context).then((_) => _fetchRuntimeCache())); } @@ -112,6 +119,9 @@ class _OnboardingPageState extends State log.info('Setup error: $e'); unawaited(Sentry.captureException('Setup error: $e', stackTrace: s)); } + if (_timer?.isActive ?? false) { + _timer?.cancel(); + } } Future _registerPushNotifications() async { From e911e4174dd091113b0b6045bff94dc9c7b16cc3 Mon Sep 17 00:00:00 2001 From: phuoc Date: Mon, 28 Oct 2024 10:24:54 +0700 Subject: [PATCH 15/57] register finalize response, remove log sensitive data Signed-off-by: phuoc --- lib/service/passkey_service.dart | 4 +++- lib/util/log.dart | 12 ++++++++++-- lib/util/passkey_utils.dart | 13 +++++++++++++ 3 files changed, 26 insertions(+), 3 deletions(-) create mode 100644 lib/util/passkey_utils.dart diff --git a/lib/service/passkey_service.dart b/lib/service/passkey_service.dart index 0d1f78480..34578c8e2 100644 --- a/lib/service/passkey_service.dart +++ b/lib/service/passkey_service.dart @@ -1,6 +1,7 @@ import 'package:autonomy_flutter/gateway/user_api.dart'; import 'package:autonomy_flutter/model/jwt.dart'; import 'package:autonomy_flutter/service/address_service.dart'; +import 'package:autonomy_flutter/util/passkey_utils.dart'; import 'package:autonomy_flutter/util/user_account_channel.dart'; import 'package:passkeys/authenticator.dart'; import 'package:passkeys/types.dart'; @@ -131,7 +132,8 @@ class PasskeyServiceImpl implements PasskeyService { final response = await _userApi.registerFinalize({ 'addressAuthentication': addressAuthentication, 'passkeyUserId': _passkeyUserId, - 'public_key_credential': _registerResponse!.toJson(), + 'credentialCreationResponse': + _registerResponse!.toCredentialCreationResponseJson(), }); await _userAccountChannel.setDidRegisterPasskey(true); await _userAccountChannel.setUserId(addressAuthentication['requester']); diff --git a/lib/util/log.dart b/lib/util/log.dart index bcbcaba87..49f4d8bce 100644 --- a/lib/util/log.dart +++ b/lib/util/log.dart @@ -159,7 +159,9 @@ class FileLogger { r'(0x[A-Fa-f0-9]{64}[\s\W])|' r'(0x[A-Fa-f0-9]{128,144}[\s\W])|' r'(eyJ[A-Za-z0-9-_]+\.eyJ[A-Za-z0-9-_]+\.[A-Za-z0-9-_.+/]*)|' - r'(\\"receipt\\":\{[^{}]*\})'); + r'(\\"receipt\\":\{[^{}]*\})|' + r'(\\"clientDataJSON\\":\\".*?\\")|' + r'(\\"attestationObject\\":\\".*?\\")'); filteredLog = filteredLog.replaceAllMapped(combinedRegex, (match) { if (match[1] != null) { @@ -192,7 +194,13 @@ class FileLogger { if (match[11] != null) { return r'\"receipt\": REDACTED_RECEIPT'; } - return ''; + if (match[12] != null) { + return r'\"clientDataJSON\":\"REDACTED_CLIENT_DATA_JSON\"'; + } + if (match[13] != null) { + return r'\"attestationObject\":\"REDACTED_ATTESTATION_OBJECT\"'; + } + return 'REDACTED_INFORMATION'; }); return filteredLog; diff --git a/lib/util/passkey_utils.dart b/lib/util/passkey_utils.dart new file mode 100644 index 000000000..5602ed14a --- /dev/null +++ b/lib/util/passkey_utils.dart @@ -0,0 +1,13 @@ +import 'package:passkeys/types.dart'; + +extension RegisterResponseTypeExt on RegisterResponseType { + Map toCredentialCreationResponseJson() => { + 'id': id, + 'rawId': rawId, + 'type': 'public-key', + 'response': { + 'clientDataJSON': clientDataJSON, + 'attestationObject': attestationObject, + } + }; +} From 4d0de744e45fb8750189717d3d4f67c2a697b4ca Mon Sep 17 00:00:00 2001 From: phuoc Date: Mon, 28 Oct 2024 11:02:42 +0700 Subject: [PATCH 16/57] refactor passkey service Signed-off-by: phuoc --- lib/service/passkey_service.dart | 53 ++++++++------------- lib/view/passkey/passkey_login_view.dart | 5 +- lib/view/passkey/passkey_register_view.dart | 1 - 3 files changed, 22 insertions(+), 37 deletions(-) diff --git a/lib/service/passkey_service.dart b/lib/service/passkey_service.dart index 34578c8e2..228a3c68a 100644 --- a/lib/service/passkey_service.dart +++ b/lib/service/passkey_service.dart @@ -9,29 +9,21 @@ import 'package:passkeys/types.dart'; abstract class PasskeyService { Future isPassKeyAvailable(); - Future logInInitiate(); + Future logInInitiate(); - Future logInRequest(); - - Future logInFinalize(); + Future logInFinalize(AuthenticateResponseType loginResponse); Future registerInitiate(); - Future registerRequest(); - Future registerFinalize(); } class PasskeyServiceImpl implements PasskeyService { final _passkeyAuthenticator = PasskeyAuthenticator(); - RegisterRequestType? _registerRequest; RegisterResponseType? _registerResponse; String? _passkeyUserId; - AuthenticateRequestType? _loginRequest; - AuthenticateResponseType? _loginResponse; - final UserApi _userApi; final UserAccountChannel _userAccountChannel; final AddressService _addressService; @@ -61,7 +53,12 @@ class PasskeyServiceImpl implements PasskeyService { await _passkeyAuthenticator.canAuthenticate(); @override - Future logInInitiate() async { + Future logInInitiate() async { + final loginRequest = await _logInSeverInitiate(); + return await _passkeyAuthenticator.authenticate(loginRequest); + } + + Future _logInSeverInitiate() async { final userId = await _userAccountChannel.getUserId(); if (userId == null) { throw Exception('User ID is not set'); @@ -72,7 +69,7 @@ class PasskeyServiceImpl implements PasskeyService { if (pubKey.rpId == null) { throw Exception('RP ID is not set'); } - _loginRequest = AuthenticateRequestType( + return AuthenticateRequestType( challenge: pubKey.challenge, allowCredentials: pubKey.allowCredentials ?? [], relyingPartyId: pubKey.rpId!, @@ -83,31 +80,27 @@ class PasskeyServiceImpl implements PasskeyService { } @override - Future logInRequest() async { - if (_loginResponse != null) { - return _loginResponse!; - } - _loginResponse = await _passkeyAuthenticator.authenticate(_loginRequest!); - return _loginResponse!; - } - - @override - Future logInFinalize() async { + Future logInFinalize(AuthenticateResponseType loginLocalResponse) async { final response = await _userApi.logInFinalize({ - 'public_key_credential': _loginResponse!.toJson(), + 'public_key_credential': loginLocalResponse.toJson(), }); return response; } @override Future registerInitiate() async { + final registerRequest = await _initializeServerRegistration(); + _registerResponse = await _passkeyAuthenticator.register(registerRequest); + } + + Future _initializeServerRegistration() async { final response = await _userApi.registerInitialize(); final pubKey = response.credentialCreationOption.publicKey; if (pubKey.authenticatorSelection == null) { throw Exception('Authenticator selection is not set'); } _passkeyUserId = response.passkeyUserID; - _registerRequest = RegisterRequestType( + return RegisterRequestType( challenge: pubKey.challenge, relyingParty: pubKey.rp, user: pubKey.user, @@ -116,17 +109,11 @@ class PasskeyServiceImpl implements PasskeyService { ); } - @override - Future registerRequest() async { - if (_registerResponse != null) { - return _registerResponse!; - } - _registerResponse = await _passkeyAuthenticator.register(_registerRequest!); - return _registerResponse!; - } - @override Future registerFinalize() async { + if (_registerResponse == null || _passkeyUserId == null) { + throw Exception('Initialize registration has not finished'); + } final addressAuthentication = await _addressService.getAddressAuthenticationMap(); final response = await _userApi.registerFinalize({ diff --git a/lib/view/passkey/passkey_login_view.dart b/lib/view/passkey/passkey_login_view.dart index ff24e69d9..17942f944 100644 --- a/lib/view/passkey/passkey_login_view.dart +++ b/lib/view/passkey/passkey_login_view.dart @@ -74,10 +74,9 @@ class _PasskeyLoginViewState extends State { _isLogging = true; }); try { - await _passkeyService.logInInitiate(); - await _passkeyService.logInRequest(); + final localResponse = await _passkeyService.logInInitiate(); await _accountService.migrateAccount(() async { - await _passkeyService.logInFinalize(); + await _passkeyService.logInFinalize(localResponse); }); if (context.mounted) { Navigator.of(context).pop(true); diff --git a/lib/view/passkey/passkey_register_view.dart b/lib/view/passkey/passkey_register_view.dart index ac5e74955..fc7b738f3 100644 --- a/lib/view/passkey/passkey_register_view.dart +++ b/lib/view/passkey/passkey_register_view.dart @@ -112,7 +112,6 @@ class _PasskeyRegisterViewState extends State { }); try { await _passkeyService.registerInitiate(); - await _passkeyService.registerRequest(); await _accountService.migrateAccount(() async { await _passkeyService.registerFinalize(); }); From 4d1a5c5939ef6c9f57cc23b021bd5021f30d0805 Mon Sep 17 00:00:00 2001 From: phuoc Date: Mon, 28 Oct 2024 11:13:27 +0700 Subject: [PATCH 17/57] login finalize payload Signed-off-by: phuoc --- lib/service/passkey_service.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/service/passkey_service.dart b/lib/service/passkey_service.dart index 228a3c68a..7e67f28c8 100644 --- a/lib/service/passkey_service.dart +++ b/lib/service/passkey_service.dart @@ -81,9 +81,7 @@ class PasskeyServiceImpl implements PasskeyService { @override Future logInFinalize(AuthenticateResponseType loginLocalResponse) async { - final response = await _userApi.logInFinalize({ - 'public_key_credential': loginLocalResponse.toJson(), - }); + final response = await _userApi.logInFinalize(loginLocalResponse.toJson()); return response; } From 8eaca39162462be44df81252ca879bbf5ada9a8d Mon Sep 17 00:00:00 2001 From: phuoc Date: Mon, 28 Oct 2024 13:25:05 +0700 Subject: [PATCH 18/57] support send log Signed-off-by: phuoc --- lib/common/environment.dart | 3 ++ .../customer_support/support_thread_page.dart | 9 ++++-- lib/util/dio_interceptors.dart | 13 ++++++-- lib/util/dio_util.dart | 3 +- lib/view/passkey/having_trouble_view.dart | 31 +++++++++++++++++++ lib/view/passkey/passkey_login_view.dart | 16 ++-------- lib/view/passkey/passkey_register_view.dart | 19 +++--------- 7 files changed, 60 insertions(+), 34 deletions(-) create mode 100644 lib/view/passkey/having_trouble_view.dart diff --git a/lib/common/environment.dart b/lib/common/environment.dart index d10947f89..086f74001 100644 --- a/lib/common/environment.dart +++ b/lib/common/environment.dart @@ -219,4 +219,7 @@ class Environment { _readKey('ONESIGNAL_APP_ID', '', isSecret: true); static String get tvKey => _readKey('TV_API_KEY', '', isSecret: true); + + static String get supportApiKey => + _readKey('SUPPORT_API_KEY', '', isSecret: true); } diff --git a/lib/screen/customer_support/support_thread_page.dart b/lib/screen/customer_support/support_thread_page.dart index 3cf9bb926..39656bf22 100644 --- a/lib/screen/customer_support/support_thread_page.dart +++ b/lib/screen/customer_support/support_thread_page.dart @@ -23,6 +23,7 @@ import 'package:autonomy_flutter/service/feralfile_service.dart'; import 'package:autonomy_flutter/shared.dart'; import 'package:autonomy_flutter/util/constants.dart'; import 'package:autonomy_flutter/util/datetime_ext.dart'; +import 'package:autonomy_flutter/util/device.dart'; import 'package:autonomy_flutter/util/jwt.dart'; import 'package:autonomy_flutter/util/log.dart' as log_util; import 'package:autonomy_flutter/util/string_ext.dart'; @@ -265,8 +266,12 @@ class _SupportThreadPageState extends State { return _userId!; } final jwt = await injector().getAuthToken(); - final data = parseJwt(jwt!.jwtToken); - _userId = data['sub'] ?? ''; + if (jwt != null) { + final data = parseJwt(jwt.jwtToken); + _userId = data['sub'] ?? ''; + } else { + _userId = await getDeviceID(); + } return _userId!; } diff --git a/lib/util/dio_interceptors.dart b/lib/util/dio_interceptors.dart index 35fab6efa..8122924c8 100644 --- a/lib/util/dio_interceptors.dart +++ b/lib/util/dio_interceptors.dart @@ -142,17 +142,24 @@ class SentryInterceptor extends InterceptorsWrapper { } class AutonomyAuthInterceptor extends Interceptor { - AutonomyAuthInterceptor(); + // use this apiKey for if jwt is not available + final String? apiKey; + + AutonomyAuthInterceptor({this.apiKey}); @override Future onRequest( RequestOptions options, RequestInterceptorHandler handler) async { final jwt = await injector().getAuthToken(); - if (jwt == null) { + if (jwt == null && apiKey == null) { unawaited(Sentry.captureMessage('JWT is null')); throw JwtException(message: 'can_not_authenticate_desc'.tr()); } - options.headers['Authorization'] = 'Bearer ${jwt.jwtToken}'; + if (jwt != null) { + options.headers['Authorization'] = 'Bearer ${jwt.jwtToken}'; + } else { + options.headers['x-api-key'] = apiKey; + } return handler.next(options); } diff --git a/lib/util/dio_util.dart b/lib/util/dio_util.dart index 17f6efe7b..d064cd04b 100644 --- a/lib/util/dio_util.dart +++ b/lib/util/dio_util.dart @@ -24,7 +24,8 @@ Dio feralFileDio(BaseOptions options) { Dio customerSupportDio(BaseOptions options) { final dio = baseDio(options); - dio.interceptors.add(AutonomyAuthInterceptor()); + dio.interceptors + .add(AutonomyAuthInterceptor(apiKey: Environment.supportApiKey)); return dio; } diff --git a/lib/view/passkey/having_trouble_view.dart b/lib/view/passkey/having_trouble_view.dart new file mode 100644 index 000000000..8f61e8e50 --- /dev/null +++ b/lib/view/passkey/having_trouble_view.dart @@ -0,0 +1,31 @@ +import 'package:autonomy_flutter/screen/app_router.dart'; +import 'package:autonomy_flutter/screen/customer_support/support_thread_page.dart'; +import 'package:autonomy_flutter/util/constants.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:feralfile_app_theme/extensions/theme_extension.dart'; +import 'package:flutter/material.dart'; + +class HavingTroubleView extends StatelessWidget { + const HavingTroubleView({super.key}); + + @override + Widget build(BuildContext context) => Padding( + padding: const EdgeInsets.only(bottom: 20), + child: GestureDetector( + onTap: () async { + await Navigator.of(context).pushNamed( + AppRouter.supportThreadPage, + arguments: NewIssuePayload( + reportIssueType: ReportIssueType.Bug, + ), + ); + }, + child: Text( + 'having_trouble'.tr(), + style: Theme.of(context).textTheme.ppMori400Grey14.copyWith( + decoration: TextDecoration.underline, + ), + ), + ), + ); +} diff --git a/lib/view/passkey/passkey_login_view.dart b/lib/view/passkey/passkey_login_view.dart index 17942f944..fa7cde74b 100644 --- a/lib/view/passkey/passkey_login_view.dart +++ b/lib/view/passkey/passkey_login_view.dart @@ -4,6 +4,7 @@ import 'package:autonomy_flutter/common/injector.dart'; import 'package:autonomy_flutter/service/account_service.dart'; import 'package:autonomy_flutter/service/passkey_service.dart'; import 'package:autonomy_flutter/util/log.dart'; +import 'package:autonomy_flutter/view/passkey/having_trouble_view.dart'; import 'package:autonomy_flutter/view/primary_button.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:feralfile_app_theme/extensions/theme_extension.dart'; @@ -96,19 +97,6 @@ class _PasskeyLoginViewState extends State { if (!_isError && !_isLogging) { return const SizedBox(); } - return Padding( - padding: const EdgeInsets.only(bottom: 20), - child: GestureDetector( - onTap: () { - //Navigator.of(context).pop(); - }, - child: Text( - 'having_trouble'.tr(), - style: Theme.of(context).textTheme.ppMori400Grey14.copyWith( - decoration: TextDecoration.underline, - ), - ), - ), - ); + return const HavingTroubleView(); } } diff --git a/lib/view/passkey/passkey_register_view.dart b/lib/view/passkey/passkey_register_view.dart index fc7b738f3..82cad9068 100644 --- a/lib/view/passkey/passkey_register_view.dart +++ b/lib/view/passkey/passkey_register_view.dart @@ -1,9 +1,13 @@ import 'dart:async'; import 'package:autonomy_flutter/common/injector.dart'; +import 'package:autonomy_flutter/screen/app_router.dart'; +import 'package:autonomy_flutter/screen/customer_support/support_thread_page.dart'; import 'package:autonomy_flutter/service/account_service.dart'; import 'package:autonomy_flutter/service/passkey_service.dart'; +import 'package:autonomy_flutter/util/constants.dart'; import 'package:autonomy_flutter/util/log.dart'; +import 'package:autonomy_flutter/view/passkey/having_trouble_view.dart'; import 'package:autonomy_flutter/view/primary_button.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:feralfile_app_theme/extensions/theme_extension.dart'; @@ -135,19 +139,6 @@ class _PasskeyRegisterViewState extends State { if (_didSuccess || (!_isError && !_registering)) { return const SizedBox(); } - return Padding( - padding: const EdgeInsets.only(bottom: 20), - child: GestureDetector( - onTap: () { - //Navigator.of(context).pop(); - }, - child: Text( - 'having_trouble'.tr(), - style: Theme.of(context).textTheme.ppMori400Grey14.copyWith( - decoration: TextDecoration.underline, - ), - ), - ), - ); + return const HavingTroubleView(); } } From d7342021226c2586f87e39bc2d204d4a91cf92fc Mon Sep 17 00:00:00 2001 From: phuoc Date: Mon, 28 Oct 2024 13:46:51 +0700 Subject: [PATCH 19/57] fix feed back Signed-off-by: phuoc --- lib/model/credential_request_option.dart | 11 ++++++----- lib/service/account_service.dart | 1 - lib/service/configuration_service.dart | 17 ++--------------- lib/util/user_account_channel.dart | 15 ++++++++++----- lib/view/passkey/passkey_login_view.dart | 5 +++++ lib/view/passkey/passkey_register_view.dart | 8 +++++--- 6 files changed, 28 insertions(+), 29 deletions(-) diff --git a/lib/model/credential_request_option.dart b/lib/model/credential_request_option.dart index c5406b15c..555fe93be 100644 --- a/lib/model/credential_request_option.dart +++ b/lib/model/credential_request_option.dart @@ -29,18 +29,19 @@ class CredProps { } // Model for Extensions (appid, appidExclude, and credProps) -class Extensions { +class CredentialExtensions { final bool? appid; final bool? appidExclude; final CredProps? credProps; - Extensions({ + CredentialExtensions({ this.appid, this.appidExclude, this.credProps, }); - factory Extensions.fromJson(Map json) => Extensions( + factory CredentialExtensions.fromJson(Map json) => + CredentialExtensions( appid: json['appid'], appidExclude: json['appidExclude'], credProps: json['credProps'] != null @@ -56,7 +57,7 @@ class PublicKeyCredentialRequestOptions { final String? rpId; final List? allowCredentials; final String? userVerification; - final Extensions? extensions; + final CredentialExtensions? extensions; PublicKeyCredentialRequestOptions({ required this.challenge, @@ -80,7 +81,7 @@ class PublicKeyCredentialRequestOptions { : null, userVerification: json['userVerification'], extensions: json['extensions'] != null - ? Extensions.fromJson(json['extensions']) + ? CredentialExtensions.fromJson(json['extensions']) : null, ); } diff --git a/lib/service/account_service.dart b/lib/service/account_service.dart index 4f0a79c35..be57ea4e6 100644 --- a/lib/service/account_service.dart +++ b/lib/service/account_service.dart @@ -733,7 +733,6 @@ class AccountServiceImpl extends AccountService { // case 5/6: update/restore app from old version using primary address if (isDoneOnboarding) { - // case 3: restore app from old version using primary address log.info('[AccountService] migrateAccount: ' 'case 5 update app from old version using primary address'); // migrate to ethereum first, then upload to account-settings diff --git a/lib/service/configuration_service.dart b/lib/service/configuration_service.dart index 65538d286..5d2f5cf2c 100644 --- a/lib/service/configuration_service.dart +++ b/lib/service/configuration_service.dart @@ -26,11 +26,7 @@ import 'package:uuid/uuid.dart'; abstract class ConfigurationService { bool didRegisterPasskey(); - Future setRegisterPasskey(bool value); - - String getRefreshToken(); - - Future setRefreshToken(String value); + Future setDidRegisterPasskey(bool value); bool didMigrateToAccountSetting(); @@ -215,7 +211,6 @@ abstract class ConfigurationService { class ConfigurationServiceImpl implements ConfigurationService { static const String keyDidRegisterPasskey = 'did_register_passkey'; - static const String keyRefreshToken = 'refresh_token'; static const String keyDidMigrateToAccountSetting = 'did_migrate_to_account_setting'; static const String keyDidShowLiveWithArt = 'did_show_live_with_art'; @@ -940,20 +935,12 @@ class ConfigurationServiceImpl implements ConfigurationService { KEY_ANNOUNCEMENT_TO_ISSUE_MAP, jsonEncode(mapJson)); } - @override - String getRefreshToken() => _preferences.getString(keyRefreshToken) ?? ''; - - @override - Future setRefreshToken(String value) async { - await _preferences.setString(keyRefreshToken, value); - } - @override bool didRegisterPasskey() => _preferences.getBool(keyDidRegisterPasskey) ?? false; @override - Future setRegisterPasskey(bool value) async { + Future setDidRegisterPasskey(bool value) async { await _preferences.setBool(keyDidRegisterPasskey, value); } } diff --git a/lib/util/user_account_channel.dart b/lib/util/user_account_channel.dart index 8eda131a1..dbdf89ade 100644 --- a/lib/util/user_account_channel.dart +++ b/lib/util/user_account_channel.dart @@ -7,6 +7,7 @@ import 'package:autonomy_flutter/service/configuration_service.dart'; import 'package:autonomy_flutter/util/log.dart'; import 'package:autonomy_flutter/util/wallet_utils.dart'; import 'package:flutter/services.dart'; +import 'package:sentry/sentry.dart'; class UserAccountChannel { final MethodChannel _channel; @@ -35,9 +36,6 @@ class UserAccountChannel { } try { final String data = await _channel.invokeMethod('getPrimaryAddress', {}); - if (data.isEmpty) { - _primaryAddress = null; - } final primaryAddressInfo = json.decode(data); _primaryAddress = AddressInfo.fromJson(primaryAddressInfo); } catch (e) { @@ -58,13 +56,20 @@ class UserAccountChannel { } } - Future setUserId(String userId, {bool isPasskeys = false}) async { + Future setUserId(String userId) async { try { await _channel.invokeMethod('setUserId', {'data': userId}); _userId = userId; return true; } catch (e) { log.info('setUserId error', e); + unawaited(Sentry.captureException( + e, + hint: Hint.withMap({ + 'method': 'setUserId', + 'userId': userId, + }), + )); return false; } } @@ -107,7 +112,7 @@ class UserAccountChannel { Future setDidRegisterPasskey(bool value) async { if (Platform.isAndroid) { - await injector().setRegisterPasskey(value); + await injector().setDidRegisterPasskey(value); return true; } final didRegister = await _channel.invokeMethod('setDidRegisterPasskey', { diff --git a/lib/view/passkey/passkey_login_view.dart b/lib/view/passkey/passkey_login_view.dart index fa7cde74b..d67e3cbd1 100644 --- a/lib/view/passkey/passkey_login_view.dart +++ b/lib/view/passkey/passkey_login_view.dart @@ -73,6 +73,7 @@ class _PasskeyLoginViewState extends State { } setState(() { _isLogging = true; + _isError = false; }); try { final localResponse = await _passkeyService.logInInitiate(); @@ -88,6 +89,10 @@ class _PasskeyLoginViewState extends State { setState(() { _isError = true; }); + } finally { + setState(() { + _isLogging = false; + }); } }, text: 'login_button'.tr(), diff --git a/lib/view/passkey/passkey_register_view.dart b/lib/view/passkey/passkey_register_view.dart index 82cad9068..b783730af 100644 --- a/lib/view/passkey/passkey_register_view.dart +++ b/lib/view/passkey/passkey_register_view.dart @@ -1,11 +1,8 @@ import 'dart:async'; import 'package:autonomy_flutter/common/injector.dart'; -import 'package:autonomy_flutter/screen/app_router.dart'; -import 'package:autonomy_flutter/screen/customer_support/support_thread_page.dart'; import 'package:autonomy_flutter/service/account_service.dart'; import 'package:autonomy_flutter/service/passkey_service.dart'; -import 'package:autonomy_flutter/util/constants.dart'; import 'package:autonomy_flutter/util/log.dart'; import 'package:autonomy_flutter/view/passkey/having_trouble_view.dart'; import 'package:autonomy_flutter/view/primary_button.dart'; @@ -113,6 +110,7 @@ class _PasskeyRegisterViewState extends State { } setState(() { _registering = true; + _isError = false; }); try { await _passkeyService.registerInitiate(); @@ -128,6 +126,10 @@ class _PasskeyRegisterViewState extends State { setState(() { _isError = true; }); + } finally { + setState(() { + _registering = false; + }); } }, text: 'get_started'.tr(), From 69ec74d413b9efb538801b1e7223d5357b0fdd63 Mon Sep 17 00:00:00 2001 From: phuoc Date: Mon, 28 Oct 2024 14:05:56 +0700 Subject: [PATCH 20/57] fix set jwt Signed-off-by: phuoc --- lib/common/injector.dart | 1 + lib/service/auth_service.dart | 6 +++++- lib/service/passkey_service.dart | 17 ++++++++++------- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/lib/common/injector.dart b/lib/common/injector.dart index 410aaf77f..e28387d1a 100644 --- a/lib/common/injector.dart +++ b/lib/common/injector.dart @@ -237,6 +237,7 @@ Future setupInjector() async { injector(), injector(), injector(), + injector(), )); injector.registerFactoryParam( diff --git a/lib/service/auth_service.dart b/lib/service/auth_service.dart index 016664698..b87755282 100644 --- a/lib/service/auth_service.dart +++ b/lib/service/auth_service.dart @@ -59,7 +59,7 @@ class AuthService { }); } final newJwt = await _userApi.refreshJWT(payload); - _jwt = newJwt; + setAuthToken(newJwt); _refreshSubscriptionStatus(newJwt, receiptData: receiptData); return newJwt; } @@ -87,6 +87,10 @@ class AuthService { injector().add(UpgradeQueryInfoEvent()); } + void setAuthToken(JWT jwt) { + _jwt = jwt; + } + Future getAuthToken() async { if (_jwt == null) { return null; diff --git a/lib/service/passkey_service.dart b/lib/service/passkey_service.dart index 7e67f28c8..f9b761a25 100644 --- a/lib/service/passkey_service.dart +++ b/lib/service/passkey_service.dart @@ -1,6 +1,6 @@ import 'package:autonomy_flutter/gateway/user_api.dart'; -import 'package:autonomy_flutter/model/jwt.dart'; import 'package:autonomy_flutter/service/address_service.dart'; +import 'package:autonomy_flutter/service/auth_service.dart'; import 'package:autonomy_flutter/util/passkey_utils.dart'; import 'package:autonomy_flutter/util/user_account_channel.dart'; import 'package:passkeys/authenticator.dart'; @@ -11,11 +11,11 @@ abstract class PasskeyService { Future logInInitiate(); - Future logInFinalize(AuthenticateResponseType loginResponse); + Future logInFinalize(AuthenticateResponseType loginResponse); Future registerInitiate(); - Future registerFinalize(); + Future registerFinalize(); } class PasskeyServiceImpl implements PasskeyService { @@ -27,11 +27,13 @@ class PasskeyServiceImpl implements PasskeyService { final UserApi _userApi; final UserAccountChannel _userAccountChannel; final AddressService _addressService; + final AuthService _authService; PasskeyServiceImpl( this._userApi, this._userAccountChannel, this._addressService, + this._authService, ); /* @@ -80,9 +82,10 @@ class PasskeyServiceImpl implements PasskeyService { } @override - Future logInFinalize(AuthenticateResponseType loginLocalResponse) async { + Future logInFinalize( + AuthenticateResponseType loginLocalResponse) async { final response = await _userApi.logInFinalize(loginLocalResponse.toJson()); - return response; + _authService.setAuthToken(response); } @override @@ -108,7 +111,7 @@ class PasskeyServiceImpl implements PasskeyService { } @override - Future registerFinalize() async { + Future registerFinalize() async { if (_registerResponse == null || _passkeyUserId == null) { throw Exception('Initialize registration has not finished'); } @@ -122,7 +125,7 @@ class PasskeyServiceImpl implements PasskeyService { }); await _userAccountChannel.setDidRegisterPasskey(true); await _userAccountChannel.setUserId(addressAuthentication['requester']); - return response; + _authService.setAuthToken(response); } } From b965460f27aa60f8e497d00384a629eba7482d8c Mon Sep 17 00:00:00 2001 From: phuoc Date: Mon, 28 Oct 2024 14:14:49 +0700 Subject: [PATCH 21/57] don't reduct response message Signed-off-by: phuoc --- lib/util/dio_interceptors.dart | 4 +++- lib/util/log.dart | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/util/dio_interceptors.dart b/lib/util/dio_interceptors.dart index 8122924c8..270f15ea8 100644 --- a/lib/util/dio_interceptors.dart +++ b/lib/util/dio_interceptors.dart @@ -34,12 +34,14 @@ class LoggingInterceptor extends Interceptor { '${Environment.feralFileAPIURL}/api/artworks', ]; + static String errorLogPrefix = 'Respond error:'; + @override void onError(DioException err, ErrorInterceptorHandler handler) { final curl = cURLRepresentation(err.requestOptions); apiLog ..info('API Request: $curl') - ..warning('Respond error: ${err.response}'); + ..warning('$errorLogPrefix ${err.response}'); return handler.next(err); } diff --git a/lib/util/log.dart b/lib/util/log.dart index 49f4d8bce..e9e9157d0 100644 --- a/lib/util/log.dart +++ b/lib/util/log.dart @@ -9,6 +9,7 @@ import 'dart:async'; import 'dart:core'; import 'dart:io'; +import 'package:autonomy_flutter/util/dio_interceptors.dart'; import 'package:flutter/foundation.dart'; import 'package:logging/logging.dart'; import 'package:path_provider/path_provider.dart'; @@ -147,6 +148,9 @@ class FileLogger { } static String _filterLog(String logText) { + if (logText.contains(LoggingInterceptor.errorLogPrefix)) { + return logText; + } String filteredLog = logText; RegExp combinedRegex = RegExp('("message":".*?")|' From c51c3414ddea09798120849ff3312f5d7b218a94 Mon Sep 17 00:00:00 2001 From: phuoc Date: Mon, 28 Oct 2024 14:47:07 +0700 Subject: [PATCH 22/57] fix login model Signed-off-by: phuoc --- lib/gateway/user_api.dart | 4 ++-- .../{ => passkey}/credential_request_option.dart | 11 +++++++---- .../{ => passkey}/passkey_creation_option.dart | 3 ++- lib/service/passkey_service.dart | 8 +++++++- lib/util/passkey_utils.dart | 14 +++++++++++++- lib/util/ui_helper.dart | 11 +++++------ 6 files changed, 36 insertions(+), 15 deletions(-) rename lib/model/{ => passkey}/credential_request_option.dart (89%) rename lib/model/{ => passkey}/passkey_creation_option.dart (96%) diff --git a/lib/gateway/user_api.dart b/lib/gateway/user_api.dart index 52661a127..5a8b6f367 100644 --- a/lib/gateway/user_api.dart +++ b/lib/gateway/user_api.dart @@ -5,9 +5,9 @@ // that can be found in the LICENSE file. // -import 'package:autonomy_flutter/model/credential_request_option.dart'; +import 'package:autonomy_flutter/model/passkey/credential_request_option.dart'; import 'package:autonomy_flutter/model/jwt.dart'; -import 'package:autonomy_flutter/model/passkey_creation_option.dart'; +import 'package:autonomy_flutter/model/passkey/passkey_creation_option.dart'; import 'package:dio/dio.dart'; import 'package:retrofit/retrofit.dart'; diff --git a/lib/model/credential_request_option.dart b/lib/model/passkey/credential_request_option.dart similarity index 89% rename from lib/model/credential_request_option.dart rename to lib/model/passkey/credential_request_option.dart index 555fe93be..c26a53ffa 100644 --- a/lib/model/credential_request_option.dart +++ b/lib/model/passkey/credential_request_option.dart @@ -1,20 +1,23 @@ +import 'package:autonomy_flutter/util/passkey_utils.dart'; import 'package:passkeys/types.dart'; // Main model class for CredentialRequestOption class CredentialRequestOption { final PublicKeyCredentialRequestOptions publicKey; - final MediationType mediation; + final MediationType? mediation; CredentialRequestOption({ required this.publicKey, - required this.mediation, + this.mediation, }); factory CredentialRequestOption.fromJson(Map json) => CredentialRequestOption( publicKey: PublicKeyCredentialRequestOptions.fromJson(json['publicKey']), - mediation: getMediationTypeFromString(json['mediation']), + mediation: json['mediation'] == null + ? null + : getMediationTypeFromString(json['mediation']), ); } @@ -76,7 +79,7 @@ class PublicKeyCredentialRequestOptions { rpId: json['rpId'], allowCredentials: json['allowCredentials'] != null ? (json['allowCredentials'] as List) - .map((cred) => CredentialType.fromJson(cred)) + .map((cred) => getCredentialTypeFromJsonFF(cred)) .toList() : null, userVerification: json['userVerification'], diff --git a/lib/model/passkey_creation_option.dart b/lib/model/passkey/passkey_creation_option.dart similarity index 96% rename from lib/model/passkey_creation_option.dart rename to lib/model/passkey/passkey_creation_option.dart index f4b543422..83acf8a51 100644 --- a/lib/model/passkey_creation_option.dart +++ b/lib/model/passkey/passkey_creation_option.dart @@ -1,3 +1,4 @@ +import 'package:autonomy_flutter/util/passkey_utils.dart'; import 'package:passkeys/types.dart'; class CredentialCreationOptionResponse { @@ -69,7 +70,7 @@ class PublicKey { timeout: json['timeout'], excludeCredentials: json['excludeCredentials'] != null ? (json['excludeCredentials'] as List) - .map((cred) => CredentialType.fromJson(cred)) + .map((cred) => getCredentialTypeFromJsonFF(cred)) .toList() : null, authenticatorSelection: json['authenticatorSelection'] != null diff --git a/lib/service/passkey_service.dart b/lib/service/passkey_service.dart index f9b761a25..bb3dbb367 100644 --- a/lib/service/passkey_service.dart +++ b/lib/service/passkey_service.dart @@ -16,6 +16,8 @@ abstract class PasskeyService { Future registerInitiate(); Future registerFinalize(); + + static String authenticationType = 'public-key'; } class PasskeyServiceImpl implements PasskeyService { @@ -48,6 +50,8 @@ class PasskeyServiceImpl implements PasskeyService { */ + static const _defaultMediation = MediationType.Optional; + static const _preferImmediatelyAvailableCredentials = false; @override @@ -75,7 +79,7 @@ class PasskeyServiceImpl implements PasskeyService { challenge: pubKey.challenge, allowCredentials: pubKey.allowCredentials ?? [], relyingPartyId: pubKey.rpId!, - mediation: response.mediation, + mediation: response.mediation ?? _defaultMediation, preferImmediatelyAvailableCredentials: _preferImmediatelyAvailableCredentials, ); @@ -84,6 +88,8 @@ class PasskeyServiceImpl implements PasskeyService { @override Future logInFinalize( AuthenticateResponseType loginLocalResponse) async { + final payload = loginLocalResponse.toJson(); + payload['type'] = PasskeyService.authenticationType; final response = await _userApi.logInFinalize(loginLocalResponse.toJson()); _authService.setAuthToken(response); } diff --git a/lib/util/passkey_utils.dart b/lib/util/passkey_utils.dart index 5602ed14a..53ab1faed 100644 --- a/lib/util/passkey_utils.dart +++ b/lib/util/passkey_utils.dart @@ -1,13 +1,25 @@ +import 'package:autonomy_flutter/service/passkey_service.dart'; import 'package:passkeys/types.dart'; extension RegisterResponseTypeExt on RegisterResponseType { Map toCredentialCreationResponseJson() => { 'id': id, 'rawId': rawId, - 'type': 'public-key', + 'type': PasskeyService.authenticationType, 'response': { 'clientDataJSON': clientDataJSON, 'attestationObject': attestationObject, } }; } + +CredentialType getCredentialTypeFromJsonFF(Map json) => + CredentialType( + type: json['type'] as String, + id: json['id'] as String, + transports: json['transports'] == null + ? [] + : (json['transports'] as List) + .map((e) => e as String) + .toList(), + ); diff --git a/lib/util/ui_helper.dart b/lib/util/ui_helper.dart index dd229e07d..ced2a37a3 100644 --- a/lib/util/ui_helper.dart +++ b/lib/util/ui_helper.dart @@ -1048,12 +1048,11 @@ class UIHelper { content: const PasskeyRegisterView(), ); - static Future showPasskeyLoginDialog(BuildContext context) async { - await showRawCenterSheet( - context, - content: const PasskeyLoginView(), - ); - } + static Future showPasskeyLoginDialog(BuildContext context) async => + await showRawCenterSheet( + context, + content: const PasskeyLoginView(), + ); static Future showRawCenterSheet(BuildContext context, {required Widget content, From 3b0613bd467886159a29a21f53676c76c1a783ac Mon Sep 17 00:00:00 2001 From: phuoc Date: Mon, 28 Oct 2024 14:51:38 +0700 Subject: [PATCH 23/57] fix login finalize payload Signed-off-by: phuoc --- lib/service/passkey_service.dart | 5 ++--- lib/util/passkey_utils.dart | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/lib/service/passkey_service.dart b/lib/service/passkey_service.dart index bb3dbb367..4162faab1 100644 --- a/lib/service/passkey_service.dart +++ b/lib/service/passkey_service.dart @@ -88,9 +88,8 @@ class PasskeyServiceImpl implements PasskeyService { @override Future logInFinalize( AuthenticateResponseType loginLocalResponse) async { - final payload = loginLocalResponse.toJson(); - payload['type'] = PasskeyService.authenticationType; - final response = await _userApi.logInFinalize(loginLocalResponse.toJson()); + final response = + await _userApi.logInFinalize(loginLocalResponse.toFFJson()); _authService.setAuthToken(response); } diff --git a/lib/util/passkey_utils.dart b/lib/util/passkey_utils.dart index 53ab1faed..97bf0b6c3 100644 --- a/lib/util/passkey_utils.dart +++ b/lib/util/passkey_utils.dart @@ -13,6 +13,20 @@ extension RegisterResponseTypeExt on RegisterResponseType { }; } +extension AuthenticateResponseTypeExt on AuthenticateResponseType { + Map toFFJson() => { + 'id': id, + 'rawId': rawId, + 'type': PasskeyService.authenticationType, + 'response': { + 'clientDataJSON': clientDataJSON, + 'authenticatorData': authenticatorData, + 'signature': signature, + 'userHandle': userHandle, + } + }; +} + CredentialType getCredentialTypeFromJsonFF(Map json) => CredentialType( type: json['type'] as String, From 8f60336252b1310e5cecc719089d90347991ef83 Mon Sep 17 00:00:00 2001 From: phuoc Date: Mon, 28 Oct 2024 15:02:13 +0700 Subject: [PATCH 24/57] fix unit test Signed-off-by: phuoc --- .../service/mock_account_service.mocks.dart | 6 +- .../mock_configuration_service.mocks.dart | 387 +++++++++--------- 2 files changed, 208 insertions(+), 185 deletions(-) diff --git a/test/generate_mock/service/mock_account_service.mocks.dart b/test/generate_mock/service/mock_account_service.mocks.dart index f6ec01660..a3b04615a 100644 --- a/test/generate_mock/service/mock_account_service.mocks.dart +++ b/test/generate_mock/service/mock_account_service.mocks.dart @@ -69,10 +69,12 @@ class MockAccountService extends _i1.Mock implements _i5.AccountService { } @override - _i6.Future migrateAccount() => (super.noSuchMethod( + _i6.Future migrateAccount( + _i6.Future Function()? createLoginJwt) => + (super.noSuchMethod( Invocation.method( #migrateAccount, - [], + [createLoginJwt], ), returnValue: _i6.Future.value(), returnValueForMissingStub: _i6.Future.value(), diff --git a/test/generate_mock/service/mock_configuration_service.mocks.dart b/test/generate_mock/service/mock_configuration_service.mocks.dart index b0e30f2a5..5b8a4b437 100644 --- a/test/generate_mock/service/mock_configuration_service.mocks.dart +++ b/test/generate_mock/service/mock_configuration_service.mocks.dart @@ -3,11 +3,11 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i6; +import 'dart:async' as _i5; -import 'package:autonomy_flutter/model/jwt.dart' as _i7; -import 'package:autonomy_flutter/model/network.dart' as _i8; -import 'package:autonomy_flutter/model/sent_artwork.dart' as _i9; +import 'package:autonomy_flutter/model/jwt.dart' as _i6; +import 'package:autonomy_flutter/model/network.dart' as _i7; +import 'package:autonomy_flutter/model/sent_artwork.dart' as _i8; import 'package:autonomy_flutter/model/shared_postcard.dart' as _i10; import 'package:autonomy_flutter/screen/chat/chat_thread_page.dart' as _i3; import 'package:autonomy_flutter/screen/interactive_postcard/postcard_detail_page.dart' @@ -17,7 +17,7 @@ import 'package:autonomy_flutter/screen/interactive_postcard/stamp_preview.dart' import 'package:autonomy_flutter/service/configuration_service.dart' as _i4; import 'package:flutter/material.dart' as _i2; import 'package:mockito/mockito.dart' as _i1; -import 'package:mockito/src/dummies.dart' as _i5; +import 'package:mockito/src/dummies.dart' as _i9; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -72,28 +72,22 @@ class MockConfigurationService extends _i1.Mock ), ) as _i2.ValueNotifier); @override - String getRefreshToken() => (super.noSuchMethod( + bool didRegisterPasskey() => (super.noSuchMethod( Invocation.method( - #getRefreshToken, + #didRegisterPasskey, [], ), - returnValue: _i5.dummyValue( - this, - Invocation.method( - #getRefreshToken, - [], - ), - ), - ) as String); + returnValue: false, + ) as bool); @override - _i6.Future setRefreshToken(String? value) => (super.noSuchMethod( + _i5.Future setDidRegisterPasskey(bool? value) => (super.noSuchMethod( Invocation.method( - #setRefreshToken, + #setDidRegisterPasskey, [value], ), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override bool didMigrateToAccountSetting() => (super.noSuchMethod( Invocation.method( @@ -103,24 +97,24 @@ class MockConfigurationService extends _i1.Mock returnValue: false, ) as bool); @override - _i6.Future setMigrateToAccountSetting(bool? value) => + _i5.Future setMigrateToAccountSetting(bool? value) => (super.noSuchMethod( Invocation.method( #setMigrateToAccountSetting, [value], ), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override - _i6.Future setDidShowLiveWithArt(bool? value) => (super.noSuchMethod( + _i5.Future setDidShowLiveWithArt(bool? value) => (super.noSuchMethod( Invocation.method( #setDidShowLiveWithArt, [value], ), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override bool didShowLiveWithArt() => (super.noSuchMethod( Invocation.method( @@ -130,15 +124,15 @@ class MockConfigurationService extends _i1.Mock returnValue: false, ) as bool); @override - _i6.Future setLastPullAnnouncementTime(int? lastPullTime) => + _i5.Future setLastPullAnnouncementTime(int? lastPullTime) => (super.noSuchMethod( Invocation.method( #setLastPullAnnouncementTime, [lastPullTime], ), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override int getLastPullAnnouncementTime() => (super.noSuchMethod( Invocation.method( @@ -148,7 +142,7 @@ class MockConfigurationService extends _i1.Mock returnValue: 0, ) as int); @override - _i6.Future setHasMerchandiseSupport( + _i5.Future setHasMerchandiseSupport( String? indexId, { bool? value = true, bool? isOverride = false, @@ -162,9 +156,9 @@ class MockConfigurationService extends _i1.Mock #isOverride: isOverride, }, ), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override bool hasMerchandiseSupport(String? indexId) => (super.noSuchMethod( Invocation.method( @@ -174,15 +168,15 @@ class MockConfigurationService extends _i1.Mock returnValue: false, ) as bool); @override - _i6.Future setPostcardChatConfig(_i3.PostcardChatConfig? config) => + _i5.Future setPostcardChatConfig(_i3.PostcardChatConfig? config) => (super.noSuchMethod( Invocation.method( #setPostcardChatConfig, [config], ), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override _i3.PostcardChatConfig getPostcardChatConfig({ required String? address, @@ -210,14 +204,14 @@ class MockConfigurationService extends _i1.Mock ), ) as _i3.PostcardChatConfig); @override - _i6.Future setDidMigrateAddress(bool? value) => (super.noSuchMethod( + _i5.Future setDidMigrateAddress(bool? value) => (super.noSuchMethod( Invocation.method( #setDidMigrateAddress, [value], ), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override bool getDidMigrateAddress() => (super.noSuchMethod( Invocation.method( @@ -227,24 +221,24 @@ class MockConfigurationService extends _i1.Mock returnValue: false, ) as bool); @override - _i6.Future setAnnouncementLastPullTime(int? lastPullTime) => + _i5.Future setAnnouncementLastPullTime(int? lastPullTime) => (super.noSuchMethod( Invocation.method( #setAnnouncementLastPullTime, [lastPullTime], ), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override - _i6.Future setOldUser() => (super.noSuchMethod( + _i5.Future setOldUser() => (super.noSuchMethod( Invocation.method( #setOldUser, [], ), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override bool getIsOldUser() => (super.noSuchMethod( Invocation.method( @@ -254,32 +248,32 @@ class MockConfigurationService extends _i1.Mock returnValue: false, ) as bool); @override - _i6.Future setIAPReceipt(String? value) => (super.noSuchMethod( + _i5.Future setIAPReceipt(String? value) => (super.noSuchMethod( Invocation.method( #setIAPReceipt, [value], ), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override - _i6.Future setIAPJWT(_i7.JWT? value) => (super.noSuchMethod( + _i5.Future setIAPJWT(_i6.JWT? value) => (super.noSuchMethod( Invocation.method( #setIAPJWT, [value], ), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override - _i6.Future setDevicePasscodeEnabled(bool? value) => (super.noSuchMethod( + _i5.Future setDevicePasscodeEnabled(bool? value) => (super.noSuchMethod( Invocation.method( #setDevicePasscodeEnabled, [value], ), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override bool isDevicePasscodeEnabled() => (super.noSuchMethod( Invocation.method( @@ -289,14 +283,14 @@ class MockConfigurationService extends _i1.Mock returnValue: false, ) as bool); @override - _i6.Future setNotificationEnabled(bool? value) => (super.noSuchMethod( + _i5.Future setNotificationEnabled(bool? value) => (super.noSuchMethod( Invocation.method( #setNotificationEnabled, [value], ), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override bool isNotificationEnabled() => (super.noSuchMethod( Invocation.method( @@ -306,14 +300,14 @@ class MockConfigurationService extends _i1.Mock returnValue: false, ) as bool); @override - _i6.Future setAnalyticEnabled(bool? value) => (super.noSuchMethod( + _i5.Future setAnalyticEnabled(bool? value) => (super.noSuchMethod( Invocation.method( #setAnalyticEnabled, [value], ), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override bool isAnalyticsEnabled() => (super.noSuchMethod( Invocation.method( @@ -323,14 +317,14 @@ class MockConfigurationService extends _i1.Mock returnValue: false, ) as bool); @override - _i6.Future setDoneOnboarding(bool? value) => (super.noSuchMethod( + _i5.Future setDoneOnboarding(bool? value) => (super.noSuchMethod( Invocation.method( #setDoneOnboarding, [value], ), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override bool isDoneOnboarding() => (super.noSuchMethod( Invocation.method( @@ -340,16 +334,16 @@ class MockConfigurationService extends _i1.Mock returnValue: false, ) as bool); @override - _i6.Future setLastTimeAskForSubscription(DateTime? date) => + _i5.Future setLastTimeAskForSubscription(DateTime? date) => (super.noSuchMethod( Invocation.method( #setLastTimeAskForSubscription, [date], ), - returnValue: _i6.Future.value(), - ) as _i6.Future); + returnValue: _i5.Future.value(), + ) as _i5.Future); @override - List getTempStorageHiddenTokenIDs({_i8.Network? network}) => + List getTempStorageHiddenTokenIDs({_i7.Network? network}) => (super.noSuchMethod( Invocation.method( #getTempStorageHiddenTokenIDs, @@ -359,10 +353,10 @@ class MockConfigurationService extends _i1.Mock returnValue: [], ) as List); @override - _i6.Future updateTempStorageHiddenTokenIDs( + _i5.Future updateTempStorageHiddenTokenIDs( List? tokenIDs, bool? isAdd, { - _i8.Network? network, + _i7.Network? network, bool? override = false, }) => (super.noSuchMethod( @@ -377,19 +371,19 @@ class MockConfigurationService extends _i1.Mock #override: override, }, ), - returnValue: _i6.Future.value(), - ) as _i6.Future); + returnValue: _i5.Future.value(), + ) as _i5.Future); @override - List<_i9.SentArtwork> getRecentlySentToken() => (super.noSuchMethod( + List<_i8.SentArtwork> getRecentlySentToken() => (super.noSuchMethod( Invocation.method( #getRecentlySentToken, [], ), - returnValue: <_i9.SentArtwork>[], - ) as List<_i9.SentArtwork>); + returnValue: <_i8.SentArtwork>[], + ) as List<_i8.SentArtwork>); @override - _i6.Future updateRecentlySentToken( - List<_i9.SentArtwork>? sentArtwork, { + _i5.Future updateRecentlySentToken( + List<_i8.SentArtwork>? sentArtwork, { bool? override = false, }) => (super.noSuchMethod( @@ -398,60 +392,60 @@ class MockConfigurationService extends _i1.Mock [sentArtwork], {#override: override}, ), - returnValue: _i6.Future.value(), - ) as _i6.Future); + returnValue: _i5.Future.value(), + ) as _i5.Future); @override - _i6.Future setReadReleaseNotesInVersion(String? version) => + _i5.Future setReadReleaseNotesInVersion(String? version) => (super.noSuchMethod( Invocation.method( #setReadReleaseNotesInVersion, [version], ), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override - _i6.Future setPreviousBuildNumber(String? value) => (super.noSuchMethod( + _i5.Future setPreviousBuildNumber(String? value) => (super.noSuchMethod( Invocation.method( #setPreviousBuildNumber, [value], ), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override - _i6.Future getAccountHMACSecret() => (super.noSuchMethod( + _i5.Future getAccountHMACSecret() => (super.noSuchMethod( Invocation.method( #getAccountHMACSecret, [], ), - returnValue: _i6.Future.value(_i5.dummyValue( + returnValue: _i5.Future.value(_i9.dummyValue( this, Invocation.method( #getAccountHMACSecret, [], ), )), - ) as _i6.Future); + ) as _i5.Future); @override - _i6.Future setLastRemindReviewDate(String? value) => + _i5.Future setLastRemindReviewDate(String? value) => (super.noSuchMethod( Invocation.method( #setLastRemindReviewDate, [value], ), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override - _i6.Future setCountOpenApp(int? value) => (super.noSuchMethod( + _i5.Future setCountOpenApp(int? value) => (super.noSuchMethod( Invocation.method( #setCountOpenApp, [value], ), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override bool showTokenDebugInfo() => (super.noSuchMethod( Invocation.method( @@ -461,58 +455,58 @@ class MockConfigurationService extends _i1.Mock returnValue: false, ) as bool); @override - _i6.Future setShowTokenDebugInfo(bool? show) => (super.noSuchMethod( + _i5.Future setShowTokenDebugInfo(bool? show) => (super.noSuchMethod( Invocation.method( #setShowTokenDebugInfo, [show], ), - returnValue: _i6.Future.value(), - ) as _i6.Future); + returnValue: _i5.Future.value(), + ) as _i5.Future); @override - _i6.Future setDoneOnboardingTime(DateTime? time) => + _i5.Future setDoneOnboardingTime(DateTime? time) => (super.noSuchMethod( Invocation.method( #setDoneOnboardingTime, [time], ), - returnValue: _i6.Future.value(), - ) as _i6.Future); + returnValue: _i5.Future.value(), + ) as _i5.Future); @override - _i6.Future setSubscriptionTime(DateTime? time) => + _i5.Future setSubscriptionTime(DateTime? time) => (super.noSuchMethod( Invocation.method( #setSubscriptionTime, [time], ), - returnValue: _i6.Future.value(), - ) as _i6.Future); + returnValue: _i5.Future.value(), + ) as _i5.Future); @override - _i6.Future setSentTezosArtworkMetric(int? hashedAddresses) => + _i5.Future setSentTezosArtworkMetric(int? hashedAddresses) => (super.noSuchMethod( Invocation.method( #setSentTezosArtworkMetric, [hashedAddresses], ), - returnValue: _i6.Future.value(), - ) as _i6.Future); + returnValue: _i5.Future.value(), + ) as _i5.Future); @override - _i6.Future reload() => (super.noSuchMethod( + _i5.Future reload() => (super.noSuchMethod( Invocation.method( #reload, [], ), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override - _i6.Future removeAll() => (super.noSuchMethod( + _i5.Future removeAll() => (super.noSuchMethod( Invocation.method( #removeAll, [], ), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override List<_i10.SharedPostcard> getSharedPostcard() => (super.noSuchMethod( Invocation.method( @@ -522,7 +516,7 @@ class MockConfigurationService extends _i1.Mock returnValue: <_i10.SharedPostcard>[], ) as List<_i10.SharedPostcard>); @override - _i6.Future updateSharedPostcard( + _i5.Future updateSharedPostcard( List<_i10.SharedPostcard>? sharedPostcards, { bool? override = false, bool? isRemoved = false, @@ -536,20 +530,20 @@ class MockConfigurationService extends _i1.Mock #isRemoved: isRemoved, }, ), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override - _i6.Future removeSharedPostcardWhere( + _i5.Future removeSharedPostcardWhere( bool Function(_i10.SharedPostcard)? test) => (super.noSuchMethod( Invocation.method( #removeSharedPostcardWhere, [test], ), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override List getListPostcardMint() => (super.noSuchMethod( Invocation.method( @@ -559,7 +553,7 @@ class MockConfigurationService extends _i1.Mock returnValue: [], ) as List); @override - _i6.Future setListPostcardMint( + _i5.Future setListPostcardMint( List? tokenID, { bool? override = false, bool? isRemoved = false, @@ -573,9 +567,9 @@ class MockConfigurationService extends _i1.Mock #isRemoved: isRemoved, }, ), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override List<_i11.StampingPostcard> getStampingPostcard() => (super.noSuchMethod( Invocation.method( @@ -585,7 +579,7 @@ class MockConfigurationService extends _i1.Mock returnValue: <_i11.StampingPostcard>[], ) as List<_i11.StampingPostcard>); @override - _i6.Future updateStampingPostcard( + _i5.Future updateStampingPostcard( List<_i11.StampingPostcard>? values, { bool? override = false, bool? isRemove = false, @@ -599,11 +593,11 @@ class MockConfigurationService extends _i1.Mock #isRemove: isRemove, }, ), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override - _i6.Future setProcessingStampPostcard( + _i5.Future setProcessingStampPostcard( List<_i11.ProcessingStampPostcard>? values, { bool? override = false, bool? isRemove = false, @@ -617,9 +611,9 @@ class MockConfigurationService extends _i1.Mock #isRemove: isRemove, }, ), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override List<_i11.ProcessingStampPostcard> getProcessingStampPostcard() => (super.noSuchMethod( @@ -630,14 +624,14 @@ class MockConfigurationService extends _i1.Mock returnValue: <_i11.ProcessingStampPostcard>[], ) as List<_i11.ProcessingStampPostcard>); @override - _i6.Future setAutoShowPostcard(bool? value) => (super.noSuchMethod( + _i5.Future setAutoShowPostcard(bool? value) => (super.noSuchMethod( Invocation.method( #setAutoShowPostcard, [value], ), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override bool isAutoShowPostcard() => (super.noSuchMethod( Invocation.method( @@ -656,7 +650,7 @@ class MockConfigurationService extends _i1.Mock returnValue: <_i12.PostcardIdentity>[], ) as List<_i12.PostcardIdentity>); @override - _i6.Future setListPostcardAlreadyShowYouDidIt( + _i5.Future setListPostcardAlreadyShowYouDidIt( List<_i12.PostcardIdentity>? value, { bool? override = false, }) => @@ -666,11 +660,11 @@ class MockConfigurationService extends _i1.Mock [value], {#override: override}, ), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override - _i6.Future setAlreadyShowPostcardUpdates( + _i5.Future setAlreadyShowPostcardUpdates( List<_i12.PostcardIdentity>? value, { bool? override = false, }) => @@ -680,9 +674,9 @@ class MockConfigurationService extends _i1.Mock [value], {#override: override}, ), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override List<_i12.PostcardIdentity> getAlreadyShowPostcardUpdates() => (super.noSuchMethod( @@ -698,7 +692,7 @@ class MockConfigurationService extends _i1.Mock #getVersionInfo, [], ), - returnValue: _i5.dummyValue( + returnValue: _i9.dummyValue( this, Invocation.method( #getVersionInfo, @@ -707,14 +701,14 @@ class MockConfigurationService extends _i1.Mock ), ) as String); @override - _i6.Future setVersionInfo(String? version) => (super.noSuchMethod( + _i5.Future setVersionInfo(String? version) => (super.noSuchMethod( Invocation.method( #setVersionInfo, [version], ), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override List getHiddenOrSentTokenIDs() => (super.noSuchMethod( Invocation.method( @@ -724,14 +718,14 @@ class MockConfigurationService extends _i1.Mock returnValue: [], ) as List); @override - _i6.Future setShowPostcardBanner(bool? bool) => (super.noSuchMethod( + _i5.Future setShowPostcardBanner(bool? bool) => (super.noSuchMethod( Invocation.method( #setShowPostcardBanner, [bool], ), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override bool getShowPostcardBanner() => (super.noSuchMethod( Invocation.method( @@ -741,14 +735,14 @@ class MockConfigurationService extends _i1.Mock returnValue: false, ) as bool); @override - _i6.Future setShowAddAddressBanner(bool? bool) => (super.noSuchMethod( + _i5.Future setShowAddAddressBanner(bool? bool) => (super.noSuchMethod( Invocation.method( #setShowAddAddressBanner, [bool], ), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override bool getShowAddAddressBanner() => (super.noSuchMethod( Invocation.method( @@ -758,7 +752,7 @@ class MockConfigurationService extends _i1.Mock returnValue: false, ) as bool); @override - _i6.Future setMerchandiseOrderIds( + _i5.Future setMerchandiseOrderIds( List? ids, { bool? override = false, }) => @@ -768,9 +762,9 @@ class MockConfigurationService extends _i1.Mock [ids], {#override: override}, ), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override List getMerchandiseOrderIds() => (super.noSuchMethod( Invocation.method( @@ -780,12 +774,39 @@ class MockConfigurationService extends _i1.Mock returnValue: [], ) as List); @override - _i6.Future setReferralCode(String? referralCode) => (super.noSuchMethod( + _i5.Future setReferralCode(String? referralCode) => (super.noSuchMethod( Invocation.method( #setReferralCode, [referralCode], ), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + void setLinkAnnouncementToIssue( + String? announcementContentId, + String? issueId, + ) => + super.noSuchMethod( + Invocation.method( + #setLinkAnnouncementToIssue, + [ + announcementContentId, + issueId, + ], + ), + returnValueForMissingStub: null, + ); + @override + String? getIssueIdByAnnouncementContentId(String? announcementContentId) => + (super.noSuchMethod(Invocation.method( + #getIssueIdByAnnouncementContentId, + [announcementContentId], + )) as String?); + @override + String? getAnnouncementContentIdByIssueId(String? issueId) => + (super.noSuchMethod(Invocation.method( + #getAnnouncementContentIdByIssueId, + [issueId], + )) as String?); } From cc3bbeda31e81494cbc1fbe747fa76dd5aa82da7 Mon Sep 17 00:00:00 2001 From: phuoc Date: Mon, 28 Oct 2024 15:32:44 +0700 Subject: [PATCH 25/57] fix authenticate android Signed-off-by: phuoc --- .../autonomy_flutter/BackupDartPlugin.kt | 53 ------------------- .../bitmark/autonomy_flutter/MainActivity.kt | 29 +++++++++- lib/util/user_account_channel.dart | 2 + 3 files changed, 30 insertions(+), 54 deletions(-) diff --git a/android/app/src/main/kotlin/com/bitmark/autonomy_flutter/BackupDartPlugin.kt b/android/app/src/main/kotlin/com/bitmark/autonomy_flutter/BackupDartPlugin.kt index 636fcb011..af35deb51 100644 --- a/android/app/src/main/kotlin/com/bitmark/autonomy_flutter/BackupDartPlugin.kt +++ b/android/app/src/main/kotlin/com/bitmark/autonomy_flutter/BackupDartPlugin.kt @@ -34,7 +34,6 @@ class BackupDartPlugin : MethodChannel.MethodCallHandler { private lateinit var client: BlockstoreClient private val primaryAddressStoreKey = "primary_address" private val userIdStoreKey = "user_id" - private val didRegisterPasskeys = "did_register_passkeys" fun createChannels(@NonNull flutterEngine: FlutterEngine, @NonNull context: Context) { this.context = context @@ -55,8 +54,6 @@ class BackupDartPlugin : MethodChannel.MethodCallHandler { "setUserId" -> setUserId(call, result) "getUserId" -> getUserId(call, result) "clearUserId" -> clearUserId(call, result) - "setDidRegisterPasskey" -> setDidRegisterPasskey(call, result) - "didRegisterPasskey" -> didRegisterPasskey(call, result) "deleteKeys" -> deleteKeys(call, result) else -> { result.notImplemented() @@ -329,56 +326,6 @@ class BackupDartPlugin : MethodChannel.MethodCallHandler { } } - private fun setDidRegisterPasskey(call: MethodCall, result: MethodChannel.Result) { - val data: Boolean = call.argument("data") ?: false - - val storeBytesBuilder = StoreBytesData.Builder() - .setKey(didRegisterPasskeys) - .setBytes(data.toString().toByteArray(Charsets.UTF_8)) - - client.storeBytes(storeBytesBuilder.build()) - .addOnSuccessListener { - - Log.e("setDidRegisterPasskey", data.toString()); - result.success(true) - } - .addOnFailureListener { e -> - Log.e("setDidRegisterPasskey", e.message ?: "") - result.success(false) - } - } - - private fun didRegisterPasskey(call: MethodCall, result: MethodChannel.Result) { - val request = RetrieveBytesRequest.Builder() - .setKeys(listOf(didRegisterPasskeys)) // Specify the key - .build() - client.retrieveBytes(request) - .addOnSuccessListener { - try { // Retrieve bytes using the key - val dataMap = it.blockstoreDataMap[userIdStoreKey] - if (dataMap != null) { - val bytes = dataMap.bytes - val resultString = bytes.toString(Charsets.UTF_8) - Log.d("didRegisterPasskey", resultString) - - - result.success(resultString.toBoolean()) - } else { - Log.e("didRegisterPasskey", "No data found for the key") - result.success(false) - } - } catch (e: Exception) { - Log.e("didRegisterPasskey", e.message ?: "Error decoding data") - //No primary address found - result.success(false) - } - } - .addOnFailureListener { - //Block store not available - result.error("didRegisterPasskey Block store error", it.message, it) - } - } - private fun deleteKeys(call: MethodCall, result: MethodChannel.Result) { val deleteRequestBuilder = DeleteBytesRequest.Builder() .setDeleteAll(true) diff --git a/android/app/src/main/kotlin/com/bitmark/autonomy_flutter/MainActivity.kt b/android/app/src/main/kotlin/com/bitmark/autonomy_flutter/MainActivity.kt index a865341f2..1ec04648b 100644 --- a/android/app/src/main/kotlin/com/bitmark/autonomy_flutter/MainActivity.kt +++ b/android/app/src/main/kotlin/com/bitmark/autonomy_flutter/MainActivity.kt @@ -13,6 +13,7 @@ import android.content.Intent import android.content.SharedPreferences import android.os.Build import android.os.Bundle +import android.util.Log import android.view.View.ACCESSIBILITY_DATA_SENSITIVE_YES import android.view.WindowManager.LayoutParams import androidx.biometric.BiometricManager @@ -20,12 +21,16 @@ import io.flutter.embedding.android.FlutterFragmentActivity import io.flutter.embedding.android.FlutterView import io.flutter.embedding.engine.FlutterEngine import io.flutter.plugin.common.MethodChannel +import java.util.concurrent.TimeUnit class MainActivity : FlutterFragmentActivity() { companion object { var isAuthenticate: Boolean = false private const val CHANNEL = "migration_util" private val secureScreenChannel = "secure_screen_channel" + private var lastAuthTime: Long = 0 + private val authenticationTimeout = TimeUnit.MINUTES.toMillis(3) + private var isThisFirstOnResume = true; } var flutterSharedPreferences: SharedPreferences? = null @@ -225,18 +230,40 @@ class MainActivity : FlutterFragmentActivity() { Context.MODE_PRIVATE ) val isEnabled = sharedPreferences.getBoolean("flutter.device_passcode", false) - if (isEnabled && !isAuthenticate) { + val didRegisterPasskey = + sharedPreferences.getBoolean("flutter.did_register_passkey", false) + if (isThisFirstOnResume && didRegisterPasskey) { + + // skip authentication if the user has already registered the passkey in open app + isThisFirstOnResume = false + // this is not conventional way to do this, but we need skip authenticate after user + // authenticate with passkey + updateAuthenticationTime() + return + } + + if (isEnabled && !isAuthenticate && needsReAuthentication()) { val biometricManager = BiometricManager.from(this) val keyguardManager = getSystemService(KEYGUARD_SERVICE) as KeyguardManager if (biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) == BiometricManager.BIOMETRIC_SUCCESS || keyguardManager.isDeviceSecure ) { val intent = Intent(this@MainActivity, AuthenticatorActivity::class.java) + updateAuthenticationTime() startActivity(intent) } } } + private fun updateAuthenticationTime() { + lastAuthTime = System.currentTimeMillis() + } + + private fun needsReAuthentication(): Boolean { + val currentTime = System.currentTimeMillis() + return (currentTime - lastAuthTime) > authenticationTimeout + } + override fun onPause() { super.onPause() isAuthenticate = false diff --git a/lib/util/user_account_channel.dart b/lib/util/user_account_channel.dart index dbdf89ade..3ae8544db 100644 --- a/lib/util/user_account_channel.dart +++ b/lib/util/user_account_channel.dart @@ -112,6 +112,8 @@ class UserAccountChannel { Future setDidRegisterPasskey(bool value) async { if (Platform.isAndroid) { + // for Android device, passkey is stored in Google Password Manager, + // so it is not synced await injector().setDidRegisterPasskey(value); return true; } From 9b5c02f611b55a15f6a582ab4b7fd8e41a33d9c0 Mon Sep 17 00:00:00 2001 From: phuoc Date: Mon, 28 Oct 2024 15:34:06 +0700 Subject: [PATCH 26/57] add required enviroment Signed-off-by: phuoc --- lib/common/environment.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/common/environment.dart b/lib/common/environment.dart index 086f74001..3eda800fd 100644 --- a/lib/common/environment.dart +++ b/lib/common/environment.dart @@ -71,6 +71,7 @@ class Environment { 'SENTRY_DSN', 'ONESIGNAL_APP_ID', 'TV_API_KEY', + 'SUPPORT_API_KEY', ]; final missingKeys = []; for (var key in keys) { From cba4f9589cc4df3d00e4baea68596e9e8703b2b3 Mon Sep 17 00:00:00 2001 From: phuoc Date: Mon, 28 Oct 2024 16:10:09 +0700 Subject: [PATCH 27/57] don't authenticate when open app ios passkey Signed-off-by: phuoc --- ios/Runner/AppDelegate.swift | 10 ++++++---- ios/Runner/SystemChannelHandler.swift | 4 ++++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index be1af8722..77af7340f 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -261,10 +261,12 @@ import Logging }) DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) { [weak self] in - if UserDefaults.standard.bool(forKey: "flutter.device_passcode") == true { - self?.showAuthenticationOverlay() - self?.authenticationVC.authentication() - } + SystemChannelHandler.shared.didRegisterPasskeyKeychain { didRegisterPasskey in + if !didRegisterPasskey { + self?.showAuthenticationOverlay() + self?.authenticationVC.authentication() + } + } } return super.application(application, didFinishLaunchingWithOptions: launchOptions) diff --git a/ios/Runner/SystemChannelHandler.swift b/ios/Runner/SystemChannelHandler.swift index c323e4089..6ca799b4d 100644 --- a/ios/Runner/SystemChannelHandler.swift +++ b/ios/Runner/SystemChannelHandler.swift @@ -227,6 +227,10 @@ class SystemChannelHandler: NSObject { } func didRegisterPasskey(call: FlutterMethodCall, result: @escaping FlutterResult) { + didRegisterPasskeyKeychain(result: result) + } + + func didRegisterPasskeyKeychain(result: @escaping FlutterResult) { let keychain = Keychain() // Safely retrieve data from Keychain From d0b2541616948eec09b910995939bd427dee1005 Mon Sep 17 00:00:00 2001 From: phuoc Date: Mon, 28 Oct 2024 16:51:17 +0700 Subject: [PATCH 28/57] fix ios syntax Signed-off-by: phuoc --- ios/Runner/AppDelegate.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 77af7340f..56300df82 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -262,7 +262,7 @@ import Logging DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) { [weak self] in SystemChannelHandler.shared.didRegisterPasskeyKeychain { didRegisterPasskey in - if !didRegisterPasskey { + if let didRegisterPasskey = didRegisterPasskey as? Bool, !didRegisterPasskey { self?.showAuthenticationOverlay() self?.authenticationVC.authentication() } From 952d7bab934c85967c424728f9c005a6a0277323 Mon Sep 17 00:00:00 2001 From: phuoc Date: Mon, 28 Oct 2024 17:08:21 +0700 Subject: [PATCH 29/57] fix comment create wallet Signed-off-by: phuoc --- lib/service/account_service.dart | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/lib/service/account_service.dart b/lib/service/account_service.dart index be57ea4e6..85a1ca93c 100644 --- a/lib/service/account_service.dart +++ b/lib/service/account_service.dart @@ -63,7 +63,9 @@ abstract class AccountService { Future isAndroidEndToEndEncryptionAvailable(); Future createNewWallet( - {String name = '', String passphrase = ''}); + {String name = '', + String passphrase = '', + Future Function()? createLoginJwt}); Future importWords(String words, String passphrase, {WalletType walletType = WalletType.MultiChain}); @@ -132,11 +134,25 @@ class AccountServiceImpl extends AccountService { @override Future createNewWallet( - {String name = '', String passphrase = ''}) async { + {String name = '', + String passphrase = '', + Future Function()? createLoginJwt}) async { final uuid = const Uuid().v4(); final walletStorage = LibAukDart.getWallet(uuid); await walletStorage.createKey(passphrase, name); log.fine('[AccountService] Created persona $uuid}'); + await _addressService.registerPrimaryAddress( + info: primary_address_channel.AddressInfo( + uuid: walletStorage.uuid, + chain: 'ethereum', + index: 0, + )); + if (createLoginJwt != null) { + await createLoginJwt(); + } + await insertNextAddressFromUuid(walletStorage.uuid, WalletType.MultiChain, + name: ''); + await androidBackupKeys(); return walletStorage; } @@ -660,17 +676,7 @@ class AccountServiceImpl extends AccountService { /// create passkeys, no need to migrate if (defaultWallet == null) { log.info('[AccountService] migrateAccount: case 1 complete new user'); - final wallet = await createNewWallet(); - await _addressService.registerPrimaryAddress( - info: primary_address_channel.AddressInfo( - uuid: wallet.uuid, - chain: 'ethereum', - index: 0, - )); - await createLoginJwt(); - await insertNextAddressFromUuid(wallet.uuid, WalletType.MultiChain, - name: ''); - await androidBackupKeys(); + await createNewWallet(); unawaited(_cloudObject.setMigrated()); log.info('[AccountService] migrateAccount: case 1 finished'); return; From 44602debaf57377cbc5e5c96f0b5afe19b93ddff Mon Sep 17 00:00:00 2001 From: phuoc Date: Mon, 28 Oct 2024 17:39:41 +0700 Subject: [PATCH 30/57] fix create wallet Signed-off-by: phuoc --- lib/service/account_service.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/service/account_service.dart b/lib/service/account_service.dart index 85a1ca93c..ab975a1ab 100644 --- a/lib/service/account_service.dart +++ b/lib/service/account_service.dart @@ -676,7 +676,9 @@ class AccountServiceImpl extends AccountService { /// create passkeys, no need to migrate if (defaultWallet == null) { log.info('[AccountService] migrateAccount: case 1 complete new user'); - await createNewWallet(); + await createNewWallet( + createLoginJwt: createLoginJwt, + ); unawaited(_cloudObject.setMigrated()); log.info('[AccountService] migrateAccount: case 1 finished'); return; From c323bb560061317e8e195bb0b584a5b4e0d64fc5 Mon Sep 17 00:00:00 2001 From: phuoc Date: Mon, 28 Oct 2024 18:02:33 +0700 Subject: [PATCH 31/57] fix association link ios Signed-off-by: phuoc --- ios/Runner/Runner InhouseDebug.entitlements | 4 ++-- ios/Runner/Runner-Inhouse.entitlements | 4 ++-- ios/Runner/Runner.entitlements | 4 ++-- ios/Runner/RunnerDebug.entitlements | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/ios/Runner/Runner InhouseDebug.entitlements b/ios/Runner/Runner InhouseDebug.entitlements index aab5d568d..1e41b6700 100644 --- a/ios/Runner/Runner InhouseDebug.entitlements +++ b/ios/Runner/Runner InhouseDebug.entitlements @@ -16,8 +16,8 @@ applinks:app.feralfile.com feralfile-app.test-app.link feralfile-app-alternate.test-app.link - accounts.dev.feralfile.com - accounts.feralfile.com + webcredentials:accounts.dev.feralfile.com + webcredentials:accounts.feralfile.com com.apple.developer.icloud-container-identifiers diff --git a/ios/Runner/Runner-Inhouse.entitlements b/ios/Runner/Runner-Inhouse.entitlements index ae9f6ba26..18606fbb2 100644 --- a/ios/Runner/Runner-Inhouse.entitlements +++ b/ios/Runner/Runner-Inhouse.entitlements @@ -16,8 +16,8 @@ applinks:app.feralfile.com feralfile-app.test-app.link feralfile-app-alternate.test-app.link - accounts.dev.feralfile.com - accounts.feralfile.com + webcredentials:accounts.dev.feralfile.com + webcredentials:accounts.feralfile.com com.apple.developer.icloud-container-identifiers diff --git a/ios/Runner/Runner.entitlements b/ios/Runner/Runner.entitlements index 7d9967ba4..a30493c26 100644 --- a/ios/Runner/Runner.entitlements +++ b/ios/Runner/Runner.entitlements @@ -16,8 +16,8 @@ applinks:app.feralfile.com feralfile-app.test-app.link feralfile-app-alternate.test-app.link - accounts.dev.feralfile.com - accounts.feralfile.com + webcredentials:accounts.dev.feralfile.com + webcredentials:accounts.feralfile.com com.apple.developer.icloud-container-identifiers diff --git a/ios/Runner/RunnerDebug.entitlements b/ios/Runner/RunnerDebug.entitlements index 6018d940a..6b3aa9e74 100644 --- a/ios/Runner/RunnerDebug.entitlements +++ b/ios/Runner/RunnerDebug.entitlements @@ -16,8 +16,8 @@ applinks:app.feralfile.com feralfile-app.test-app.link feralfile-app-alternate.test-app.link - accounts.dev.feralfile.com - accounts.feralfile.com + webcredentials:accounts.dev.feralfile.com + webcredentials:accounts.feralfile.com com.apple.developer.icloud-container-identifiers From f931ec8a33c76fa205a31606e1eaaf828270bc83 Mon Sep 17 00:00:00 2001 From: phuoc Date: Mon, 28 Oct 2024 20:49:44 +0700 Subject: [PATCH 32/57] refactor saving passkey related key Signed-off-by: phuoc --- .../autonomy_flutter/BackupDartPlugin.kt | 67 ----------------- ios/Runner/AppDelegate.swift | 9 --- ios/Runner/SystemChannelHandler.swift | 34 --------- lib/screen/onboarding_page.dart | 4 +- .../forget_exist/forget_exist_bloc.dart | 4 +- lib/service/passkey_service.dart | 53 ++++++++++++-- lib/util/user_account_channel.dart | 71 ------------------- 7 files changed, 50 insertions(+), 192 deletions(-) diff --git a/android/app/src/main/kotlin/com/bitmark/autonomy_flutter/BackupDartPlugin.kt b/android/app/src/main/kotlin/com/bitmark/autonomy_flutter/BackupDartPlugin.kt index af35deb51..a8a4cb059 100644 --- a/android/app/src/main/kotlin/com/bitmark/autonomy_flutter/BackupDartPlugin.kt +++ b/android/app/src/main/kotlin/com/bitmark/autonomy_flutter/BackupDartPlugin.kt @@ -33,7 +33,6 @@ class BackupDartPlugin : MethodChannel.MethodCallHandler { private lateinit var disposables: CompositeDisposable private lateinit var client: BlockstoreClient private val primaryAddressStoreKey = "primary_address" - private val userIdStoreKey = "user_id" fun createChannels(@NonNull flutterEngine: FlutterEngine, @NonNull context: Context) { this.context = context @@ -51,9 +50,6 @@ class BackupDartPlugin : MethodChannel.MethodCallHandler { "setPrimaryAddress" -> setPrimaryAddress(call, result) "getPrimaryAddress" -> getPrimaryAddress(call, result) "clearPrimaryAddress" -> clearPrimaryAddress(call, result) - "setUserId" -> setUserId(call, result) - "getUserId" -> getUserId(call, result) - "clearUserId" -> clearUserId(call, result) "deleteKeys" -> deleteKeys(call, result) else -> { result.notImplemented() @@ -263,69 +259,6 @@ class BackupDartPlugin : MethodChannel.MethodCallHandler { } } - private fun setUserId(call: MethodCall, result: MethodChannel.Result) { - val data: String = call.argument("data") ?: return - - val storeBytesBuilder = StoreBytesData.Builder() - .setKey(userIdStoreKey) - .setBytes(data.toByteArray(Charsets.UTF_8)) - - client.storeBytes(storeBytesBuilder.build()) - .addOnSuccessListener { - - Log.e("setUserId", "user id set successfully"); - result.success(true) - } - .addOnFailureListener { e -> - Log.e("setUserId", e.message ?: "") - result.error("setUserId error", e.message, e) - } - } - - private fun getUserId(call: MethodCall, result: MethodChannel.Result) { - val request = RetrieveBytesRequest.Builder() - .setKeys(listOf(userIdStoreKey)) // Specify the key - .build() - client.retrieveBytes(request) - .addOnSuccessListener { - try { // Retrieve bytes using the key - val dataMap = it.blockstoreDataMap[userIdStoreKey] - if (dataMap != null) { - val bytes = dataMap.bytes - val idString = bytes.toString(Charsets.UTF_8) - Log.d("getUserId", idString) - - - result.success(idString) - } else { - Log.e("getUserId", "No data found for the key") - result.success(null) - } - } catch (e: Exception) { - Log.e("getUserId", e.message ?: "Error decoding data") - //No primary address found - result.success(null) - } - } - .addOnFailureListener { - //Block store not available - result.error("getUserId Block store error", it.message, it) - } - } - - private fun clearUserId(call: MethodCall, result: MethodChannel.Result) { - val retrieveRequest = DeleteBytesRequest.Builder() - .setKeys(listOf(userIdStoreKey)) - .build() - client.deleteBytes(retrieveRequest) - .addOnSuccessListener { - result.success(it) - } - .addOnFailureListener { - result.error("deleteUserId error", it.message, it) - } - } - private fun deleteKeys(call: MethodCall, result: MethodChannel.Result) { val deleteRequestBuilder = DeleteBytesRequest.Builder() .setDeleteAll(true) diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 56300df82..c921b61fa 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -193,15 +193,6 @@ import Logging case "clearPrimaryAddress": SystemChannelHandler.shared.clearPrimaryAddress(call: call) - case "getUserId": - SystemChannelHandler.shared.getUserId(call: call, result: result) - - case "setUserId": - SystemChannelHandler.shared.setUserId(call: call, result: result) - - case "clearUserId": - SystemChannelHandler.shared.clearUserId(call: call) - case "didRegisterPasskey": SystemChannelHandler.shared.didRegisterPasskey(call: call, result: result) diff --git a/ios/Runner/SystemChannelHandler.swift b/ios/Runner/SystemChannelHandler.swift index 6ca799b4d..8f0cb6c57 100644 --- a/ios/Runner/SystemChannelHandler.swift +++ b/ios/Runner/SystemChannelHandler.swift @@ -174,40 +174,6 @@ class SystemChannelHandler: NSObject { return } - - func setUserId(call: FlutterMethodCall, result: @escaping FlutterResult) { - guard let args = call.arguments as? [String: Any], - let data = args["data"] as? String else { - result(false) - return - } - let keychain = Keychain() - if keychain.set(data.data(using: .utf8)!, forKey: Constant.userIdKey) { - result(true) - } else { - result(false) - } - } - - func getUserId(call: FlutterMethodCall, result: @escaping FlutterResult) { - let keychain = Keychain() - - guard let data = keychain.getData(Constant.userIdKey, isSync: true), - let userId = String(data: data, encoding: .utf8) else { - result("") - return - } - - result(userId) - } - - func clearUserId(call: FlutterMethodCall) { - let keychain = Keychain() - - keychain.remove(key: Constant.userIdKey, isSync: true) - return - } - func setDidRegisterPasskey(call: FlutterMethodCall, result: @escaping FlutterResult) { // Safely extract the arguments and handle cases where "data" is nil or invalid, default to false let args = call.arguments as? [String: Any] diff --git a/lib/screen/onboarding_page.dart b/lib/screen/onboarding_page.dart index d78d456be..00c978dfe 100644 --- a/lib/screen/onboarding_page.dart +++ b/lib/screen/onboarding_page.dart @@ -27,7 +27,6 @@ import 'package:autonomy_flutter/util/metric_helper.dart'; import 'package:autonomy_flutter/util/notification_util.dart'; import 'package:autonomy_flutter/util/style.dart'; import 'package:autonomy_flutter/util/ui_helper.dart'; -import 'package:autonomy_flutter/util/user_account_channel.dart'; import 'package:autonomy_flutter/view/back_appbar.dart'; import 'package:autonomy_flutter/view/primary_button.dart'; import 'package:autonomy_flutter/view/responsive.dart'; @@ -56,7 +55,6 @@ class _OnboardingPageState extends State final _passkeyService = injector.get(); final _authService = injector.get(); - final _userAccountChannel = injector.get(); final _onboardingLogo = Semantics( label: 'onboarding_logo', @@ -175,7 +173,7 @@ class _OnboardingPageState extends State }); } else { log.info('Passkey is supported. Authenticate with passkey'); - final didRegisterPasskey = await _userAccountChannel.didRegisterPasskey(); + final didRegisterPasskey = await _passkeyService.didRegisterPasskey(); final didLoginSuccess = didRegisterPasskey ? await _loginWithPasskey() diff --git a/lib/screen/settings/forget_exist/forget_exist_bloc.dart b/lib/screen/settings/forget_exist/forget_exist_bloc.dart index f22709836..00f47a3f5 100644 --- a/lib/screen/settings/forget_exist/forget_exist_bloc.dart +++ b/lib/screen/settings/forget_exist/forget_exist_bloc.dart @@ -24,6 +24,7 @@ import 'package:autonomy_flutter/service/configuration_service.dart'; import 'package:autonomy_flutter/service/hive_store_service.dart'; import 'package:autonomy_flutter/service/iap_service.dart'; import 'package:autonomy_flutter/service/metric_client_service.dart'; +import 'package:autonomy_flutter/service/passkey_service.dart'; import 'package:autonomy_flutter/shared.dart'; import 'package:autonomy_flutter/util/log.dart'; import 'package:autonomy_flutter/util/notification_util.dart'; @@ -68,6 +69,7 @@ class ForgetExistBloc extends AuBloc { await _cloudDatabase.removeAll(); await _appDatabase.removeAll(); await _nftCollectionDatabase.removeAll(); + await injector().setDidRegisterPasskey(false); await _configurationService.removeAll(); await injector().emptyCache(); await DefaultCacheManager().emptyCache(); @@ -78,7 +80,7 @@ class ForgetExistBloc extends AuBloc { await injector().clear(); injector().clear(); injector().clearReceipt(); - injector().reset(); + unawaited(injector().reset()); await FileLogger.clear(); await SentryBreadcrumbLogger.clear(); diff --git a/lib/service/passkey_service.dart b/lib/service/passkey_service.dart index 4162faab1..05718bf43 100644 --- a/lib/service/passkey_service.dart +++ b/lib/service/passkey_service.dart @@ -1,8 +1,11 @@ +import 'dart:io'; + import 'package:autonomy_flutter/gateway/user_api.dart'; import 'package:autonomy_flutter/service/address_service.dart'; import 'package:autonomy_flutter/service/auth_service.dart'; +import 'package:autonomy_flutter/service/configuration_service.dart'; import 'package:autonomy_flutter/util/passkey_utils.dart'; -import 'package:autonomy_flutter/util/user_account_channel.dart'; +import 'package:flutter/services.dart'; import 'package:passkeys/authenticator.dart'; import 'package:passkeys/types.dart'; @@ -17,6 +20,10 @@ abstract class PasskeyService { Future registerFinalize(); + Future didRegisterPasskey(); + + Future setDidRegisterPasskey(bool value); + static String authenticationType = 'public-key'; } @@ -27,16 +34,22 @@ class PasskeyServiceImpl implements PasskeyService { String? _passkeyUserId; final UserApi _userApi; - final UserAccountChannel _userAccountChannel; + final ConfigurationService _configurationService; final AddressService _addressService; final AuthService _authService; + late final MethodChannel _iosMigrationChannel; + PasskeyServiceImpl( this._userApi, - this._userAccountChannel, + this._configurationService, this._addressService, this._authService, - ); + ) { + if (Platform.isIOS) { + _iosMigrationChannel = const MethodChannel('migration_util'); + } + } /* static final AuthenticatorSelectionType _defaultAuthenticatorSelection = @@ -65,7 +78,9 @@ class PasskeyServiceImpl implements PasskeyService { } Future _logInSeverInitiate() async { - final userId = await _userAccountChannel.getUserId(); + // userId is the address that sign the message when register, + // which is the primary address + final userId = await _addressService.getPrimaryAddress(); if (userId == null) { throw Exception('User ID is not set'); } @@ -128,10 +143,34 @@ class PasskeyServiceImpl implements PasskeyService { 'credentialCreationResponse': _registerResponse!.toCredentialCreationResponseJson(), }); - await _userAccountChannel.setDidRegisterPasskey(true); - await _userAccountChannel.setUserId(addressAuthentication['requester']); + await setDidRegisterPasskey(true); _authService.setAuthToken(response); } + + @override + Future didRegisterPasskey() async { + if (Platform.isAndroid) { + return _configurationService.didRegisterPasskey(); + } + final didRegister = + await _iosMigrationChannel.invokeMethod('didRegisterPasskey', {}); + return didRegister; + } + + @override + Future setDidRegisterPasskey(bool value) async { + if (Platform.isAndroid) { + // for Android device, passkey is stored in Google Password Manager, + // so it is not synced + await _configurationService.setDidRegisterPasskey(value); + return true; + } + final didRegister = + await _iosMigrationChannel.invokeMethod('setDidRegisterPasskey', { + 'data': value, + }); + return didRegister; + } } extension RegisterResponseTypeExt on RegisterResponseType { diff --git a/lib/util/user_account_channel.dart b/lib/util/user_account_channel.dart index 3ae8544db..ff53c36f0 100644 --- a/lib/util/user_account_channel.dart +++ b/lib/util/user_account_channel.dart @@ -2,12 +2,9 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; -import 'package:autonomy_flutter/common/injector.dart'; -import 'package:autonomy_flutter/service/configuration_service.dart'; import 'package:autonomy_flutter/util/log.dart'; import 'package:autonomy_flutter/util/wallet_utils.dart'; import 'package:flutter/services.dart'; -import 'package:sentry/sentry.dart'; class UserAccountChannel { final MethodChannel _channel; @@ -17,7 +14,6 @@ class UserAccountChannel { ? const MethodChannel('migration_util') : const MethodChannel('backup'); - String? _userId; AddressInfo? _primaryAddress; Future setPrimaryAddress(AddressInfo info) async { @@ -55,73 +51,6 @@ class UserAccountChannel { return false; } } - - Future setUserId(String userId) async { - try { - await _channel.invokeMethod('setUserId', {'data': userId}); - _userId = userId; - return true; - } catch (e) { - log.info('setUserId error', e); - unawaited(Sentry.captureException( - e, - hint: Hint.withMap({ - 'method': 'setUserId', - 'userId': userId, - }), - )); - return false; - } - } - - Future getUserId() async { - if (_userId != null) { - return _userId!; - } - try { - final userId = await _channel.invokeMethod('getUserId', {}); - _userId = userId; - return _userId!; - } catch (e) { - log.info('getUserId error', e); - return null; - } - } - - Future clearUserId() async { - try { - await _channel.invokeMethod('clearUserId', {}); - _userId = null; - } catch (e) { - log.info('clearUserId error', e); - } - } - - Future didCreateUser() async { - final userId = await getUserId(); - return userId != null; - } - - Future didRegisterPasskey() async { - if (Platform.isAndroid) { - return injector().didRegisterPasskey(); - } - final didRegister = await _channel.invokeMethod('didRegisterPasskey', {}); - return didRegister; - } - - Future setDidRegisterPasskey(bool value) async { - if (Platform.isAndroid) { - // for Android device, passkey is stored in Google Password Manager, - // so it is not synced - await injector().setDidRegisterPasskey(value); - return true; - } - final didRegister = await _channel.invokeMethod('setDidRegisterPasskey', { - 'data': value, - }); - return didRegister; - } } class AddressInfo { From a0862fb6042e287a7fc210b2af476957c5064ef0 Mon Sep 17 00:00:00 2001 From: phuoc Date: Mon, 28 Oct 2024 22:09:14 +0700 Subject: [PATCH 33/57] handle login error Signed-off-by: phuoc --- analysis_options.yaml | 2 +- assets | 2 +- lib/util/account_error_handler.dart | 43 +++++++++++++++++++++ lib/view/passkey/passkey_login_view.dart | 7 +++- lib/view/passkey/passkey_register_view.dart | 7 +++- 5 files changed, 57 insertions(+), 4 deletions(-) create mode 100644 lib/util/account_error_handler.dart diff --git a/analysis_options.yaml b/analysis_options.yaml index 22be3a062..b1bd4ac49 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -96,7 +96,7 @@ linter: sort_unnamed_constructors_first: true type_annotate_public_apis: true type_init_formals: true - type_literal_in_constant_pattern: true + type_literal_in_constant_pattern: false unawaited_futures: true unnecessary_brace_in_string_interps: true unnecessary_breaks: true diff --git a/assets b/assets index 6816c10e3..4ce2a151a 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 6816c10e32f4c0c1e44e987b30daf167e2045098 +Subproject commit 4ce2a151a6c61000374cb1a413ad5c8867603aef diff --git a/lib/util/account_error_handler.dart b/lib/util/account_error_handler.dart new file mode 100644 index 000000000..c79692f3e --- /dev/null +++ b/lib/util/account_error_handler.dart @@ -0,0 +1,43 @@ +import 'package:autonomy_flutter/util/error_handler.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:passkeys/exceptions.dart'; + +class SignInException { + final Exception exception; + final SignType type; + + SignInException(this.exception, this.type); +} + +enum SignType { + register, + login, +} + +class AccountErrorHandler { + static Future showDialog(BuildContext context, + SignInException signException, StackTrace stackTrace) async { + String? title; + String? message; + switch (signException.type) { + case SignType.register: + title = 'register_error_title'.tr(); + message = 'register_error_message'.tr(); + case SignType.login: + title = 'login_error_title'.tr(); + message = 'login_error_message'.tr(); + } + switch (signException.exception.runtimeType) { + case PasskeyAuthCancelledException: + message = 'auth_cancelled_message'.tr(); + case MissingGoogleSignInException: + case SyncAccountNotAvailableException: + case ExcludeCredentialsCanNotBeRegisteredException: + message = 'sign_google_account_message'.tr(); + case NoCredentialsAvailableException: + message = 'no_credentials_available_message'.tr(); + } + return await showErrorDialog(context, title, message, 'close'.tr()); + } +} diff --git a/lib/view/passkey/passkey_login_view.dart b/lib/view/passkey/passkey_login_view.dart index d67e3cbd1..df642b788 100644 --- a/lib/view/passkey/passkey_login_view.dart +++ b/lib/view/passkey/passkey_login_view.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:autonomy_flutter/common/injector.dart'; import 'package:autonomy_flutter/service/account_service.dart'; import 'package:autonomy_flutter/service/passkey_service.dart'; +import 'package:autonomy_flutter/util/account_error_handler.dart'; import 'package:autonomy_flutter/util/log.dart'; import 'package:autonomy_flutter/view/passkey/having_trouble_view.dart'; import 'package:autonomy_flutter/view/primary_button.dart'; @@ -83,9 +84,13 @@ class _PasskeyLoginViewState extends State { if (context.mounted) { Navigator.of(context).pop(true); } - } catch (e, stackTrace) { + } on Exception catch (e, stackTrace) { log.info('Failed to login with passkey: $e'); unawaited(Sentry.captureException(e, stackTrace: stackTrace)); + if (context.mounted) { + await AccountErrorHandler.showDialog( + context, SignInException(e, SignType.login), stackTrace); + } setState(() { _isError = true; }); diff --git a/lib/view/passkey/passkey_register_view.dart b/lib/view/passkey/passkey_register_view.dart index b783730af..043793a44 100644 --- a/lib/view/passkey/passkey_register_view.dart +++ b/lib/view/passkey/passkey_register_view.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:autonomy_flutter/common/injector.dart'; import 'package:autonomy_flutter/service/account_service.dart'; import 'package:autonomy_flutter/service/passkey_service.dart'; +import 'package:autonomy_flutter/util/account_error_handler.dart'; import 'package:autonomy_flutter/util/log.dart'; import 'package:autonomy_flutter/view/passkey/having_trouble_view.dart'; import 'package:autonomy_flutter/view/primary_button.dart'; @@ -120,9 +121,13 @@ class _PasskeyRegisterViewState extends State { setState(() { _didSuccess = true; }); - } catch (e, stackTrace) { + } on Exception catch (e, stackTrace) { log.info('Failed to register passkey: $e'); unawaited(Sentry.captureException(e, stackTrace: stackTrace)); + if (context.mounted) { + await AccountErrorHandler.showDialog( + context, SignInException(e, SignType.register), stackTrace); + } setState(() { _isError = true; }); From 5fa0ada55ca6605d63a18dc01ed16092002bd9d5 Mon Sep 17 00:00:00 2001 From: phuoc Date: Mon, 28 Oct 2024 22:24:13 +0700 Subject: [PATCH 34/57] rename param, organize import Signed-off-by: phuoc --- lib/gateway/user_api.dart | 2 +- lib/service/passkey_service.dart | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/gateway/user_api.dart b/lib/gateway/user_api.dart index 5a8b6f367..80ae9a4c6 100644 --- a/lib/gateway/user_api.dart +++ b/lib/gateway/user_api.dart @@ -5,8 +5,8 @@ // that can be found in the LICENSE file. // -import 'package:autonomy_flutter/model/passkey/credential_request_option.dart'; import 'package:autonomy_flutter/model/jwt.dart'; +import 'package:autonomy_flutter/model/passkey/credential_request_option.dart'; import 'package:autonomy_flutter/model/passkey/passkey_creation_option.dart'; import 'package:dio/dio.dart'; import 'package:retrofit/retrofit.dart'; diff --git a/lib/service/passkey_service.dart b/lib/service/passkey_service.dart index 05718bf43..13b5d3f5b 100644 --- a/lib/service/passkey_service.dart +++ b/lib/service/passkey_service.dart @@ -14,7 +14,7 @@ abstract class PasskeyService { Future logInInitiate(); - Future logInFinalize(AuthenticateResponseType loginResponse); + Future logInFinalize(AuthenticateResponseType authenticateResponse); Future registerInitiate(); @@ -102,9 +102,9 @@ class PasskeyServiceImpl implements PasskeyService { @override Future logInFinalize( - AuthenticateResponseType loginLocalResponse) async { + AuthenticateResponseType authenticateResponse) async { final response = - await _userApi.logInFinalize(loginLocalResponse.toFFJson()); + await _userApi.logInFinalize(authenticateResponse.toFFJson()); _authService.setAuthToken(response); } From 432df838d27ed99ca01e92c9a836fb5de4adb83a Mon Sep 17 00:00:00 2001 From: phuoc Date: Tue, 29 Oct 2024 09:31:43 +0700 Subject: [PATCH 35/57] show authentication failed and option retry Signed-off-by: phuoc --- assets | 2 +- lib/view/passkey/passkey_login_view.dart | 70 +++++++++--------- lib/view/passkey/passkey_register_view.dart | 82 ++++++++++++--------- 3 files changed, 80 insertions(+), 74 deletions(-) diff --git a/assets b/assets index 4ce2a151a..3cb094f23 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 4ce2a151a6c61000374cb1a413ad5c8867603aef +Subproject commit 3cb094f234b0b3434155d17088d345082e394bb9 diff --git a/lib/view/passkey/passkey_login_view.dart b/lib/view/passkey/passkey_login_view.dart index df642b788..1f8841e16 100644 --- a/lib/view/passkey/passkey_login_view.dart +++ b/lib/view/passkey/passkey_login_view.dart @@ -3,7 +3,6 @@ import 'dart:async'; import 'package:autonomy_flutter/common/injector.dart'; import 'package:autonomy_flutter/service/account_service.dart'; import 'package:autonomy_flutter/service/passkey_service.dart'; -import 'package:autonomy_flutter/util/account_error_handler.dart'; import 'package:autonomy_flutter/util/log.dart'; import 'package:autonomy_flutter/view/passkey/having_trouble_view.dart'; import 'package:autonomy_flutter/view/primary_button.dart'; @@ -48,7 +47,7 @@ class _PasskeyLoginViewState extends State { ); Widget _getTitle(BuildContext context) => Text( - 'login_title'.tr(), + _isError ? 'authentication_failed'.tr() : 'login_title'.tr(), style: Theme.of(context).textTheme.ppMori700Black16, ); @@ -56,7 +55,7 @@ class _PasskeyLoginViewState extends State { final theme = Theme.of(context); final style = theme.textTheme.ppMori400Black14; return Text( - 'login_desc'.tr(), + _isError ? 'passkey_error_desc'.tr() : 'login_desc'.tr(), style: style, ); } @@ -67,42 +66,39 @@ class _PasskeyLoginViewState extends State { Widget _getAction(BuildContext context) => PrimaryAsyncButton( key: const Key('login_button'), - enabled: !_isError, - onTap: () async { - if (_isLogging) { - return; - } - setState(() { - _isLogging = true; - _isError = false; - }); - try { - final localResponse = await _passkeyService.logInInitiate(); - await _accountService.migrateAccount(() async { - await _passkeyService.logInFinalize(localResponse); - }); - if (context.mounted) { - Navigator.of(context).pop(true); - } - } on Exception catch (e, stackTrace) { - log.info('Failed to login with passkey: $e'); - unawaited(Sentry.captureException(e, stackTrace: stackTrace)); - if (context.mounted) { - await AccountErrorHandler.showDialog( - context, SignInException(e, SignType.login), stackTrace); - } - setState(() { - _isError = true; - }); - } finally { - setState(() { - _isLogging = false; - }); - } - }, - text: 'login_button'.tr(), + onTap: _login, + text: _isError ? 'try_again'.tr() : 'login_button'.tr(), ); + Future _login() async { + if (_isLogging) { + return; + } + setState(() { + _isLogging = true; + _isError = false; + }); + try { + final localResponse = await _passkeyService.logInInitiate(); + await _accountService.migrateAccount(() async { + await _passkeyService.logInFinalize(localResponse); + }); + if (mounted) { + Navigator.of(context).pop(true); + } + } on Exception catch (e, stackTrace) { + log.info('Failed to login with passkey: $e'); + unawaited(Sentry.captureException(e, stackTrace: stackTrace)); + setState(() { + _isError = true; + }); + } finally { + setState(() { + _isLogging = false; + }); + } + } + Widget _havingTrouble(BuildContext context) { if (!_isError && !_isLogging) { return const SizedBox(); diff --git a/lib/view/passkey/passkey_register_view.dart b/lib/view/passkey/passkey_register_view.dart index 043793a44..e5afa9840 100644 --- a/lib/view/passkey/passkey_register_view.dart +++ b/lib/view/passkey/passkey_register_view.dart @@ -3,7 +3,6 @@ import 'dart:async'; import 'package:autonomy_flutter/common/injector.dart'; import 'package:autonomy_flutter/service/account_service.dart'; import 'package:autonomy_flutter/service/passkey_service.dart'; -import 'package:autonomy_flutter/util/account_error_handler.dart'; import 'package:autonomy_flutter/util/log.dart'; import 'package:autonomy_flutter/view/passkey/having_trouble_view.dart'; import 'package:autonomy_flutter/view/primary_button.dart'; @@ -49,7 +48,11 @@ class _PasskeyRegisterViewState extends State { ); Widget _getTitle(BuildContext context) => Text( - _didSuccess ? 'passkey_created'.tr() : 'introducing_passkey'.tr(), + _didSuccess + ? 'passkey_created'.tr() + : _isError + ? 'authentication_failed'.tr() + : 'introducing_passkey'.tr(), style: Theme.of(context).textTheme.ppMori700Black16, ); @@ -66,6 +69,16 @@ class _PasskeyRegisterViewState extends State { ], ); } + if (_isError) { + return Column( + children: [ + Text( + 'passkey_error_desc'.tr(), + style: style, + ), + ], + ); + } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -104,44 +117,41 @@ class _PasskeyRegisterViewState extends State { } return PrimaryAsyncButton( key: const Key('register_button'), - enabled: !_isError, - onTap: () async { - if (_registering) { - return; - } - setState(() { - _registering = true; - _isError = false; - }); - try { - await _passkeyService.registerInitiate(); - await _accountService.migrateAccount(() async { - await _passkeyService.registerFinalize(); - }); - setState(() { - _didSuccess = true; - }); - } on Exception catch (e, stackTrace) { - log.info('Failed to register passkey: $e'); - unawaited(Sentry.captureException(e, stackTrace: stackTrace)); - if (context.mounted) { - await AccountErrorHandler.showDialog( - context, SignInException(e, SignType.register), stackTrace); - } - setState(() { - _isError = true; - }); - } finally { - setState(() { - _registering = false; - }); - } - }, + onTap: _register, text: 'get_started'.tr(), - processingText: 'creating_passkey'.tr(), + processingText: _isError ? 'try_again'.tr() : 'creating_passkey'.tr(), ); } + Future _register() async { + if (_registering) { + return; + } + setState(() { + _registering = true; + _isError = false; + }); + try { + await _passkeyService.registerInitiate(); + await _accountService.migrateAccount(() async { + await _passkeyService.registerFinalize(); + }); + setState(() { + _didSuccess = true; + }); + } on Exception catch (e, stackTrace) { + log.info('Failed to register passkey: $e'); + unawaited(Sentry.captureException(e, stackTrace: stackTrace)); + setState(() { + _isError = true; + }); + } finally { + setState(() { + _registering = false; + }); + } + } + Widget _havingTrouble(BuildContext context) { if (_didSuccess || (!_isError && !_registering)) { return const SizedBox(); From 732a6560bad15340cf6bc316541154c4e82feb85 Mon Sep 17 00:00:00 2001 From: phuoc Date: Tue, 29 Oct 2024 09:32:27 +0700 Subject: [PATCH 36/57] delete authentication failed pop up Signed-off-by: phuoc --- lib/util/account_error_handler.dart | 43 ----------------------------- 1 file changed, 43 deletions(-) delete mode 100644 lib/util/account_error_handler.dart diff --git a/lib/util/account_error_handler.dart b/lib/util/account_error_handler.dart deleted file mode 100644 index c79692f3e..000000000 --- a/lib/util/account_error_handler.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'package:autonomy_flutter/util/error_handler.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:passkeys/exceptions.dart'; - -class SignInException { - final Exception exception; - final SignType type; - - SignInException(this.exception, this.type); -} - -enum SignType { - register, - login, -} - -class AccountErrorHandler { - static Future showDialog(BuildContext context, - SignInException signException, StackTrace stackTrace) async { - String? title; - String? message; - switch (signException.type) { - case SignType.register: - title = 'register_error_title'.tr(); - message = 'register_error_message'.tr(); - case SignType.login: - title = 'login_error_title'.tr(); - message = 'login_error_message'.tr(); - } - switch (signException.exception.runtimeType) { - case PasskeyAuthCancelledException: - message = 'auth_cancelled_message'.tr(); - case MissingGoogleSignInException: - case SyncAccountNotAvailableException: - case ExcludeCredentialsCanNotBeRegisteredException: - message = 'sign_google_account_message'.tr(); - case NoCredentialsAvailableException: - message = 'no_credentials_available_message'.tr(); - } - return await showErrorDialog(context, title, message, 'close'.tr()); - } -} From ba46e858531f4e9d1f0026dba4464363d0d90c05 Mon Sep 17 00:00:00 2001 From: phuoc Date: Tue, 29 Oct 2024 10:32:51 +0700 Subject: [PATCH 37/57] add more param to request/authenticate Signed-off-by: phuoc --- lib/service/passkey_service.dart | 5 +++++ lib/view/passkey/passkey_login_view.dart | 2 +- lib/view/passkey/passkey_register_view.dart | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/service/passkey_service.dart b/lib/service/passkey_service.dart index 13b5d3f5b..84960e9dd 100644 --- a/lib/service/passkey_service.dart +++ b/lib/service/passkey_service.dart @@ -97,6 +97,8 @@ class PasskeyServiceImpl implements PasskeyService { mediation: response.mediation ?? _defaultMediation, preferImmediatelyAvailableCredentials: _preferImmediatelyAvailableCredentials, + timeout: pubKey.timeout, + userVerification: pubKey.userVerification, ); } @@ -127,6 +129,9 @@ class PasskeyServiceImpl implements PasskeyService { user: pubKey.user, authSelectionType: pubKey.authenticatorSelection!, excludeCredentials: pubKey.excludeCredentials ?? [], + attestation: pubKey.attestation, + timeout: pubKey.timeout, + pubKeyCredParams: pubKey.pubKeyCredParams, ); } diff --git a/lib/view/passkey/passkey_login_view.dart b/lib/view/passkey/passkey_login_view.dart index 1f8841e16..fd31568d2 100644 --- a/lib/view/passkey/passkey_login_view.dart +++ b/lib/view/passkey/passkey_login_view.dart @@ -86,7 +86,7 @@ class _PasskeyLoginViewState extends State { if (mounted) { Navigator.of(context).pop(true); } - } on Exception catch (e, stackTrace) { + } catch (e, stackTrace) { log.info('Failed to login with passkey: $e'); unawaited(Sentry.captureException(e, stackTrace: stackTrace)); setState(() { diff --git a/lib/view/passkey/passkey_register_view.dart b/lib/view/passkey/passkey_register_view.dart index e5afa9840..e25ff1793 100644 --- a/lib/view/passkey/passkey_register_view.dart +++ b/lib/view/passkey/passkey_register_view.dart @@ -139,7 +139,7 @@ class _PasskeyRegisterViewState extends State { setState(() { _didSuccess = true; }); - } on Exception catch (e, stackTrace) { + } catch (e, stackTrace) { log.info('Failed to register passkey: $e'); unawaited(Sentry.captureException(e, stackTrace: stackTrace)); setState(() { From 9158c84921c2d4be31013baaac6690e35b0efb8d Mon Sep 17 00:00:00 2001 From: phuoc Date: Tue, 29 Oct 2024 10:47:36 +0700 Subject: [PATCH 38/57] check os version to support passkey Signed-off-by: phuoc --- lib/service/passkey_service.dart | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/lib/service/passkey_service.dart b/lib/service/passkey_service.dart index 84960e9dd..635ef3b90 100644 --- a/lib/service/passkey_service.dart +++ b/lib/service/passkey_service.dart @@ -5,6 +5,7 @@ import 'package:autonomy_flutter/service/address_service.dart'; import 'package:autonomy_flutter/service/auth_service.dart'; import 'package:autonomy_flutter/service/configuration_service.dart'; import 'package:autonomy_flutter/util/passkey_utils.dart'; +import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/services.dart'; import 'package:passkeys/authenticator.dart'; import 'package:passkeys/types.dart'; @@ -69,7 +70,21 @@ class PasskeyServiceImpl implements PasskeyService { @override Future isPassKeyAvailable() async => - await _passkeyAuthenticator.canAuthenticate(); + await _passkeyAuthenticator.canAuthenticate() && await _doesOSSupport(); + + Future _doesOSSupport() async { + final deviceInfo = DeviceInfoPlugin(); + if (Platform.isAndroid) { + final androidInfo = await deviceInfo.androidInfo; + // Android 9 (API 28) and above + return androidInfo.version.sdkInt >= 28; + } else { + final iosInfo = await deviceInfo.iosInfo; + // iOS 16 and above + final osVersion = iosInfo.systemVersion.split('.').first; + return (int.tryParse(osVersion) ?? 0) >= 16; + } + } @override Future logInInitiate() async { From 6e23337ff4f0d6926bdc3dfcd1a1cee43483c7de Mon Sep 17 00:00:00 2001 From: phuoc Date: Tue, 29 Oct 2024 10:55:49 +0700 Subject: [PATCH 39/57] fix comment Signed-off-by: phuoc --- lib/service/configuration_service.dart | 8 ++++---- lib/service/passkey_service.dart | 4 ++-- .../service/mock_account_service.mocks.dart | 3 +++ .../service/mock_configuration_service.mocks.dart | 9 +++++---- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/lib/service/configuration_service.dart b/lib/service/configuration_service.dart index 5d2f5cf2c..1425c3e09 100644 --- a/lib/service/configuration_service.dart +++ b/lib/service/configuration_service.dart @@ -24,9 +24,9 @@ import 'package:uuid/uuid.dart'; //ignore_for_file: constant_identifier_names abstract class ConfigurationService { - bool didRegisterPasskey(); + bool didRegisterPasskeyAndroid(); - Future setDidRegisterPasskey(bool value); + Future setDidRegisterPasskeyAndroid(bool value); bool didMigrateToAccountSetting(); @@ -936,11 +936,11 @@ class ConfigurationServiceImpl implements ConfigurationService { } @override - bool didRegisterPasskey() => + bool didRegisterPasskeyAndroid() => _preferences.getBool(keyDidRegisterPasskey) ?? false; @override - Future setDidRegisterPasskey(bool value) async { + Future setDidRegisterPasskeyAndroid(bool value) async { await _preferences.setBool(keyDidRegisterPasskey, value); } } diff --git a/lib/service/passkey_service.dart b/lib/service/passkey_service.dart index 635ef3b90..9bbef375c 100644 --- a/lib/service/passkey_service.dart +++ b/lib/service/passkey_service.dart @@ -170,7 +170,7 @@ class PasskeyServiceImpl implements PasskeyService { @override Future didRegisterPasskey() async { if (Platform.isAndroid) { - return _configurationService.didRegisterPasskey(); + return _configurationService.didRegisterPasskeyAndroid(); } final didRegister = await _iosMigrationChannel.invokeMethod('didRegisterPasskey', {}); @@ -182,7 +182,7 @@ class PasskeyServiceImpl implements PasskeyService { if (Platform.isAndroid) { // for Android device, passkey is stored in Google Password Manager, // so it is not synced - await _configurationService.setDidRegisterPasskey(value); + await _configurationService.setDidRegisterPasskeyAndroid(value); return true; } final didRegister = diff --git a/test/generate_mock/service/mock_account_service.mocks.dart b/test/generate_mock/service/mock_account_service.mocks.dart index a3b04615a..c27755ee3 100644 --- a/test/generate_mock/service/mock_account_service.mocks.dart +++ b/test/generate_mock/service/mock_account_service.mocks.dart @@ -167,6 +167,7 @@ class MockAccountService extends _i1.Mock implements _i5.AccountService { _i6.Future<_i2.WalletStorage> createNewWallet({ String? name = r'', String? passphrase = r'', + _i6.Future Function()? createLoginJwt, }) => (super.noSuchMethod( Invocation.method( @@ -175,6 +176,7 @@ class MockAccountService extends _i1.Mock implements _i5.AccountService { { #name: name, #passphrase: passphrase, + #createLoginJwt: createLoginJwt, }, ), returnValue: _i6.Future<_i2.WalletStorage>.value(_FakeWalletStorage_0( @@ -185,6 +187,7 @@ class MockAccountService extends _i1.Mock implements _i5.AccountService { { #name: name, #passphrase: passphrase, + #createLoginJwt: createLoginJwt, }, ), )), diff --git a/test/generate_mock/service/mock_configuration_service.mocks.dart b/test/generate_mock/service/mock_configuration_service.mocks.dart index 5b8a4b437..4572dcb6c 100644 --- a/test/generate_mock/service/mock_configuration_service.mocks.dart +++ b/test/generate_mock/service/mock_configuration_service.mocks.dart @@ -72,17 +72,18 @@ class MockConfigurationService extends _i1.Mock ), ) as _i2.ValueNotifier); @override - bool didRegisterPasskey() => (super.noSuchMethod( + bool didRegisterPasskeyAndroid() => (super.noSuchMethod( Invocation.method( - #didRegisterPasskey, + #didRegisterPasskeyAndroid, [], ), returnValue: false, ) as bool); @override - _i5.Future setDidRegisterPasskey(bool? value) => (super.noSuchMethod( + _i5.Future setDidRegisterPasskeyAndroid(bool? value) => + (super.noSuchMethod( Invocation.method( - #setDidRegisterPasskey, + #setDidRegisterPasskeyAndroid, [value], ), returnValue: _i5.Future.value(), From 5c8358d530c715f8f0e461c071c76dc758e3884f Mon Sep 17 00:00:00 2001 From: phuoc Date: Tue, 29 Oct 2024 14:14:32 +0700 Subject: [PATCH 40/57] stop loading animation when open login dialog Signed-off-by: phuoc --- lib/screen/onboarding_page.dart | 14 ++++++++++++-- lib/util/ui_helper.dart | 23 +++++++++++++---------- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/lib/screen/onboarding_page.dart b/lib/screen/onboarding_page.dart index 00c978dfe..de71ff34b 100644 --- a/lib/screen/onboarding_page.dart +++ b/lib/screen/onboarding_page.dart @@ -52,6 +52,7 @@ class _OnboardingPageState extends State final metricClient = injector.get(); final deepLinkService = injector.get(); Timer? _timer; + bool _loadingAnimation = true; final _passkeyService = injector.get(); final _authService = injector.get(); @@ -174,10 +175,19 @@ class _OnboardingPageState extends State } else { log.info('Passkey is supported. Authenticate with passkey'); final didRegisterPasskey = await _passkeyService.didRegisterPasskey(); - + if (mounted) { + setState(() { + _loadingAnimation = false; + }); + } final didLoginSuccess = didRegisterPasskey ? await _loginWithPasskey() : await _registerPasskey(); + if (mounted) { + setState(() { + _loadingAnimation = true; + }); + } if (didLoginSuccess != true) { throw Exception('Failed to login with passkey'); } @@ -206,7 +216,7 @@ class _OnboardingPageState extends State const Spacer(), PrimaryButton( text: 'h_loading...'.tr(), - isProcessing: true, + isProcessing: _loadingAnimation, enabled: false, disabledColor: AppColor.auGreyBackground, textColor: AppColor.white, diff --git a/lib/util/ui_helper.dart b/lib/util/ui_helper.dart index ced2a37a3..48cd2c0b1 100644 --- a/lib/util/ui_helper.dart +++ b/lib/util/ui_helper.dart @@ -1054,21 +1054,24 @@ class UIHelper { content: const PasskeyLoginView(), ); - static Future showRawCenterSheet(BuildContext context, - {required Widget content, - double horizontalPadding = 20, - Color backgroundColor = AppColor.white}) async { + static Future showRawCenterSheet( + BuildContext context, { + required Widget content, + double horizontalPadding = 20, + Color boxColor = AppColor.white, + Color backgroundColor = Colors.transparent, + }) async { UIHelper.hideInfoDialog(context); return await showCupertinoModalPopup( context: context, - builder: (context) => Padding( - padding: EdgeInsets.symmetric(horizontal: horizontalPadding), - child: Scaffold( - backgroundColor: Colors.transparent, - body: Center( + builder: (context) => Scaffold( + backgroundColor: backgroundColor, + body: Padding( + padding: EdgeInsets.symmetric(horizontal: horizontalPadding), + child: Center( child: Container( decoration: BoxDecoration( - color: backgroundColor, + color: boxColor, borderRadius: BorderRadius.circular(5), ), padding: const EdgeInsets.symmetric( From 5c61bd50da6137fd10fef79f438ab978c6035a43 Mon Sep 17 00:00:00 2001 From: phuoc Date: Tue, 29 Oct 2024 15:07:57 +0700 Subject: [PATCH 41/57] store didRegisterPasskey android on blockStore Signed-off-by: phuoc --- .../autonomy_flutter/BackupDartPlugin.kt | 54 +++++++++++++++++++ lib/screen/onboarding_page.dart | 4 +- .../forget_exist/forget_exist_bloc.dart | 3 +- lib/service/configuration_service.dart | 14 ----- lib/service/passkey_service.dart | 46 ++-------------- lib/util/user_account_channel.dart | 12 +++++ 6 files changed, 76 insertions(+), 57 deletions(-) diff --git a/android/app/src/main/kotlin/com/bitmark/autonomy_flutter/BackupDartPlugin.kt b/android/app/src/main/kotlin/com/bitmark/autonomy_flutter/BackupDartPlugin.kt index a8a4cb059..3bcba670e 100644 --- a/android/app/src/main/kotlin/com/bitmark/autonomy_flutter/BackupDartPlugin.kt +++ b/android/app/src/main/kotlin/com/bitmark/autonomy_flutter/BackupDartPlugin.kt @@ -33,6 +33,7 @@ class BackupDartPlugin : MethodChannel.MethodCallHandler { private lateinit var disposables: CompositeDisposable private lateinit var client: BlockstoreClient private val primaryAddressStoreKey = "primary_address" + private val didRegisterPasskeys = "did_register_passkeys" fun createChannels(@NonNull flutterEngine: FlutterEngine, @NonNull context: Context) { this.context = context @@ -51,6 +52,8 @@ class BackupDartPlugin : MethodChannel.MethodCallHandler { "getPrimaryAddress" -> getPrimaryAddress(call, result) "clearPrimaryAddress" -> clearPrimaryAddress(call, result) "deleteKeys" -> deleteKeys(call, result) + "setDidRegisterPasskey" -> setDidRegisterPasskey(call, result) + "didRegisterPasskey" -> didRegisterPasskey(call, result) else -> { result.notImplemented() } @@ -246,6 +249,57 @@ class BackupDartPlugin : MethodChannel.MethodCallHandler { } } + private fun setDidRegisterPasskey(call: MethodCall, result: MethodChannel.Result) { + val data: Boolean = call.argument("data") ?: false + + val storeBytesBuilder = StoreBytesData.Builder() + .setKey(didRegisterPasskeys) + .setBytes(data.toString().toByteArray(Charsets.UTF_8)) + + client.storeBytes(storeBytesBuilder.build()) + .addOnSuccessListener { + + Log.e("setDidRegisterPasskey", data.toString()); + result.success(true) + } + .addOnFailureListener { e -> + Log.e("setDidRegisterPasskey", e.message ?: "") + result.success(false) + } + } + + private fun didRegisterPasskey(call: MethodCall, result: MethodChannel.Result) { + val request = RetrieveBytesRequest.Builder() + .setKeys(listOf(didRegisterPasskeys)) // Specify the key + .build() + client.retrieveBytes(request) + .addOnSuccessListener { + try { // Retrieve bytes using the key + val dataMap = it.blockstoreDataMap[didRegisterPasskeys] + if (dataMap != null) { + val bytes = dataMap.bytes + val resultString = bytes.toString(Charsets.UTF_8) + Log.d("didRegisterPasskey", resultString) + + + result.success(resultString.toBoolean()) + } else { + Log.e("didRegisterPasskey", "No data found for the key") + result.success(false) + } + } catch (e: Exception) { + Log.e("didRegisterPasskey", e.message ?: "Error decoding data") + //No primary address found + result.success(false) + } + } + .addOnFailureListener { + //Block store not available + result.error("didRegisterPasskey Block store error", it.message, it) + } + } + + private fun clearPrimaryAddress(call: MethodCall, result: MethodChannel.Result) { val retrieveRequest = DeleteBytesRequest.Builder() .setKeys(listOf(primaryAddressStoreKey)) diff --git a/lib/screen/onboarding_page.dart b/lib/screen/onboarding_page.dart index de71ff34b..e0b212781 100644 --- a/lib/screen/onboarding_page.dart +++ b/lib/screen/onboarding_page.dart @@ -27,6 +27,7 @@ import 'package:autonomy_flutter/util/metric_helper.dart'; import 'package:autonomy_flutter/util/notification_util.dart'; import 'package:autonomy_flutter/util/style.dart'; import 'package:autonomy_flutter/util/ui_helper.dart'; +import 'package:autonomy_flutter/util/user_account_channel.dart'; import 'package:autonomy_flutter/view/back_appbar.dart'; import 'package:autonomy_flutter/view/primary_button.dart'; import 'package:autonomy_flutter/view/responsive.dart'; @@ -55,6 +56,7 @@ class _OnboardingPageState extends State bool _loadingAnimation = true; final _passkeyService = injector.get(); + final _userAccountChannel = injector.get(); final _authService = injector.get(); final _onboardingLogo = Semantics( @@ -174,7 +176,7 @@ class _OnboardingPageState extends State }); } else { log.info('Passkey is supported. Authenticate with passkey'); - final didRegisterPasskey = await _passkeyService.didRegisterPasskey(); + final didRegisterPasskey = await _userAccountChannel.didRegisterPasskey(); if (mounted) { setState(() { _loadingAnimation = false; diff --git a/lib/screen/settings/forget_exist/forget_exist_bloc.dart b/lib/screen/settings/forget_exist/forget_exist_bloc.dart index 00f47a3f5..da1d06902 100644 --- a/lib/screen/settings/forget_exist/forget_exist_bloc.dart +++ b/lib/screen/settings/forget_exist/forget_exist_bloc.dart @@ -28,6 +28,7 @@ import 'package:autonomy_flutter/service/passkey_service.dart'; import 'package:autonomy_flutter/shared.dart'; import 'package:autonomy_flutter/util/log.dart'; import 'package:autonomy_flutter/util/notification_util.dart'; +import 'package:autonomy_flutter/util/user_account_channel.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:nft_collection/database/nft_collection_database.dart'; @@ -69,7 +70,7 @@ class ForgetExistBloc extends AuBloc { await _cloudDatabase.removeAll(); await _appDatabase.removeAll(); await _nftCollectionDatabase.removeAll(); - await injector().setDidRegisterPasskey(false); + await injector().setDidRegisterPasskey(false); await _configurationService.removeAll(); await injector().emptyCache(); await DefaultCacheManager().emptyCache(); diff --git a/lib/service/configuration_service.dart b/lib/service/configuration_service.dart index 1425c3e09..25ce3ba78 100644 --- a/lib/service/configuration_service.dart +++ b/lib/service/configuration_service.dart @@ -24,10 +24,6 @@ import 'package:uuid/uuid.dart'; //ignore_for_file: constant_identifier_names abstract class ConfigurationService { - bool didRegisterPasskeyAndroid(); - - Future setDidRegisterPasskeyAndroid(bool value); - bool didMigrateToAccountSetting(); Future setMigrateToAccountSetting(bool value); @@ -210,7 +206,6 @@ abstract class ConfigurationService { } class ConfigurationServiceImpl implements ConfigurationService { - static const String keyDidRegisterPasskey = 'did_register_passkey'; static const String keyDidMigrateToAccountSetting = 'did_migrate_to_account_setting'; static const String keyDidShowLiveWithArt = 'did_show_live_with_art'; @@ -934,15 +929,6 @@ class ConfigurationServiceImpl implements ConfigurationService { await _preferences.setString( KEY_ANNOUNCEMENT_TO_ISSUE_MAP, jsonEncode(mapJson)); } - - @override - bool didRegisterPasskeyAndroid() => - _preferences.getBool(keyDidRegisterPasskey) ?? false; - - @override - Future setDidRegisterPasskeyAndroid(bool value) async { - await _preferences.setBool(keyDidRegisterPasskey, value); - } } enum ConflictAction { diff --git a/lib/service/passkey_service.dart b/lib/service/passkey_service.dart index 9bbef375c..b964230e3 100644 --- a/lib/service/passkey_service.dart +++ b/lib/service/passkey_service.dart @@ -3,10 +3,9 @@ import 'dart:io'; import 'package:autonomy_flutter/gateway/user_api.dart'; import 'package:autonomy_flutter/service/address_service.dart'; import 'package:autonomy_flutter/service/auth_service.dart'; -import 'package:autonomy_flutter/service/configuration_service.dart'; import 'package:autonomy_flutter/util/passkey_utils.dart'; +import 'package:autonomy_flutter/util/user_account_channel.dart'; import 'package:device_info_plus/device_info_plus.dart'; -import 'package:flutter/services.dart'; import 'package:passkeys/authenticator.dart'; import 'package:passkeys/types.dart'; @@ -21,10 +20,6 @@ abstract class PasskeyService { Future registerFinalize(); - Future didRegisterPasskey(); - - Future setDidRegisterPasskey(bool value); - static String authenticationType = 'public-key'; } @@ -35,22 +30,16 @@ class PasskeyServiceImpl implements PasskeyService { String? _passkeyUserId; final UserApi _userApi; - final ConfigurationService _configurationService; + final UserAccountChannel _userAccountChannel; final AddressService _addressService; final AuthService _authService; - late final MethodChannel _iosMigrationChannel; - PasskeyServiceImpl( this._userApi, - this._configurationService, + this._userAccountChannel, this._addressService, this._authService, - ) { - if (Platform.isIOS) { - _iosMigrationChannel = const MethodChannel('migration_util'); - } - } + ); /* static final AuthenticatorSelectionType _defaultAuthenticatorSelection = @@ -163,34 +152,9 @@ class PasskeyServiceImpl implements PasskeyService { 'credentialCreationResponse': _registerResponse!.toCredentialCreationResponseJson(), }); - await setDidRegisterPasskey(true); + await _userAccountChannel.setDidRegisterPasskey(true); _authService.setAuthToken(response); } - - @override - Future didRegisterPasskey() async { - if (Platform.isAndroid) { - return _configurationService.didRegisterPasskeyAndroid(); - } - final didRegister = - await _iosMigrationChannel.invokeMethod('didRegisterPasskey', {}); - return didRegister; - } - - @override - Future setDidRegisterPasskey(bool value) async { - if (Platform.isAndroid) { - // for Android device, passkey is stored in Google Password Manager, - // so it is not synced - await _configurationService.setDidRegisterPasskeyAndroid(value); - return true; - } - final didRegister = - await _iosMigrationChannel.invokeMethod('setDidRegisterPasskey', { - 'data': value, - }); - return didRegister; - } } extension RegisterResponseTypeExt on RegisterResponseType { diff --git a/lib/util/user_account_channel.dart b/lib/util/user_account_channel.dart index ff53c36f0..e23d86b23 100644 --- a/lib/util/user_account_channel.dart +++ b/lib/util/user_account_channel.dart @@ -51,6 +51,18 @@ class UserAccountChannel { return false; } } + + Future didRegisterPasskey() async { + final didRegister = await _channel.invokeMethod('didRegisterPasskey', {}); + return didRegister; + } + + Future setDidRegisterPasskey(bool value) async { + final didRegister = await _channel.invokeMethod('setDidRegisterPasskey', { + 'data': value, + }); + return didRegister; + } } class AddressInfo { From 078090c6a27cef8597a09cab09272e85868dc2d2 Mon Sep 17 00:00:00 2001 From: phuoc Date: Tue, 29 Oct 2024 15:36:30 +0700 Subject: [PATCH 42/57] migrate then login Signed-off-by: phuoc --- lib/screen/settings/forget_exist/forget_exist_bloc.dart | 1 - lib/view/passkey/passkey_login_view.dart | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/screen/settings/forget_exist/forget_exist_bloc.dart b/lib/screen/settings/forget_exist/forget_exist_bloc.dart index da1d06902..1de3ce895 100644 --- a/lib/screen/settings/forget_exist/forget_exist_bloc.dart +++ b/lib/screen/settings/forget_exist/forget_exist_bloc.dart @@ -24,7 +24,6 @@ import 'package:autonomy_flutter/service/configuration_service.dart'; import 'package:autonomy_flutter/service/hive_store_service.dart'; import 'package:autonomy_flutter/service/iap_service.dart'; import 'package:autonomy_flutter/service/metric_client_service.dart'; -import 'package:autonomy_flutter/service/passkey_service.dart'; import 'package:autonomy_flutter/shared.dart'; import 'package:autonomy_flutter/util/log.dart'; import 'package:autonomy_flutter/util/notification_util.dart'; diff --git a/lib/view/passkey/passkey_login_view.dart b/lib/view/passkey/passkey_login_view.dart index fd31568d2..82bb2ccb2 100644 --- a/lib/view/passkey/passkey_login_view.dart +++ b/lib/view/passkey/passkey_login_view.dart @@ -79,8 +79,8 @@ class _PasskeyLoginViewState extends State { _isError = false; }); try { - final localResponse = await _passkeyService.logInInitiate(); await _accountService.migrateAccount(() async { + final localResponse = await _passkeyService.logInInitiate(); await _passkeyService.logInFinalize(localResponse); }); if (mounted) { From 228be25ff33394e14db6a29e0d073a84559a8084 Mon Sep 17 00:00:00 2001 From: phuoc Date: Tue, 29 Oct 2024 19:22:28 +0700 Subject: [PATCH 43/57] fix cache primary address, text Signed-off-by: phuoc --- lib/service/address_service.dart | 22 ++++----------------- lib/util/user_account_channel.dart | 2 +- lib/view/passkey/passkey_register_view.dart | 4 ++-- 3 files changed, 7 insertions(+), 21 deletions(-) diff --git a/lib/service/address_service.dart b/lib/service/address_service.dart index 7b8f618e1..78aca0f00 100644 --- a/lib/service/address_service.dart +++ b/lib/service/address_service.dart @@ -25,19 +25,8 @@ class AddressService { AddressService(this._primaryAddressChannel, this._cloudObject); - AddressInfo? _primaryAddressInfo; - - Future getPrimaryAddressInfo() async { - if (_primaryAddressInfo != null) { - log.info('[AddressService] (Already set) Primary address info:' - ' ${_primaryAddressInfo?.toJson()}'); - return _primaryAddressInfo; - } - _primaryAddressInfo = await _primaryAddressChannel.getPrimaryAddress(); - log.info('[AddressService] Primary address info:' - ' ${_primaryAddressInfo?.toJson()}'); - return _primaryAddressInfo; - } + Future getPrimaryAddressInfo() async => + await _primaryAddressChannel.getPrimaryAddress(); Future migrateToEthereumAddress( AddressInfo currentPrimaryAddress) async { @@ -74,11 +63,8 @@ class AddressService { } } - Future clearPrimaryAddress() async { - _primaryAddressInfo = null; - await _primaryAddressChannel.clearPrimaryAddress(); - return true; - } + Future clearPrimaryAddress() async => + await _primaryAddressChannel.clearPrimaryAddress(); Future getAddress({required AddressInfo info}) async { final walletStorage = WalletStorage(info.uuid); diff --git a/lib/util/user_account_channel.dart b/lib/util/user_account_channel.dart index e23d86b23..6d4cafe35 100644 --- a/lib/util/user_account_channel.dart +++ b/lib/util/user_account_channel.dart @@ -42,9 +42,9 @@ class UserAccountChannel { } Future clearPrimaryAddress() async { + _primaryAddress = null; try { final result = await _channel.invokeMethod('clearPrimaryAddress', {}); - _primaryAddress = null; return result; } catch (e) { log.info('clearPrimaryAddress error', e); diff --git a/lib/view/passkey/passkey_register_view.dart b/lib/view/passkey/passkey_register_view.dart index e25ff1793..3213d06c6 100644 --- a/lib/view/passkey/passkey_register_view.dart +++ b/lib/view/passkey/passkey_register_view.dart @@ -118,8 +118,8 @@ class _PasskeyRegisterViewState extends State { return PrimaryAsyncButton( key: const Key('register_button'), onTap: _register, - text: 'get_started'.tr(), - processingText: _isError ? 'try_again'.tr() : 'creating_passkey'.tr(), + text: _isError ? 'try_again'.tr() : 'get_started'.tr(), + processingText: 'creating_passkey'.tr(), ); } From 09d85914c0d52efc7299f1c3a99ef3bf5bb6e69c Mon Sep 17 00:00:00 2001 From: phuoc Date: Wed, 30 Oct 2024 09:10:25 +0700 Subject: [PATCH 44/57] bring back default color Signed-off-by: phuoc --- lib/view/passkey/passkey_login_view.dart | 3 ++- lib/view/passkey/passkey_register_view.dart | 4 +++- lib/view/primary_button.dart | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/view/passkey/passkey_login_view.dart b/lib/view/passkey/passkey_login_view.dart index 82bb2ccb2..a23de9eef 100644 --- a/lib/view/passkey/passkey_login_view.dart +++ b/lib/view/passkey/passkey_login_view.dart @@ -7,7 +7,7 @@ import 'package:autonomy_flutter/util/log.dart'; import 'package:autonomy_flutter/view/passkey/having_trouble_view.dart'; import 'package:autonomy_flutter/view/primary_button.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:feralfile_app_theme/extensions/theme_extension.dart'; +import 'package:feralfile_app_theme/feral_file_app_theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; @@ -66,6 +66,7 @@ class _PasskeyLoginViewState extends State { Widget _getAction(BuildContext context) => PrimaryAsyncButton( key: const Key('login_button'), + color: AppColor.feralFileLightBlue, onTap: _login, text: _isError ? 'try_again'.tr() : 'login_button'.tr(), ); diff --git a/lib/view/passkey/passkey_register_view.dart b/lib/view/passkey/passkey_register_view.dart index 3213d06c6..f058ac7c8 100644 --- a/lib/view/passkey/passkey_register_view.dart +++ b/lib/view/passkey/passkey_register_view.dart @@ -7,7 +7,7 @@ import 'package:autonomy_flutter/util/log.dart'; import 'package:autonomy_flutter/view/passkey/having_trouble_view.dart'; import 'package:autonomy_flutter/view/primary_button.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:feralfile_app_theme/extensions/theme_extension.dart'; +import 'package:feralfile_app_theme/feral_file_app_theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; @@ -110,6 +110,7 @@ class _PasskeyRegisterViewState extends State { if (_didSuccess) { return PrimaryButton( text: 'continue'.tr(), + color: AppColor.feralFileLightBlue, onTap: () { Navigator.of(context).pop(true); }, @@ -117,6 +118,7 @@ class _PasskeyRegisterViewState extends State { } return PrimaryAsyncButton( key: const Key('register_button'), + color: AppColor.feralFileLightBlue, onTap: _register, text: _isError ? 'try_again'.tr() : 'get_started'.tr(), processingText: 'creating_passkey'.tr(), diff --git a/lib/view/primary_button.dart b/lib/view/primary_button.dart index 84e6c0201..3a75c21a6 100644 --- a/lib/view/primary_button.dart +++ b/lib/view/primary_button.dart @@ -41,7 +41,7 @@ class PrimaryButton extends StatelessWidget { child: ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: - enabled ? color ?? AppColor.feralFileLightBlue : disabledColor, + enabled ? color ?? AppColor.feralFileHighlight : disabledColor, padding: elevatedPadding, shadowColor: Colors.transparent, disabledForegroundColor: disabledColor, From 0298953e01a5d4e5079bc97b4ff0061542b7c075 Mon Sep 17 00:00:00 2001 From: phuoc Date: Wed, 30 Oct 2024 14:02:49 +0700 Subject: [PATCH 45/57] feedback: remove text Signed-off-by: phuoc --- assets | 2 +- lib/view/passkey/passkey_register_view.dart | 16 +++------------- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/assets b/assets index 92591acb3..f8f48b3db 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 92591acb3c4f50c337432ddaa615e59e6e1a71af +Subproject commit f8f48b3dbe3309205eee577924fc1308acc3e216 diff --git a/lib/view/passkey/passkey_register_view.dart b/lib/view/passkey/passkey_register_view.dart index f058ac7c8..30049971c 100644 --- a/lib/view/passkey/passkey_register_view.dart +++ b/lib/view/passkey/passkey_register_view.dart @@ -79,19 +79,9 @@ class _PasskeyRegisterViewState extends State { ], ); } - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'introducing_passkey_desc_1'.tr(), - style: style, - ), - const SizedBox(height: 10), - Text( - 'introducing_passkey_desc_2'.tr(), - style: style, - ), - ], + return Text( + 'introducing_passkey_desc_1'.tr(), + style: style, ); } From fffc0eaa4cc01535b9f17b5a8707c1dc4d3e3b4c Mon Sep 17 00:00:00 2001 From: phuoc Date: Wed, 30 Oct 2024 14:07:34 +0700 Subject: [PATCH 46/57] feedback: loading button Signed-off-by: phuoc --- lib/screen/onboarding_page.dart | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/lib/screen/onboarding_page.dart b/lib/screen/onboarding_page.dart index e0b212781..6ab4765f6 100644 --- a/lib/screen/onboarding_page.dart +++ b/lib/screen/onboarding_page.dart @@ -53,7 +53,7 @@ class _OnboardingPageState extends State final metricClient = injector.get(); final deepLinkService = injector.get(); Timer? _timer; - bool _loadingAnimation = true; + bool _loadingButton = true; final _passkeyService = injector.get(); final _userAccountChannel = injector.get(); @@ -179,7 +179,7 @@ class _OnboardingPageState extends State final didRegisterPasskey = await _userAccountChannel.didRegisterPasskey(); if (mounted) { setState(() { - _loadingAnimation = false; + _loadingButton = false; }); } final didLoginSuccess = didRegisterPasskey @@ -187,7 +187,7 @@ class _OnboardingPageState extends State : await _registerPasskey(); if (mounted) { setState(() { - _loadingAnimation = true; + _loadingButton = true; }); } if (didLoginSuccess != true) { @@ -216,14 +216,17 @@ class _OnboardingPageState extends State child: Column( children: [ const Spacer(), - PrimaryButton( - text: 'h_loading...'.tr(), - isProcessing: _loadingAnimation, - enabled: false, - disabledColor: AppColor.auGreyBackground, - textColor: AppColor.white, - indicatorColor: AppColor.white, - ), + if (_loadingButton) + PrimaryButton( + text: 'h_loading...'.tr(), + isProcessing: true, + enabled: false, + disabledColor: AppColor.auGreyBackground, + textColor: AppColor.white, + indicatorColor: AppColor.white, + ) + else + const SizedBox(), ], ), ), From 1747f8c5fae2482342fcbe67d0af803e0f96562c Mon Sep 17 00:00:00 2001 From: phuoc Date: Wed, 30 Oct 2024 14:35:32 +0700 Subject: [PATCH 47/57] fix ask face id when open app Signed-off-by: phuoc --- ios/Runner/AppDelegate.swift | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index c921b61fa..44b4c8ba5 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -252,12 +252,14 @@ import Logging }) DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) { [weak self] in - SystemChannelHandler.shared.didRegisterPasskeyKeychain { didRegisterPasskey in - if let didRegisterPasskey = didRegisterPasskey as? Bool, !didRegisterPasskey { - self?.showAuthenticationOverlay() - self?.authenticationVC.authentication() + if UserDefaults.standard.bool(forKey: "flutter.device_passcode") == true { + SystemChannelHandler.shared.didRegisterPasskeyKeychain { didRegisterPasskey in + if let didRegisterPasskey = didRegisterPasskey as? Bool, !didRegisterPasskey { + self?.showAuthenticationOverlay() + self?.authenticationVC.authentication() + } } - } + } } return super.application(application, didFinishLaunchingWithOptions: launchOptions) From 8231bde0e3c86784a579bbc3a223d18666a7522a Mon Sep 17 00:00:00 2001 From: phuoc Date: Wed, 30 Oct 2024 14:39:06 +0700 Subject: [PATCH 48/57] delete cache primary address Signed-off-by: phuoc --- lib/util/user_account_channel.dart | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/lib/util/user_account_channel.dart b/lib/util/user_account_channel.dart index 6d4cafe35..30bbf163a 100644 --- a/lib/util/user_account_channel.dart +++ b/lib/util/user_account_channel.dart @@ -14,35 +14,27 @@ class UserAccountChannel { ? const MethodChannel('migration_util') : const MethodChannel('backup'); - AddressInfo? _primaryAddress; - Future setPrimaryAddress(AddressInfo info) async { try { await _channel .invokeMethod('setPrimaryAddress', {'data': info.toString()}); - _primaryAddress = info; } catch (e) { log.info('setPrimaryAddress error: $e'); } } Future getPrimaryAddress() async { - if (_primaryAddress != null) { - return _primaryAddress; - } try { final String data = await _channel.invokeMethod('getPrimaryAddress', {}); final primaryAddressInfo = json.decode(data); - _primaryAddress = AddressInfo.fromJson(primaryAddressInfo); + return AddressInfo.fromJson(primaryAddressInfo); } catch (e) { log.info('getPrimaryAddress error: $e'); - _primaryAddress = null; } - return _primaryAddress; + return null; } Future clearPrimaryAddress() async { - _primaryAddress = null; try { final result = await _channel.invokeMethod('clearPrimaryAddress', {}); return result; From 41c22c1886f52dc8c9a79cd8677114041f8f2392 Mon Sep 17 00:00:00 2001 From: phuoc Date: Thu, 31 Oct 2024 13:20:17 +0700 Subject: [PATCH 49/57] auto login Signed-off-by: phuoc --- lib/screen/onboarding_page.dart | 21 +++++++++- lib/util/ui_helper.dart | 5 ++- lib/view/passkey/passkey_login_view.dart | 49 ++++++++---------------- 3 files changed, 37 insertions(+), 38 deletions(-) diff --git a/lib/screen/onboarding_page.dart b/lib/screen/onboarding_page.dart index 6ab4765f6..05d1644b8 100644 --- a/lib/screen/onboarding_page.dart +++ b/lib/screen/onboarding_page.dart @@ -196,8 +196,25 @@ class _OnboardingPageState extends State } } - Future _loginWithPasskey() async => - await UIHelper.showPasskeyLoginDialog(context); + Future _loginWithPasskey() async { + try { + await _loginAndMigrate(); + return true; + } catch (e, s) { + log.info('Failed to login with passkey: $e'); + unawaited(Sentry.captureException(e, stackTrace: s)); + if (mounted) { + return await UIHelper.showPasskeyLoginDialog(context, _loginAndMigrate); + } + } + } + + Future _loginAndMigrate() async { + await injector().migrateAccount(() async { + final localResponse = await _passkeyService.logInInitiate(); + await _passkeyService.logInFinalize(localResponse); + }); + } Future _registerPasskey() async => await UIHelper.showPasskeyRegisterDialog(context); diff --git a/lib/util/ui_helper.dart b/lib/util/ui_helper.dart index 48cd2c0b1..99f9422d4 100644 --- a/lib/util/ui_helper.dart +++ b/lib/util/ui_helper.dart @@ -1048,10 +1048,11 @@ class UIHelper { content: const PasskeyRegisterView(), ); - static Future showPasskeyLoginDialog(BuildContext context) async => + static Future showPasskeyLoginDialog( + BuildContext context, Future Function() onRetry) async => await showRawCenterSheet( context, - content: const PasskeyLoginView(), + content: PasskeyLoginRetryView(onRetry: onRetry), ); static Future showRawCenterSheet( diff --git a/lib/view/passkey/passkey_login_view.dart b/lib/view/passkey/passkey_login_view.dart index a23de9eef..76de5b289 100644 --- a/lib/view/passkey/passkey_login_view.dart +++ b/lib/view/passkey/passkey_login_view.dart @@ -1,8 +1,5 @@ import 'dart:async'; -import 'package:autonomy_flutter/common/injector.dart'; -import 'package:autonomy_flutter/service/account_service.dart'; -import 'package:autonomy_flutter/service/passkey_service.dart'; import 'package:autonomy_flutter/util/log.dart'; import 'package:autonomy_flutter/view/passkey/having_trouble_view.dart'; import 'package:autonomy_flutter/view/primary_button.dart'; @@ -12,19 +9,17 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; -class PasskeyLoginView extends StatefulWidget { - const PasskeyLoginView({super.key}); +class PasskeyLoginRetryView extends StatefulWidget { + final Future Function() onRetry; + + const PasskeyLoginRetryView({required this.onRetry, super.key}); @override - State createState() => _PasskeyLoginViewState(); + State createState() => _PasskeyLoginRetryViewState(); } -class _PasskeyLoginViewState extends State { - final _passkeyService = injector.get(); - final _accountService = injector.get(); - - bool _isError = false; - bool _isLogging = false; +class _PasskeyLoginRetryViewState extends State { + bool _isRetrying = false; @override Widget build(BuildContext context) => Column( @@ -42,12 +37,12 @@ class _PasskeyLoginViewState extends State { const SizedBox(height: 20), _getAction(context), const SizedBox(height: 20), - _havingTrouble(context) + const HavingTroubleView(), ], ); Widget _getTitle(BuildContext context) => Text( - _isError ? 'authentication_failed'.tr() : 'login_title'.tr(), + 'authentication_failed'.tr(), style: Theme.of(context).textTheme.ppMori700Black16, ); @@ -55,7 +50,7 @@ class _PasskeyLoginViewState extends State { final theme = Theme.of(context); final style = theme.textTheme.ppMori400Black14; return Text( - _isError ? 'passkey_error_desc'.tr() : 'login_desc'.tr(), + 'passkey_error_desc'.tr(), style: style, ); } @@ -68,42 +63,28 @@ class _PasskeyLoginViewState extends State { key: const Key('login_button'), color: AppColor.feralFileLightBlue, onTap: _login, - text: _isError ? 'try_again'.tr() : 'login_button'.tr(), + text: 'try_again'.tr(), ); Future _login() async { - if (_isLogging) { + if (_isRetrying) { return; } setState(() { - _isLogging = true; - _isError = false; + _isRetrying = true; }); try { - await _accountService.migrateAccount(() async { - final localResponse = await _passkeyService.logInInitiate(); - await _passkeyService.logInFinalize(localResponse); - }); + await widget.onRetry(); if (mounted) { Navigator.of(context).pop(true); } } catch (e, stackTrace) { log.info('Failed to login with passkey: $e'); unawaited(Sentry.captureException(e, stackTrace: stackTrace)); - setState(() { - _isError = true; - }); } finally { setState(() { - _isLogging = false; + _isRetrying = false; }); } } - - Widget _havingTrouble(BuildContext context) { - if (!_isError && !_isLogging) { - return const SizedBox(); - } - return const HavingTroubleView(); - } } From 447badc72c6410dc610f986d5d7d22a4cd78575d Mon Sep 17 00:00:00 2001 From: phuoc Date: Tue, 29 Oct 2024 18:20:17 +0700 Subject: [PATCH 50/57] support anonymous Signed-off-by: phuoc --- lib/common/injector.dart | 1 + lib/gateway/customer_support_api.dart | 53 +++++++---- lib/gateway/customer_support_api.g.dart | 87 +++++++++++++++++-- lib/model/customer_support.dart | 5 ++ lib/model/customer_support.g.dart | 2 + .../customer_support/support_thread_page.dart | 4 +- lib/service/configuration_service.dart | 34 ++++++++ lib/service/customer_support_service.dart | 36 +++++++- lib/util/customer_support_utils.dart | 19 ++++ lib/util/dio_interceptors.dart | 50 +++++++++-- lib/util/dio_util.dart | 4 +- .../mock_configuration_service.mocks.dart | 54 +++++++----- 12 files changed, 293 insertions(+), 56 deletions(-) create mode 100644 lib/util/customer_support_utils.dart diff --git a/lib/common/injector.dart b/lib/common/injector.dart index e28387d1a..a70472dc8 100644 --- a/lib/common/injector.dart +++ b/lib/common/injector.dart @@ -292,6 +292,7 @@ Future setupInjector() async { ), ), baseUrl: Environment.customerSupportURL), + injector(), )); injector.registerLazySingleton( diff --git a/lib/gateway/customer_support_api.dart b/lib/gateway/customer_support_api.dart index 4b614238c..09a28206d 100644 --- a/lib/gateway/customer_support_api.dart +++ b/lib/gateway/customer_support_api.dart @@ -11,38 +11,59 @@ import 'package:retrofit/retrofit.dart'; part 'customer_support_api.g.dart'; -@RestApi(baseUrl: "") +@RestApi(baseUrl: '') abstract class CustomerSupportApi { factory CustomerSupportApi(Dio dio, {String baseUrl}) = _CustomerSupportApi; - @GET("/v1/issues") - Future> getIssues(); + static const String issuesPath = '/v1/issues/'; - @GET("/v1/issues/{issueID}") + static const String apiKeyHeader = 'x-api-key'; + static const String deviceIdHeader = 'x-device-id'; + + @GET(issuesPath) + Future> getIssues({ + @Header('Authorization') required String token, + }); + + @GET(issuesPath) + Future> getAnonymousIssues({ + @Header(apiKeyHeader) required String apiKey, + @Header(deviceIdHeader) required String deviceId, + }); + + @GET('/v1/issues/{issueID}') Future getDetails( - @Path("issueID") String issueID, { - @Query("reverse") bool reverse = true, + @Path('issueID') String issueID, { + @Query('reverse') bool reverse = true, }); - @POST("/v1/issues/") + @POST(issuesPath) Future createIssue( - @Body() Map body, - ); + @Body() Map body, { + @Header('Authorization') required String token, + }); + + @POST(issuesPath) + Future createAnonymousIssue( + @Body() Map body, { + @Header(apiKeyHeader) required String apiKey, + @Header(deviceIdHeader) required String deviceId, + }); - @POST("/v1/issues/{issueID}") + @POST('/v1/issues/{issueID}') Future commentIssue( - @Path("issueID") String issueID, + @Path('issueID') String issueID, @Body() Map body, ); - @PATCH("/v1/issues/{issueID}/reopen") + @PATCH('/v1/issues/{issueID}/reopen') Future reOpenIssue( - @Path("issueID") String issueID, + @Path('issueID') String issueID, ); - @POST("/v1/issues/{issueID}/rate/{rating}") + @POST('/v1/issues/{issueID}/rate/{rating}') Future rateIssue( - @Path("issueID") String issueID, - @Path("rating") int rating, + @Path('issueID') String issueID, + @Path('rating') int rating, ); } diff --git a/lib/gateway/customer_support_api.g.dart b/lib/gateway/customer_support_api.g.dart index 30b4e6897..5282c7097 100644 --- a/lib/gateway/customer_support_api.g.dart +++ b/lib/gateway/customer_support_api.g.dart @@ -19,10 +19,47 @@ class _CustomerSupportApi implements CustomerSupportApi { String? baseUrl; @override - Future> getIssues() async { + Future> getIssues({required String token}) async { const _extra = {}; final queryParameters = {}; - final _headers = {}; + final _headers = {r'Authorization': token}; + _headers.removeWhere((k, v) => v == null); + final Map? _data = null; + final _result = + await _dio.fetch>(_setStreamType>(Options( + method: 'GET', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/v1/issues/', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + )))); + var value = _result.data! + .map((dynamic i) => Issue.fromJson(i as Map)) + .toList(); + return value; + } + + @override + Future> getAnonymousIssues({ + required String apiKey, + required String deviceId, + }) async { + const _extra = {}; + final queryParameters = {}; + final _headers = { + r'x-api-key': apiKey, + r'x-device-id': deviceId, + }; + _headers.removeWhere((k, v) => v == null); final Map? _data = null; final _result = await _dio.fetch>(_setStreamType>(Options( @@ -32,7 +69,7 @@ class _CustomerSupportApi implements CustomerSupportApi { ) .compose( _dio.options, - '/v1/issues', + '/v1/issues/', queryParameters: queryParameters, data: _data, ) @@ -78,10 +115,50 @@ class _CustomerSupportApi implements CustomerSupportApi { } @override - Future createIssue(Map body) async { + Future createIssue( + Map body, { + required String token, + }) async { const _extra = {}; final queryParameters = {}; - final _headers = {}; + final _headers = {r'Authorization': token}; + _headers.removeWhere((k, v) => v == null); + final _data = {}; + _data.addAll(body); + final _result = await _dio.fetch>( + _setStreamType(Options( + method: 'POST', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/v1/issues/', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + )))); + final value = PostedMessageResponse.fromJson(_result.data!); + return value; + } + + @override + Future createAnonymousIssue( + Map body, { + required String apiKey, + required String deviceId, + }) async { + const _extra = {}; + final queryParameters = {}; + final _headers = { + r'x-api-key': apiKey, + r'x-device-id': deviceId, + }; + _headers.removeWhere((k, v) => v == null); final _data = {}; _data.addAll(body); final _result = await _dio.fetch>( diff --git a/lib/model/customer_support.dart b/lib/model/customer_support.dart index 2147608ed..34ce12574 100644 --- a/lib/model/customer_support.dart +++ b/lib/model/customer_support.dart @@ -37,6 +37,8 @@ class Issue implements ChatThread { Message? firstMessage; @JsonKey(name: 'announcement_content_id') String? announcementContentId; + @JsonKey(name: 'user_id') + String? userId; // only on local @JsonKey(includeFromJson: false, includeToJson: false) @@ -55,6 +57,7 @@ class Issue implements ChatThread { required this.rating, this.draft, this.announcementContentId, + this.userId, }); factory Issue.fromJson(Map json) => _$IssueFromJson(json); @@ -85,6 +88,7 @@ class Issue implements ChatThread { Message? firstMessage, String? announcementContentId, DraftCustomerSupport? draft, + String? userId, }) => Issue( issueID: issueID ?? this.issueID, @@ -100,6 +104,7 @@ class Issue implements ChatThread { announcementContentId: announcementContentId ?? this.announcementContentId, draft: draft ?? this.draft, + userId: userId ?? this.userId, ); @override diff --git a/lib/model/customer_support.g.dart b/lib/model/customer_support.g.dart index 56052f33c..ccc1449ef 100644 --- a/lib/model/customer_support.g.dart +++ b/lib/model/customer_support.g.dart @@ -22,6 +22,7 @@ Issue _$IssueFromJson(Map json) => Issue( : Message.fromJson(json['first_message'] as Map), rating: (json['rating'] as num).toInt(), announcementContentId: json['announcement_content_id'] as String?, + userId: json['user_id'] as String?, ); Map _$IssueToJson(Issue instance) => { @@ -36,6 +37,7 @@ Map _$IssueToJson(Issue instance) => { 'last_message': instance.lastMessage, 'first_message': instance.firstMessage, 'announcement_content_id': instance.announcementContentId, + 'user_id': instance.userId, }; SendAttachment _$SendAttachmentFromJson(Map json) => diff --git a/lib/screen/customer_support/support_thread_page.dart b/lib/screen/customer_support/support_thread_page.dart index 39656bf22..4795e9022 100644 --- a/lib/screen/customer_support/support_thread_page.dart +++ b/lib/screen/customer_support/support_thread_page.dart @@ -708,7 +708,9 @@ class _SupportThreadPageState extends State { return; } final issueDetails = await _customerSupportService.getDetails(_issueID!); - await _getUserId(); + if (issueDetails.issue.userId != null) { + _userId = issueDetails.issue.userId; + } final parsedMessages = (await Future.wait( issueDetails.messages.map((e) => _convertChatMessage(e, null)))) .expand((i) => i) diff --git a/lib/service/configuration_service.dart b/lib/service/configuration_service.dart index 25ce3ba78..c84cdf460 100644 --- a/lib/service/configuration_service.dart +++ b/lib/service/configuration_service.dart @@ -24,6 +24,14 @@ import 'package:uuid/uuid.dart'; //ignore_for_file: constant_identifier_names abstract class ConfigurationService { + String? getAnonymousDeviceId(); + + Future createAnonymousDeviceId(); + + List getAnonymousIssueIds(); + + Future addAnonymousIssueId(List issueIds); + bool didMigrateToAccountSetting(); Future setMigrateToAccountSetting(bool value); @@ -206,6 +214,8 @@ abstract class ConfigurationService { } class ConfigurationServiceImpl implements ConfigurationService { + static const String keyAnonymousDeviceId = 'anonymous_device_id'; + static const String keyAnonymousIssueIds = 'anonymous_issue_ids'; static const String keyDidMigrateToAccountSetting = 'did_migrate_to_account_setting'; static const String keyDidShowLiveWithArt = 'did_show_live_with_art'; @@ -929,6 +939,30 @@ class ConfigurationServiceImpl implements ConfigurationService { await _preferences.setString( KEY_ANNOUNCEMENT_TO_ISSUE_MAP, jsonEncode(mapJson)); } + + @override + String? getAnonymousDeviceId() => + _preferences.getString(keyAnonymousDeviceId); + + @override + Future createAnonymousDeviceId() async { + final uuid = const Uuid().v4(); + final anonymousDeviceId = 'device-$uuid'; + await _preferences.setString(keyAnonymousDeviceId, anonymousDeviceId); + return anonymousDeviceId; + } + + @override + Future addAnonymousIssueId(List issueIds) { + final currentIssueIds = getAnonymousIssueIds() + ..addAll(issueIds) + ..unique(); + return _preferences.setStringList(keyAnonymousIssueIds, currentIssueIds); + } + + @override + List getAnonymousIssueIds() => + _preferences.getStringList(keyAnonymousIssueIds) ?? []; } enum ConflictAction { diff --git a/lib/service/customer_support_service.dart b/lib/service/customer_support_service.dart index 1107261e8..1f285b5ee 100644 --- a/lib/service/customer_support_service.dart +++ b/lib/service/customer_support_service.dart @@ -9,6 +9,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'package:autonomy_flutter/common/environment.dart'; import 'package:autonomy_flutter/common/injector.dart'; import 'package:autonomy_flutter/database/dao/draft_customer_support_dao.dart'; import 'package:autonomy_flutter/database/entity/draft_customer_support.dart'; @@ -16,6 +17,8 @@ import 'package:autonomy_flutter/gateway/customer_support_api.dart'; import 'package:autonomy_flutter/model/announcement/announcement_local.dart'; import 'package:autonomy_flutter/model/customer_support.dart'; import 'package:autonomy_flutter/service/announcement/announcement_service.dart'; +import 'package:autonomy_flutter/service/auth_service.dart'; +import 'package:autonomy_flutter/service/configuration_service.dart'; import 'package:autonomy_flutter/util/device.dart'; import 'package:autonomy_flutter/util/log.dart'; import 'package:autonomy_flutter/view/user_agent_utils.dart'; @@ -74,6 +77,7 @@ class CustomerSupportServiceImpl extends CustomerSupportService { final DraftCustomerSupportDao _draftCustomerSupportDao; final CustomerSupportApi _customerSupportApi; + final ConfigurationService _configurationService; @override List errorMessages = []; @@ -92,6 +96,7 @@ class CustomerSupportServiceImpl extends CustomerSupportService { CustomerSupportServiceImpl( this._draftCustomerSupportDao, this._customerSupportApi, + this._configurationService, ); bool _isProcessingDraftMessages = false; @@ -99,8 +104,21 @@ class CustomerSupportServiceImpl extends CustomerSupportService { Future> _getIssues() async { final issues = []; try { - final listIssues = await _customerSupportApi.getIssues(); - issues.addAll(listIssues); + final jwt = await injector().getAuthToken(); + if (jwt != null) { + final listIssues = await _customerSupportApi.getIssues( + token: 'Bearer ${jwt.jwtToken}'); + issues.addAll(listIssues); + } + final anonymousDeviceId = _configurationService.getAnonymousDeviceId(); + if (anonymousDeviceId != null) { + final listAnonymousIssues = + await _customerSupportApi.getAnonymousIssues( + apiKey: Environment.supportApiKey, + deviceId: anonymousDeviceId, + ); + issues.addAll(listAnonymousIssues); + } } catch (e) { log.info('[CS-Service] getIssues error: $e'); unawaited(Sentry.captureException(e)); @@ -428,8 +446,18 @@ class CustomerSupportServiceImpl extends CustomerSupportService { if (artworkReportID != null && artworkReportID.isNotEmpty) { payload['artwork_report_id'] = artworkReportID; } - - return await _customerSupportApi.createIssue(payload); + final jwt = await injector().getAuthToken(); + if (jwt != null) { + return await _customerSupportApi.createIssue(payload, + token: jwt.jwtToken); + } else { + final anonymousDeviceId = _configurationService.getAnonymousDeviceId() ?? + await _configurationService.createAnonymousDeviceId(); + final issue = await _customerSupportApi.createAnonymousIssue(payload, + apiKey: Environment.supportApiKey, deviceId: anonymousDeviceId); + await _configurationService.addAnonymousIssueId([issue.issueID]); + return issue; + } } Future commentIssue(String issueID, String? message, diff --git a/lib/util/customer_support_utils.dart b/lib/util/customer_support_utils.dart new file mode 100644 index 000000000..f1a06b855 --- /dev/null +++ b/lib/util/customer_support_utils.dart @@ -0,0 +1,19 @@ +import 'package:autonomy_flutter/common/injector.dart'; +import 'package:autonomy_flutter/model/customer_support.dart'; +import 'package:autonomy_flutter/service/configuration_service.dart'; + +extension IssueExtension on Issue { + bool get isAnonymous { + final config = injector(); + final userId = config.getAnonymousDeviceId(); + return userId != null && userId != this.userId; + } +} + +extension CustomerSupportHeaderExt on Map { + bool get isAnonymous { + final config = injector(); + final userId = config.getAnonymousDeviceId(); + return userId != null && userId != this['x-device-id']; + } +} diff --git a/lib/util/dio_interceptors.dart b/lib/util/dio_interceptors.dart index 270f15ea8..99143fbdf 100644 --- a/lib/util/dio_interceptors.dart +++ b/lib/util/dio_interceptors.dart @@ -10,8 +10,10 @@ import 'dart:convert'; import 'package:autonomy_flutter/common/environment.dart'; import 'package:autonomy_flutter/common/injector.dart'; +import 'package:autonomy_flutter/gateway/customer_support_api.dart'; import 'package:autonomy_flutter/model/ff_account.dart'; import 'package:autonomy_flutter/service/auth_service.dart'; +import 'package:autonomy_flutter/service/configuration_service.dart'; import 'package:autonomy_flutter/service/network_issue_manager.dart'; import 'package:autonomy_flutter/util/error_handler.dart'; import 'package:autonomy_flutter/util/exception.dart'; @@ -144,23 +146,55 @@ class SentryInterceptor extends InterceptorsWrapper { } class AutonomyAuthInterceptor extends Interceptor { - // use this apiKey for if jwt is not available - final String? apiKey; - - AutonomyAuthInterceptor({this.apiKey}); + AutonomyAuthInterceptor(); @override Future onRequest( RequestOptions options, RequestInterceptorHandler handler) async { final jwt = await injector().getAuthToken(); - if (jwt == null && apiKey == null) { + if (jwt == null) { unawaited(Sentry.captureMessage('JWT is null')); throw JwtException(message: 'can_not_authenticate_desc'.tr()); } - if (jwt != null) { - options.headers['Authorization'] = 'Bearer ${jwt.jwtToken}'; + + options.headers['Authorization'] = 'Bearer ${jwt.jwtToken}'; + + return handler.next(options); + } +} + +class CustomerSupportInterceptor extends Interceptor { + CustomerSupportInterceptor(); + + final _configurationService = injector(); + + @override + Future onRequest( + RequestOptions options, RequestInterceptorHandler handler) async { + final isCustomHeaderApi = options.path == CustomerSupportApi.issuesPath; + + if (isCustomHeaderApi) { + // do nothing get list issues, create issue: add header at api level } else { - options.headers['x-api-key'] = apiKey; + final jwt = await injector().getAuthToken(); + final pathElements = options.path.split('/'); + final anonymousIssueIds = + injector().getAnonymousIssueIds(); + if (pathElements.any((element) => anonymousIssueIds.contains(element))) { + // get issue details, add header + options.headers[CustomerSupportApi.apiKeyHeader] = + Environment.supportApiKey; + options.headers[CustomerSupportApi.deviceIdHeader] = + _configurationService.getAnonymousDeviceId(); + } else { + // other api, add jwt + if (jwt != null) { + options.headers['Authorization'] = 'Bearer ${jwt.jwtToken}'; + } else { + unawaited(Sentry.captureMessage('JWT is null')); + throw JwtException(message: 'can_not_authenticate_desc'.tr()); + } + } } return handler.next(options); diff --git a/lib/util/dio_util.dart b/lib/util/dio_util.dart index 25b73e514..ffe40fbbd 100644 --- a/lib/util/dio_util.dart +++ b/lib/util/dio_util.dart @@ -23,8 +23,8 @@ Dio feralFileDio(BaseOptions options) { Dio customerSupportDio(BaseOptions options) { final dio = baseDio(options); - dio.interceptors - .add(AutonomyAuthInterceptor(apiKey: Environment.supportApiKey)); + dio.interceptors.add(CustomerSupportInterceptor()); + dio.interceptors.add(LoggingInterceptor()); return dio; } diff --git a/test/generate_mock/service/mock_configuration_service.mocks.dart b/test/generate_mock/service/mock_configuration_service.mocks.dart index 4572dcb6c..1f208d945 100644 --- a/test/generate_mock/service/mock_configuration_service.mocks.dart +++ b/test/generate_mock/service/mock_configuration_service.mocks.dart @@ -5,9 +5,9 @@ // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i5; -import 'package:autonomy_flutter/model/jwt.dart' as _i6; -import 'package:autonomy_flutter/model/network.dart' as _i7; -import 'package:autonomy_flutter/model/sent_artwork.dart' as _i8; +import 'package:autonomy_flutter/model/jwt.dart' as _i7; +import 'package:autonomy_flutter/model/network.dart' as _i8; +import 'package:autonomy_flutter/model/sent_artwork.dart' as _i9; import 'package:autonomy_flutter/model/shared_postcard.dart' as _i10; import 'package:autonomy_flutter/screen/chat/chat_thread_page.dart' as _i3; import 'package:autonomy_flutter/screen/interactive_postcard/postcard_detail_page.dart' @@ -17,7 +17,7 @@ import 'package:autonomy_flutter/screen/interactive_postcard/stamp_preview.dart' import 'package:autonomy_flutter/service/configuration_service.dart' as _i4; import 'package:flutter/material.dart' as _i2; import 'package:mockito/mockito.dart' as _i1; -import 'package:mockito/src/dummies.dart' as _i9; +import 'package:mockito/src/dummies.dart' as _i6; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -72,19 +72,33 @@ class MockConfigurationService extends _i1.Mock ), ) as _i2.ValueNotifier); @override - bool didRegisterPasskeyAndroid() => (super.noSuchMethod( + _i5.Future createAnonymousDeviceId() => (super.noSuchMethod( Invocation.method( - #didRegisterPasskeyAndroid, + #createAnonymousDeviceId, [], ), - returnValue: false, - ) as bool); + returnValue: _i5.Future.value(_i6.dummyValue( + this, + Invocation.method( + #createAnonymousDeviceId, + [], + ), + )), + ) as _i5.Future); @override - _i5.Future setDidRegisterPasskeyAndroid(bool? value) => + List getAnonymousIssueIds() => (super.noSuchMethod( + Invocation.method( + #getAnonymousIssueIds, + [], + ), + returnValue: [], + ) as List); + @override + _i5.Future addAnonymousIssueId(List? issueIds) => (super.noSuchMethod( Invocation.method( - #setDidRegisterPasskeyAndroid, - [value], + #addAnonymousIssueId, + [issueIds], ), returnValue: _i5.Future.value(), returnValueForMissingStub: _i5.Future.value(), @@ -258,7 +272,7 @@ class MockConfigurationService extends _i1.Mock returnValueForMissingStub: _i5.Future.value(), ) as _i5.Future); @override - _i5.Future setIAPJWT(_i6.JWT? value) => (super.noSuchMethod( + _i5.Future setIAPJWT(_i7.JWT? value) => (super.noSuchMethod( Invocation.method( #setIAPJWT, [value], @@ -344,7 +358,7 @@ class MockConfigurationService extends _i1.Mock returnValue: _i5.Future.value(), ) as _i5.Future); @override - List getTempStorageHiddenTokenIDs({_i7.Network? network}) => + List getTempStorageHiddenTokenIDs({_i8.Network? network}) => (super.noSuchMethod( Invocation.method( #getTempStorageHiddenTokenIDs, @@ -357,7 +371,7 @@ class MockConfigurationService extends _i1.Mock _i5.Future updateTempStorageHiddenTokenIDs( List? tokenIDs, bool? isAdd, { - _i7.Network? network, + _i8.Network? network, bool? override = false, }) => (super.noSuchMethod( @@ -375,16 +389,16 @@ class MockConfigurationService extends _i1.Mock returnValue: _i5.Future.value(), ) as _i5.Future); @override - List<_i8.SentArtwork> getRecentlySentToken() => (super.noSuchMethod( + List<_i9.SentArtwork> getRecentlySentToken() => (super.noSuchMethod( Invocation.method( #getRecentlySentToken, [], ), - returnValue: <_i8.SentArtwork>[], - ) as List<_i8.SentArtwork>); + returnValue: <_i9.SentArtwork>[], + ) as List<_i9.SentArtwork>); @override _i5.Future updateRecentlySentToken( - List<_i8.SentArtwork>? sentArtwork, { + List<_i9.SentArtwork>? sentArtwork, { bool? override = false, }) => (super.noSuchMethod( @@ -420,7 +434,7 @@ class MockConfigurationService extends _i1.Mock #getAccountHMACSecret, [], ), - returnValue: _i5.Future.value(_i9.dummyValue( + returnValue: _i5.Future.value(_i6.dummyValue( this, Invocation.method( #getAccountHMACSecret, @@ -693,7 +707,7 @@ class MockConfigurationService extends _i1.Mock #getVersionInfo, [], ), - returnValue: _i9.dummyValue( + returnValue: _i6.dummyValue( this, Invocation.method( #getVersionInfo, From ebdb5a7da71e34c1e675181a991a035a9a92246a Mon Sep 17 00:00:00 2001 From: phuoc Date: Thu, 31 Oct 2024 13:49:54 +0700 Subject: [PATCH 51/57] reuse customer support anonymous issue id Signed-off-by: phuoc --- lib/view/passkey/having_trouble_view.dart | 46 ++++++++++++++--------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/lib/view/passkey/having_trouble_view.dart b/lib/view/passkey/having_trouble_view.dart index 8f61e8e50..bf29b0f21 100644 --- a/lib/view/passkey/having_trouble_view.dart +++ b/lib/view/passkey/having_trouble_view.dart @@ -1,5 +1,7 @@ +import 'package:autonomy_flutter/common/injector.dart'; import 'package:autonomy_flutter/screen/app_router.dart'; import 'package:autonomy_flutter/screen/customer_support/support_thread_page.dart'; +import 'package:autonomy_flutter/service/configuration_service.dart'; import 'package:autonomy_flutter/util/constants.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:feralfile_app_theme/extensions/theme_extension.dart'; @@ -9,23 +11,33 @@ class HavingTroubleView extends StatelessWidget { const HavingTroubleView({super.key}); @override - Widget build(BuildContext context) => Padding( - padding: const EdgeInsets.only(bottom: 20), - child: GestureDetector( - onTap: () async { - await Navigator.of(context).pushNamed( - AppRouter.supportThreadPage, - arguments: NewIssuePayload( - reportIssueType: ReportIssueType.Bug, + Widget build(BuildContext context) { + final lastLoginIssueId = + injector().getAnonymousIssueIds().lastOrNull; + return Padding( + padding: const EdgeInsets.only(bottom: 20), + child: GestureDetector( + onTap: () async { + final payload = lastLoginIssueId == null + ? NewIssuePayload( + reportIssueType: ReportIssueType.Bug, + ) + : DetailIssuePayload( + reportIssueType: ReportIssueType.Bug, + issueID: lastLoginIssueId, + ); + await Navigator.of(context).pushNamed( + AppRouter.supportThreadPage, + arguments: payload, + ); + }, + child: Text( + 'having_trouble'.tr(), + style: Theme.of(context).textTheme.ppMori400Grey14.copyWith( + decoration: TextDecoration.underline, ), - ); - }, - child: Text( - 'having_trouble'.tr(), - style: Theme.of(context).textTheme.ppMori400Grey14.copyWith( - decoration: TextDecoration.underline, - ), - ), ), - ); + ), + ); + } } From 3c28a7666aac2de80b25b86784c28282db0450ef Mon Sep 17 00:00:00 2001 From: phuoc Date: Thu, 31 Oct 2024 14:37:08 +0700 Subject: [PATCH 52/57] fix user id, text controller dispose, issue order Signed-off-by: phuoc --- lib/model/customer_support.dart | 3 +-- .../customer_support/support_thread_page.dart | 26 +++++++------------ lib/service/customer_support_service.dart | 1 + 3 files changed, 11 insertions(+), 19 deletions(-) diff --git a/lib/model/customer_support.dart b/lib/model/customer_support.dart index 34ce12574..810b07d81 100644 --- a/lib/model/customer_support.dart +++ b/lib/model/customer_support.dart @@ -108,8 +108,7 @@ class Issue implements ChatThread { ); @override - // TODO: implement sortTime - DateTime get sortTime => timestamp; + DateTime get sortTime => lastMessage?.timestamp ?? timestamp; } @JsonSerializable() diff --git a/lib/screen/customer_support/support_thread_page.dart b/lib/screen/customer_support/support_thread_page.dart index 4795e9022..f69e7c31e 100644 --- a/lib/screen/customer_support/support_thread_page.dart +++ b/lib/screen/customer_support/support_thread_page.dart @@ -23,7 +23,6 @@ import 'package:autonomy_flutter/service/feralfile_service.dart'; import 'package:autonomy_flutter/shared.dart'; import 'package:autonomy_flutter/util/constants.dart'; import 'package:autonomy_flutter/util/datetime_ext.dart'; -import 'package:autonomy_flutter/util/device.dart'; import 'package:autonomy_flutter/util/jwt.dart'; import 'package:autonomy_flutter/util/log.dart' as log_util; import 'package:autonomy_flutter/util/string_ext.dart'; @@ -193,7 +192,6 @@ class _SupportThreadPageState extends State { @override void initState() { - unawaited(_getUserId()); unawaited(injector().processMessages()); injector() .triggerReloadMessages @@ -261,20 +259,6 @@ class _SupportThreadPageState extends State { } } - Future _getUserId() async { - if (_userId != null) { - return _userId!; - } - final jwt = await injector().getAuthToken(); - if (jwt != null) { - final data = parseJwt(jwt.jwtToken); - _userId = data['sub'] ?? ''; - } else { - _userId = await getDeviceID(); - } - return _userId!; - } - Future _addDebugLog() async { Navigator.of(context).pop(); @@ -299,7 +283,6 @@ class _SupportThreadPageState extends State { .removeListener(_loadIssueDetails); _customerSupportService.customerSupportUpdate .removeListener(_loadCustomerSupportUpdates); - _textEditingController.dispose(); memoryValues.viewingSupportThreadIssueID = null; super.dispose(); @@ -705,6 +688,15 @@ class _SupportThreadPageState extends State { Future _loadIssueDetails() async { if (_issueID == null) { + final jwt = await injector().getAuthToken(); + if (jwt != null) { + final data = parseJwt(jwt.jwtToken); + if (data['sub'] != _userId) { + setState(() { + _userId = data['sub']; + }); + } + } return; } final issueDetails = await _customerSupportService.getDetails(_issueID!); diff --git a/lib/service/customer_support_service.dart b/lib/service/customer_support_service.dart index 1f285b5ee..da090b89b 100644 --- a/lib/service/customer_support_service.dart +++ b/lib/service/customer_support_service.dart @@ -119,6 +119,7 @@ class CustomerSupportServiceImpl extends CustomerSupportService { ); issues.addAll(listAnonymousIssues); } + issues.sort((a, b) => b.sortTime.compareTo(a.sortTime)); } catch (e) { log.info('[CS-Service] getIssues error: $e'); unawaited(Sentry.captureException(e)); From d6b4a0b176dff8fa63e3206c55eb3e66316cef19 Mon Sep 17 00:00:00 2001 From: phuoc Date: Thu, 31 Oct 2024 14:46:17 +0700 Subject: [PATCH 53/57] hide error binding popup Signed-off-by: phuoc --- lib/util/error_handler.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/util/error_handler.dart b/lib/util/error_handler.dart index 730bf78c1..eb1ee4c57 100644 --- a/lib/util/error_handler.dart +++ b/lib/util/error_handler.dart @@ -118,12 +118,12 @@ ErrorEvent translateError(Object exception) { if (exception is JwtException) { return ErrorEvent(exception, 'can_not_authenticate'.tr(), exception.message, - ErrorItemState.close); + ErrorItemState.getReport); } if (exception is ErrorBindingException) { return ErrorEvent(exception, 'binding_data_issue'.tr(), exception.message, - ErrorItemState.getReport); + ErrorItemState.general); } return ErrorEvent( From 189050336c64ee2aa020364926ced32cff27b42f Mon Sep 17 00:00:00 2001 From: phuoc Date: Thu, 31 Oct 2024 16:13:54 +0700 Subject: [PATCH 54/57] fix support authorization header Signed-off-by: phuoc --- lib/service/customer_support_service.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/service/customer_support_service.dart b/lib/service/customer_support_service.dart index da090b89b..6551dd4ca 100644 --- a/lib/service/customer_support_service.dart +++ b/lib/service/customer_support_service.dart @@ -450,7 +450,7 @@ class CustomerSupportServiceImpl extends CustomerSupportService { final jwt = await injector().getAuthToken(); if (jwt != null) { return await _customerSupportApi.createIssue(payload, - token: jwt.jwtToken); + token: 'Bearer ${jwt.jwtToken}'); } else { final anonymousDeviceId = _configurationService.getAnonymousDeviceId() ?? await _configurationService.createAnonymousDeviceId(); From a11daf1422c1f5fea02a800865113b6d1567ee92 Mon Sep 17 00:00:00 2001 From: phuoc Date: Fri, 1 Nov 2024 11:12:35 +0700 Subject: [PATCH 55/57] fix onboarding loading Signed-off-by: phuoc --- lib/screen/onboarding_page.dart | 55 +++++++++++++++++--------------- lib/service/passkey_service.dart | 28 +++++++++------- 2 files changed, 45 insertions(+), 38 deletions(-) diff --git a/lib/screen/onboarding_page.dart b/lib/screen/onboarding_page.dart index 05d1644b8..c43a8bdcc 100644 --- a/lib/screen/onboarding_page.dart +++ b/lib/screen/onboarding_page.dart @@ -53,7 +53,6 @@ class _OnboardingPageState extends State final metricClient = injector.get(); final deepLinkService = injector.get(); Timer? _timer; - bool _loadingButton = true; final _passkeyService = injector.get(); final _userAccountChannel = injector.get(); @@ -177,19 +176,10 @@ class _OnboardingPageState extends State } else { log.info('Passkey is supported. Authenticate with passkey'); final didRegisterPasskey = await _userAccountChannel.didRegisterPasskey(); - if (mounted) { - setState(() { - _loadingButton = false; - }); - } + log.info('Passkey registered: $didRegisterPasskey'); final didLoginSuccess = didRegisterPasskey ? await _loginWithPasskey() : await _registerPasskey(); - if (mounted) { - setState(() { - _loadingButton = true; - }); - } if (didLoginSuccess != true) { throw Exception('Failed to login with passkey'); } @@ -203,9 +193,13 @@ class _OnboardingPageState extends State } catch (e, s) { log.info('Failed to login with passkey: $e'); unawaited(Sentry.captureException(e, stackTrace: s)); - if (mounted) { - return await UIHelper.showPasskeyLoginDialog(context, _loginAndMigrate); + if (!mounted) { + return false; } + final result = + await UIHelper.showPasskeyLoginDialog(context, _loginAndMigrate); + _passkeyService.isShowingLoginDialog.value = false; + return result; } } @@ -216,8 +210,12 @@ class _OnboardingPageState extends State }); } - Future _registerPasskey() async => - await UIHelper.showPasskeyRegisterDialog(context); + Future _registerPasskey() async { + _passkeyService.isShowingLoginDialog.value = true; + final result = await UIHelper.showPasskeyRegisterDialog(context); + _passkeyService.isShowingLoginDialog.value = false; + return result; + } @override Widget build(BuildContext context) => Scaffold( @@ -233,17 +231,22 @@ class _OnboardingPageState extends State child: Column( children: [ const Spacer(), - if (_loadingButton) - PrimaryButton( - text: 'h_loading...'.tr(), - isProcessing: true, - enabled: false, - disabledColor: AppColor.auGreyBackground, - textColor: AppColor.white, - indicatorColor: AppColor.white, - ) - else - const SizedBox(), + ValueListenableBuilder( + valueListenable: _passkeyService.isShowingLoginDialog, + builder: (context, value, child) { + if (value) { + return const SizedBox(); + } + return PrimaryButton( + text: 'h_loading...'.tr(), + isProcessing: true, + enabled: false, + disabledColor: AppColor.auGreyBackground, + textColor: AppColor.white, + indicatorColor: AppColor.white, + ); + }, + ), ], ), ), diff --git a/lib/service/passkey_service.dart b/lib/service/passkey_service.dart index b964230e3..a8f626f20 100644 --- a/lib/service/passkey_service.dart +++ b/lib/service/passkey_service.dart @@ -6,6 +6,7 @@ import 'package:autonomy_flutter/service/auth_service.dart'; import 'package:autonomy_flutter/util/passkey_utils.dart'; import 'package:autonomy_flutter/util/user_account_channel.dart'; import 'package:device_info_plus/device_info_plus.dart'; +import 'package:flutter/cupertino.dart'; import 'package:passkeys/authenticator.dart'; import 'package:passkeys/types.dart'; @@ -20,6 +21,8 @@ abstract class PasskeyService { Future registerFinalize(); + ValueNotifier get isShowingLoginDialog; + static String authenticationType = 'public-key'; } @@ -41,22 +44,15 @@ class PasskeyServiceImpl implements PasskeyService { this._authService, ); - /* - static final AuthenticatorSelectionType _defaultAuthenticatorSelection = - AuthenticatorSelectionType( - requireResidentKey: false, - residentKey: 'discouraged', - userVerification: 'preferred', - ); - - static const _defaultRelayingPartyId = 'accounts.dev.feralfile.com'; - - */ + final ValueNotifier _isShowingLoginDialog = ValueNotifier(false); static const _defaultMediation = MediationType.Optional; static const _preferImmediatelyAvailableCredentials = false; + @override + ValueNotifier get isShowingLoginDialog => _isShowingLoginDialog; + @override Future isPassKeyAvailable() async => await _passkeyAuthenticator.canAuthenticate() && await _doesOSSupport(); @@ -78,7 +74,15 @@ class PasskeyServiceImpl implements PasskeyService { @override Future logInInitiate() async { final loginRequest = await _logInSeverInitiate(); - return await _passkeyAuthenticator.authenticate(loginRequest); + return await _authenticate(loginRequest); + } + + Future _authenticate( + AuthenticateRequestType loginRequest) async { + _isShowingLoginDialog.value = true; + final response = await _passkeyAuthenticator.authenticate(loginRequest); + _isShowingLoginDialog.value = false; + return response; } Future _logInSeverInitiate() async { From 6be9da825d22f2a449ee79d950dce83c05156aa2 Mon Sep 17 00:00:00 2001 From: phuoc Date: Fri, 1 Nov 2024 11:55:25 +0700 Subject: [PATCH 56/57] fix comment Signed-off-by: phuoc --- .../customer_support/support_thread_page.dart | 8 ++- lib/util/dio_interceptors.dart | 1 + lib/util/dio_util.dart | 1 - lib/view/passkey/having_trouble_view.dart | 55 +++++++++---------- 4 files changed, 33 insertions(+), 32 deletions(-) diff --git a/lib/screen/customer_support/support_thread_page.dart b/lib/screen/customer_support/support_thread_page.dart index f69e7c31e..01cda95ec 100644 --- a/lib/screen/customer_support/support_thread_page.dart +++ b/lib/screen/customer_support/support_thread_page.dart @@ -692,9 +692,11 @@ class _SupportThreadPageState extends State { if (jwt != null) { final data = parseJwt(jwt.jwtToken); if (data['sub'] != _userId) { - setState(() { - _userId = data['sub']; - }); + if (mounted) { + setState(() { + _userId = data['sub']; + }); + } } } return; diff --git a/lib/util/dio_interceptors.dart b/lib/util/dio_interceptors.dart index 99143fbdf..abbcad59a 100644 --- a/lib/util/dio_interceptors.dart +++ b/lib/util/dio_interceptors.dart @@ -154,6 +154,7 @@ class AutonomyAuthInterceptor extends Interceptor { final jwt = await injector().getAuthToken(); if (jwt == null) { unawaited(Sentry.captureMessage('JWT is null')); + log.info('JWT is null when calling ${options.uri}'); throw JwtException(message: 'can_not_authenticate_desc'.tr()); } diff --git a/lib/util/dio_util.dart b/lib/util/dio_util.dart index ffe40fbbd..7d78178bd 100644 --- a/lib/util/dio_util.dart +++ b/lib/util/dio_util.dart @@ -24,7 +24,6 @@ Dio feralFileDio(BaseOptions options) { Dio customerSupportDio(BaseOptions options) { final dio = baseDio(options); dio.interceptors.add(CustomerSupportInterceptor()); - dio.interceptors.add(LoggingInterceptor()); return dio; } diff --git a/lib/view/passkey/having_trouble_view.dart b/lib/view/passkey/having_trouble_view.dart index bf29b0f21..b9a0096d0 100644 --- a/lib/view/passkey/having_trouble_view.dart +++ b/lib/view/passkey/having_trouble_view.dart @@ -11,33 +11,32 @@ class HavingTroubleView extends StatelessWidget { const HavingTroubleView({super.key}); @override - Widget build(BuildContext context) { - final lastLoginIssueId = - injector().getAnonymousIssueIds().lastOrNull; - return Padding( - padding: const EdgeInsets.only(bottom: 20), - child: GestureDetector( - onTap: () async { - final payload = lastLoginIssueId == null - ? NewIssuePayload( - reportIssueType: ReportIssueType.Bug, - ) - : DetailIssuePayload( - reportIssueType: ReportIssueType.Bug, - issueID: lastLoginIssueId, - ); - await Navigator.of(context).pushNamed( - AppRouter.supportThreadPage, - arguments: payload, - ); - }, - child: Text( - 'having_trouble'.tr(), - style: Theme.of(context).textTheme.ppMori400Grey14.copyWith( - decoration: TextDecoration.underline, - ), + Widget build(BuildContext context) => Padding( + padding: const EdgeInsets.only(bottom: 20), + child: GestureDetector( + onTap: () async { + final lastLoginIssueId = injector() + .getAnonymousIssueIds() + .lastOrNull; + final payload = lastLoginIssueId == null + ? NewIssuePayload( + reportIssueType: ReportIssueType.Bug, + ) + : DetailIssuePayload( + reportIssueType: ReportIssueType.Bug, + issueID: lastLoginIssueId, + ); + await Navigator.of(context).pushNamed( + AppRouter.supportThreadPage, + arguments: payload, + ); + }, + child: Text( + 'having_trouble'.tr(), + style: Theme.of(context).textTheme.ppMori400Grey14.copyWith( + decoration: TextDecoration.underline, + ), + ), ), - ), - ); - } + ); } From a15802175404ea9b39a74d0f70851d3f19f2316d Mon Sep 17 00:00:00 2001 From: phuoc Date: Fri, 1 Nov 2024 12:41:47 +0700 Subject: [PATCH 57/57] fix comment Signed-off-by: phuoc --- lib/util/dio_interceptors.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/util/dio_interceptors.dart b/lib/util/dio_interceptors.dart index abbcad59a..ab43792e3 100644 --- a/lib/util/dio_interceptors.dart +++ b/lib/util/dio_interceptors.dart @@ -172,12 +172,11 @@ class CustomerSupportInterceptor extends Interceptor { @override Future onRequest( RequestOptions options, RequestInterceptorHandler handler) async { - final isCustomHeaderApi = options.path == CustomerSupportApi.issuesPath; + final isIgnoreHeaderApi = options.path == CustomerSupportApi.issuesPath; - if (isCustomHeaderApi) { + if (isIgnoreHeaderApi) { // do nothing get list issues, create issue: add header at api level } else { - final jwt = await injector().getAuthToken(); final pathElements = options.path.split('/'); final anonymousIssueIds = injector().getAnonymousIssueIds(); @@ -188,6 +187,7 @@ class CustomerSupportInterceptor extends Interceptor { options.headers[CustomerSupportApi.deviceIdHeader] = _configurationService.getAnonymousDeviceId(); } else { + final jwt = await injector().getAuthToken(); // other api, add jwt if (jwt != null) { options.headers['Authorization'] = 'Bearer ${jwt.jwtToken}';