How to access Firebase custom claims on client using SwiftUI? - swift

I am attempting to access a user's custom claim on my iOS client in order to govern UI flow at login, following the example in the Firebase documentation. However, when I try to integrate the coding example provided it causes multiple "Type 'Void' cannot conform to 'View'" errors in Xcode and does not compile.
In my code below, I've simply added a User object on which getIDTokenResult is called and I display a LoginView if that user is nil.
Why am I receiving these errors?
struct ContentView: View {
#ObservedObject private var authenticationService = AuthenticationService()
var body: some View {
if let user = authenticationService.user {
user.getIDTokenResult(completion: { (result, error) in
guard let admin = result?.claims?["admin"] as? NSNumber else {
// Show regular UI.
ClientHomeView()
return
}
if admin.boolValue {
// Show admin UI.
AdminHomeView()
} else {
// Show regular UI.
ClientHomeView()
}
})
}
else {
LoginView()
}
}
}

Related

How to process deep link from an aps notification using the same #EnvironmentObject used in onOpenUrl without using a singleton in AppDelegate

I am trying to coordinate my deep link with push notifications so they both process my custom url scheme in the same manner and navigate to the appropriate view. The challenge seems to be with push notifications and how to process the link passed, through an apn from Azure Notification Hubs, using the same #EnvironmentObject that the onOpenUrl uses without breaking the SwiftUI paradigm and using a singleton.
Here is how I trigger the notification on my simulator, which works fine and navigates me to the appropriate view:
xcrun simctl openurl booted "myapp://user/details/123456"
Which triggers this the onOpenUrl in this code:
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(sessionInfo)
.onOpenURL { url in
print("onOpenURL: \(url)")
if sessionInfo.processDeepLink(url: url) {
print("deep link TRUE")
} else {
print("deep link FALSE")
}
}
}
}
And all my DeepLinks work just as desired. I wanted to trigger them from a notification so I created an apns file with the same link that worked using xcrun:
{
"aps": {
"alert": { // alert data },
"badge": 1,
"link_url":"myapp://user/details/123456"
}
}
and pushed it to the simulator like this:
xcrun simctl push xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx com.myco.myapp test.apn
How do I reference my object from the AppDelegate which gets the message:
func notificationHub(_ notificationHub: MSNotificationHub!, didReceivePushNotification message: MSNotificationHubMessage!) {
print("notificationHub...")
let userInfo = ["message": message!]
print("user: ")
NotificationCenter.default.post(name: NSNotification.Name("MessageReceived"), object: nil, userInfo: userInfo)
if (UIApplication.shared.applicationState == .background) {
print("Notification received in the background")
} else {
print("Notification received in the foreground")
}
UIApplication.shared.applicationIconBadgeNumber = 4
}
I looked at this post, but couldn't relate the components to my app, possibly due to the NotificationHub part of it. I also saw this post, but again didn't know how to connect it.
I saw this post and it looks like push notification and deep linking are two different things. I could use the same logic if I could access the SessionInfo object from the AppDelegate. I'm concerned about messing around in there given I'm new to iOS development. Here is what I'm using in the App:
#StateObject var sessionInfo: SessionInfo = SessionInfo()
This post seems to cover it but I'm still having trouble. I don't know what this means:
static let shared = AppState()
And my SessionInfo is decorated with #MainActor. When I access it from other places I use:
#EnvironmentObject var sessionInfo: SessionInfo
I also saw this post, which there was no selected answer, but the one which did exist seemed to recommend making the AppDelegate and EnvrionmentObject and push it into the ContentView. I think what I really need is the AppDelegate when the notification arrives to update something shared/published to the SessionInfo object so the url is parsed and the navigation kicked off. This seems backwards to me.
This post makes the AppDelegate an ObservableObject with a property which is published and makes the AppDelegate an EnvrionmentObject, so when the value is updated, it's published. If it were the navigation link/object that would work but something would still need to process it and it would not make sense for the onOpenUrl to use the AppDelegate, so again I think this is backwards.
If I did follow the post where there is a static SessionInfo object in the SessionInfo class, singleton, that means I would need to remove the #EnvironmentObject var sessionInfo: SessionInfo from the ContentView and the .environmentObject(sessionInfo) on the main View I am using I think and instead instantiate the shared object in each view where it is used. Right? It seems like I followed this whole #EnvrionmentObject, #StateObject, #MainActor paradigm and would have to abandon it. I'm not sure if that is right or what the tradeoffs are.
Most recently this post seems to be pretty in-depth, but introduces a new element, UNUserNotificationCenter, which I heard referenced in this youtube video.
This article was very helpful for the notification part.
Azure NotificationHubs the message info is in message.userInfo["aps"] vs userInfo["aps"] in the example or most places I have seen it. Not much documentation on MSNotificationHubMessage:
func notificationHub(_ notificationHub: MSNotificationHub, didReceivePushNotification message: MSNotificationHubMessage) {
print("notificationHub...")
let title = message.title ?? ""
let body = message.body ?? ""
print("title: \(title)")
print("body: \(body)")
let userInfo = ["message": message]
NotificationCenter.default.post(name: NSNotification.Name("MessageReceived"), object: nil, userInfo: userInfo)
guard let aps = message.userInfo["aps"] as? [String: AnyObject] else {
return
}
...
}
Second, this post provided the answer which I adapted for my project:
final class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate, MSNotificationHubDelegate {
var navMgr: NavigationManager = NavigationManager()
...
}
and
#UIApplicationDelegateAdaptor private var appDelegate: AppDelegate
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(sessionInfo)
.environmentObject(appDelegate.navMgr)
.onOpenURL { url in
print("onOpenURL: \(url)")
if sessionInfo.processDeepLink(url: url) {
print("deep link TRUE")
} else {
print("deep link FALSE")
}
}
}
}

How to observe login state?

I want View Controllers to be aware of every change in login status. Do I have to make a single tone and subscribe?
Singleton.swift
class Singleton {
static let shared = Singleton()
let isLogin: BehaviorRelay<Bool>
private init() {
isLogin = BehaviorRelay<Bool>(value: false)
}
}
SomeViewController
class SomeVc: UIViewController {
Sigleton.shared.isLogin.subscribe(.....)
}
No you don't need a Singleton...
Here's code I use in actual production. This code is in my application(_:didFinishLaunchingWithOptions:) method.
_ = UserDefaults.standard.rx.observe(String.self, "token")
.map { $0 ?? "" }
.filter { $0.isEmpty }
.bind(onNext: presentScene(animated: true) { _ in
LoginViewController.scene { $0.connect() }
})
When the user logs in, I save a token in UserDefaults, when the user logs out, I remove it. The above code will present my LoginViewController when the user logs out.
If any other view controller needs to track the login state of the user, they can also subscribe to the token observable.
The presentScene(animated:_:) function and scene(_:) method both come from my CLE Library

Swift: Shared Keychain returns nil in Widgets and WidgetIntent

I am attempting to share information (in this case, a refreshToken which I use to refresh a accessToken) between my app, a Widget Extension and a Widget Intent via a shared Keychain, but for some reason, the Keychain always return nil in the Widget and WidgetIntent.
I have checked that I have the following:
Enabled Keychain Sharing, and set the same keychain groups (ie com.mycompany.myapp)
Prefix the accessGroup with App ID (ie ABCDEFGHIJ.com.mycompany.myapp)
Code:
//In my Constants.swift
struct Constants {
struct Keychain {
static let KeychainAccessGroupKey = "ABCDEFGHIJ.com.mycompany.myapp"
static let RefreshTokenKey = "refreshToken"
}
}
//In widgets entry view
//Text always prints "no RF", meaning refreshToken in the keychain is nil
struct WidgetEntryView : View {
var body: some View {
let keychain = Keychain(accessGroup: Constants.Keychain.KeychainAccessGroupKey)
let refreshToken = try? keychain.getString(Constants.Keychain.RefreshTokenKey)
Text(refreshToken ?? "no RF")
.font(.caption)
}
}
//In app, I implemented a button to pull the refreshToken, and it is not nil
ToolbarItem(placement: .navigationBarTrailing) {
Button("All apps") {
let keychain = Keychain(accessGroup: Constants.Keychain.KeychainAccessGroupKey)
let refreshToken = try? keychain.getString(Constants.Keychain.RefreshTokenKey)
print("RF", refreshToken) // Not nil
}
}
I use this KeychainAccess library for my keychain implementation. I am always running on simulator, and the target OS is iOS14.
Where could I have gone wrong?

ASWebAuthenticationSession in SWIFTUI

I've been trying to figure out how to integrate an ASWebAuthenticationSession (to perform login via Oauth) with SwiftUI. I can't find any documentation on the same online anywhere and was wondering if someone with more SwiftUI and iOS dev experience could tell me how I can achieve something along the lines. I currently need a system for someone to click the Login button which then opens a ASWebAuthSession and allows the user to login before redirecting them back to my app and loading another SwiftUI view.
I have in my ContentView one button whos calls this function :
func getAuthTokenWithWebLogin() {
let authURL = URL(string: "https://test-login.blabla.no/connect/authorize?scope=openid%20profile%20AppFramework%20offline_access&response_type=code&client_id=<blalblalba>&redirect_uri=https://integration-partner/post-login")
let callbackUrlScheme = "no.blabla:/oauthdirect"
webAuthSession = ASWebAuthenticationSession.init(url: authURL!, callbackURLScheme: callbackUrlScheme, completionHandler: { (callBack:URL?, error:Error?) in
// handle auth response
guard error == nil, let successURL = callBack else {
return
}
let oauthToken = NSURLComponents(string: (successURL.absoluteString))?.queryItems?.filter({$0.name == "code"}).first
// Do what you now that you've got the token, or use the callBack URL
print(oauthToken ?? "No OAuth Token")
})
// Kick it off
webAuthSession?.start()
}
But I get this error:
Cannot start ASWebAuthenticationSession without providing presentation
context. Set presentationContextProvider before calling -start.
How should I do this in SwiftUI? Any examples would be fantastic!
With .webAuthenticationSession(isPresented:content) modifier in BetterSafariView, you can easily use ASWebAuthenticationSession in SwiftUI. It handles the work related to providing presentation context.
import SwiftUI
import BetterSafariView
struct ContentView: View {
#State private var startingWebAuthenticationSession = false
var body: some View {
Button("Start WebAuthenticationSession") {
self.startingWebAuthenticationSession = true
}
.webAuthenticationSession(isPresented: $startingWebAuthenticationSession) {
WebAuthenticationSession(
url: URL(string: "https://github.com/login/oauth/authorize?client_id=\(clientID)")!,
callbackURLScheme: "myapp"
) { callbackURL, error in
print(callbackURL, error)
}
}
}
}

realm::IncorrectThreadException: Realm accessed from incorrect thread (with ReactiveSwift)

I have a view model that's fetching some data from my API, and when I get data back, I'm setting it both in Realm and on two MutableProperties on the view model
userService.getBrowseData().on(
failed: { error in
// TODO: Should call a closure the VC passes in here with an error to show
}, value: { banners, lists in
browseStoreService.store(banners: banners, lists: lists)
self.banners.value = banners
self.lists.value = lists
}
).startWithCompleted { self.dataLoaded.value = true }
In my view controller, I'm getting a crash at the line where I instantiate and set a ListViewViewModel
disposable += viewModel?.dataLoaded.producer
.observe(on: UIScheduler())
.startWithValues { loaded in
if loaded {
guard let viewModel = self.viewModel else { return }
self.carousel?.reloadData()
for list in viewModel.lists.value {
let view = ListView()
view.viewModel = ListViewViewModel(list: list)
view.listViewDelegate = self
view.listItemViewDelegate = self
scrollingStackView.addArrangedSubview(view: view)
constrain(view) { view in
view.width == view.superview!.width
view.left == view.superview!.left
view.right == view.superview!.right
}
}
}
}
The strange thing to me is that I'm just setting the data in realm, I'm not doing any additional fetch to get the data out of realm which would make sense to cause a crash, especially if the write hadn't finished.
In this case, I'm taking an array of banners and an array of lists in my response from the API, sending them into my storeService to set them in realm and assigning those same arrays to their respective MutableProperties.
Any idea what could be going here that would be causing this crash?
Edit:
The store function looks like this:
func store(banners: [Banner], lists: [List]) {
let realm = try! Realm()
do {
try realm.write {
realm.add(banners, update: true)
realm.add(lists, update: true)
}
} catch {
return
}
realm.refresh()
}
and both models have a primary key of id