Xcode: The official IDE for iOS development. Install it from the Mac App Store.
This app was built using AppAuth, an open-source client SDK for communicating with OAuth 2.0 and OpenID Connect providers. AppAuth supports iOS 7 and newer.
While this sample application doesn't have login functionality without FusionAuth, a more typical integration will replace an existing login system with FusionAuth.
In that case, the system might look like this before FusionAuth is introduced.
Request flow during login before FusionAuth
The login flow will look like this after FusionAuth is introduced.
Request flow during login after FusionAuth
In general, you are introducing FusionAuth in order to normalize and consolidate user data. This helps make sure it is consistent and up-to-date as well as offloading your login security and functionality to FusionAuth.
In this section, you’ll get FusionAuth up and running and use Git to create a new application.
First, grab the code from the repository and change to that directory.
git clone https://github.com/FusionAuth/fusionauth-quickstart-swift-ios-native
cd fusionauth-quickstart-swift-ios-native
You'll find a Docker Compose file (docker-compose.yml
) and an environment variables configuration file (.env
) in the root directory of the repo.
Assuming you have Docker installed, you can stand up FusionAuth on your machine with the following.
docker compose up -d
Here you are using a bootstrapping feature of FusionAuth called Kickstart. When FusionAuth comes up for the first time, it will look at the kickstart/kickstart.json
file and configure FusionAuth to your specified state.
If you ever want to reset the FusionAuth application, you need to delete the volumes created by Docker Compose by executing docker compose down -v
, then re-run docker compose up -d
.
FusionAuth will be initially configured with these settings:
e9fdb985-9173-4e01-9d73-ac2d60d1dc8e
.super-secret-secret-that-should-be-regenerated-for-production
.richard@example.com
and the password is password
.admin@example.com
and the password is password
.http://localhost:9011/
.You can log in to the FusionAuth admin UI and look around if you want to, but with Docker and Kickstart, everything will already be configured correctly.
If you want to see where the FusionAuth values came from, they can be found in the FusionAuth app. The tenant Id is found on the Tenants page. To see the Client Id and Client Secret, go to the Applications page and click the View
icon under the actions for the ChangeBank application. You'll find the Client Id and Client Secret values in the OAuth configuration
section.
The .env
file contains passwords. In a real application, always add this file to your .gitignore
file and never commit secrets to version control.
Your FusionAuth instance is now running on a different machine (your computer) than the mobile app will run (either a real device or an emulator), which means that it won’t be able to access localhost
.
If the device and your computer are not connected to the same network or if you have something that blocks connections (like a firewall), learn how to expose a local FusionAuth instance to the internet. In summary, the process entails configuring ngrok on your local system, starting your FusionAuth instance on port 9011, and subsequently executing the following command.
ngrok http --request-header-add 'X-Forwarded-Port:443' 9011
This will generate a public URL that you can use to access FusionAuth when developing the app.
If the device (either real or emulator) and your computer are connected to the same network, you can use the local IP Address for your machine (for example, 192.168.15.2
). Here are a few articles to help you find your IP address, depending on the operating system you are running:
Now you will create an iOS app. While this section builds a simple iOS app using AppAuth, you can use the same configuration to integrate your existing app with FusionAuth.
Open Xcode and click File -> New Project . Choose “iOS” as the platform, and “App” as the application type. Click Next.
Set Product Name to ChangeBank
. Set the Organization Identifier to FusionAuth
.
If you use different values here, you will need to update the redirect URLs in your FusionAuth application to match your changes.
Click Next, and choose a location on your computer to save the project to.
Now you have a standard “Hello World” app. You can run it in the simulator by clicking the play button in the top left corner of Xcode or selecting Product -> Run from the menu bar.
You’ll use the AppAuth Library, which simplifies integrating with FusionAuth and creating a secure web application.
There are many ways to add the AppAuth library to your project. The simplest way without installing any additional tools is to add the package to the project.
Select File -> Add Package Dependency from the menu bar. In the search bar, paste in https://github.com/openid/AppAuth-ios
. In the list of packages select “appauth-ios” and click Add Package. You will be asked to choose the packages to install. On the “Add to Target” column select Changebank
for both “AppAuth” and “AppAuthCore”. Click Add Package to add the packages to your project.
Before you start coding, you need to add some assets and styles to your project that will be referenced later.
Add the ChangeBank logo to the project. Download it from here. Click on the Assets directory in the project navigator to open the asset manager. Drag and drop the downloaded changebank.png
file into the asset manager.
Add an app icon. Download it from here. Click on the Assets directory in the project navigator to open the asset manager. Click the AppIcon item in the asset manager. Now drag and drop the downloaded AppIcon.png
file into the AppIcon placeholder in the asset manager.
Add a new styles file to the project. Select File -> New -> File from the menu bar. Select Swift File as the template. Click Next. Set the name of the file to Styles.swift
. Click Create.
Add the following code to the Styles.swift
file:
//
// Styles.swift
// Change Bank
//
import Foundation
import SwiftUI
struct PrimaryButtonStyle: ButtonStyle {
var color: Color = Color(red: 0.0353, green: 0.3882, blue: 0.1412)
func makeBody(configuration: Configuration) -> some View {
configuration.label.padding([
.trailing, .leading], 48)
.padding([.top, .bottom], 12).background(color)
.foregroundColor(.white).clipShape(Capsule())
}
}
struct SecondaryButtonStyle: ButtonStyle {
var color: Color = .white
var textColor: Color = Color(red: 0.0353, green: 0.3882, blue: 0.1412)
func makeBody(configuration: Configuration) -> some View {
configuration.label
.padding([.trailing, .leading], 48)
.padding([.top, .bottom], 12)
.background(color)
.foregroundColor(textColor)
.clipShape(Capsule())
.overlay(Capsule().stroke(textColor, lineWidth: 2)) // overlay with Capsule stroke for the border
}
}
struct CurrencyTextFieldStyle: TextFieldStyle {
func _body(configuration: TextField<Self._Label>) -> some View {
HStack {
Text("$").foregroundColor(Color(red: 0.0353, green: 0.3882, blue: 0.1412))
configuration
}
.padding(10)
.overlay(
Capsule().stroke(Color(red: 0.0353, green: 0.3882, blue: 0.1412), lineWidth: 2)
)
.keyboardType(.numberPad)
.textContentType(.oneTimeCode)
}
}
To store the settings necessary to connect to the FusionAuth instance, you need to add a Property List file (or “p-list file” for short) to the project. None of the settings stored are sensitive, so you can safely store them in an unencrypted p-list file.
Select File -> New -> File from the menu bar. Select Property List in the Resource section as the template. Click Next. Set the name of the file to ChangeBank
. Click Create. The file will be opened in the p-list editor.
Expand the “Root” item by clicking the arrow > button, then click the + button next to the “Root” item. Name the new item authCredentials
, and select the Type as Dictionary
.
Now click the + button next to the “authCredentials” item. Name the new item clientId
, and select the Type as String
. In the “Value” column, enter e9fdb985-9173-4e01-9d73-ac2d60d1dc8e
.
Expand the “authCredentials” item by clicking the arrow > button. Click the + button next to the “authCredentials” item again. Name the new item issuer
, and select the Type as String
. In the “Value” column, enter http://localhost:9011
.
To read the settings from the p-list file, you need to add a new class to the project. Select File -> New -> File from the menu bar. Select Swift File as the template and click Next. Set the name of the file to AppSettingsReader.swift
. Click Create.
Add the following code to the AppSettingsReader.swift
file:
//
// AppSettingsReader.swift
// ChangeBank
//
import Foundation
struct AppSettingsRoot : Decodable {
internal let authCredentials : AuthCredentials
}
struct AuthCredentials : Decodable {
internal let clientId : String
internal let issuer : String
}
class AppSettingsReader {
func loadAppSettings() -> AppSettingsRoot {
let url = Bundle.main.url(forResource: "ChangeBank", withExtension:"plist")!
let data = try! Data(contentsOf: url)
let appSettings = try! PropertyListDecoder().decode(AppSettingsRoot.self, from: data)
return appSettings
}
}
This code adds new structs to hold the credentials and issuer settings, and a class to read the settings from the p-list file.
The AppAuth library was built for UIKit. To use it in a SwiftUI app, you need to implement some bridge logic between the two frameworks.
The main component to add is an AppDelegate
class. This class will be responsible for coordinating the breakout browser window to handle the OAuth flow and interfacing with AppAuth.
Select File -> New -> File from the menu bar. Select Swift File as the template. Click Next. Set the name of the file to AppDelegate.swift
. Click Create.
Add the following code to the AppDelegate.swift
file:
//
// AppDelegate.swift
// Change Bank
import AppAuth
import SwiftUI
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var currentAuthorizationFlow: OIDExternalUserAgentSession?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
return true
}
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
if let authorizationFlow = self.currentAuthorizationFlow, authorizationFlow.resumeExternalUserAgentFlow(with: url) {
self.currentAuthorizationFlow = nil
return true
}
return false
}
}
The AppDelegate
class extends UIResponder
and adopts the UIApplicationDelegate
protocol, which means it listens for events in the iOS app’s lifecycle.
In the application(_ app:UIApplication open url: URL options: )
method, the code checks if the URL being opened corresponds to an authorization flow and, if so, it tries to resume this flow. This flow relates to the situation where a user has completed a login or logout function in a web browser and the app is reopened via a callback URL from FusionAuth. The resumeExternalUserAgentFlow(with:)
method is a part of AppAuth that handles the incoming URL and determines whether it corresponds to the AppAuth flow that was initiated, proceeding with the process if it matches.
The next step is to add an authentication flow manager class to the app. This class will be responsible for discovering the auth server endpoints and keys, initiating login and logout flows, and handling the results of those flows. It will also be responsible for storing the user’s authentication state and retrieving the user’s profile information from FusionAuth.
Select the File -> New -> File from the menu bar. Select Swift File as the template. Click Next. Set the name of the file to UserAuth.swift
. Click Create.
Copy the following code into the UserAuth.swift
file:
//
// UserAuth.swift
// ChangeBank
//
import Foundation
import SwiftUI
import AppAuth
import UIKit
class UserAuth: ObservableObject {
@Published var isLoggedIn: Bool = false
@Published var email: String = ""
@Published var given_name: String = ""
@Published var family_name: String = ""
private let redirectUrl: String = "\(Bundle.main.bundleIdentifier ?? ""):/oauth2redirect/ios-provider";
private var config : OIDServiceConfiguration?
private var authState : OIDAuthState?
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
private let authStateKey : String = "authState"
private let suiteName : String = "com.fusionauth.changebank"
init() {
loadState()
discoverConfiguration()
}
// MARK: - Login and Logout
func login() {
let appSettings = AppSettingsReader().loadAppSettings()
let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene
let presentingView = windowScene?.windows.first?.rootViewController
// Create redirectURI from redirectURL string
guard let redirectURI = URL(string: redirectUrl) else {
print("Error creating URL for : \(redirectUrl)")
return
}
// Create login request
let request = OIDAuthorizationRequest(configuration: config!, clientId: appSettings.authCredentials.clientId, clientSecret: nil, scopes: ["openid", "profile", "email", "offline_access"],
redirectURL: redirectURI, responseType: OIDResponseTypeCode, additionalParameters: nil)
// performs authentication request
appDelegate.currentAuthorizationFlow = OIDAuthState.authState(byPresenting: request, presenting: presentingView!) { (authState, error) in
if let authState = authState {
self.setAuthState(state: authState)
self.saveState()
self.fetchUserInfo()
} else {
print("Authorization error: \(error?.localizedDescription ?? "DEFAULT_ERROR")")
}
}
}
func logout() {
let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene
let presentingView = windowScene?.windows.first?.rootViewController
// Create redirectURI from redirectURL string
guard let redirectURI = URL(string: redirectUrl) else {
print("Error creating URL for : \(redirectUrl)")
return
}
guard let idToken = authState?.lastTokenResponse?.idToken else { return }
// Create logout request
let request = OIDEndSessionRequest(configuration: config!, idTokenHint: idToken, postLogoutRedirectURL: redirectURI, additionalParameters: nil)
guard let userAgent = OIDExternalUserAgentIOS(presenting: presentingView!) else { return }
// performs logout request
appDelegate.currentAuthorizationFlow = OIDAuthorizationService.present(request, externalUserAgent: userAgent, callback: { (_, error) in
self.setAuthState(state: nil)
self.saveState()
self.email = "-"
self.given_name = "-"
self.family_name = "-"
})
}
// MARK: - Loading & Saving State
func loadState() {
guard let data = UserDefaults(suiteName: suiteName)?.object(forKey: authStateKey) as? Data else {
return
}
do {
let authState = try NSKeyedUnarchiver.unarchivedObject(ofClasses: [OIDAuthState.self], from: data) as? OIDAuthState
self.setAuthState(state: authState)
// Fetch user info if user authenticated
fetchUserInfo()
} catch {
self.setAuthState(state: nil)
print(error)
}
}
// Save user state to local
func saveState() {
guard let data = try? NSKeyedArchiver.archivedData(withRootObject: authState as Any, requiringSecureCoding: true) else {
return
}
if let userDefaults = UserDefaults(suiteName: suiteName) {
userDefaults.set(data, forKey: authStateKey)
userDefaults.synchronize()
}
}
// Set user auth state
func setAuthState(state: OIDAuthState?) {
if (authState == state) {
return;
}
authState = state;
isLoggedIn = state?.isAuthorized == true
}
// MARK: Get OIDC info
func discoverConfiguration() {
let appSettings = AppSettingsReader().loadAppSettings()
guard let issuerUrl = URL(string: appSettings.authCredentials.issuer) else {
print("Error creating URL for : \(appSettings.authCredentials.issuer)")
return
}
// Get auth server endpoints
OIDAuthorizationService.discoverConfiguration(forIssuer: issuerUrl) { configuration, error in
if(error != nil) {
print("Error: \(error?.localizedDescription ?? "DEFAULT_ERROR")")
} else {
self.config = configuration
}
}
}
func fetchUserInfo() {
guard let userinfoEndpoint = authState?.lastAuthorizationResponse.request.configuration.discoveryDocument?.userinfoEndpoint else {
print("Userinfo endpoint not declared in discovery document")
return
}
print("Performing userinfo request")
let currentAccessToken: String? = authState?.lastTokenResponse?.accessToken
authState?.performAction() { (accessToken, idToken, error) in
if error != nil {
print("Error fetching fresh tokens: \(error?.localizedDescription ?? "ERROR")")
return
}
guard let accessToken = accessToken else {
print("Error getting accessToken")
return
}
if currentAccessToken != accessToken {
print("Access token was refreshed automatically (\(currentAccessToken ?? "CURRENT_ACCESS_TOKEN") to \(accessToken))")
} else {
print("Access token was fresh and not updated \(accessToken)")
}
var urlRequest = URLRequest(url: userinfoEndpoint)
urlRequest.allHTTPHeaderFields = ["Authorization":"Bearer \(accessToken)"]
let task = URLSession.shared.dataTask(with: urlRequest) { data, response, error in
DispatchQueue.main.async {
guard error == nil else {
print("HTTP request failed \(error?.localizedDescription ?? "ERROR")")
return
}
guard let response = response as? HTTPURLResponse else {
print("Non-HTTP response")
return
}
guard let data = data else {
print("HTTP response data is empty")
return
}
var json: [AnyHashable: Any]?
do {
json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any]
} catch {
print("JSON Serialization Error")
}
if response.statusCode != 200 {
let responseText: String? = String(data: data, encoding: String.Encoding.utf8)
if response.statusCode == 401 {
let oauthError = OIDErrorUtilities.resourceServerAuthorizationError(withCode: 0, errorResponse: json, underlyingError: error)
self.authState?.update(withAuthorizationError: oauthError)
print("Authorization Error (\(oauthError)). Response: \(responseText ?? "RESPONSE_TEXT")")
} else {
print("HTTP: \(response.statusCode), Response: \(responseText ?? "RESPONSE_TEXT")")
}
return
}
// Create profile info string
if let json = json {
self.email = json["email"] as! String
self.given_name = json["given_name"] as! String
self.family_name = json["family_name"] as! String
}
}
}
task.resume()
}
}
}
Since the UserAuth
class will be used by multiple views to determine the user’s logged in status and profile information, it is declared as an ObservableObject
. This means that any SwiftUI views that use the UserAuth
class will be notified when the UserAuth
object changes. As we only want one instance of the UserAuth
class, it should be instantiated in the main App
class, and passed to the views that need it via the root view environmentObject
method. This will make the UserAuth
object available to all child views of the root view, and any views that need to access the UserAuth
object can declare it as an @EnvironmentObject
property.
Update the App
class in the ChangBankApp.swift
file to instantiate the UserAuth
object and pass it to the root view:
//
// Change_BankApp.swift
// Change Bank
//
import SwiftUI
@main
struct ChangeBankApp: App {
var userAuth = UserAuth()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(userAuth)
.preferredColorScheme(.light)
}
}
}
ContentView
ViewThe ContentView
view is the root view of the app that will display the login screen if the user is not logged in, or the main app screens if the user is logged in. The UserAuth
object is accessed via the @EnvironmentObject
property wrapper, and the isLoggedIn
property is used to determine which view to display.
Update the ContentView
class in the ContentView.swift
file as follows.
//
// ContentView.swift
// ChangeBank
//
import SwiftUI
import AppAuth
struct ContentView: View {
@EnvironmentObject var userAuth: UserAuth
var body: some View {
if userAuth.isLoggedIn {
// Top logo
VStack(alignment: .leading) {
HStack {
Image("changebank")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 150)
.padding()
Spacer()
}
.frame(maxWidth: .infinity, alignment: .topLeading)
TabView {
HomeView().tabItem {
Label("Home", systemImage: "house")
}
MakeChangeView().tabItem {
Label("Make Change", systemImage: "centsign.circle")
}
}.accentColor(Color(red: 0.0353, green: 0.3882, blue: 0.1412))
}
} else {
LoginView()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
LoginView
ViewThe LoginView
view is displayed when the user is not logged in and contains a button that initiates the login flow. The UserAuth
object is accessed via the @EnvironmentObject
property wrapper, and the login
method is called when the login button is pressed.
Select File -> New -> File from the menu bar. Select Swift File as the template. Click Next. Set the name of the file to LoginView.swift
. Click Create.
Copy the following code into the LoginView.swift
file:
//
// LoginView.swift
// ChangeBank
//
import SwiftUI
import AppAuth
struct LoginView: View {
@EnvironmentObject var userAuth: UserAuth
var body: some View {
VStack {
Image("changebank")
.resizable()
.aspectRatio(contentMode: .fit)
Text("Welcome to ChangeBank!")
Button("Login"){
userAuth.login()
}.buttonStyle(PrimaryButtonStyle())
}
.padding()
}
}
struct LoginView_Previews: PreviewProvider {
static var previews: some View {
LoginView()
}
}
The HomeView
view is displayed when the user is logged in. It greets the user by their name, retrieved from FusionAuth, and contains a button that initiates the logout flow. The UserAuth
object is accessed via the @EnvironmentObject
property wrapper, and its logout
method is called when the logout button is pressed.
Select File -> New -> File from the menu bar. Select Swift File as the template. Click Next. Set the name of the file to HomeView.swift
. Click Create.
Copy the following code into the HomeView.swift
file:
//
// HomeView.swift
// ChangeBank
//
import SwiftUI
struct HomeView: View {
@EnvironmentObject var userAuth: UserAuth
var body: some View {
VStack{
Text("Welcome \(userAuth.given_name) \(userAuth.family_name)")
.padding(.bottom, 20).font(.headline)
Text("Your balance is:")
Text("$0.00").font(.largeTitle)
Button("Log out"){
userAuth.logout()
}.buttonStyle(SecondaryButtonStyle())
}
}
}
struct HomeView_Previews: PreviewProvider {
static var previews: some View {
HomeView()
}
}
ChangeBank’s main business is to convert your notes to coins. The MakeChangeView
view is available when the user is logged in. It takes an amount in dollars and returns the equivalent amount in nickels and pennies.
Select File -> New -> File from the menu bar. Select Swift File as the template. Click Next. Set the name of the file to MakeChangeView.swift
. Click Create.
Copy the following code into the MakeChangeView.swift
file.
//
// MakeChangeView.swift
// ChangeBank
//
import SwiftUI
struct MakeChangeView: View {
@State private var dollarValue: String = ""
@State private var nickels: Int = 0
@State private var pennies: Int = 0
@State private var changeOutput: String = ""
var body: some View {
VStack {
Text("Make Change").font(.largeTitle)
TextField("Enter dollar value", text: $dollarValue)
.textFieldStyle(CurrencyTextFieldStyle())
.frame(maxWidth: 200)
Button("Make Change") {
makeChange()
}
.buttonStyle(PrimaryButtonStyle())
Text(changeOutput).padding([.top, .leading, .trailing], 50).font(.callout)
}
.padding()
}
private func makeChange() {
guard var value = Double(dollarValue) else {
print("no dollars found :( \(dollarValue)")
changeOutput = "Please enter a dollar amount to convert. "
return
}
value = Double(Int(value * 100))/100 // truncate to 2 decimals to look like money
let totalPennies = Int(value * 100)
nickels = totalPennies / 5
pennies = totalPennies % 5
let pennyUnit = pennies != 1 ? "pennies" : "penny"
let nickelUnit = nickels != 1 ? "nickels" : "nickel"
changeOutput = "We can make change for $\(value) with \(nickels) \(nickelUnit) and \(pennies) \(pennyUnit)"
}
}
struct MakeChangeView_Previews: PreviewProvider {
static var previews: some View {
MakeChangeView()
}
}
You can run the application on a simulator or iOS device.
To run the app in the simulator, click the play button in the top-left corner of Xcode or select Product -> Run from the menu bar. The app will be built and run in the simulator.
Click Login. You should see a system notification, and then be redirected to FusionAuth in a popup browser. Log in with the username richard@example.com
and the password password
. You should see the home screen displaying your name, your balance, and a logout button.
At the bottom of the screen, you’ll see a tab bar with two tabs. Click the Make Change tab. Enter an amount in dollars, and click Make Change. You should see the equivalent amount in nickels and pennies displayed.
Navigate back to the home screen by clicking the Home tab. Click Logout. You should see another system notification and then be redirected to FusionAuth to log out. Finally, you’ll be returned to the login screen.
To run the app on your device, you need to make a few changes. This is because the app will be running on a different device to the FusionAuth instance, so it won’t be able to access the FusionAuth instance via localhost
.
A simple way to get around this is to use ngrok, a tool that allows you to expose a local server to the internet. You can follow the guide here. Note the URL ngrok gave you as you’ll need it soon.
Now that you have the URL, you need to update the FusionAuth Tenant issuer to make sure it matches the given address.
Log in to the FusionAuth admin UI, browse to Tenants in the sidebar, and click on the “Default” tenant to edit it. Paste the complete address (with protocol and domain) you copied from ngrok into the Issuer field (for example, https://6d1e-2804-431-c7c9-739-4703-98a7-4b81-5ba6.ngrok-free.app
). Save the tenant by clicking the icon in the top-right corner.
Navigate to Applications and click on the “Example Apple SwiftUI iOS App” application. Click on the JWT tab, change both Access token signing key and Id token signing key to Auto generate a new key on save...
and save the application.
You must create new keys after modifying the Tenant because the Issuer field is embedded in the key.
Now you need to update the ChangeBank.plist
file to use the ngrok URL. Open the ChangeBank.plist
file in the project navigator, and change the value of the issuer
key to the ngrok URL.
Now you can run the app on your device. Connect your device to your computer via USB. In Xcode, select your device from the device menu in the top-left corner of the window. Click the play button in the top left corner of Xcode, or select Product -> Run from the menu bar. The app will be built and run on your device.
Note that you need to enable developer mode on your device to run the app there. You will also need to associate a developer team with your Xcode project. You can do this by selecting the project file in the project navigator and selecting your team in the Signing & Capabilities tab. If you don’t have a developer account, you can use your personal Apple ID as the team.
This quickstart is a great way to get a proof of concept up and running quickly, but to run your application in production, there are some things you're going to want to do.
FusionAuth gives you the ability to customize just about everything to do with the user's experience and the integration of your application. This includes:
Make sure you have the right values in the ChangeBank.plist
file. Double-check the Issuer
in the Tenant to make sure it matches the public URL that FusionAuth is running at.
You can always pull down a complete running application and compare what’s different.
git clone https://github.com/FusionAuth/fusionauth-quickstart-swift-ios-native.git
cd fusionauth-quickstart-swift-ios-native
docker compose up
Then open and run the Xcode project in the complete-application
directory.