Skip to content

Commit

Permalink
Merge pull request #26 from passageidentity/PSG-4175
Browse files Browse the repository at this point in the history
PSG-4175
  • Loading branch information
SinaSeylani authored Jul 23, 2024
2 parents 384ac19 + f26ac3d commit d53001e
Show file tree
Hide file tree
Showing 9 changed files with 163 additions and 46 deletions.
2 changes: 1 addition & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ dependencies {
implementation "com.facebook.react:react-native:+"
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"

implementation "id.passage.android:passage:1.7.0"
implementation "id.passage.android:passage:1.8.2"
implementation "com.google.code.gson:gson:2.9.0"
}

Expand Down
2 changes: 1 addition & 1 deletion android/gradle.properties
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
PassageReactNative_kotlinVersion=1.7.0
PassageReactNative_kotlinVersion=1.8.10
PassageReactNative_minSdkVersion=21
PassageReactNative_targetSdkVersion=31
PassageReactNative_compileSdkVersion=34
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -409,5 +409,49 @@ class PassageReactNativeModule(reactContext: ReactApplicationContext) :
}

// endregion

// Hosted Auth Region

@ReactMethod
fun hostedAuthStart(promise: Promise) {
CoroutineScope(Dispatchers.IO).launch {
try {
passage.hostedAuthStart();
promise.resolve(null);
} catch (e: Exception) {
var errorCode = "START_HOSTED_AUTH_ERROR"
promise.reject(errorCode, e.message, e);
}
}
}

@ReactMethod
fun hostedLogout(promise: Promise) {
CoroutineScope(Dispatchers.IO).launch {
try {
passage.hostedLogout()
promise.resolve(null);
} catch (e: Exception) {
val error = "HOSTED_LOGOUT_ERROR"
promise.reject(error, e.message, e);
}
}
}

@ReactMethod
fun hostedAuthFinish(code: String, state: String, promise: Promise) {
CoroutineScope(Dispatchers.IO).launch {
try {
val authResultWithIdToken = passage.hostedAuthFinish(code, state)
val jsonString = Gson().toJson(authResultWithIdToken.first)
promise.resolve(jsonString)
} catch (e: Exception) {
val error = "FINISH_HOSTED_AUTH_ERROR"
promise.reject(error, e.message, e);
}
}
}

// endregion

}
7 changes: 7 additions & 0 deletions ios/PassageReactNative.m
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,13 @@ @interface RCT_EXTERN_MODULE(PassageReactNative, NSObject)
withResolver:(RCTPromiseResolveBlock)resolve
withRejecter:(RCTPromiseRejectBlock)reject);

RCT_EXTERN_METHOD(hostedAuth:(RCTPromiseResolveBlock)resolve
withRejecter:(RCTPromiseRejectBlock)reject);


RCT_EXTERN_METHOD(hostedLogout:(RCTPromiseResolveBlock)resolve
withRejecter:(RCTPromiseRejectBlock)reject);

+ (BOOL)requiresMainQueueSetup
{
return NO;
Expand Down
43 changes: 36 additions & 7 deletions ios/PassageReactNative.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class PassageReactNative: NSObject {
}
let authResult = try await passage.registerWithPasskey(identifier: identifier, options: passkeyCreationOptions)
resolve(authResult.toJsonString())
} catch PassageASAuthorizationError.canceled {
} catch RegisterWithPasskeyError.canceled {
reject("USER_CANCELLED", "User cancelled interaction", nil)
} catch {
reject("PASSKEY_ERROR", "\(error)", nil)
Expand All @@ -67,7 +67,7 @@ class PassageReactNative: NSObject {
do {
let authResult = try await passage.loginWithPasskey(identifier: identifier)
resolve(authResult.toJsonString())
} catch PassageASAuthorizationError.canceled {
} catch LoginWithPasskeyError.canceled {
reject("USER_CANCELLED", "User cancelled interaction", nil)
} catch {
reject("PASSKEY_ERROR", "\(error)", nil)
Expand Down Expand Up @@ -134,7 +134,7 @@ class PassageReactNative: NSObject {
resolve(authResult.toJsonString())
} catch {
var errorCode = "OTP_ERROR"
if case PassageOTPError.exceededAttempts = error {
if case OneTimePasscodeActivateError.exceededAttempts = error {
errorCode = "OTP_ACTIVATION_EXCEEDED_ATTEMPTS"
}
reject(errorCode, "\(error)", nil)
Expand Down Expand Up @@ -279,10 +279,7 @@ class PassageReactNative: NSObject {
) {
Task {
do {
guard let appInfo = try await PassageAuth.appInfo() else {
reject("APP_INFO_ERROR", "Error getting app info.", nil)
return
}
let appInfo = try await passage.appInfo()
resolve(appInfo.toJsonString())
} catch {
reject("APP_INFO_ERROR", "\(error)", nil)
Expand Down Expand Up @@ -422,5 +419,37 @@ class PassageReactNative: NSObject {
}
}
}

@objc(hostedAuth:withRejecter:)
func hostedAuth(
resolve: @escaping RCTPromiseResolveBlock,
reject: @escaping RCTPromiseRejectBlock
) -> Void {
Task {
do {
let authResult = try await passage.hostedAuth()
resolve(authResult.toJsonString())
} catch {
let errorCode = "HOSTED_AUTH_ERROR"
reject(errorCode, "\(error)", nil)
}
}
}

@objc(hostedLogout:withRejecter:)
func hostedLogout(
resolve: @escaping RCTPromiseResolveBlock,
reject: @escaping RCTPromiseRejectBlock
) {
Task {
do {
try await passage.hostedLogout()
resolve(nil)
} catch {
let errorCode = "LOGOUT_HOSTED_AUTH_ERROR"
reject(errorCode, "\(error)", nil)
}
}
}

}
27 changes: 1 addition & 26 deletions ios/PassageReactNativeExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ internal extension AuthResult {
func toDictionary() -> [String: Any] {
var authResultDict: [String : Any] = [
"authToken": authToken,
"redirectUrl": redirectURL,
"refreshToken": refreshToken,
"refreshTokenExpiration": refreshTokenExpiration
]
Expand Down Expand Up @@ -79,19 +78,14 @@ internal extension AppInfo {
func toDictionary() -> [String: Any] {
var appInfoDict: [String : Any] = [
"allowedIdentifier": allowedIdentifier,
"authFallbackMethod": authFallbackMethod?.rawValue,
"authOrigin": authOrigin,
"id": id,
"loginURL": loginURL,
"name": name,
"publicSignup": publicSignup,
"redirectURL": redirectURL,
"requiredIdentifier": requiredIdentifier,
"requireEmailVerification": requireEmailVerification,
"requireIdentifierVerification": requireIdentifierVerification,
"sessionTimeoutLength": sessionTimeoutLength,
"userMetadataSchema": userMetadataSchema?.map { $0.toDictionary() },
"authMethods": authMethods?.toDictionary(),
"sessionTimeoutLength": sessionTimeoutLength
]
return appInfoDict
}
Expand All @@ -102,25 +96,6 @@ internal extension AppInfo {

}

internal extension UserMetadataSchema {

func toDictionary() -> [String: Any] {
var dict: [String : Any] = [
"fieldName": fieldName,
"friendlyName": friendlyName,
"id": id,
"profile": profile,
"registration": registration,
"type": type
]
return dict
}

func toJsonString() -> String {
return dictToJsonString(toDictionary())
}

}

internal extension AuthMethods {
func toDictionary() -> [String: Any] {
Expand Down
2 changes: 1 addition & 1 deletion passage-react-native.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Pod::Spec.new do |s|

s.dependency "React-Core"

s.dependency 'Passage', '1.6.0'
s.dependency 'Passage', '1.8.1'
s.platform = :ios, '16.0'

# Don't install the dependencies when we run `pod install` in the old architecture.
Expand Down
58 changes: 56 additions & 2 deletions src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { NativeModules, Platform } from 'react-native';
import { waitForDeepLinkQueryValue } from './utils/waitForDeepLinkQueryValue';
import { waitForDeepLinkQueryValues } from './utils/waitForDeepLinkQueryValues';

const LINKING_ERROR =
`The package 'passage-react-native' doesn't seem to be linked. Make sure: \n\n` +
Expand Down Expand Up @@ -31,6 +31,9 @@ export enum PassageErrorCode {
TokenError = 'TOKEN_ERROR',
AppInfoError = 'APP_INFO_ERROR',
SocialAuthError = 'SOCIAL_AUTH_ERROR',
StartHostedAuthError = 'START_HOSTED_AUTH_ERROR',
FinisHostedAuthError = 'FINISH_HOSTED_AUTH_ERROR',
LogoutHostedAuthError = 'LOGOUT_HOSTED_AUTH_ERROR',
}

export class PassageError extends Error {
Expand Down Expand Up @@ -181,6 +184,8 @@ type VoidMethod = () => Promise<void>;
type ChangeEmail = (newEmail: string) => Promise<string>;
type ChangePhone = (newPhone: string) => Promise<string>;
type GetCurrentUser = () => Promise<PassageUser | null>;
type AuthorizeWithHostedLogin = () => Promise<AuthResult>;
type HostedLogout = () => Promise<void>;

/**
* The Passage class is used to perform authentication and user operations.
Expand Down Expand Up @@ -415,7 +420,8 @@ class Passage {
// The Android native "authorizeWith" method opens a Chrome Tab and returns void.
await PassageReactNative.authorizeWith(connection);
// Wait for a redirect back into the app with the auth code.
const authCode = await waitForDeepLinkQueryValue('code');
const authCodeObj = await waitForDeepLinkQueryValues(['code']);
const authCode = authCodeObj.code;
authResultJson = await PassageReactNative.finishSocialAuthentication(
authCode
);
Expand Down Expand Up @@ -613,6 +619,54 @@ class Passage {
throw new PassageError(error.code, error.message);
}
};

/**
* Authentication Method for Hosted Apps
* If your Passage app is Hosted, use this method to register and log in your user.
* This method will open up a Passage login experience
* @param {string} clientSecret You hosted app's client secret, found in Passage Console's OIDC Settings.n
* @returns {Promise<AuthResultWithIdToken>} A data object that includes AuthResult and idToken
* @throws {PassageError}
*/
authorizeWithHostedLogin: AuthorizeWithHostedLogin =
async (): Promise<AuthResult> => {
try {
if (Platform.OS === 'ios') {
// The iOS native "hostedAuthStart" method returns an AuthResult directly.
const result = await PassageReactNative.hostedAuth();
return JSON.parse(result);
} else {
// The Android native "hostedAuthStart" method opens a Chrome Tab and returns void.
await PassageReactNative.hostedAuthStart();
// Wait for a redirect back into the app with the auth code.
const { code: authCode, state } = await waitForDeepLinkQueryValues([
'code',
'state',
]);
let result = await PassageReactNative.hostedAuthFinish(
authCode,
state
);
return JSON.parse(result);
}
} catch (error: any) {
throw new PassageError(error.code, error.message);
}
};

/**
* Logout Method for Hosted Apps
*
* If your Passage app is Hosted, use this method to log out your user. This method will briefly open up a web view where it will log out the
*/

hostedLogout: HostedLogout = async (): Promise<void> => {
try {
await PassageReactNative.hostedLogout();
} catch (error: any) {
throw new PassageError(error.code, error.message);
}
};
}

export default Passage;
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,31 @@ const getUrlParamValue = (url: string, param: string): string | null => {
*
* Note that this function works best in a "singleTask" Android app.
*
* @param parameter The parameter of the value to return from Deep Link.
* @returns string
* @param parameters Array of parameters to return from Deep Link.
* @returns object
* @throws
*/
export const waitForDeepLinkQueryValue = async (parameter: string) => {
export const waitForDeepLinkQueryValues = async (
parameters: string[]
): Promise<{ [key: string]: string }> => {
return new Promise(async (resolve, reject) => {
// We need to listen for a Deep Link url event, and attempt to extract the parameter
// We need to listen for a Deep Link url event, and attempt to extract the parameters
// from the redirect url.
const linkingListener = Linking.addEventListener('url', async (event) => {
appStateListener.remove();
linkingListener.remove();
const { url } = event;
const value = getUrlParamValue(url, parameter);
if (!value) {
return reject(new Error('Missing query parameter in redirect url.'));
const result: { [key: string]: string } = {};
for (let parameter of parameters) {
const value = getUrlParamValue(url, parameter);
if (!value) {
return reject(
new Error(`Missing query parameter ${parameter} in redirect url.`)
);
}
result[parameter] = value;
}
resolve(value);
resolve(result);
});
// We need to listen for an AppState change event in the case that the user returns to
// the app without having been redirected.
Expand Down

0 comments on commit d53001e

Please sign in to comment.