Skip to content

Commit

Permalink
feat: support split sign in/up flow (#40)
Browse files Browse the repository at this point in the history
* feat: modified tokenResponse type

* feat: added intent to apple sign in

* feat: added internal requestSignIn func with custom js sign in options

* feat: added check for app-config sign_in_up_flow enabled

* feat: updated naming on appconfig hub.auth param

* feat: removed token from TokenResponse

* feat: consilidated signInOptions into one function

* fix: added default intent for fetchToken
  • Loading branch information
mfmurray committed Jan 26, 2023
1 parent 0c22965 commit 282544c
Show file tree
Hide file tree
Showing 4 changed files with 134 additions and 48 deletions.
79 changes: 44 additions & 35 deletions Sources/Rownd/Models/AppleSignUpCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,15 @@ struct AppleSignInData: Codable {

class AppleSignUpCoordinator: NSObject, ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding {
var parent: Rownd?
var intent: RowndSignInIntent?

init(_ parent: Rownd) {
self.parent = parent
super.init()
}

@objc func didTapButton() {
func signIn(_ intent: RowndSignInIntent?) {
self.intent = intent
//Create an object of the ASAuthorizationAppleIDProvider
let appleIDProvider = ASAuthorizationAppleIDProvider()
//Create a request
Expand Down Expand Up @@ -85,45 +87,52 @@ class AppleSignUpCoordinator: NSObject, ASAuthorizationControllerDelegate, ASAut
if let identityToken = identityToken,
let urlContent = NSString(data: identityToken, encoding: String.Encoding.ascii.rawValue) {
let idToken = urlContent as String
Auth.fetchToken(idToken: idToken) { authState in
DispatchQueue.main.async {
store.dispatch(store.state.auth.onReceiveAuthTokens(
AuthState(accessToken: authState?.accessToken, refreshToken: authState?.refreshToken)
))

store.dispatch(SetLastSignInMethod(payload: SignInMethodTypes.apple))

store.dispatch(Thunk<RowndState> { dispatch, getState in
guard let state = getState() else { return }
Auth.fetchToken(idToken: idToken, intent: intent) { [self] tokenResponse in
if (tokenResponse?.userType == UserType.NewUser && intent == RowndSignInIntent.signIn) {
Rownd.requestSignIn(jsFnOptions: RowndSignInJsOptions(token: idToken, loginStep: RowndSignInLoginStep.NoAccount, intent: RowndSignInIntent.signIn ))
} else {
DispatchQueue.main.async {
store.dispatch(store.state.auth.onReceiveAuthTokens(
AuthState(accessToken: tokenResponse?.accessToken, refreshToken: tokenResponse?.refreshToken)
))

var userData = state.user.data
store.dispatch(SetLastSignInMethod(payload: SignInMethodTypes.apple))

let defaults = UserDefaults.standard
//use UserDefault values for Email and fullName if available
if let userAppleSignInData = defaults.object(forKey: appleSignInDataKey) as? Data {
let decoder = JSONDecoder()
if let loadedAppleSignInData = try? decoder.decode(AppleSignInData.self, from: userAppleSignInData) {
userData["email"] = AnyCodable.init(loadedAppleSignInData.email)
userData["first_name"] = AnyCodable.init(loadedAppleSignInData.firstName)
userData["last_name"] = AnyCodable.init(loadedAppleSignInData.lastName)
userData["full_name"] = AnyCodable.init(loadedAppleSignInData.fullName)
store.dispatch(Thunk<RowndState> { dispatch, getState in
guard let state = getState() else { return }

var userData = state.user.data

let defaults = UserDefaults.standard
//use UserDefault values for Email and fullName if available
if let userAppleSignInData = defaults.object(forKey: appleSignInDataKey) as? Data {
let decoder = JSONDecoder()
if let loadedAppleSignInData = try? decoder.decode(AppleSignInData.self, from: userAppleSignInData) {
userData["email"] = AnyCodable.init(loadedAppleSignInData.email)
userData["first_name"] = AnyCodable.init(loadedAppleSignInData.firstName)
userData["last_name"] = AnyCodable.init(loadedAppleSignInData.lastName)
userData["full_name"] = AnyCodable.init(loadedAppleSignInData.fullName)
}
} else {
if let email = email {
userData["email"] = AnyCodable.init(email)
userData["first_name"] = AnyCodable.init(fullName?.givenName)
userData["last_name"] = AnyCodable.init(fullName?.familyName)
userData["full_name"] = AnyCodable.init(String("\(fullName?.givenName) \(fullName?.familyName)"))
}
}
} else {
if let email = email {
userData["email"] = AnyCodable.init(email)
userData["first_name"] = AnyCodable.init(fullName?.givenName)
userData["last_name"] = AnyCodable.init(fullName?.familyName)
userData["full_name"] = AnyCodable.init(String("\(fullName?.givenName) \(fullName?.familyName)"))

if (!userData.isEmpty) {
DispatchQueue.main.async {
dispatch(UserData.save(userData))
}
}
}

if (!userData.isEmpty) {
DispatchQueue.main.async {
dispatch(UserData.save(userData))
}
}
})

Rownd.requestSignIn(jsFnOptions: RowndSignInJsOptions(loginStep: RowndSignInLoginStep.Success,intent: self.intent, userType: tokenResponse?.userType))
})
}
}

}
} else {
logger.trace("apple sign credential alternative")
Expand Down
2 changes: 2 additions & 0 deletions Sources/Rownd/Models/Context/AppConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,13 @@ extension AppHubConfigState: Codable {

public struct AppHubAuthConfigState: Hashable {
public var signInMethods: SignInMethods?
public var useExplicitSignUpFlow: Bool?
}

extension AppHubAuthConfigState: Codable {
enum CodingKeys: String, CodingKey {
case signInMethods = "sign_in_methods"
case useExplicitSignUpFlow = "use_explicit_sign_up_flow"
}
}

Expand Down
33 changes: 27 additions & 6 deletions Sources/Rownd/Models/Context/Auth.swift
Original file line number Diff line number Diff line change
Expand Up @@ -136,22 +136,43 @@ func authReducer(action: Action, state: AuthState?) -> AuthState {

// MARK: Token / auth API calls

public enum UserType: String, Codable {
case NewUser = "new_user"
case ExistingUser = "existing_user"
}

struct TokenRequest: Codable {
var refreshToken: String?
var idToken: String?
var appId: String?
var intent: RowndSignInIntent?

enum CodingKeys: String, CodingKey {
case refreshToken = "refresh_token"
case idToken = "id_token"
case appId = "app_id"
case intent
}
}

struct TokenResponse: Codable {
var refreshToken: String?
var accessToken: String?
var userType: UserType?

enum CodingKeys: String, CodingKey {
case refreshToken = "refresh_token"
case accessToken = "access_token"
case userType = "user_type"
}
}


struct TokenResource: APIResource {

var headers: Dictionary<String, String>?

typealias ModelType = AuthState
typealias ModelType = TokenResponse

var methodPath: String {
return "/hub/auth/token"
Expand All @@ -163,19 +184,19 @@ struct TokenResource: APIResource {
class Auth {
static func fetchToken(_ token: String) async -> String? {
await withCheckedContinuation { continuation in
fetchToken(idToken: token) { tokenResp in
fetchToken(idToken: token, intent: nil) { tokenResp in
continuation.resume(returning: tokenResp?.accessToken)
}
}
}

static func fetchToken(idToken: String, withCompletion completion: @escaping (AuthState?) -> Void) -> Void {
static func fetchToken(idToken: String, intent: RowndSignInIntent?, withCompletion completion: @escaping (TokenResponse?) -> Void) -> Void {
guard let appId = store.state.appConfig.id else { return completion(nil) }
let tokenRequest = TokenRequest(idToken: idToken, appId: appId)
let tokenRequest = TokenRequest(idToken: idToken, appId: appId, intent: intent)
return fetchToken(tokenRequest: tokenRequest, withCompletion: completion)
}

static func fetchToken(tokenRequest: TokenRequest, withCompletion completion: @escaping (AuthState?) -> Void) -> Void {
static func fetchToken(tokenRequest: TokenRequest, withCompletion completion: @escaping (TokenResponse?) -> Void) -> Void {
var resource = TokenResource()
resource.headers = [
"Content-Type": "application/json"
Expand Down
68 changes: 61 additions & 7 deletions Sources/Rownd/Rownd.swift
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,14 @@ public class Rownd: NSObject {
}

public static func requestSignIn(with: RowndSignInHint, completion: (() -> Void)? = nil) {
requestSignIn(with: with, signInOptions: RowndSignInOptions(), completion: completion)
}

public static func requestSignIn(with: RowndSignInHint, signInOptions: RowndSignInOptions?, completion: (() -> Void)? = nil) {
var signInOptions = determineSignInOptions(signInOptions)
switch with {
case .appleId:
appleSignUpCoordinator?.didTapButton()
appleSignUpCoordinator?.signIn(signInOptions?.intent)
case .passkey:
passkeyCoordinator.signInWith()
case .googleId:
Expand Down Expand Up @@ -157,11 +162,17 @@ public class Rownd: NSObject {

if let idToken = authentication.idToken {
logger.debug("Successully completed Google sign-in")
Auth.fetchToken(idToken: idToken) { authState in
DispatchQueue.main.async {
store.dispatch(SetAuthState(payload: AuthState(accessToken: authState?.accessToken, refreshToken: authState?.refreshToken)))
store.dispatch(UserData.fetch())
store.dispatch(SetLastSignInMethod(payload: SignInMethodTypes.google))
Auth.fetchToken(idToken: idToken, intent: signInOptions?.intent) { tokenResponse in
if (tokenResponse?.userType == UserType.NewUser && signInOptions?.intent == RowndSignInIntent.signIn) {
requestSignIn(jsFnOptions: RowndSignInJsOptions(token: idToken, loginStep: RowndSignInLoginStep.NoAccount, intent: RowndSignInIntent.signIn ))
} else {
DispatchQueue.main.async {
store.dispatch(SetAuthState(payload: AuthState(accessToken: tokenResponse?.accessToken, refreshToken: tokenResponse?.refreshToken)))
store.dispatch(UserData.fetch())
store.dispatch(SetLastSignInMethod(payload: SignInMethodTypes.google))

requestSignIn(jsFnOptions: RowndSignInJsOptions(loginStep: RowndSignInLoginStep.Success,intent: signInOptions?.intent, userType: tokenResponse?.userType))
}
}
}

Expand All @@ -180,9 +191,14 @@ public class Rownd: NSObject {
}

public static func requestSignIn(_ signInOptions: RowndSignInOptions?) {
var signInOptions = determineSignInOptions(signInOptions)
let _ = inst.displayHub(.signIn, jsFnOptions: signInOptions ?? RowndSignInOptions() )
}

internal static func requestSignIn(jsFnOptions: Encodable?) {
let _ = inst.displayHub(.signIn, jsFnOptions: jsFnOptions)
}

public static func connectAuthenticator(with: RowndConnectSignInHint, completion: (() -> Void)? = nil) {
switch with {
case .passkey:
Expand Down Expand Up @@ -258,6 +274,17 @@ public class Rownd: NSObject {
}
}

internal static func determineSignInOptions(_ signInOptions: RowndSignInOptions?) -> RowndSignInOptions? {
var signInOptions = signInOptions
if (signInOptions?.intent == RowndSignInIntent.signUp || signInOptions?.intent == RowndSignInIntent.signIn) {
if (store.state.appConfig.config?.hub?.auth?.useExplicitSignUpFlow != true) {
signInOptions?.intent = nil
logger.error("Sign in with intent: SignIn/SignUp is not enabled. Turn it on in the Rownd platform")
}
}
return signInOptions
}

// MARK: Internal methods
private func loadAppleSignIn() {
//If we want to check if the AppleId userIdentifier is still valid
Expand Down Expand Up @@ -424,17 +451,44 @@ public enum RowndConnectSignInHint {
}

public struct RowndSignInOptions: Encodable {
public init(postSignInRedirect: String? = Rownd.config.postSignInRedirect) {
public init(postSignInRedirect: String? = Rownd.config.postSignInRedirect, intent: RowndSignInIntent? = nil) {
self.postSignInRedirect = postSignInRedirect
self.intent = intent
}

public var postSignInRedirect: String? = Rownd.config.postSignInRedirect
public var intent: RowndSignInIntent? = nil

enum CodingKeys: String, CodingKey {
case intent
case postSignInRedirect = "post_login_redirect"
}
}

public enum RowndSignInIntent: String, Codable {
case signIn = "sign_in"
case signUp = "sign_up"
}

internal enum RowndSignInLoginStep: String, Codable {
case Init = "init"
case NoAccount = "no_account"
case Success = "success"
}

internal struct RowndSignInJsOptions: Encodable {
public var token: String? = nil
public var loginStep: RowndSignInLoginStep? = nil
public var intent: RowndSignInIntent? = nil
public var userType: UserType? = nil

enum CodingKeys: String, CodingKey {
case token, intent
case loginStep = "login_step"
case userType = "user_type"
}
}

public struct RowndConnectPasskeySignInOptions: Encodable {
public var status: Status? = nil
public var biometricType: String? = ""
Expand Down

0 comments on commit 282544c

Please sign in to comment.