diff --git a/app/lib/backend/http/api/apps.dart b/app/lib/backend/http/api/apps.dart index c27c9eb293..cc9873e335 100644 --- a/app/lib/backend/http/api/apps.dart +++ b/app/lib/backend/http/api/apps.dart @@ -505,7 +505,6 @@ Future getTwitterProfileData(String handle) async { } } - Future<(bool, String?)> verifyTwitterOwnership(String username, String handle, String? personaId) async { var url = '${Env.apiBaseUrl}v1/personas/twitter/verify-ownership?username=$username&handle=$handle'; if (personaId != null) { @@ -587,3 +586,21 @@ Future generateUsername(String handle) async { return null; } } + +Future migrateAppOwnerId(String oldId) async { + var response = await makeApiCall( + url: '${Env.apiBaseUrl}v1/apps/migrate-owner?old_id=$oldId', + headers: {}, + body: '', + method: 'POST', + ); + try { + if (response == null || response.statusCode != 200) return false; + log('migrateAppOwnerId: ${response.body}'); + return true; + } catch (e, stackTrace) { + debugPrint(e.toString()); + CrashReporting.reportHandledCrash(e, stackTrace); + return false; + } +} diff --git a/app/lib/pages/onboarding/auth.dart b/app/lib/pages/onboarding/auth.dart index a40a234d3b..5ef0a84df6 100644 --- a/app/lib/pages/onboarding/auth.dart +++ b/app/lib/pages/onboarding/auth.dart @@ -40,10 +40,7 @@ class _AuthComponentState extends State { SizedBox(height: MediaQuery.of(context).textScaleFactor > 1.0 ? 18 : 32), if (Platform.isIOS) ...[ SignInButton.withApple( - title: (FirebaseAuth.instance.currentUser?.isAnonymous == true && - SharedPreferencesUtil().hasPersonaCreated) - ? 'Link with Apple' - : 'Sign in with Apple', + title: 'Sign in with Apple', onTap: () async { final user = FirebaseAuth.instance.currentUser; if (user != null && user.isAnonymous && SharedPreferencesUtil().hasPersonaCreated) { @@ -103,10 +100,7 @@ class _AuthComponentState extends State { ], const SizedBox(height: 12), SignInButton.withGoogle( - title: (FirebaseAuth.instance.currentUser?.isAnonymous == true && - SharedPreferencesUtil().hasPersonaCreated) - ? 'Link with Google' - : 'Sign in with Google', + title: 'Sign in with Google', onTap: () async { final user = FirebaseAuth.instance.currentUser; if (user != null && user.isAnonymous && SharedPreferencesUtil().hasPersonaCreated) { diff --git a/app/lib/providers/auth_provider.dart b/app/lib/providers/auth_provider.dart index 0eac0b51cb..e097a4c2a8 100644 --- a/app/lib/providers/auth_provider.dart +++ b/app/lib/providers/auth_provider.dart @@ -9,6 +9,7 @@ import 'package:friend_private/utils/analytics/mixpanel.dart'; import 'package:instabug_flutter/instabug_flutter.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:google_sign_in/google_sign_in.dart'; +import 'package:friend_private/backend/http/api/apps.dart' as apps_api; class AuthenticationProvider extends BaseProvider { final FirebaseAuth _auth = FirebaseAuth.instance; @@ -143,14 +144,37 @@ class AuthenticationProvider extends BaseProvider { idToken: googleAuth.idToken, ); - await FirebaseAuth.instance.currentUser?.linkWithCredential(credential); + try { + await FirebaseAuth.instance.currentUser?.linkWithCredential(credential); + } catch (e) { + if (e is FirebaseAuthException && e.code == 'credential-already-in-use') { + // Get existing user credentials + final existingCred = e.credential; + final oldUserId = FirebaseAuth.instance.currentUser?.uid; + + // Sign out current anonymous user + await FirebaseAuth.instance.signOut(); + + // Sign in with existing account + await FirebaseAuth.instance.signInWithCredential(existingCred!); + final newUserId = FirebaseAuth.instance.currentUser?.uid; + await getIdToken(); + + SharedPreferencesUtil().onboardingCompleted = false; + SharedPreferencesUtil().uid = newUserId ?? ''; + SharedPreferencesUtil().email = FirebaseAuth.instance.currentUser?.email ?? ''; + SharedPreferencesUtil().givenName = FirebaseAuth.instance.currentUser?.displayName?.split(' ')[0] ?? ''; + if (oldUserId != null && newUserId != null) { + await migrateAppOwnerId(oldUserId); + } + return; + } + AppSnackbar.showSnackbarError('Failed to link with Google, please try again.'); + rethrow; + } } catch (e) { print('Error linking with Google: $e'); - if (e is FirebaseAuthException && e.code == 'credential-already-in-use') { - AppSnackbar.showSnackbarError('An account with this email already exists on our platform.'); - } else { - AppSnackbar.showSnackbarError('Failed to link with Apple, please try again.'); - } + AppSnackbar.showSnackbarError('Failed to link with Google, please try again.'); rethrow; } finally { setLoading(false); @@ -161,17 +185,44 @@ class AuthenticationProvider extends BaseProvider { setLoading(true); try { final appleProvider = AppleAuthProvider(); - await FirebaseAuth.instance.currentUser?.linkWithProvider(appleProvider); - } catch (e) { - print('Error linking with Apple: $e'); - if (e is FirebaseAuthException && e.code == 'credential-already-in-use') { - AppSnackbar.showSnackbarError('An account with this email already exists on our platform.'); - } else { + try { + await FirebaseAuth.instance.currentUser?.linkWithProvider(appleProvider); + } catch (e) { + if (e is FirebaseAuthException && e.code == 'credential-already-in-use') { + // Get existing user credentials + final existingCred = e.credential; + final oldUserId = FirebaseAuth.instance.currentUser?.uid; + + // Sign out current anonymous user + await FirebaseAuth.instance.signOut(); + + // Sign in with existing account + await FirebaseAuth.instance.signInWithCredential(existingCred!); + final newUserId = FirebaseAuth.instance.currentUser?.uid; + await getIdToken(); + + SharedPreferencesUtil().onboardingCompleted = false; + SharedPreferencesUtil().uid = newUserId ?? ''; + SharedPreferencesUtil().email = FirebaseAuth.instance.currentUser?.email ?? ''; + SharedPreferencesUtil().givenName = FirebaseAuth.instance.currentUser?.displayName?.split(' ')[0] ?? ''; + if (oldUserId != null && newUserId != null) { + await migrateAppOwnerId(oldUserId); + } + return; + } AppSnackbar.showSnackbarError('Failed to link with Apple, please try again.'); + rethrow; } + } catch (e) { + print('Error linking with Apple: $e'); + AppSnackbar.showSnackbarError('Failed to link with Apple, please try again.'); rethrow; } finally { setLoading(false); } } + + Future migrateAppOwnerId(String oldId) async { + return await apps_api.migrateAppOwnerId(oldId); + } } diff --git a/backend/database/apps.py b/backend/database/apps.py index 5edadb7858..e8679208b5 100644 --- a/backend/database/apps.py +++ b/backend/database/apps.py @@ -347,3 +347,11 @@ def add_persona_to_db(persona_data: dict): def update_persona_in_db(persona_data: dict): persona_ref = db.collection('plugins_data').document(persona_data['id']) persona_ref.update(persona_data) + + +def migrate_app_owner_id_db(new_id: str, old_id: str): + filters = [FieldFilter('uid', '==', old_id), FieldFilter('deleted', '==', False)] + apps_ref = db.collection('plugins_data').where(filter=BaseCompositeFilter('AND', filters)).stream() + for app in apps_ref: + app_ref = db.collection('plugins_data').document(app.id) + app_ref.update({'uid': new_id}) diff --git a/backend/routers/apps.py b/backend/routers/apps.py index ad388dce3c..b338ac524e 100644 --- a/backend/routers/apps.py +++ b/backend/routers/apps.py @@ -9,7 +9,7 @@ from database.apps import change_app_approval_status, get_unapproved_public_apps_db, \ add_app_to_db, update_app_in_db, delete_app_from_db, update_app_visibility_in_db, \ get_personas_by_username_db, get_persona_by_id_db, delete_persona_db, get_persona_by_twitter_handle_db, \ - get_persona_by_username_db + get_persona_by_username_db, migrate_app_owner_id_db from database.auth import get_user_from_uid from database.notifications import get_token_only from database.redis_db import delete_generic_cache, get_specific_user_review, increase_app_installs_count, \ @@ -92,7 +92,7 @@ def create_app(app_data: str = Form(...), file: UploadFile = File(...), uid=Depe if 'external_integration' in data: ext_int = data['external_integration'] if (not ext_int.get('app_home_url') and - ext_int.get('auth_steps') and + ext_int.get('auth_steps') and len(ext_int['auth_steps']) == 1): ext_int['app_home_url'] = ext_int['auth_steps'][0]['url'] @@ -106,7 +106,8 @@ def create_app(app_data: str = Form(...), file: UploadFile = File(...), uid=Depe @router.post('/v1/personas', tags=['v1']) -async def create_persona(persona_data: str = Form(...), file: UploadFile = File(...), uid=Depends(auth.get_current_user_uid)): +async def create_persona(persona_data: str = Form(...), file: UploadFile = File(...), + uid=Depends(auth.get_current_user_uid)): data = json.loads(persona_data) data['approved'] = False data['deleted'] = False @@ -221,7 +222,7 @@ def update_app(app_id: str, app_data: str = Form(...), file: UploadFile = File(N if 'external_integration' in data: ext_int = data['external_integration'] if (not ext_int.get('app_home_url') and - ext_int.get('auth_steps') and + ext_int.get('auth_steps') and len(ext_int['auth_steps']) == 1): ext_int['app_home_url'] = ext_int['auth_steps'][0]['url'] @@ -467,10 +468,10 @@ async def get_twitter_profile_data(handle: str, uid: str = Depends(auth.get_curr @router.get('/v1/personas/twitter/verify-ownership', tags=['v1']) async def verify_twitter_ownership_tweet( - username: str, - handle: str, - uid: str = Depends(auth.get_current_user_uid), - persona_id: str | None = None + username: str, + handle: str, + uid: str = Depends(auth.get_current_user_uid), + persona_id: str | None = None ): # Get user info to check auth provider user = get_user_from_uid(uid) @@ -509,6 +510,9 @@ async def get_twitter_initial_message(username: str, uid: str = Depends(auth.get return {'message': ''} +@router.post('/v1/apps/migrate-owner', tags=['v1']) +def migrate_app_owner(old_id, uid: str = Depends(auth.get_current_user_uid)): + migrate_app_owner_id_db(uid, old_id) # ******************************************************