Flutter uni_links duplicate the app every time a link is clicked - flutter

I am implementing a password recovery function based on the url sent to the email. Opening the app based on that url was successful. But instead of directly opening the required page in the app that is in the background, it duplicates the app. Although it still leads me to the password recovery page, now there will be 2 same apps running side by side
Procedure
Enter your email to send the password reset link
Click submit
Open the email containing the recovery link
Duplicate the app and open a recovery password page
Things what happen
Splash screen, first page open in the app, I am trying to do as instructed from uni_links package but still no success. Currently the function getInitialLink has the effect of opening the app based on the recovery link
class SplashController extends GetxController {
final SharedPreferencesHelper _helper = Get.find<SharedPreferencesHelper>();
late StreamSubscription sub;
#override
void onReady() async {
super.onReady();
await checkToken();
}
Future<void> checkToken() async {
await Future.delayed(Duration(seconds: 3));
var token = _helper.getToken();
if (token == null) {
Get.offNamed(Routes.LOGIN);
} else {
Get.offNamed(Routes.MAIN);
}
}
#override
void onInit() {
super.onInit();
initUniLinks();
}
Future<Null> initUniLinks() async {
// Platform messages may fail, so we use a try/catch PlatformException.
try {
String? initialLink = await getInitialLink();
if (initialLink != null) {
print("okay man");
Get.toNamed(Routes.RECOVERY);
}
sub = getLinksStream().listen((link) {
}, onError: (err) {
});
} on PlatformException {
// Handle exception by warning the user their action did not succeed
// return?
}
}
}

I found the solution, actually this answer is already on Stackoverflow, and it's really simple.
In the AndroidManifest.xml file of the app. Find "android:launchMode" and change its old value to singleTask. And here is the result
android:launchMode="singleTask"

Related

Flutter - Firebase Dynamic Link not Working while app is in kill mode

I have integrated Firebase Dynamic link in my Flutter application to open and navigate application users to specific screen in app.
For that first of all I have added below plugin in pubspec.yaml file:
firebase_dynamic_links: ^5.0.5
Then, I have created a separate class to handle related stuffs as below:
class DynamicLinkService {
late BuildContext context;
FirebaseDynamicLinks dynamicLinks = FirebaseDynamicLinks.instance;
Future<void> initDynamicLinks(BuildContext context) async {
this.context = context;
dynamicLinks.onLink.listen((dynamicLinkData) {
var dynamicLink=dynamicLinkData.link.toString();
if (dynamicLink.isNotEmpty &&
dynamicLink.startsWith(ApiConstants.baseUrl) &&
dynamicLink.contains("?")) {
//Getting data here and navigating...
...
...
...
}
}).onError((error) {
print("This is error >>> "+error.message);
});
}
}
Now, I am initialising Deep-link as below in my home_screen:
final DynamicLinkService _dynamicLinkService = DynamicLinkService();
and then calling below method in initState()
#override
void initState() {
SchedulerBinding.instance.addPostFrameCallback((_) async {
await _dynamicLinkService.initDynamicLinks(context);
});
}
This is working like a charm! when my application is in recent mode or in background mode.
But the issue is when the application is closed/Killed, clicking on dynamic link just open the app but could not navigate.
What might be the issue? Thanks in advance.
Let me answer my own question, It might be useful for someone!
So, In above code I forgot to add code to handle dynamic link while the app is in closed/kill mode.
We need to add this code separately:
//this is when the app is in closed/kill mode
final PendingDynamicLinkData? initialLink = await FirebaseDynamicLinks.instance.getInitialLink();
if (initialLink != null) {
handleDynamicLink(initialLink);
}
So, final code looks like as below:
//this is when the app is in closed/kill mode
final PendingDynamicLinkData? initialLink = await FirebaseDynamicLinks.instance.getInitialLink();
if (initialLink != null) {
handleDynamicLink(initialLink);
}
//this is when the app is in recent/background mode
dynamicLinks.onLink.listen((dynamicLinkData) {
handleDynamicLink(dynamicLinkData);
}).onError((error) {
print("This is error >>> "+error.message);
});
Its working like a charm now! That's All.

Deep linking is not working when app is not in background (app closed/killed)

I have implemented this method so that when a user clicks the dynamic link it will be redirected to a specific page. Everything works alright while the app is running, but when I kill/close the app and try to do the same thing, it opens the app on the initial screen (Home Page). How can I make it work in this case?
Future<void> initDynamicLinks() async {
FirebaseDynamicLinks.instance.onLink.listen((dynamicLinkData) {
id = dynamicLinkData.link
.toString()
.substring(dynamicLinkData.link.toString().lastIndexOf('/') + 1);
Get.to(
() => Page(
id: id,
),
);
}).onError((error) {
if (kDebugMode) {
print(error.message);
}
});
}
void initState() {
// TODO: implement initState
initDynamicLinks();
super.initState();
}
I think .onLink.listen() function only get hit when app is resumed from background.
If you want your deeplink work when app have a fresh start then just put this code above .onLink.listen() function...
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
final PendingDynamicLinkData data = await FirebaseDynamicLinks.instance.getInitialLink();
final Uri deepLink = data?.link;
// Here you should navigate to your desired screen
Hope it helps you

linkWithCredential and Flutter Web with Apple

I have a use case where a user, on Flutter Web, needs to link an Apple auth with their existing account, and the email may not match.
However, the only available method for Flutter Web Apple Authentication is signInWithPopUp. If the user's apple email is different from the User firebase account email, a new firebase account is created, and a user is returned, short circuiting the process of linking, this creates a new account in firebase, and I'm unable to linkWithCredential.
My method to try to link accounts is as follows:
Future<String> linkWithAppleWeb() async {
try {
final User user = _auth.currentUser!;
final provider = OAuthProvider("apple.com")
..addScope('email')
..addScope('name');
await _auth.signInWithPopup(provider).then((appleCredential) async {
final authCredential = appleCredential.credential;
await user.linkWithCredential(authCredential!).then((result) {
DatabaseService().updateUserSocialAuth(user.uid, 'apple');
return 'Success';
}).catchError((e) {
return 'Error: $e';
});
});
} catch (e) {
return 'Error: $e';
}
return 'Success';
}
As you would expect, my project starts with Streaming a User Object, and when the pop up signs in, it returns the new user, which rebuilds the entire project. Is there a way to authenticate an apple user without returning a new user? I can link a google or phone authorization method fine. It's apple that is problematic. I don't fully understand why Google doesn't break in the same way, other than Firebase put in the work to ensure the functionality of linking for GoogleSignIn().signIn() I'm not using other social auth methods, and I don't use password/email.
This method is not documented in the Flutter Fire Docs, but works perfectly:
Future<String> linkWithAppleWeb() async {
try {
final User user = _auth.currentUser!;
final provider = OAuthProvider("apple.com")
..addScope('email')
..addScope('name');
await user.linkWithPopup(provider).then((result) {
DatabaseService().updateUserSocialAuth(user.uid, 'apple');
return 'Success';
}).catchError((e) {
return 'Error: $e';
});
} catch (e) {
debugPrint('auth linkWithGoogle error: ${e.toString()}');
return 'Error: $e';
}
return 'Success';
}

Keep the user logged in flutter (The app has 2 different login and main, one for Client and one for Driver)

I am doing an app in flutter and I am working on the authentication part. I want to know how I can keep my user logged in after I reload the app. Now the thing is that my app has 2 kinds of users (Client and Driver). So each has its own space, like sign in and sign up and main (after logging in).
This is the code that I used for logging.
class Initializer extends StatefulWidget {
// Access to this Screen
static String id = 'initializer';
#override
_InitializerState createState() => _InitializerState();
}
class _InitializerState extends State<Initializer> {
// Firebase Stuff
final _auth = FirebaseAuth.instance;
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
User _user;
// To Check if There's a Driver
bool isDriver = true;
void getCurrentUser() async {
try {
final getCurrentUser = _auth.currentUser;
if (getCurrentUser != null) {
getUserKind();
_user = getCurrentUser;
}
} catch (e) {
print(e);
}
}
getUserKind() async {
try {
// To fetch Database for Driver
final QuerySnapshot checkOfDriver =
await _firestore.collection('driver').where('uid', isEqualTo: _user.uid).get().catchError((error) {
print(error);
});
if (checkOfDriver.docs.isEmpty)
setState(() {
isDriver = false;
});
else
setState(() {
isDriver = true;
});
} catch (e) {
print(e);
return null;
}
}
#override
void setState(fn) {
if (mounted) {
super.setState(fn);
}
}
#override
void initState() {
super.initState();
getCurrentUser();
}
#override
Widget build(BuildContext context) {
getCurrentUser();
SizeConfig().init(context);
return _user == null
? WelcomeScreen()
: isDriver
? DriverMain()
: ClientMain();
}
}
It's actually working but not properly, because when I reload the app while I'm logging in as a Client, the app shows me DriverMain at the beginning for one second then it switches to the right side which is ClientMain and that causes me some errors sometimes, and it's not an efficient work anyway.
So, what I should add to the code or ...
Firebase already persists the users credentials, and restores them automatically when the app restarts.
But this is an asynchronous process, as it requires a call to the server. By the time your getCurrentUser = _auth.currentUser code runs, that asynchronous process hasn't finished yet, so you get null.
To properly respond to the auth state being restored (and other changes), you'll want to use an auth state change listener as shown in the documentation on authentication state:
FirebaseAuth.instance
.authStateChanges()
.listen((User? user) {
if (user == null) {
print('User is currently signed out!');
} else {
print('User is signed in!');
}
});
If you want to use this in your UI, you'll typically wrap it in a StreamBuilder instead of calling listen yourself.

Exception handling in flutter + VS Code

I have a simple functionality for user authentication. When user clicks Login buttonm than callback calls login method from SecurityBloc which in its turn calls execute method of ApiProvider.
If user enter wrong password than method _handleResponse throws ApiException with error description which I am expecting to be catched in method login of SecurityBloc. And it works as expected when I run project under the web. I see snackbar with error message.
The problem occurred when I run project under Android. When user enter wrong password than VS Code (I use it) stops on line with throw ApiException('invalid authentication response');, i.e. debugger thinks that this exception is unhandled! But it is catched and handled (see code). When I click button continue on debugger control panel, the highlighted row jumps over the code and at the end I see the error message in snackbar.
So is it possible to skip (fix) this situation? Maybe is it knowing bug and there is a workaround?
P.S. If I checked off the "Uncaught exception" checkbox looks fine but it is not the case because now I may pass really uncaucht exceptions.
Any ideas?
class ApiProvider {
/// Executes HTTP request
Map<String, dynamic> execute(url, query, ...) async {
final response = await http.post(url,query:query);
return _handleResponse(response);
}
/// Parses HTTP response
Map<String, dynamic> _handleResponse(Response response) {
if (!response.contains('user')) {
throw ApiException('invalid password');
}
... // other statements
}
}
class SecurityBloc {
Future<AuthEvent> login(String user, String password) async {
try {
final data = api.execute()
if (data == null) {
throw ApiException('invalid authentication response');
}
final token = _parseData(data); // Can throws FormatException
return AuthEvent.ok(token);
} on ClientException catch(e) {
return AuthEvent.error(e.message);
} on FormatException catch(e) {
return AuthEvent.error(e.message);
} on ApiException catch(e) {
return AuthEvent.error(e.message);
}
}
}
class _LoginState extends State<Login> {
final securityBloc = SecutiryBloc();
#override
Widget build(BuildContext context) {
return
...
FlatButton(
child: Text('Login'),
onPressed: () async {
final authEvent = await securityBloc.login(...);
if (authEvent.failed) {
ScaffoldMessenger.of(context).showSnackbar(...); // Show authentication error
} else {
// access granted
}
},
),
...
}
Please see this https://github.com/FirebaseExtended/flutterfire/issues/3475 that I raised...if you follow the thread and the link at the end it would appear a fix was posted to master in early October....don't know when it will make it through the releases to stable. Basically though, this is an issue that impacts the IDE and won't manifest itself in the released app on a device.