diff --git a/README.md b/README.md index cb613c0..84e8ecc 100644 --- a/README.md +++ b/README.md @@ -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 @@ -51,7 +52,6 @@ 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 @@ -59,7 +59,6 @@ Copy FronteggDomain to future steps from [Frontegg Portal Domain](https://portal - 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 @@ -67,7 +66,7 @@ your root project directory, this file will store values to be used variables by ```xml - + baseUrl @@ -136,7 +135,6 @@ your root project directory, this file will store values to be used variables by } ``` - ### UIKit integration - ### Add Frontegg UIKit Wrapper @@ -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 + + + + + baseUrl + https://[DOMAIN_HOST_FROM_PREVIOUS_STEP] + clientId + [CLIENT_ID_FROM_PREVIOUS_STEP] + + + embeddedMode + + + + +``` ### Config iOS associated domain Configuring your iOS associated domain is required for Magic Link authentication / Reset Password / Activate Account. diff --git a/Sources/FronteggSwift/FronteggApp.swift b/Sources/FronteggSwift/FronteggApp.swift index b92292f..4c4e3d6 100644 --- a/Sources/FronteggSwift/FronteggApp.swift +++ b/Sources/FronteggSwift/FronteggApp.swift @@ -44,8 +44,4 @@ public class FronteggApp { logger.info("Frontegg baseURL: \(self.baseUrl)") } - - - - } diff --git a/Sources/FronteggSwift/FronteggAuth.swift b/Sources/FronteggSwift/FronteggAuth.swift index 04daaff..060b11c 100644 --- a/Sources/FronteggSwift/FronteggAuth.swift +++ b/Sources/FronteggSwift/FronteggAuth.swift @@ -8,6 +8,8 @@ import Foundation import WebKit import Combine import AuthenticationServices +import UIKit +import SwiftUI public class FronteggAuth: ObservableObject { @@ -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 @@ -32,7 +39,7 @@ public class FronteggAuth: ObservableObject { private let credentialManager: CredentialManager public let api: Api private var subscribers = Set() - private var webAuthentication: WebAuthentication? = nil + var webAuthentication: WebAuthentication = WebAuthentication() init (baseUrl:String, clientId: String, api:Api, credentialManager: CredentialManager) { @@ -40,6 +47,7 @@ public class FronteggAuth: ObservableObject { 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) @@ -58,7 +66,7 @@ public class FronteggAuth: ObservableObject { await self.refreshTokenIfNeeded() } } - }else { + } else { self.isLoading = false self.initializing = false } @@ -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) { @@ -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() { @@ -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 } } } @@ -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 } @@ -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)"))) @@ -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 { @@ -220,7 +232,7 @@ 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 { @@ -228,8 +240,8 @@ public class FronteggAuth: ObservableObject { 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 @@ -243,20 +255,55 @@ 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)){ @@ -264,9 +311,29 @@ public class FronteggAuth: ObservableObject { 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 { + 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 @@ -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 } diff --git a/Sources/FronteggSwift/embedded/CustomWebView.swift b/Sources/FronteggSwift/embedded/CustomWebView.swift new file mode 100644 index 0000000..960c43b --- /dev/null +++ b/Sources/FronteggSwift/embedded/CustomWebView.swift @@ -0,0 +1,250 @@ +// +// CustomWebView.swift +// +// Created by David Frontegg on 24/10/2022. +// + +import Foundation +import WebKit +import SwiftUI +import AuthenticationServices + + +class CustomWebView: WKWebView, WKNavigationDelegate { + var accessoryView: UIView? + private let fronteggAuth: FronteggAuth = FronteggAuth.shared + private let logger = getLogger("CustomWebView") + private var lastResponseStatusCode: Int? = nil + + override var inputAccessoryView: UIView? { + // remove/replace the default accessory view + return accessoryView + } + + private func isCancelledAsAuthenticationLoginError(_ error: Error) -> Bool { + (error as NSError).code == ASWebAuthenticationSessionError.canceledLogin.rawValue + } + func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction) async -> WKNavigationActionPolicy { + logger.trace("navigationAction check for \(navigationAction.request.url?.absoluteString ?? "no Url")") + if let url = navigationAction.request.url { + let urlType = getOverrideUrlType(url: url) + + logger.info("urlType: \(urlType)") + switch(urlType){ + + case .SocialLoginRedirectToBrowser: do { + + return self.handleSocialLoginRedirectToBrowser(webView, url) + } + case .HostedLoginCallback: do { + return self.handleHostedLoginCallback(webView, url) + } + case .SocialOauthPreLogin: do { + return self.setSocialLoginRedirectUri(webView, url) + } + default: + return .allow + } + } else { + logger.warning("failed to get url from navigationAction") + self.fronteggAuth.webLoading = false + return .allow + } + } + + func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) { + logger.trace("didStartProvisionalNavigation") + if let url = webView.url { + let urlType = getOverrideUrlType(url: url) + + logger.info("urlType: \(urlType)") + + let isUnknown = (urlType == .Unknown) + if(fronteggAuth.externalLink != isUnknown) { + fronteggAuth.externalLink = isUnknown + } + + if(urlType != .SocialLoginRedirectToBrowser && + urlType != .SocialOauthPreLogin){ + + if(fronteggAuth.webLoading == false) { + fronteggAuth.webLoading = true + } + } + + logger.info("startProvisionalNavigation webLoading = \(fronteggAuth.webLoading)") + logger.info("isExternalLink = \(fronteggAuth.externalLink)") + } else { + logger.warning("failed to get url from didStartProvisionalNavigation()") + self.fronteggAuth.webLoading = false + } + } + + func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + logger.trace("didFinish") + if let url = webView.url { + let urlType = getOverrideUrlType(url: url) + logger.info("urlType: \(urlType), for: \(url.absoluteString)") + + + if urlType == .loginRoutes || urlType == .Unknown { + logger.info("hiding Loader screen") + if(fronteggAuth.webLoading) { + fronteggAuth.webLoading = false + } + } else if let statusCode = self.lastResponseStatusCode { + self.lastResponseStatusCode = nil; + self.fronteggAuth.webLoading = false + + + webView.evaluateJavaScript("JSON.parse(document.body.innerText).errors.join('\\n')") { [self] res, err in + let errorMessage = res as? String ?? "Unknown error occured" + + logger.error("Failed to load page: \(errorMessage), status: \(statusCode)") + self.fronteggAuth.webLoading = false + let content = generateErrorPage(message: errorMessage, url: url.absoluteString,status: statusCode); + webView.loadHTMLString(content, baseURL: nil); + } + } + } else { + logger.warning("failed to get url from didFinishNavigation()") + self.fronteggAuth.webLoading = false + } + } + + func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, + decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) { + + if let response = navigationResponse.response as? HTTPURLResponse { + if response.statusCode >= 400 && response.statusCode != 500, let url = response.url { + let urlType = getOverrideUrlType(url: url) + logger.info("urlType: \(urlType), for: \(url.absoluteString)") + + if(urlType == .internalRoutes && response.mimeType == "application/json"){ + self.lastResponseStatusCode = response.statusCode + decisionHandler(.allow) + return + } + } + } + decisionHandler(.allow) + } + + func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError _error: Error) { + let error = _error as NSError + let statusCode = error.code + if(statusCode==102){ + // interrupted by frontegg webview + return; + } + + let errorMessage = error.localizedDescription; + let url = "\(error.userInfo["NSErrorFailingURLKey"] ?? error.userInfo)" + logger.error("Failed to load page: \(errorMessage), status: \(statusCode), \(error)") + self.fronteggAuth.webLoading = false + let content = generateErrorPage(message: errorMessage, url: url, status: statusCode); + webView.loadHTMLString(content, baseURL: nil); + } + + + private func handleHostedLoginCallback(_ webView: WKWebView, _ url: URL) -> WKNavigationActionPolicy { + logger.trace("handleHostedLoginCallback, url: \(url)") + guard let queryItems = getQueryItems(url.absoluteString), + let code = queryItems["code"], + let savedCodeVerifier = CredentialManager.getCodeVerifier() else { + logger.error("failed to get extract code from hostedLoginCallback url") + logger.info("Restast the process by generating a new authorize url") + let (url, codeVerifier) = AuthorizeUrlGenerator().generate() + CredentialManager.saveCodeVerifier(codeVerifier) + _ = webView.load(URLRequest(url: url)) + return .cancel + } + + DispatchQueue.global(qos: .userInitiated).async { + Task { + FronteggAuth.shared.handleHostedLoginCallback(code, savedCodeVerifier ) { res in + switch (res) { + case .success(let user): + print("User \(user.id)") + + case .failure(let error): + print("Error \(error)") + let (url, codeVerifier) = AuthorizeUrlGenerator().generate() + CredentialManager.saveCodeVerifier(codeVerifier) + _ = webView.load(URLRequest(url: url)) + } + } + } + } + return .cancel + } + + + private func setSocialLoginRedirectUri(_ webView:WKWebView, _ url:URL) -> WKNavigationActionPolicy { + + logger.trace("setSocialLoginRedirectUri()") + let queryItems = [ + URLQueryItem(name: "redirectUri", value: generateRedirectUri()), + ] + var urlComps = URLComponents(string: url.absoluteString)! + + if(urlComps.query?.contains("redirectUri") ?? false){ + logger.trace("redirectUri exist, forward navigation to webView") + return .allow + } + + urlComps.queryItems = (urlComps.queryItems ?? []) + queryItems + + + logger.trace("added redirectUri to socialLogin auth url \(urlComps.url!)") + _ = webView.load(URLRequest(url: urlComps.url!)) + return .cancel + } + + private func handleSocialLoginRedirectToBrowser(_ webView:WKWebView, _ socialLoginUrl:URL) -> WKNavigationActionPolicy{ + + logger.trace("handleSocialLoginRedirectToBrowser()") + let queryItems = [ + URLQueryItem(name: "prompt", value: "select_account"), + ] + var urlComps = URLComponents(string: socialLoginUrl.absoluteString)! + urlComps.queryItems = (urlComps.queryItems ?? []) + queryItems + + let url = urlComps.url! + + fronteggAuth.webLoading = false + fronteggAuth.webAuthentication.webAuthSession?.cancel() + fronteggAuth.webAuthentication = WebAuthentication() + + fronteggAuth.webAuthentication.view = self + fronteggAuth.webAuthentication.start(url) { callbackUrl, error in + + if(error != nil){ + if(self.isCancelledAsAuthenticationLoginError(error!)){ + print("Social login authentication canceled") + }else { + print("Failed to login with social login \(error?.localizedDescription ?? "unknown error")") + let (newUrl, codeVerifier) = AuthorizeUrlGenerator().generate() + CredentialManager.saveCodeVerifier(codeVerifier) + _ = webView.load(URLRequest(url: newUrl)) + } + }else if (callbackUrl == nil){ + print("Failed to login with social login \(error?.localizedDescription ?? "unknown error")") + let (newUrl, codeVerifier) = AuthorizeUrlGenerator().generate() + CredentialManager.saveCodeVerifier(codeVerifier) + _ = webView.load(URLRequest(url: newUrl)) + }else { + let components = URLComponents(url: callbackUrl!, resolvingAgainstBaseURL: false)! + let query = components.query! + let resultUrl = URL(string: + "\(self.fronteggAuth.baseUrl)/oauth/account/social/success?\(query)")! + _ = webView.load(URLRequest(url: resultUrl)) + + + } + } + + + return .cancel + } +} diff --git a/Sources/FronteggSwift/embedded/EmbeddedLoginModal.swift b/Sources/FronteggSwift/embedded/EmbeddedLoginModal.swift new file mode 100644 index 0000000..4939670 --- /dev/null +++ b/Sources/FronteggSwift/embedded/EmbeddedLoginModal.swift @@ -0,0 +1,47 @@ +// +// EmbeddedLoginModal.swift +// +// +// Created by David Frontegg on 15/09/2023. +// + +import Foundation +import SwiftUI + + +public struct EmbeddedLoginModal: View { + @StateObject var fronteggAuth = FronteggApp.shared.auth + public var parentVC: UIViewController? = nil + + + init(parentVC: UIViewController? = nil) { + self.parentVC = parentVC + } + + + public var body: some View { + ZStack { + if(fronteggAuth.initializing || fronteggAuth.showLoader) { + DefaultLoader() + } else if !fronteggAuth.initializing + && !fronteggAuth.showLoader + && fronteggAuth.isAuthenticated { + + DefaultLoader().onAppear() { + parentVC?.presentedViewController?.dismiss(animated: true) + } + } else { + EmbeddedLoginPage() + } + + } + .environmentObject(fronteggAuth) + } +} + +struct EmbeddedLoginModal_Previews: PreviewProvider { + static var previews: some View { + EmbeddedLoginModal() + } +} + diff --git a/Sources/FronteggSwift/embedded/EmbeddedLoginPage.swift b/Sources/FronteggSwift/embedded/EmbeddedLoginPage.swift new file mode 100644 index 0000000..2b293cf --- /dev/null +++ b/Sources/FronteggSwift/embedded/EmbeddedLoginPage.swift @@ -0,0 +1,63 @@ +// +// EmbeddedLoginPage.swift +// +// +// Created by David Frontegg on 13/09/2023. +// + +import Foundation +import SwiftUI +import UIKit + +public struct EmbeddedLoginPage: View { + @EnvironmentObject var fronteggAuth: FronteggAuth + + public var body: some View { + ZStack { + NavigationView{ + VStack(alignment: .center) { + ZStack{ + FronteggWebView() + if fronteggAuth.webLoading { + DefaultLoader() + } + } + } + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button("Back to login") { + fronteggAuth.accessToken = nil + fronteggAuth.refreshToken = nil + fronteggAuth.user = nil + fronteggAuth.isAuthenticated = false + fronteggAuth.isLoading = true + fronteggAuth.initializing = true + fronteggAuth.pendingAppLink = nil + fronteggAuth.appLink = false + fronteggAuth.externalLink = false + fronteggAuth.logout() + } + } + } + .navigationBarHidden(!fronteggAuth.externalLink) + .navigationTitle("") + .navigationBarTitleDisplayMode(.inline) + + .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center) + + .ignoresSafeArea(fronteggAuth.externalLink ? [] : [.all]) + + } + + } + .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center) + .ignoresSafeArea() + + } +} + +struct FronteggLoginPage_Previews: PreviewProvider { + static var previews: some View { + EmbeddedLoginPage() + } +} diff --git a/Sources/FronteggSwift/embedded/FronteggWKContentController.swift b/Sources/FronteggSwift/embedded/FronteggWKContentController.swift new file mode 100644 index 0000000..bb0236e --- /dev/null +++ b/Sources/FronteggSwift/embedded/FronteggWKContentController.swift @@ -0,0 +1,41 @@ +// +// FronteggWKContentController.swift +// +// +// Created by David Frontegg on 15/09/2023. +// + +import Foundation +import WebKit + + +class FronteggWKContentController: NSObject, WKScriptMessageHandler{ + + func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { + if message.name == "fronteggNative" { + if let functionName = message.body as? String { + self.handleFunctionCall(functionName: functionName) + } + } + } + func handleFunctionCall(functionName: String) { + switch functionName { + case "showLoader": + // Call your native function to show the loader + FronteggAuth.shared.webLoading = true + case "hideLoader": + // Call your native function to show the loader + FronteggAuth.shared.webLoading = false + default: + break + } + } + + func showNativeLoaderFunction() { + // Your native implementation to show the loader + } + + + + +} diff --git a/Sources/FronteggSwift/embedded/FronteggWebView.swift b/Sources/FronteggSwift/embedded/FronteggWebView.swift new file mode 100644 index 0000000..8a2f6cd --- /dev/null +++ b/Sources/FronteggSwift/embedded/FronteggWebView.swift @@ -0,0 +1,88 @@ +// +// FronteggWebView.swift +// +// Created by David Frontegg on 24/10/2022. +// + +import Foundation +import WebKit +import SwiftUI +import AuthenticationServices + + + +struct FronteggWebView: UIViewRepresentable { + typealias UIViewType = WKWebView + + let webView: CustomWebView + + private var fronteggAuth: FronteggAuth + + init() { + self.fronteggAuth = FronteggApp.shared.auth; + + let metadataSource:String = "let interval = setInterval(function(){" + + " if(document.getElementsByTagName('head').length > 0){" + + " clearInterval(interval);" + + " var meta = document.createElement('meta');" + + " meta.name = 'viewport';" + + " meta.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no';" + + " var head = document.getElementsByTagName('head')[0];" + + " head.appendChild(meta);" + + " var style = document.createElement('style');" + + " style.innerHTML = 'html {font-size: 16px;}';" + + " style.setAttribute('type', 'text/css');" + + " document.head.appendChild(style); " + + " }" + + "}, 10);" + + + + let userContentController: WKUserContentController = WKUserContentController() + userContentController.add(FronteggWKContentController(), name: "fronteggNative") + userContentController.addUserScript(WKUserScript(source: metadataSource, injectionTime: .atDocumentEnd, forMainFrameOnly: false)) + + let conf = WKWebViewConfiguration() + conf.userContentController = userContentController + conf.websiteDataStore = WKWebsiteDataStore.default() + + webView = CustomWebView(frame: .zero, configuration: conf) + webView.navigationDelegate = webView; + + + } + + func makeUIView(context: Context) -> WKWebView { + let url: URL + let codeVerifier: String; + if fronteggAuth.pendingAppLink != nil { + url = fronteggAuth.pendingAppLink! + codeVerifier = CredentialManager.getCodeVerifier()! + fronteggAuth.pendingAppLink = nil + } else { + (url, codeVerifier) = AuthorizeUrlGenerator().generate() + CredentialManager.saveCodeVerifier(codeVerifier) + } + + + let request = URLRequest(url: url, cachePolicy: .useProtocolCachePolicy) + webView.load(request) + + return webView + } + + func updateUIView(_ uiView: WKWebView, context: Context) { + + } +} + + +fileprivate final class InputAccessoryHackHelper: NSObject { + @objc var inputAccessoryView: AnyObject? { return nil } +} + +extension WKWebView { + override open var safeAreaInsets: UIEdgeInsets { + return UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) + } +} diff --git a/Sources/FronteggSwift/models/CredentialAssertion.swift b/Sources/FronteggSwift/models/CredentialAssertion.swift new file mode 100644 index 0000000..bc969d3 --- /dev/null +++ b/Sources/FronteggSwift/models/CredentialAssertion.swift @@ -0,0 +1,77 @@ +// +// CredentialAssertion.swift +// +// +// Created by David Frontegg on 15/09/2023. +// + +import Foundation + + + +public struct PKCredentialRP: Codable { + + enum DecodeError: Error { + case invalidJsonData + } + public var id: String + public var name: String +} + + +public struct PKCredentialUser: Codable { + + enum DecodeError: Error { + case invalidJsonData + } + public var id: String + public var name: String + public var displayName: String +} + +public struct PKPublicKeyCreds: Codable { + + enum DecodeError: Error { + case invalidJsonData + } + + public var type: String + public var alg: Int +} + + + +public struct PKAuthenticatorSelection: Codable { + + enum DecodeError: Error { + case invalidJsonData + } + + public var userVerification: String +} + + +public struct PKCredentialAssertionOption: Codable { + + enum DecodeError: Error { + case invalidJsonData + } + + public var rp: PKCredentialRP + public var user: PKCredentialUser + public var challenge:String + public var pubKeyCredParams: [PKPublicKeyCreds] + public var timeout: Int + public var attestation: String? + public var authenticatorSelection: PKAuthenticatorSelection? + +} + +public struct PKCredentialAssertion: Codable { + + enum DecodeError: Error { + case invalidJsonData + } + + public var options: PKCredentialAssertionOption +} diff --git a/Sources/FronteggSwift/services/Api.swift b/Sources/FronteggSwift/services/Api.swift index 0ee3eff..c15c0bd 100644 --- a/Sources/FronteggSwift/services/Api.swift +++ b/Sources/FronteggSwift/services/Api.swift @@ -28,10 +28,7 @@ public class Api { } } - - - - private func putRequest(path:String, body: [String: Any?]) async throws -> (Data, URLResponse) { + internal func putRequest(path:String, body: [String: Any?]) async throws -> (Data, URLResponse) { let urlStr = "\(self.baseUrl)/\(path)" guard let url = URL(string: urlStr) else { throw ApiError.invalidUrl("invalid url: \(urlStr)") @@ -53,7 +50,7 @@ public class Api { return try await URLSession.shared.data(for: request) } - private func postRequest(path:String, body: [String: Any?], additionalHeaders: [String: String] = [:]) async throws -> (Data, URLResponse) { + internal func postRequest(path:String, body: [String: Any?], additionalHeaders: [String: String] = [:]) async throws -> (Data, URLResponse) { let urlStr = "\(self.baseUrl)/\(path)" guard let url = URL(string: urlStr) else { throw ApiError.invalidUrl("invalid url: \(urlStr)") @@ -78,7 +75,7 @@ public class Api { return try await URLSession.shared.data(for: request) } - private func getRequest(path:String, accessToken:String?, refreshToken: String? = nil) async throws -> (Data, URLResponse) { + internal func getRequest(path:String, accessToken:String?, refreshToken: String? = nil) async throws -> (Data, URLResponse) { let urlStr = "\(self.baseUrl)/\(path)" guard let url = URL(string: urlStr) else { @@ -107,6 +104,8 @@ public class Api { "refresh_token": refreshToken, ]) + let text = String(data: data, encoding: .utf8)! + print("result \(text)") return try JSONDecoder().decode(AuthResponse.self, from: data) } catch { print(error) @@ -151,7 +150,7 @@ public class Api { public func switchTenant(tenantId: String) async throws -> Void { - try await putRequest(path: "identity/resources/users/v1/tenant", body: ["tenantId":tenantId]) + _ = try await putRequest(path: "identity/resources/users/v1/tenant", body: ["tenantId":tenantId]) } internal func logout(accessToken: String?, refreshToken: String?) async { @@ -170,4 +169,5 @@ public class Api { } + } diff --git a/Sources/FronteggSwift/services/Authentication.swift b/Sources/FronteggSwift/services/Authentication.swift index 1bebac4..0337865 100644 --- a/Sources/FronteggSwift/services/Authentication.swift +++ b/Sources/FronteggSwift/services/Authentication.swift @@ -5,11 +5,14 @@ // import AuthenticationServices +import UIKit class WebAuthentication: NSObject, ObservableObject, ASWebAuthenticationPresentationContextProviding { + + var view: UIView? = nil func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { - return ASPresentationAnchor() + return self.view?.window ?? ASPresentationAnchor() } override func responds(to aSelector: Selector!) -> Bool { return true @@ -18,20 +21,20 @@ class WebAuthentication: NSObject, ObservableObject, ASWebAuthenticationPresenta var webAuthSession: ASWebAuthenticationSession? - - func start(_ websiteURL:URL, completionHandler: @escaping ASWebAuthenticationSession.CompletionHandler){ + func start(_ websiteURL:URL, completionHandler: @escaping ASWebAuthenticationSession.CompletionHandler) { let bundleIdentifier = try! PlistHelper.fronteggConfig().bundleIdentifier - webAuthSession = ASWebAuthenticationSession.init( + let webAuthSession = ASWebAuthenticationSession.init( url: websiteURL, callbackURLScheme: bundleIdentifier, completionHandler: completionHandler) // Run the session - webAuthSession?.presentationContextProvider = self - webAuthSession?.prefersEphemeralWebBrowserSession = false + webAuthSession.presentationContextProvider = self + webAuthSession.prefersEphemeralWebBrowserSession = false - DispatchQueue.main.async { - self.webAuthSession?.start() - } + + self.webAuthSession = webAuthSession + webAuthSession.start() + } } diff --git a/Sources/FronteggSwift/services/CredentialManager.swift b/Sources/FronteggSwift/services/CredentialManager.swift index 0909577..0c048ce 100644 --- a/Sources/FronteggSwift/services/CredentialManager.swift +++ b/Sources/FronteggSwift/services/CredentialManager.swift @@ -110,4 +110,12 @@ public class CredentialManager { logger.error("Failed to logout from Frontegg Services, errSec: \(status)") } } + + static func saveCodeVerifier(_ codeVerifier: String) { + UserDefaults.standard.set(codeVerifier, forKey: KeychainKeys.codeVerifier.rawValue) + } + + static func getCodeVerifier() -> String? { + return UserDefaults.standard.string(forKey: KeychainKeys.codeVerifier.rawValue) + } } diff --git a/Sources/FronteggSwift/utils/AuthorizeUrlGenerator.swift b/Sources/FronteggSwift/utils/AuthorizeUrlGenerator.swift index cf0ce65..138a191 100644 --- a/Sources/FronteggSwift/utils/AuthorizeUrlGenerator.swift +++ b/Sources/FronteggSwift/utils/AuthorizeUrlGenerator.swift @@ -52,7 +52,7 @@ public class AuthorizeUrlGenerator { var authorizeUrl = URLComponents(string: baseUrl)! - authorizeUrl.path = "/frontegg/oauth/authorize" + authorizeUrl.path = "/oauth/authorize" authorizeUrl.queryItems = [ URLQueryItem(name: "redirect_uri", value: redirectUri), URLQueryItem(name: "response_type", value: "code"), @@ -69,7 +69,7 @@ public class AuthorizeUrlGenerator { if let encodedURL = url.absoluteString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) { var loginUrl = URLComponents(string: baseUrl)! - loginUrl.path = "/frontegg/oauth/logout" + loginUrl.path = "/oauth/logout" loginUrl.queryItems = [ URLQueryItem(name: "post_logout_redirect_uri", value: encodedURL), ] diff --git a/Sources/FronteggSwift/utils/Constants.swift b/Sources/FronteggSwift/utils/Constants.swift new file mode 100644 index 0000000..b3b97bd --- /dev/null +++ b/Sources/FronteggSwift/utils/Constants.swift @@ -0,0 +1,36 @@ +// +// File.swift +// +// +// Created by David Frontegg on 13/09/2023. +// + +import Foundation + +struct URLConstants { + + static let oauthUrls = [ + "https://www.facebook.com", + "https://accounts.google.com", + "https://github.com/login/oauth/authorize", + "https://login.microsoftonline.com", + "https://slack.com/openid/connect/authorize", + "https://appleid.apple.com", + "https://www.linkedin.com/oauth/" + ] + + static let successLoginRoutes:[String] = [ + "/oauth/account/social/success", + ] + static let loginRoutes:[String] = [ + "/oauth/account/", + ] + + static func generateRedirectUri(_ baseUrl:String) -> String { + return "\(baseUrl)/oauth/mobile/callback" + } + + static func generateSocialLoginRedirectUri(_ baseUrl:String) -> String { + return "\(baseUrl)/oauth/account/social/success" + } +} diff --git a/Sources/FronteggSwift/utils/Logger.swift b/Sources/FronteggSwift/utils/Logger.swift index 5ba13f3..d7e532a 100644 --- a/Sources/FronteggSwift/utils/Logger.swift +++ b/Sources/FronteggSwift/utils/Logger.swift @@ -8,24 +8,24 @@ import Foundation public class Logger { - public enum Level: String, Codable, CaseIterable { + public enum Level: Int, Codable, CaseIterable { /// Appropriate for messages that contain information normally of use only when /// tracing the execution of a program. - case trace + case trace = 0 /// Appropriate for messages that contain information normally of use only when /// debugging a program. - case debug + case debug = 1 /// Appropriate for informational messages. - case info + case info = 2 /// Appropriate for messages that are not error conditions, but more severe than /// `.notice`. - case warning + case warning = 3 /// Appropriate for error conditions. - case error + case error = 4 /// Appropriate for critical error conditions that usually require immediate /// attention. @@ -33,7 +33,7 @@ public class Logger { /// When a `critical` message is logged, the logging backend (`LogHandler`) is free to perform /// more heavy-weight operations to capture system state (such as capturing stack traces) to facilitate /// debugging. - case critical + case critical = 5 } public var logLevel: Logger.Level = Level.error diff --git a/Sources/FronteggSwift/utils/PlistHelper.swift b/Sources/FronteggSwift/utils/PlistHelper.swift index 90037d3..6e4daf1 100644 --- a/Sources/FronteggSwift/utils/PlistHelper.swift +++ b/Sources/FronteggSwift/utils/PlistHelper.swift @@ -46,15 +46,39 @@ struct PlistHelper { return logLevel } + let map = [ + "trace": 0, + "debug": 1, + "info": 2, + "warn": 3, + "error": 4, + "critical": 5 + ] + let bundle = Bundle.main; if let path = bundle.path(forResource: "Frontegg", ofType: "plist"), let values = NSDictionary(contentsOfFile: path) as? [String: Any], let logLevelStr = values["logLevel"] as? String, - let logLevel = Logger.Level.init(rawValue: logLevelStr) { + let logLevelNum = map[logLevelStr], + let logLevel = Logger.Level.init(rawValue: logLevelNum) { return logLevel } return Logger.Level.warning } + + public static func isEmbeddedMode() -> Bool { + + let bundle = Bundle.main; + if let path = bundle.path(forResource: "Frontegg", ofType: "plist"), + let values = NSDictionary(contentsOfFile: path) as? [String: Any], + let embeddedMode = values["embeddedMode"] as? Bool { + + return embeddedMode + } + + return true + } } + diff --git a/Sources/FronteggSwift/utils/UrlHelper.swift b/Sources/FronteggSwift/utils/UrlHelper.swift index 39c9736..4cee676 100644 --- a/Sources/FronteggSwift/utils/UrlHelper.swift +++ b/Sources/FronteggSwift/utils/UrlHelper.swift @@ -64,6 +64,64 @@ public func generateRedirectUri() -> String { exit(1) } - return "\(bundleIdentifier)://\(urlComponents.host!)/ios/oauth/callback" + return "\(bundleIdentifier.lowercased())://\(urlComponents.host!)/ios/oauth/callback" + +} + + +enum OverrideUrlType { + case HostedLoginCallback + case SocialLoginRedirectToBrowser + case SocialOauthPreLogin + case loginRoutes + case internalRoutes + case Unknown +} + + +func isSocialLoginPath(_ string: String) -> Bool { + let patterns = [ + "^/frontegg/identity/resources/auth/v2/user/sso/default/[^/]+/prelogin$", + "^/identity/resources/auth/v2/user/sso/default/[^/]+/prelogin$" + ] + + for pattern in patterns { + if let regex = try? NSRegularExpression(pattern: pattern, options: []) { + let matches = regex.matches(in: string, options: [], range: NSRange(location: 0, length: string.utf16.count)) + if !matches.isEmpty { + return true + } + } + } + + return false +} +func getOverrideUrlType (url: URL) -> OverrideUrlType { + + let urlStr = url.absoluteString + + if urlStr.starts(with: FronteggApp.shared.baseUrl) { + + if(isSocialLoginPath(url.path)){ + return .SocialOauthPreLogin + } + if((URLConstants.successLoginRoutes.first { url.path.hasPrefix($0)}) != nil) { + return .internalRoutes + } + if((URLConstants.loginRoutes.first { url.path.hasPrefix($0)}) != nil) { + return .loginRoutes + } + + return .internalRoutes + } + + if(url.absoluteString.starts(with: generateRedirectUri())){ + return .HostedLoginCallback + } + if((URLConstants.oauthUrls.first { urlStr.hasPrefix($0)}) != nil) { + return .SocialLoginRedirectToBrowser + } + + return .Unknown } diff --git a/Sources/FronteggSwift/utils/generateErrorPage.swift b/Sources/FronteggSwift/utils/generateErrorPage.swift new file mode 100644 index 0000000..d27e947 --- /dev/null +++ b/Sources/FronteggSwift/utils/generateErrorPage.swift @@ -0,0 +1,12 @@ +// +// generateErrorPage.swift +// +// +// Created by David Frontegg on 13/09/2023. +// + +import Foundation + +func generateErrorPage(message:String, url:String, status: Int) -> String { + return "Fatal Error
\(message)
Status Code:\(status)
Url:
\(url)
" +} diff --git a/Sources/FronteggSwift/views/DefaultLoader.swift b/Sources/FronteggSwift/views/DefaultLoader.swift index b0da87f..ecce439 100644 --- a/Sources/FronteggSwift/views/DefaultLoader.swift +++ b/Sources/FronteggSwift/views/DefaultLoader.swift @@ -9,13 +9,19 @@ import SwiftUI public struct DefaultLoader: View { + static var customLoaderView: AnyView? = nil public init(){} public var body: some View { ZStack { Color(red: 0.95, green: 0.95, blue: 0.95).ignoresSafeArea(.all) VStack { - ProgressView() + if(DefaultLoader.customLoaderView != nil){ + DefaultLoader.customLoaderView + }else { + ProgressView() + } + } } diff --git a/Sources/FronteggSwift/views/FronteggWrapper.swift b/Sources/FronteggSwift/views/FronteggWrapper.swift index f110563..9743d42 100644 --- a/Sources/FronteggSwift/views/FronteggWrapper.swift +++ b/Sources/FronteggSwift/views/FronteggWrapper.swift @@ -10,24 +10,22 @@ import SwiftUI public struct FronteggWrapper: View { var content: () -> Content - var loaderView: AnyView @StateObject var fronteggAuth = FronteggApp.shared.auth public init(loaderView: AnyView, @ViewBuilder content: @escaping () -> Content) { self.content = content - self.loaderView = loaderView + DefaultLoader.customLoaderView = loaderView } public init(@ViewBuilder content: @escaping () -> Content) { self.content = content - self.loaderView = AnyView(DefaultLoader()) } public var body: some View { ZStack { if fronteggAuth.initializing || fronteggAuth.showLoader || fronteggAuth.appLink { - self.loaderView + DefaultLoader() } else { Group(content: content) } diff --git a/demo-embedded/demo-embedded.xcodeproj/project.pbxproj b/demo-embedded/demo-embedded.xcodeproj/project.pbxproj new file mode 100644 index 0000000..a15dd86 --- /dev/null +++ b/demo-embedded/demo-embedded.xcodeproj/project.pbxproj @@ -0,0 +1,432 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + 412401B72AB20B0900AAC758 /* demo_embeddedApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 412401B62AB20B0900AAC758 /* demo_embeddedApp.swift */; }; + 412401BB2AB20B0A00AAC758 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 412401BA2AB20B0A00AAC758 /* Assets.xcassets */; }; + 412401BE2AB20B0A00AAC758 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 412401BD2AB20B0A00AAC758 /* Preview Assets.xcassets */; }; + 412401CD2AB20BF800AAC758 /* TenantsTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 412401C72AB20BF800AAC758 /* TenantsTab.swift */; }; + 412401CE2AB20BF800AAC758 /* TenantsList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 412401C82AB20BF800AAC758 /* TenantsList.swift */; }; + 412401CF2AB20BF800AAC758 /* ProfileInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 412401CA2AB20BF800AAC758 /* ProfileInfo.swift */; }; + 412401D02AB20BF800AAC758 /* ProfilePicture.swift in Sources */ = {isa = PBXBuildFile; fileRef = 412401CB2AB20BF800AAC758 /* ProfilePicture.swift */; }; + 412401D12AB20BF800AAC758 /* ProfileTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 412401CC2AB20BF800AAC758 /* ProfileTab.swift */; }; + 412401D32AB20C1000AAC758 /* MyApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 412401D22AB20C1000AAC758 /* MyApp.swift */; }; + 412401D52AB20C5600AAC758 /* LoaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 412401D42AB20C5600AAC758 /* LoaderView.swift */; }; + 412401DC2AB20D6F00AAC758 /* FronteggSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 412401DB2AB20D6F00AAC758 /* FronteggSwift */; }; + 412401DE2AB2117700AAC758 /* Frontegg.plist in Resources */ = {isa = PBXBuildFile; fileRef = 412401DD2AB2117700AAC758 /* Frontegg.plist */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 412401B32AB20B0900AAC758 /* demo-embedded.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "demo-embedded.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 412401B62AB20B0900AAC758 /* demo_embeddedApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = demo_embeddedApp.swift; sourceTree = ""; }; + 412401BA2AB20B0A00AAC758 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 412401BD2AB20B0A00AAC758 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + 412401C72AB20BF800AAC758 /* TenantsTab.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TenantsTab.swift; sourceTree = ""; }; + 412401C82AB20BF800AAC758 /* TenantsList.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TenantsList.swift; sourceTree = ""; }; + 412401CA2AB20BF800AAC758 /* ProfileInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProfileInfo.swift; sourceTree = ""; }; + 412401CB2AB20BF800AAC758 /* ProfilePicture.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProfilePicture.swift; sourceTree = ""; }; + 412401CC2AB20BF800AAC758 /* ProfileTab.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProfileTab.swift; sourceTree = ""; }; + 412401D22AB20C1000AAC758 /* MyApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyApp.swift; sourceTree = ""; }; + 412401D42AB20C5600AAC758 /* LoaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoaderView.swift; sourceTree = ""; }; + 412401D92AB20D5C00AAC758 /* frontegg-swift */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "frontegg-swift"; path = ..; sourceTree = ""; }; + 412401DD2AB2117700AAC758 /* Frontegg.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Frontegg.plist; sourceTree = ""; }; + 41A5FCA52AB4A0500022E386 /* demo-embedded.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "demo-embedded.entitlements"; sourceTree = ""; }; + 41AB118B2AB26F7900219FA9 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 412401B02AB20B0900AAC758 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 412401DC2AB20D6F00AAC758 /* FronteggSwift in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 412401AA2AB20B0900AAC758 = { + isa = PBXGroup; + children = ( + 412401C42AB20B5C00AAC758 /* Packages */, + 412401B52AB20B0900AAC758 /* demo-embedded */, + 412401B42AB20B0900AAC758 /* Products */, + 412401DA2AB20D6F00AAC758 /* Frameworks */, + ); + sourceTree = ""; + }; + 412401B42AB20B0900AAC758 /* Products */ = { + isa = PBXGroup; + children = ( + 412401B32AB20B0900AAC758 /* demo-embedded.app */, + ); + name = Products; + sourceTree = ""; + }; + 412401B52AB20B0900AAC758 /* demo-embedded */ = { + isa = PBXGroup; + children = ( + 41A5FCA52AB4A0500022E386 /* demo-embedded.entitlements */, + 41AB118B2AB26F7900219FA9 /* Info.plist */, + 412401DD2AB2117700AAC758 /* Frontegg.plist */, + 412401C92AB20BF800AAC758 /* profile */, + 412401C62AB20BF800AAC758 /* tasks */, + 412401B62AB20B0900AAC758 /* demo_embeddedApp.swift */, + 412401D22AB20C1000AAC758 /* MyApp.swift */, + 412401D42AB20C5600AAC758 /* LoaderView.swift */, + 412401BA2AB20B0A00AAC758 /* Assets.xcassets */, + 412401BC2AB20B0A00AAC758 /* Preview Content */, + ); + path = "demo-embedded"; + sourceTree = ""; + }; + 412401BC2AB20B0A00AAC758 /* Preview Content */ = { + isa = PBXGroup; + children = ( + 412401BD2AB20B0A00AAC758 /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; + 412401C42AB20B5C00AAC758 /* Packages */ = { + isa = PBXGroup; + children = ( + 412401D92AB20D5C00AAC758 /* frontegg-swift */, + ); + name = Packages; + sourceTree = ""; + }; + 412401C62AB20BF800AAC758 /* tasks */ = { + isa = PBXGroup; + children = ( + 412401C72AB20BF800AAC758 /* TenantsTab.swift */, + 412401C82AB20BF800AAC758 /* TenantsList.swift */, + ); + path = tasks; + sourceTree = ""; + }; + 412401C92AB20BF800AAC758 /* profile */ = { + isa = PBXGroup; + children = ( + 412401CA2AB20BF800AAC758 /* ProfileInfo.swift */, + 412401CB2AB20BF800AAC758 /* ProfilePicture.swift */, + 412401CC2AB20BF800AAC758 /* ProfileTab.swift */, + ); + path = profile; + sourceTree = ""; + }; + 412401DA2AB20D6F00AAC758 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 412401B22AB20B0900AAC758 /* demo-embedded */ = { + isa = PBXNativeTarget; + buildConfigurationList = 412401C12AB20B0A00AAC758 /* Build configuration list for PBXNativeTarget "demo-embedded" */; + buildPhases = ( + 412401AF2AB20B0900AAC758 /* Sources */, + 412401B02AB20B0900AAC758 /* Frameworks */, + 412401B12AB20B0900AAC758 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "demo-embedded"; + packageProductDependencies = ( + 412401DB2AB20D6F00AAC758 /* FronteggSwift */, + ); + productName = "demo-embedded"; + productReference = 412401B32AB20B0900AAC758 /* demo-embedded.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 412401AB2AB20B0900AAC758 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1430; + LastUpgradeCheck = 1430; + TargetAttributes = { + 412401B22AB20B0900AAC758 = { + CreatedOnToolsVersion = 14.3.1; + }; + }; + }; + buildConfigurationList = 412401AE2AB20B0900AAC758 /* Build configuration list for PBXProject "demo-embedded" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 412401AA2AB20B0900AAC758; + productRefGroup = 412401B42AB20B0900AAC758 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 412401B22AB20B0900AAC758 /* demo-embedded */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 412401B12AB20B0900AAC758 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 412401DE2AB2117700AAC758 /* Frontegg.plist in Resources */, + 412401BE2AB20B0A00AAC758 /* Preview Assets.xcassets in Resources */, + 412401BB2AB20B0A00AAC758 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 412401AF2AB20B0900AAC758 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 412401CF2AB20BF800AAC758 /* ProfileInfo.swift in Sources */, + 412401CE2AB20BF800AAC758 /* TenantsList.swift in Sources */, + 412401D12AB20BF800AAC758 /* ProfileTab.swift in Sources */, + 412401CD2AB20BF800AAC758 /* TenantsTab.swift in Sources */, + 412401D32AB20C1000AAC758 /* MyApp.swift in Sources */, + 412401D02AB20BF800AAC758 /* ProfilePicture.swift in Sources */, + 412401B72AB20B0900AAC758 /* demo_embeddedApp.swift in Sources */, + 412401D52AB20C5600AAC758 /* LoaderView.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 412401BF2AB20B0A00AAC758 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 412401C02AB20B0A00AAC758 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 412401C22AB20B0A00AAC758 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = "demo-embedded/demo-embedded.entitlements"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"demo-embedded/Preview Content\""; + DEVELOPMENT_TEAM = 3R2X6557U2; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "demo-embedded/Info.plist"; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.frontegg.demo; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 412401C32AB20B0A00AAC758 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = "demo-embedded/demo-embedded.entitlements"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"demo-embedded/Preview Content\""; + DEVELOPMENT_TEAM = 3R2X6557U2; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "demo-embedded/Info.plist"; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.frontegg.demo; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 412401AE2AB20B0900AAC758 /* Build configuration list for PBXProject "demo-embedded" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 412401BF2AB20B0A00AAC758 /* Debug */, + 412401C02AB20B0A00AAC758 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 412401C12AB20B0A00AAC758 /* Build configuration list for PBXNativeTarget "demo-embedded" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 412401C22AB20B0A00AAC758 /* Debug */, + 412401C32AB20B0A00AAC758 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCSwiftPackageProductDependency section */ + 412401DB2AB20D6F00AAC758 /* FronteggSwift */ = { + isa = XCSwiftPackageProductDependency; + productName = FronteggSwift; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 412401AB2AB20B0900AAC758 /* Project object */; +} diff --git a/demo-embedded/demo-embedded.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/demo-embedded/demo-embedded.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/demo-embedded/demo-embedded.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/demo-embedded/demo-embedded.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/demo-embedded/demo-embedded.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/demo-embedded/demo-embedded.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/demo-embedded/demo-embedded.xcodeproj/xcshareddata/xcschemes/demo-embedded.xcscheme b/demo-embedded/demo-embedded.xcodeproj/xcshareddata/xcschemes/demo-embedded.xcscheme new file mode 100644 index 0000000..885e83c --- /dev/null +++ b/demo-embedded/demo-embedded.xcodeproj/xcshareddata/xcschemes/demo-embedded.xcscheme @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/demo-embedded/demo-embedded/Assets.xcassets/AccentColor.colorset/Contents.json b/demo-embedded/demo-embedded/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/demo-embedded/demo-embedded/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/demo-embedded/demo-embedded/Assets.xcassets/AppIcon.appiconset/Contents.json b/demo-embedded/demo-embedded/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..13613e3 --- /dev/null +++ b/demo-embedded/demo-embedded/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/demo-embedded/demo-embedded/Assets.xcassets/Contents.json b/demo-embedded/demo-embedded/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/demo-embedded/demo-embedded/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/demo-embedded/demo-embedded/Frontegg.plist b/demo-embedded/demo-embedded/Frontegg.plist new file mode 100644 index 0000000..ab8dc61 --- /dev/null +++ b/demo-embedded/demo-embedded/Frontegg.plist @@ -0,0 +1,14 @@ + + + + + embeddedMode + + baseUrl + https://auth.davidantoon.me + clientId + b6adfe4c-d695-4c04-b95f-3ec9fd0c6cca + logLevel + trace + + diff --git a/demo-embedded/demo-embedded/Info.plist b/demo-embedded/demo-embedded/Info.plist new file mode 100644 index 0000000..1a1bf62 --- /dev/null +++ b/demo-embedded/demo-embedded/Info.plist @@ -0,0 +1,11 @@ + + + + + UIBackgroundModes + + fetch + processing + + + diff --git a/demo-embedded/demo-embedded/LoaderView.swift b/demo-embedded/demo-embedded/LoaderView.swift new file mode 100644 index 0000000..60d68f5 --- /dev/null +++ b/demo-embedded/demo-embedded/LoaderView.swift @@ -0,0 +1,27 @@ +// +// LoaderView.swift +// demo-embedded +// +// Created by David Frontegg on 13/12/2022. +// + +import SwiftUI + +struct LoaderView: View { + var body: some View { + Color(red: 0.95, green: 0.95, blue: 0.95).ignoresSafeArea(.all) + VStack { + Image("SplashIcon") + .resizable() + .frame(width: 100, height: 100) + ProgressView() + + } + } +} + +struct LoaderView_Previews: PreviewProvider { + static var previews: some View { + LoaderView() + } +} diff --git a/demo-embedded/demo-embedded/MyApp.swift b/demo-embedded/demo-embedded/MyApp.swift new file mode 100644 index 0000000..043e083 --- /dev/null +++ b/demo-embedded/demo-embedded/MyApp.swift @@ -0,0 +1,52 @@ +// +// MyApp.swift +// demo-embedded +// +// Created by David Frontegg on 14/11/2022. +// + +import SwiftUI +import FronteggSwift + +struct MyApp: View { + @EnvironmentObject var fronteggAuth: FronteggAuth + + var body: some View { + ZStack { + if fronteggAuth.isAuthenticated { + TabView { + ProfileTab() + .tabItem { + Image(systemName: "person.circle") + Text("Profile") + } + TenantsTab() + .tabItem { + Image(systemName: "checklist") + Text("Tenants") + } + } + } else { + + // EmbeddedLoginPage() + + DefaultLoader().onAppear(){ + if(!FronteggAuth.shared.isAuthenticated){ + fronteggAuth.login() + } + } +// Button(action: { +// fronteggAuth.login() +// }) { +// Text("Open Login") +// } + } + } + } +} + +struct MyApp_Previews: PreviewProvider { + static var previews: some View { + MyApp() + } +} diff --git a/demo-embedded/demo-embedded/Preview Content/Preview Assets.xcassets/Contents.json b/demo-embedded/demo-embedded/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/demo-embedded/demo-embedded/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/demo-embedded/demo-embedded/demo-embedded.entitlements b/demo-embedded/demo-embedded/demo-embedded.entitlements new file mode 100644 index 0000000..9f51680 --- /dev/null +++ b/demo-embedded/demo-embedded/demo-embedded.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.developer.associated-domains + + webcredentials:auth.davidantoon.me + applinks:auth.davidantoon.me + webcredentials:*.davidantoon.me + + + diff --git a/demo-embedded/demo-embedded/demo_embeddedApp.swift b/demo-embedded/demo-embedded/demo_embeddedApp.swift new file mode 100644 index 0000000..5b785f8 --- /dev/null +++ b/demo-embedded/demo-embedded/demo_embeddedApp.swift @@ -0,0 +1,20 @@ +// +// demo_embeddedApp.swift +// demo-embedded +// +// Created by David Frontegg on 13/09/2023. +// + +import SwiftUI +import FronteggSwift + +@main +struct demo_embeddedApp: App { + var body: some Scene { + WindowGroup { + FronteggWrapper() { + MyApp() + } + } + } +} diff --git a/demo-embedded/demo-embedded/profile/ProfileInfo.swift b/demo-embedded/demo-embedded/profile/ProfileInfo.swift new file mode 100644 index 0000000..b197e3f --- /dev/null +++ b/demo-embedded/demo-embedded/profile/ProfileInfo.swift @@ -0,0 +1,37 @@ +// +// ProfileInfo.swift +// +// Created by David Frontegg on 14/11/2022. +// + +import SwiftUI +import FronteggSwift + +struct ProfileInfo: View { + @EnvironmentObject var fronteggAuth: FronteggAuth + + var body: some View { + VStack(alignment: .leading) { + Text(fronteggAuth.user?.name ?? "Name") + .font(.title) + + HStack { + Text(fronteggAuth.user?.email ?? "Email") + .font(.subheadline) + Spacer() + Text("Admin") + .font(.subheadline) + } + Text(fronteggAuth.user?.activeTenant.name ?? "no tenant") + .font(.subheadline) + Spacer() + } + .padding() + } +} + +struct ProfileInfo_Previews: PreviewProvider { + static var previews: some View { + ProfileInfo() + } +} diff --git a/demo-embedded/demo-embedded/profile/ProfilePicture.swift b/demo-embedded/demo-embedded/profile/ProfilePicture.swift new file mode 100644 index 0000000..76717c1 --- /dev/null +++ b/demo-embedded/demo-embedded/profile/ProfilePicture.swift @@ -0,0 +1,43 @@ +// +// ProfilePicture.swift +// +// Created by David Frontegg on 14/11/2022. +// + +import SwiftUI +import FronteggSwift + +struct ProfilePicture: View { + @EnvironmentObject var fronteggAuth: FronteggAuth + + + var body: some View { + if #available(iOS 15.0, *) { + AsyncImage( + url: URL(string: self.fronteggAuth.user?.profilePictureUrl ?? ""), + content: { image in + image + .resizable() + .frame(width: 160, height: 160) + .clipShape(Circle()) + .shadow(radius: 2) + }, + placeholder: { + Image("ProfileImg") + .resizable() + .frame(width: 160, height: 160) + .clipShape(Circle()) + .shadow(radius: 2) + } + ) + } else { + // Fallback on earlier versions + } + } +} + +struct ProfilePicture_Previews: PreviewProvider { + static var previews: some View { + ProfilePicture() + } +} diff --git a/demo-embedded/demo-embedded/profile/ProfileTab.swift b/demo-embedded/demo-embedded/profile/ProfileTab.swift new file mode 100644 index 0000000..805145c --- /dev/null +++ b/demo-embedded/demo-embedded/profile/ProfileTab.swift @@ -0,0 +1,48 @@ +// +// ProfileTab.swift +// +// Created by David Frontegg on 14/11/2022. +// + +import SwiftUI +import FronteggSwift + +struct ProfileTab: View { + + @EnvironmentObject var fronteggAuth: FronteggAuth + + + var body: some View { + NavigationView { + VStack{ + ProfilePicture() + .padding(.top, 60) + .padding(.bottom, 40) + + ProfileInfo() + + Button("register-passkeys") { + +// fronteggAuth.registerPasskeys() +// +// + } + Spacer() + Button("Logout") { + fronteggAuth.logout() + } + .foregroundColor(.red) + .font(.title2) + .padding(.bottom, 40) + } + + .navigationTitle("Profile") + } + } +} + +struct ProfileTab_Previews: PreviewProvider { + static var previews: some View { + ProfileTab() + } +} diff --git a/demo-embedded/demo-embedded/tasks/TenantsList.swift b/demo-embedded/demo-embedded/tasks/TenantsList.swift new file mode 100644 index 0000000..a10e513 --- /dev/null +++ b/demo-embedded/demo-embedded/tasks/TenantsList.swift @@ -0,0 +1,36 @@ + +import SwiftUI +import FronteggSwift + +struct TenantsList: View { + @EnvironmentObject var fronteggAuth: FronteggAuth + @State var switchingTenant:String? = nil + + var body: some View { + + ForEach(fronteggAuth.user?.tenants.sorted(by: { t1, t2 in + return t1.name < t2.name + }) ?? [], id: \.tenantId.self) { item in + Button(action: { + switchingTenant = item.tenantId + fronteggAuth.switchTenant(tenantId: item.tenantId) { _ in + switchingTenant = nil + } + }) { + Text("\(item.name)\(fronteggAuth.user?.activeTenant.tenantId == item.tenantId ? " (active)" : switchingTenant == item.tenantId ? " (swithcing...)" : "")") + .font(.title2) + .padding(.bottom, 8) + }.frame(maxWidth: .infinity, alignment: .leading) // Make the entire row clickable + .contentShape(Rectangle()) + } + + } +} + +struct TaskList_Previews: PreviewProvider { + + + static var previews: some View { + TenantsList() + } +} diff --git a/demo-embedded/demo-embedded/tasks/TenantsTab.swift b/demo-embedded/demo-embedded/tasks/TenantsTab.swift new file mode 100644 index 0000000..3e240ae --- /dev/null +++ b/demo-embedded/demo-embedded/tasks/TenantsTab.swift @@ -0,0 +1,28 @@ +// +// TasksTab.swift +// +// Created by David Frontegg on 14/11/2022. +// + + +import SwiftUI + +struct TenantsTab: View { + + var body: some View { + NavigationView { + VStack(alignment: .leading) { + TenantsList() + Spacer() + }.padding(.all, 20) + + .navigationTitle("Tenants") + } + } +} + +struct TasksTab_Previews: PreviewProvider { + static var previews: some View { + TenantsTab() + } +}