Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion app/lib/backend/http/api/apps.dart
Original file line number Diff line number Diff line change
Expand Up @@ -505,7 +505,6 @@ Future<Map?> 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) {
Expand Down Expand Up @@ -587,3 +586,21 @@ Future<String?> generateUsername(String handle) async {
return null;
}
}

Future<bool> 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;
}
}
10 changes: 2 additions & 8 deletions app/lib/pages/onboarding/auth.dart
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,7 @@ class _AuthComponentState extends State<AuthComponent> {
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) {
Expand Down Expand Up @@ -103,10 +100,7 @@ class _AuthComponentState extends State<AuthComponent> {
],
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) {
Expand Down
75 changes: 63 additions & 12 deletions app/lib/providers/auth_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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<bool> migrateAppOwnerId(String oldId) async {
return await apps_api.migrateAppOwnerId(oldId);
}
}
8 changes: 8 additions & 0 deletions backend/database/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -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})
20 changes: 12 additions & 8 deletions backend/routers/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, \
Expand Down Expand Up @@ -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']

Expand All @@ -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
Expand Down Expand Up @@ -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']

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)


# ******************************************************
Expand Down