Skip to content

Commit

Permalink
Merge pull request #37 from frontegg/FR-15142-support-login-popup
Browse files Browse the repository at this point in the history
  • Loading branch information
amirjaron authored Feb 6, 2024
2 parents b7974f5 + ce28596 commit f9182db
Show file tree
Hide file tree
Showing 2 changed files with 140 additions and 40 deletions.
144 changes: 114 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

![Frontegg_SwiftUI_SDK](https://github.com/frontegg/frontegg-ios-swift/raw/master/logo.png)

Frontegg is a web platform where SaaS companies can set up their fully managed, scalable and brand aware - SaaS features
Expand All @@ -23,6 +22,7 @@ and integrate them into their SaaS portals in up to 5 lines of code.
- [Embedded Webview vs ASWebAuthenticationSession](#embedded-webview-vs-aswebauthenticationsession)
- [Config iOS associated domain](#config-ios-associated-domain)
- [Multi-Region support](#multi-region-support)
- [Login with ASWebAuthenticationSession](#login-with-aswebauthenticationsession)

## Project Requirements

Expand All @@ -36,14 +36,14 @@ Major platform versions are supported, starting from:

- iOS **> 14**


## Getting Started

### Prepare Frontegg workspace

Navigate to [Frontegg Portal Settings](https://portal.frontegg.com/development/settings), If you don't have application
follow integration steps after signing up.
Copy FronteggDomain to future steps from [Frontegg Portal Domain](https://portal.frontegg.com/development/settings/domains)
Copy FronteggDomain to future steps
from [Frontegg Portal Domain](https://portal.frontegg.com/development/settings/domains)

### Setup Hosted Login

Expand All @@ -62,19 +62,20 @@ Copy FronteggDomain to future steps from [Frontegg Portal Domain](https://portal

### 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:
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">
<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>
</dict>
<dict>
<key>baseUrl</key>
<string>https://[DOMAIN_HOST_FROM_PREVIOUS_STEP]</string>
<key>clientId</key>
<string>[CLIENT_ID_FROM_PREVIOUS_STEP]</string>
</dict>
</plist>
```

Expand Down Expand Up @@ -102,7 +103,7 @@ your root project directory, this file will store values to be used variables by
- Modify `MyApp.swift` file to render content if user is authenticated:
1. Add `@EnvironmentObject var fronteggAuth: FronteggAuth` to
2. Render your entire application based on `fronteggAuth.isAuthenticated`

```swift
struct MyApp: View {
@EnvironmentObject var fronteggAuth: FronteggAuth
Expand All @@ -127,7 +128,7 @@ your root project directory, this file will store values to be used variables by
- ### Add custom loading screen

To use your own `LoadingView` / `SplashScreen`:

- Build your loading view in separated file
- Pass `LoadingView` as AnyView to the FronteggWrapper
```swift
Expand Down Expand Up @@ -174,12 +175,12 @@ your root project directory, this file will store values to be used variables by

}
```
- Create a new ViewController for AuthenticationController:
- Create a new ViewController for AuthenticationController:
1. Change viewController's class to `AuthenticationController`
2. Set Storyboard ID to `fronteggController`
3. Make sure that the view controller is the initial view controller
![AuthenticationController](./assets/README_authentication_controller.png)
![AuthenticationController](./assets/README_authentication_controller.png)

- Setup SceneDelegate for Frontegg universal links:
```swift
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
Expand Down Expand Up @@ -256,10 +257,12 @@ 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:
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"?>
Expand All @@ -280,15 +283,21 @@ By default Frontegg SDK will use Embedded Webview, to use ASWebAuthenticationSes
```

### Config iOS associated domain

Configuring your iOS associated domain is required for Magic Link authentication / Reset Password / Activate Account.

In order to add your iOS associated domain to your Frontegg application, you will need to update in each of your integrated Frontegg Environments the iOS associated domain that you would like to use with that Environment. Send a POST request to `https://api.frontegg.com/vendors/resources/associated-domains/v1/ios` with the following payload:
In order to add your iOS associated domain to your Frontegg application, you will need to update in each of your
integrated Frontegg Environments the iOS associated domain that you would like to use with that Environment. Send a POST
request to `https://api.frontegg.com/vendors/resources/associated-domains/v1/ios` with the following payload:

```
{
“appId”:[YOUR_ASSOCIATED_DOMAIN]
}
```
In order to use our API’s, follow [this guide](‘https://docs.frontegg.com/reference/getting-started-with-your-api’) to generate a vendor token.

In order to use our API’s, follow [this guide](‘https://docs.frontegg.com/reference/getting-started-with-your-api’) to
generate a vendor token.

Next, you will need to add your associated domain to your iOS application. To do so, follow the steps below:

Expand All @@ -302,9 +311,9 @@ Next, you will need to add your associated domain to your iOS application. To do
7. Enter your associated domain in the format `webcredentials:[YOUR_ASSOCIATED_DOMAIN]`.
8. Click Done.

`[YOUR_ASSOCIATED_DOMAIN]` is the associated domain that you would like to use with your iOS application.
For example, if you would like to use `https://example.com` as your associated domain, you would enter `applinks:example.com` and `webcredentials:example.com`.

`[YOUR_ASSOCIATED_DOMAIN]` is the associated domain that you would like to use with your iOS application.
For example, if you would like to use `https://example.com` as your associated domain, you would
enter `applinks:example.com` and `webcredentials:example.com`.

## Multi-Region Support

Expand All @@ -315,49 +324,56 @@ This guide outlines the steps to configure your iOS application to support multi
First, adjust your Frontegg.plist file to handle multiple regions:

**Modifications**:

- **Remove** the existing `baseUrl` and `clientId` keys.
- **Add** a new array key named `regions`. This array will hold dictionaries for each region.

Example Frontegg.plist Structure:

```xml

<key>regions</key>
<array>
<dict>
<dict>
<key>key</key>
<string>us-region</string>
<key>baseUrl</key>
<string>https://us-region-api.frontegg.com</string>
<key>clientId</key>
<string>your-client-id-for-us-region</string>
</dict>
<!-- Add additional regions in a similar format -->
</dict>
<!-- Add additional regions in a similar format -->
</array>
```

### Step 2: Add Associated Domains for Each Region

For each region, configure the associated domains in your application's settings. This is vital for proper API routing and authentication.
For each region, configure the associated domains in your application's settings. This is vital for proper API routing
and authentication.

Example Associated Domain Configuration:
[demo-multi-region.entitlements](demo-multi-region%2Fdemo-multi-region%2Fdemo-multi-region.entitlements)

Follow [Config iOS associated domain](#config-ios-associated-domain) to add your iOS associated domain to your Frontegg application.

Follow [Config iOS associated domain](#config-ios-associated-domain) to add your iOS associated domain to your Frontegg
application.

### Step 3: Implement Region Selection UI

The final step is to implement a UI for the user to select their region. **This can be done in any way you see fit**.
The final step is to implement a UI for the user to select their region. **This can be done in any way you see fit**.
The example application uses a simple picker view to allow the user to select their region.

**Important Considerations**
- **Switching Regions**: To switch regions, update the selection in UserDefaults. If issues arise, a **re-installation** of the application might be necessary.

- **Switching Regions**: To switch regions, update the selection in UserDefaults. If issues arise, a **re-installation**
of the application might be necessary.
- **Data Isolation**: Ensure data handling and APIs are region-specific to prevent data leakage between regions.

| Select EU Region | Select US Region |
|:--------------------------------------------------------:|:--------------------------------------------------------:|
| ![eu-region-example.gif](assets%2Feu-region-example.gif) | ![us-region-example.gif](assets%2Fus-region-example.gif) |

Example Region Selection UI:

```swift
import SwiftUI
import FronteggSwift
Expand Down Expand Up @@ -402,3 +418,71 @@ struct SelectRegionView: View {
}
}
```

## Login with ASWebAuthenticationSession

Starting from version 1.3.0, the Frontegg SDK has introduced support for ASWebAuthenticationSession, enhancing the login
experience. This new feature allows for a more streamlined and secure authentication process.

A `loginWithPopup` method has added with parameters to accommodate the integration of ASWebAuthenticationSession:

- `window`: Specifies the window where the login view controller will be presented. If this parameter is not provided,
the SDK will default to using the key window.
- `ephemeralSession`: A Boolean flag indicating if the session should be ephemeral. By default, this is set to `true`.
- `loginHint`: An optional parameter for the login hint to be used during the login process. It is `nil` by default.
- `loginAction`: An optional parameter that defines the login action to be used. It also defaults to `nil`.
- `completion`: A completion handler that is called once the login process concludes.

Below are examples demonstrating how to utilize ASWebAuthenticationSession with the Frontegg SDK in both UIKit and SwiftUI applications.


### UIKit Implementation

For developers using UIKit, the login process can be initiated as follows:

```swift
import UIKit
import FronteggSwift

class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}

@IBAction func loginButtonTapped() {
FronteggAuth.shared.loginWithPopup(window: self.view.window) { result in
switch result {
case .success(let user):
print("User logged in: \(user)")
case .failure(let error):
print("Error logging in: \(error)")
}
}
}
}
```

### SwiftUI Implementation

For those preferring SwiftUI, the integration is similarly straightforward:

```swift
import SwiftUI
import FronteggSwift

struct ContentView: View {
@EnvironmentObject var fronteggAuth: FronteggAuth

var body: some View {
VStack {
if fronteggAuth.isAuthenticated {
Text("User Authenticated")
} else {
Button("Login") {
fronteggAuth.loginWithPopup()
}
}
}
}
}
```
36 changes: 26 additions & 10 deletions Sources/FronteggSwift/FronteggAuth.swift
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ public class FronteggAuth: ObservableObject {
}
}
} catch {
print(error)
logger.error("Failed to load user data, \(error)")
DispatchQueue.main.sync {
self.refreshToken = nil
self.accessToken = nil
Expand Down Expand Up @@ -261,7 +261,7 @@ public class FronteggAuth: ObservableObject {

Task {

print("Going to exchange token")
logger.info("Going to exchange token")
let (responseData, error) = await api.exchangeToken(
code: code,
redirectUrl: redirectUri,
Expand All @@ -281,13 +281,13 @@ public class FronteggAuth: ObservableObject {
}

do {
print("Going to load user data")
logger.info("Going to load user data")
let user = try await self.api.me(accessToken: data.access_token)
await setCredentials(accessToken: data.access_token, refreshToken: data.refresh_token)

completion(.success(user!))
} catch {
print("Failed to load user data: \(error.localizedDescription)")
logger.error("Failed to load user data: \(error.localizedDescription)")
completion(.failure(FronteggError.authError("Failed to load user data: \(error.localizedDescription)")))
setIsLoading(false)
return
Expand Down Expand Up @@ -361,6 +361,23 @@ public class FronteggAuth: ObservableObject {
}


public func loginWithPopup(window: UIWindow?, ephemeralSesion: Bool? = true, loginHint: String? = nil, loginAction: String? = nil, _completion: FronteggAuth.CompletionHandler? = nil) {

self.webAuthentication.webAuthSession?.cancel()
self.webAuthentication = WebAuthentication()
self.webAuthentication.window = window;
self.webAuthentication.ephemeralSesion = ephemeralSesion ?? true

let completion = _completion ?? { res in

}

let oauthCallback = createOauthCallbackHandler(completion)
let (authorizeUrl, codeVerifier) = AuthorizeUrlGenerator.shared.generate(loginHint: loginHint, loginAction: loginAction)
CredentialManager.saveCodeVerifier(codeVerifier)
self.webAuthentication.start(authorizeUrl, completionHandler: oauthCallback)
}

internal func getRootVC() -> UIViewController? {


Expand All @@ -387,7 +404,7 @@ public class FronteggAuth: ObservableObject {
}


func loginWithSSO(email: String, _ _completion: FronteggAuth.CompletionHandler? = nil) {
public func loginWithSSO(email: String, _ _completion: FronteggAuth.CompletionHandler? = nil) {
let completion = _completion ?? { res in

}
Expand Down Expand Up @@ -440,14 +457,13 @@ public class FronteggAuth: ObservableObject {
hostingController.modalPresentationStyle = .fullScreen

if(rootVC.presentedViewController?.classForCoder == hostingController.classForCoder){
print("same");
rootVC.presentedViewController?.dismiss(animated: false)
}

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

} else {
print(FronteggError.authError("Unable to find root viewController"))
logger.critical(FronteggError.authError("Unable to find root viewController").localizedDescription)
exit(500)
}
}
Expand All @@ -462,7 +478,7 @@ public class FronteggAuth: ObservableObject {
self.pendingAppLink = url
self.webLoading = true
guard let rootVC = self.getRootVC() else {
print(FronteggError.authError("Unable to find root viewController"))
logger.error(FronteggError.authError("Unable to find root viewController").localizedDescription)
return false;
}

Expand All @@ -486,9 +502,9 @@ public class FronteggAuth: ObservableObject {

switch (res) {
case .success(let user) :
print("User \(user.id)")
self.logger.trace("User \(user.id)")
case .failure(let error) :
print("Error \(error)")
self.logger.trace("Error \(error)")
}
}
self.webAuthentication.start(url, completionHandler: oauthCallback)
Expand Down

0 comments on commit f9182db

Please sign in to comment.