iOS Firebase Dynamic link not handled on cold start iOS 14 - ios

When opening dynamic links on my device, my function in SceneDelegate that handles dynamic links runs fine while the app is running in the background but not when the app is completely shut off. When I do click the dynamic link while the app is off, the app opens but the dynamic link is not handled. My scene delegate function looks like this:
SceneDelegate Function
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
guard let _ = (scene as? UIWindowScene) else { return }
if let incomingURL = userActivity.webpageURL {
print("Incoming URL is \(incomingURL)")
_ = DynamicLinks.dynamicLinks().handleUniversalLink(incomingURL) { (dynamicLink, error) in
guard error == nil else{
print("Found an error! \(error!.localizedDescription)")
return
}
if let dynamicLink = dynamicLink {
self.handleIncomingDynamicLink(dynamicLink)
}
}
}
}
any help understanding this issue is greatly appreciated!

Since your target is iOS 14 , You can use latest APIs,
So you can use .onOpenURL() modifier on any view.
.onOpenURL { url in
_ = DynamicLinks.dynamicLinks().handleUniversalLink(url) { (dynamicLink, error) in
guard error == nil else{
print("Found an error! \(error!.localizedDescription)")
return
}
if let dynamicLink = dynamicLink {
self.handleIncomingDynamicLink(dynamicLink)
}
}
}

I was having the same issue, but I used the solution proposed from #Yodagama and it works totally fine. In my case I am loading a firebase dynamic link. The only problem I am facing is that if the user doesn't have the app installed, the link first redirects you to apple store to download it and once you have it you have to open the link again in order to perform the function.
I am putting my code here, hope it helps somebody:
import SwiftUI
import Firebase
#main
struct Universal_Link_SwiftUI2_0App: App {
init() {
FirebaseApp.configure()
}
var body: some Scene {
WindowGroup {
ContentView().onOpenURL { url in
_ = DynamicLinks.dynamicLinks().handleUniversalLink(url) { (dynamicLink, error) in
guard error == nil else{
print("Found an error! \(error!.localizedDescription)")
return
}
if let dynamicLink = dynamicLink {
self.handleIncomingDynamicLink(dynamicLink)
}
}
}
}
}

Related

Handling dynamic link from Firebase in swift

I have made a dynamic link in Firebase, which op my iOS app, when I click it.
The problem is, that I can't print information about the link.
I use this function in the AppDelegate file to handle the dynamic link:
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: #escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
print("Handeling dynamic link")
if let incomingURL = userActivity.webpageURL {
print("Incoming URL is \(incomingURL)")
let linkHandled = DynamicLinks.dynamicLinks().handleUniversalLink(incomingURL) { (dynamicLink, error) in
guard error == nil else {
print("Found an error! \(error!.localizedDescription)")
return
}
if let dynamicLink = dynamicLink {
self.handleIncomingDynamicLink(dynamicLink)
}
}
if linkHandled {
return true
} else {
// Maybe do other things with our incoming url
return false
}
}
return false
}
When I click on a dynamic link and open the app, none of the print statements is printed to the console.
It seems like this function is never tapped into.
The handleDynamicLink function is:
func handleIncomingDynamicLink(_ dynamicLink: DynamicLink) {
guard let url = dynamicLink.url else {
print("That is weird. My dynamic link object has no url")
return
}
print("Your incoming link parameter is \(url.absoluteString)")
}
I want to print information about the url so I can debug and use the information in the url to redirect to pages in the app.
When I'm testing this, I run the app on an iPhone connected to my mac.
I don't run it on the iOS simulator.
The code in your UIApplication delegate method looks good. The only difference I see with the way you have yours compared to mine is I'm returning true in the last line & you're returning false.
By the way, put breakpoints in that UIApplication delegate method & let me know if you're ever even getting to the first line in that delegate method. Are you able to hit any breakpoints in your handleIncomingDynamicLink() method?
Also, try adding this in applicationDidBecomeActive:
func applicationDidBecomeActive(_ application: UIApplication) {
guard let url = self.launchURL else { return }
self.launchURL = nil
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0, execute: { // wait to init. notifs.
DynamicLinks.dynamicLinks().handleUniversalLink(url) { (dynamiclink, error) in
if let dynamiclink = dynamiclink {
self.handleIncomingDynamicLink(dynamiclink)
}
}
})
}
And add this:
func application(_ app: UIApplication,
open url: URL,
options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
print("RECEIVED A URL THROUGH A CUSTOM SCHEME: \(url.absoluteString)")
if let dynamicLink = DynamicLinks.dynamicLinks().dynamicLink(fromCustomSchemeURL: url) {
self.handleIncomingDynamicLink(dynamicLink)
return true
} else { // Maybe handle Google or Facebook sign-in here
return false
}
}

How to update state from Phone view on WatchOS view?

I'm using a companion app to authorize a user with a 3rd party service. Once authorized, I update a UserDefaults variable to true. On the companion app side, the view updates correctly and shows that the user has been authenticated. However, on the watch OS side the view does not update. Would I need to use the Watch Connectivity API and send a message to the watch to update the state? Or is there a simple way?
Phone App
struct AuthenticationView: View {
#State private var startingWebAuthenticationSession = false
#AppStorage("authorized") private var authorized = false
var body: some View {
Group {
if !authorized {
VStack {
Button("Connect", action: { self.startingWebAuthenticationSession = true })
.webAuthenticationSession(isPresented: $startingWebAuthenticationSession) {
WebAuthenticationSession(
url: URL(string: "https://service.com/oauth/authorize?scope=email%2Cread_stats&response_type=code&redirect_uri=watch%3A%2F%2Foauth-callback&client_id=\(clientId)")!,
callbackURLScheme: callbackURLScheme
) { callbackURL, error in
guard error == nil, let successURL = callbackURL else {
return
}
let oAuthCode = NSURLComponents(string: (successURL.absoluteString))?.queryItems?.filter({$0.name == "code"}).first
guard let authorizationCode = oAuthCode?.value else { return }
let url = URL(string: "https://service.com/oauth/token")
var request = URLRequest(url: url!)
request.httpMethod = "POST"
let params = "client_id=\(clientId)&client_secret=\(clientSecret)&grant_type=authorization_code&code=\(authorizationCode)&redirect_uri=\(callbackURLScheme)://oauth-callback";
request.httpBody = params.data(using: String.Encoding.utf8);
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
if let error = error {
print("Error took place \(error)")
return
}
if let data = data, let response = String(data: data, encoding: .utf8) {
let accessTokenResponse: AccessTokenResponse = try! JSONDecoder().decode(AccessTokenResponse.self, from: response.data(using: .utf8)!)
let defaults = UserDefaults.standard
authorized = true
startingWebAuthenticationSession = false
defaults.set(accessTokenResponse.access_token, forKey: DefaultsKeys.accessToken) //TODO: Store securely
ConnectivityService.shared.send(authorized: true)
}
}
task.resume()
}
.prefersEphemeralWebBrowserSession(false)
}
}
}
else {
VStack {
Text("Authenticated!")
}
}
}
}
}
WatchOS
import SwiftUI
struct ConnectView: View {
#ObservedObject var connectivityService: ConnectivityService
var body: some View {
if !$connectivityService.authorized.wrappedValue {
VStack {
Text("Open the app on your primary device to connect.")
}
}
else {
//Some other view
}
}
}
EDIT:
Trying with Watch Connectivity API but the issue I'm experiencing is that when I authenticate from the phone, it'll take some time for the ConnectView to update the authorized variable. I know Watch Connectivity API doesn't update right away but at minimum I'd need some way for the watch to pick up that a secret access token has been retrieved and it can transition to the next view; whether that's through a shared state variable, UserDefaults, or whatever other mechanism.
Here is the ConnectivityService class I'm using:
import Foundation
import Combine
import WatchConnectivity
final class ConnectivityService: NSObject, ObservableObject {
static let shared = ConnectivityService()
#Published var authorized: Bool = false
override private init() {
super.init()
#if !os(watchOS)
guard WCSession.isSupported() else {
return
}
#endif
WCSession.default.delegate = self
WCSession.default.activate()
}
public func send(authorized: Bool, errorHandler: ((Error) -> Void)? = nil) {
guard WCSession.default.activationState == .activated else {
return
}
#if os(watchOS)
guard WCSession.default.isCompanionAppInstalled else {
return
}
#else
guard WCSession.default.isWatchAppInstalled else {
return
}
#endif
let authorizationInfo: [String: Bool] = [
DefaultsKeys.authorized: authorized
]
WCSession.default.sendMessage(authorizationInfo, replyHandler: nil)
WCSession.default.transferUserInfo(authorizationInfo)
}
}
extension ConnectivityService: WCSessionDelegate {
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { }
func session(
_ session: WCSession,
didReceiveUserInfo userInfo: [String: Any] = [:]
) {
let key = DefaultsKeys.authorized
guard let authorized = userInfo[key] as? Bool else {
return
}
self.authorized = authorized
}
func session(_ session: WCSession, didReceiveMessage message: [String : Any]) {
self.authorized = true
}
#if os(iOS)
func sessionDidBecomeInactive(_ session: WCSession) {
}
func sessionDidDeactivate(_ session: WCSession) {
WCSession.default.activate()
}
#endif
}
I tried doing these two lines but they have varying results:
WCSession.default.sendMessage(authorizationInfo, replyHandler: nil)
WCSession.default.transferUserInfo(authorizationInfo)
In the first line, XCode will say that no watch app could be found, even though I'm connected to both physical devices through XCode; launch phone first then watch. I believe the first one is immediate and the second is more of when the queue feels like it. Sometimes if I hard close the watch app, it'll pick up the state change in the authorized variable, sometimes it won't. Very frustrating inter-device communication.
UserDefaults doesn't pick up the access token value on the watch side. Maybe I have to use App Groups?
I do see this error on the Watch side:
Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates.
So I thought to try and encapsulate the self.authorized = authorized call into something like:
DispatchQueue.main.async {
self.authorized = authorized
}
But it didn't do anything as far as solving the immediate state change issue.

Dynamic link object has no url

I created the DynamicLink for my firebase project when I am trying to receive the link I am getting "That's weird. My dynamic link object has no url".
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: #escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
if let incomingURL = userActivity.webpageURL{
print("Incoming URL is \(incomingURL)")
let linkHandled = DynamicLinks.dynamicLinks().handleUniversalLink(incomingURL)
{(dynamicLink, error) in
guard error == nil else{
print("Found an error! \(error!.localizedDescription)")
return
}
if let dynamicLink = dynamicLink{
self.handleIncomingDynamicLink(dynamicLink)
}
}
if linkHandled{
return true
}
else{
return false
}
}
return false
}
func handleIncomingDynamicLink(_ dynamicLink: DynamicLink){
guard let url = dynamicLink.url else{
print("That's weird. My dynamic link object has no url")
return
}
print("Your incoming link parameter is\(url.absoluteString)")
}
After checking all the blogs and posted this issue on firebase, I didn't find any solution for this but I came up with this concrete solution and it will work definitely
here: dynamicLinkURL is your main dynamic link and shortHandURL is your deeplink URL which is associated with your dynamic link. I hope the below snippet will help you.
func dynamicLinkhandler(_ dynamicLinkURL: URL, onCompletion: #escaping(_ success: Bool) -> Void) {
URLSession.shared.dataTask(with: dynamicLinkURL) { (data, response, error) in
guard error == nil else {
print("Found Error \(String(describing: error?.localizedDescription)))")
return
}
guard let shortHandURL = response?.url, shortHandURL != dynamicLinkURL else {
print("Thats Weird, my dynamic link has no URL")
onCompletion(false)
return
}
onCompletion(true)
}.resume()
}
Double check that the bundle id you set up in the dynamic link wizard creation within the firebase console it's the one you are running the app into.
I have three different bundle ids (dev, enterprise, production) and, for instance, if a set in the link the production bundle id but the app runs the dev bundle id, instead of returning back some error it returned an honest dynamicLink object but with a nil value in the url.

Firebase Passwordless Email Authentication doesn't open app on iOS

I'm trying and failing badly to implement the cool Firebase email link login feature. I successfully setup sending an email link. However, I can't get the email link to open up the app. It just opens up the preview page like it can't open the app.
I've tested the dynamic link I setup and I can get it to open up the app in a device. I just can't get the email link to do the same.
Code in my app:
func sendFirebaseEmailLink() {
let actionCodeSettings = ActionCodeSettings.init()
// userEmail comes from a textField
let email = userEmail
actionCodeSettings.url = URL.init(string: String(format: "https://<myappname>.firebaseapp.com/?email=%#", email))
// The sign-in operation has to always be completed in the app.
actionCodeSettings.handleCodeInApp = true
actionCodeSettings.setIOSBundleID(Bundle.main.bundleIdentifier!)
Auth.auth().sendSignInLink(toEmail: email,
actionCodeSettings: actionCodeSettings) { error in
if let error = error {
print(error.localizedDescription)
return
}
else {
UserDefaults.standard.set(email, forKey: "Email")
print("email sent to user")
}
}
}
When I say I've successfully gotten my dynamic link to open the app what I mean is when I follow the link I created (mylinkname.page.link/emaillogin) on a device that has the app installed, it opens the app. Because of that and [this helpful Firebase video][1] on setting up a dynamic link it seems like I've got those details correct and the issue is with the code, but I'm new to this so I'm not sure.
I've spend few days going around in circles to figure this out, and trying to parse the dense Firebase documentation, so any ideas are greatly appreciated.
I finally figured it out. The code was fine. It was an issue related to the dynamic link. I had a couple links setup in Firebase because I had to create a new Bundle ID at one point. When I deleted out the old one in Firebase the email link started working.
It shows up in my app association site like this, and oddly still does even though I deleted out the old link, but at least it works now!
{"applinks":{"apps":[],"details":[{"appID":"TEAMID.com.OLDBUNDLEIDENTIFIER.APPNAME","paths":["NOT //*","/*"]},{"appID":"TEAMID.com.NEWBUNDLEIDENTIFIER.APPNAME","paths":["NOT //","/"]}]}}
UPDATE: My full code to implement passwordless email login is below. It was painful for me to piece together using the documentation so hopefully this saves you the trouble.
Key steps assuming you understand the basics of Firebase Setup.
1) Setup a Dynamic Link Using the Firebase Video tutorial.
2) Code in View Controller:
var userEmail: String?
var link: String?
func sendFirebaseEmailLink() {
let actionCodeSettings = ActionCodeSettings.init()
let email = userEmail
actionCodeSettings.url = URL.init(string: String(format: "https://<myappname>.page.link/emaillogin/?email=%#", email!))
// The sign-in operation has to always be completed in the app.
actionCodeSettings.handleCodeInApp = true
actionCodeSettings.setIOSBundleID(Bundle.main.bundleIdentifier!)
Auth.auth().sendSignInLink(toEmail: email!,
actionCodeSettings: actionCodeSettings) { error in
if let error = error {
print(error.localizedDescription)
return
}
else {
UserDefaults.standard.set(email, forKey: "Email")
print("email sent to user")
}
// TODO: Notify user to check email and click the link.
}
}
// Sign in user after they clicked email link called from AppDelegate
#objc func signInUserAfterEmailLinkClick() {
// Get link url string from the dynamic link captured in AppDelegate.
if let link = UserDefaults.standard.value(forKey: "Link") as? String {
self.link = link
}
// Sign user in with the link and email.
Auth.auth().signIn(withEmail: userEmail!, link: link!) { (result, error) in
if error == nil && result != nil {
if (Auth.auth().currentUser?.isEmailVerified)! {
print("User verified with passwordless email")
// TODO: Do something after user verified like present a new View Controller
}
else {
print("User NOT verified by passwordless email")
}
}
else {
print("Error with passwordless email verfification: \(error?.localizedDescription ?? "Strangely, no error avaialble.")")
}
}
}
3) Code in AppDelegate
// For Passwordless Email Login to Handle Dynamic Link after User Clicks Email Link
func application(_ application: UIApplication, continue userActivity: NSUserActivity,
restorationHandler: #escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
if let incomingURL = userActivity.webpageURL {
print("Incoming URL is \(incomingURL)")
// Parse incoming
let linkHandled = DynamicLinks.dynamicLinks().handleUniversalLink(incomingURL) { (dynamicLink, error) in
guard error == nil else {
print("Found an error: \(error!.localizedDescription)")
return
}
if let dynamicLink = dynamicLink {
self.handleIncomingDynamicLink(dynamicLink)
}
}
if linkHandled {
return true
}
else {
// Maybe do other things with dynamic links in future?
return false
}
}
return false
}
// Handles the link and saves it to userDefaults to assist with login.
func handleIncomingDynamicLink(_ dynamicLink: DynamicLink) {
guard let url = dynamicLink.url else {
print("My dynamic link object has no url")
return
}
print("Incoming link parameter is \(url.absoluteString)")
let link = url.absoluteString
if Auth.auth().isSignIn(withEmailLink: link) {
// Save link to userDefaults to help finalize login.
UserDefaults.standard.set(link, forKey: "Link")
// Send notification to ViewController to push the First Time Login VC
NotificationCenter.default.post(
name: Notification.Name("SuccessfulPasswordlessEmailNotification"), object: nil, userInfo: nil)
}
}
For anyone using SwiftUI with AppDelegate and SceneDelegate files instead of UIKit, here's what I've done:
Create a function to send a link to the user's email
func sendSignLink(email: String) async throws {
do {
let actionCodeSettings = ActionCodeSettings()
actionCodeSettings.url = URL(string: "*enter your Firebase Dynamic link here*")
actionCodeSettings.handleCodeInApp = true
actionCodeSettings.setIOSBundleID(Bundle.main.bundleIdentifier!)
try await Auth.auth().sendSignInLink(toEmail: email, actionCodeSettings: actionCodeSettings)
UserDefaults.standard.set(email, forKey: "email")
}
catch {
throw error
}
}
In the SceneDelegate file, import FirebaseDynamicLinks and add the below code
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
if let incomingURL = userActivity.webpageURL {
print("\n \nIncoming URL is \(incomingURL)")
_ = DynamicLinks.dynamicLinks().handleUniversalLink(incomingURL) { (dynamicLink, error) in
guard error == nil else {
print("\n \nError with handling incoming URL: \(error!.localizedDescription)")
return
}
if let dynamicLink = dynamicLink {
guard let url = dynamicLink.url else {
print("\n \nDynamic link object has no url")
return
}
print("\n \nIncoming link parameter is \(url.absoluteString)")
let link = url.absoluteString
if Auth.auth().isSignIn(withEmailLink: link) {
// Send notification to trigger the rest of the sign in sequence
NotificationCenter.default.post(name: Notification.Name("Success"), object: nil, userInfo: ["link": link])
} else {
// Send error notification
NotificationCenter.default.post(name: Notification.Name("Error"), object: nil, userInfo: nil)
}
}
}
}
}
Create a function to handle the sign in after the user has clicked on the link in their email
func signInWithEmail(link: String) async throws {
do {
let email = UserDefaults.standard.value(forKey: "email")
try await Auth.auth().signIn(withEmail: email, link: link)
}
catch {
throw error
}
}
In a relevant view, handle the notifications which get posted
struct MyView: View {
var body: some View {
VStack {
Text("View")
}
.onReceive(NotificationCenter.default.publisher(for: Notification.Name("Success"))) { notificationInfo in
if let userInfo = notificationInfo.userInfo {
if let link = userInfo["link"] as? String {
Task.init {
do {
try await signInWithEmail(link: link)
} catch {
print(error)
}
}
}
}
}
.onReceive(NotificationCenter.default.publisher(for: Notification.Name("Error"))) { _ in
//do something with error
}
}
}

Cannot delete user during the registration when ios APP is killed firebase

I am now currently working on an IOS app. So I want to delete the user account during the registration when app get killed such as crash, or users force to close.
So I create NSNotification (appWillTerminate). As you can see from the code below, whenever app is closed at this page, the notification will get triggered, because I can see my print statement "hello world" in the console. I do not think there are any syntax error or anything. But it just can't delete the user from Authentication.
#objc func deleteAccount(notification: NSNotification){
print("hello world")
guard let userID = Auth.auth().currentUser?.uid else {return}
Database.database().reference().child("users").child(userID).removeValue(completionBlock: { (error, ref) in
if(error != nil){
print(error)
}
let user = Auth.auth().currentUser
user?.delete { error in
if let error = error {
print(error.localizedDescription)
// An error happened.
} else {
print("delete success")
do{
try Auth.auth().signOut()
}catch let logoutError{
print(logoutError)
}
}
}
})
}

Resources