Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FR-13508 - Support Embedded mode #14

Merged
merged 11 commits into from
Oct 16, 2023
30 changes: 26 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ and integrate them into their SaaS portals in up to 5 lines of code.
- [UIKit Integration](#uikit-integration)
- [Add Frontegg UIKit Wrapper](#add-frontegg-uikit-wrapper)
- [Add custom UIKit loading screen (coming-soon)](#Add-custom-uikit-loading-screen)
- [Embedded Webview vs ASWebAuthenticationSession](#embedded-webview-vs-aswebauthenticationsession)
- [Config iOS associated domain](#config-ios-associated-domain)

## Project Requirements
Expand Down Expand Up @@ -51,23 +52,21 @@ Copy FronteggDomain to future steps from [Frontegg Portal Domain](https://portal
- Replace `IOS_BUNDLE_IDENTIFIER` with your application identifier
- Replace `FRONTEGG_BASE_URL` with your frontegg base url


### Add frontegg package to the project

- Open you project
- Choose File -> Add Packages
- Enter `https://github.com/frontegg/frontegg-ios-swift` in search field
- Press `Add Package`


### Create Frontegg plist file

To setup your SwiftUI application to communicate with Frontegg, you have to create a new file named `Frontegg.plist` under
your root project directory, this file will store values to be used variables by Frontegg SDK:

```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN">
<plist version="1.0">
<dict>
<key>baseUrl</key>
Expand Down Expand Up @@ -136,7 +135,6 @@ your root project directory, this file will store values to be used variables by
}
```


### UIKit integration

- ### Add Frontegg UIKit Wrapper
Expand Down Expand Up @@ -267,7 +265,31 @@ your root project directory, this file will store values to be used variables by

```

### Embedded Webview vs ASWebAuthenticationSession

Frontegg SDK supports two authentication methods:
- Embedded Webview
- ASWebAuthenticationSession

By default Frontegg SDK will use Embedded Webview, to use ASWebAuthenticationSession you have to set `embeddedMode` to `NO` in `Frontegg.plist` file:

```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN">
<plist version="1.0">
<dict>
<key>baseUrl</key>
<string>https://[DOMAIN_HOST_FROM_PREVIOUS_STEP]</string>
<key>clientId</key>
<string>[CLIENT_ID_FROM_PREVIOUS_STEP]</string>

<!-- START -->
<key>embeddedMode</key>
<true/>
<!-- END -->
</dict>
</plist>
```

### Config iOS associated domain
Configuring your iOS associated domain is required for Magic Link authentication / Reset Password / Activate Account.
Expand Down
4 changes: 0 additions & 4 deletions Sources/FronteggSwift/FronteggApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,4 @@ public class FronteggApp {
logger.info("Frontegg baseURL: \(self.baseUrl)")
}





}
107 changes: 87 additions & 20 deletions Sources/FronteggSwift/FronteggAuth.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import Foundation
import WebKit
import Combine
import AuthenticationServices
import UIKit
import SwiftUI


public class FronteggAuth: ObservableObject {
Expand All @@ -16,11 +18,16 @@ public class FronteggAuth: ObservableObject {
@Published public var user: User?
@Published public var isAuthenticated = false
@Published public var isLoading = true
@Published public var webLoading = true
@Published public var initializing = true
@Published public var showLoader = true
@Published public var appLink: Bool = false
@Published public var externalLink: Bool = false
public var embeddedMode: Bool = false

public var baseUrl = ""
public var clientId = ""
public var pendingAppLink: URL? = nil



Expand All @@ -32,14 +39,15 @@ public class FronteggAuth: ObservableObject {
private let credentialManager: CredentialManager
public let api: Api
private var subscribers = Set<AnyCancellable>()
private var webAuthentication: WebAuthentication? = nil
var webAuthentication: WebAuthentication = WebAuthentication()

init (baseUrl:String, clientId: String, api:Api, credentialManager: CredentialManager) {

self.baseUrl = baseUrl
self.clientId = clientId
self.credentialManager = credentialManager
self.api = api
self.embeddedMode = PlistHelper.isEmbeddedMode()

self.$initializing.combineLatest(self.$isAuthenticated, self.$isLoading).sink(){ (initializingValue, isAuthenticatedValue, isLoadingValue) in
self.showLoader = initializingValue || (!isAuthenticatedValue && isLoadingValue)
Expand All @@ -58,7 +66,7 @@ public class FronteggAuth: ObservableObject {
await self.refreshTokenIfNeeded()
}
}
}else {
} else {
self.isLoading = false
self.initializing = false
}
Expand All @@ -81,6 +89,11 @@ public class FronteggAuth: ObservableObject {
self.user = user
self.isAuthenticated = true
self.appLink = false
self.initializing = false
self.appLink = false

// isLoading must be at the bottom
self.isLoading = false

let offset = Double((decode["exp"] as! Int) - Int(Date().timeIntervalSince1970)) * 0.9
DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + offset) {
Expand All @@ -96,14 +109,13 @@ public class FronteggAuth: ObservableObject {
self.accessToken = nil
self.user = nil
self.isAuthenticated = false
self.initializing = false
self.appLink = false

// isLoading must be at the last bottom
self.isLoading = false
}
}

DispatchQueue.main.sync {
self.isLoading = false
self.initializing = false
self.appLink = false
}
}

public func logout() {
Expand All @@ -123,12 +135,14 @@ public class FronteggAuth: ObservableObject {

DispatchQueue.main.async {
self.isAuthenticated = false
self.isLoading = false
self.user = nil
self.accessToken = nil
self.refreshToken = nil
self.initializing = false
self.appLink = false

// isLoading must be at the last bottom
self.isLoading = false
}
}
}
Expand All @@ -139,7 +153,7 @@ public class FronteggAuth: ObservableObject {

}

func refreshTokenIfNeeded() async {
public func refreshTokenIfNeeded() async {
guard let refreshToken = self.refreshToken, let accessToken = self.accessToken else {
return
}
Expand Down Expand Up @@ -187,8 +201,6 @@ public class FronteggAuth: ObservableObject {
await setCredentials(accessToken: data.access_token, refreshToken: data.refresh_token)

completion(.success(user!))

setIsLoading(false)
} catch {
print("Failed to load user data: \(error.localizedDescription)")
completion(.failure(FronteggError.authError("Failed to load user data: \(error.localizedDescription)")))
Expand All @@ -207,7 +219,7 @@ public class FronteggAuth: ObservableObject {
}

internal func createOauthCallbackHandler(_ completion: @escaping FronteggAuth.CompletionHandler) -> ((URL?, Error?) -> Void) {

return { callbackUrl, error in

if error != nil {
Expand All @@ -220,16 +232,16 @@ public class FronteggAuth: ObservableObject {
completion(.failure(FronteggError.authError(errorMessage)))
return
}


self.logger.trace("handleHostedLoginCallback, url: \(url)")
guard let queryItems = getQueryItems(url.absoluteString), let code = queryItems["code"] else {
let error = FronteggError.authError("Failed to get extract code from hostedLoginCallback url")
completion(.failure(error))
return
}

guard let codeVerifier = try? self.credentialManager.get(key: KeychainKeys.codeVerifier.rawValue) else {
guard let codeVerifier = CredentialManager.getCodeVerifier() else {
let error = FronteggError.authError("IlligalState, codeVerifier not found")
completion(.failure(error))
return
Expand All @@ -243,30 +255,85 @@ public class FronteggAuth: ObservableObject {

public func login(_ _completion: FronteggAuth.CompletionHandler? = nil) {

if(self.embeddedMode){
self.embeddedLogin(_completion)
return
}

let completion = _completion ?? { res in

}
self.webAuthentication?.webAuthSession?.cancel()
self.webAuthentication.webAuthSession?.cancel()
self.webAuthentication = WebAuthentication()

let oauthCallback = createOauthCallbackHandler(completion)
let (authorizeUrl, codeVerifier) = AuthorizeUrlGenerator.shared.generate()
try! credentialManager.save(key: KeychainKeys.codeVerifier.rawValue, value: codeVerifier)

self.webAuthentication!.start(authorizeUrl, completionHandler: oauthCallback)
self.webAuthentication.start(authorizeUrl, completionHandler: oauthCallback)

}


internal func getRootVC() -> UIViewController? {

var rootVC: UIViewController? = nil

if let lastWindow = UIApplication.shared.windows.last {
rootVC = lastWindow.rootViewController
} else if let appDelegate = UIApplication.shared.delegate,
let window = appDelegate.window {
rootVC = window!.rootViewController
}

return rootVC
}


public func embeddedLogin(_ _completion: FronteggAuth.CompletionHandler? = nil) {

if let rootVC = self.getRootVC() {
let loginModal = EmbeddedLoginModal(parentVC: rootVC)
let hostingController = UIHostingController(rootView: loginModal)
hostingController.modalPresentationStyle = .fullScreen

rootVC.present(hostingController, animated: false, completion: nil)

} else {
print(FronteggError.authError("Unable to find root viewController"))
exit(500)
}
}
public func handleOpenUrl(_ url: URL) -> Bool {

if(!url.absoluteString.hasPrefix(self.baseUrl)){
self.appLink = false
return false
}

if(self.embeddedMode){
self.pendingAppLink = url
self.webLoading = true
guard let rootVC = self.getRootVC() else {
print(FronteggError.authError("Unable to find root viewController"))
return false;
}

let loginModal = EmbeddedLoginModal(parentVC: rootVC)
let hostingController = UIHostingController(rootView: loginModal)
hostingController.modalPresentationStyle = .fullScreen

let presented = rootVC.presentedViewController
if presented is UIHostingController<EmbeddedLoginModal> {
rootVC.presentedViewController?.dismiss(animated: false)
}
rootVC.present(hostingController, animated: false, completion: nil)
return true;
}

self.appLink = true

self.webAuthentication?.webAuthSession?.cancel()
self.webAuthentication.webAuthSession?.cancel()
self.webAuthentication = WebAuthentication()
let oauthCallback = createOauthCallbackHandler() { res in

Expand All @@ -277,7 +344,7 @@ public class FronteggAuth: ObservableObject {
print("Error \(error)")
}
}
self.webAuthentication!.start(url, completionHandler: oauthCallback)
self.webAuthentication.start(url, completionHandler: oauthCallback)

return true
}
Expand Down
Loading