diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index 84b6094..a21846e 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -20,19 +20,19 @@ runs: env: GITHUB_TOKEN: ${{ inputs.github_token }} - - name: Setup JDK 11 + - name: Setup JDK 17 uses: actions/setup-java@v3 if: ${{ inputs.gpg_signing_key == '' }} with: - java-version: 11 + java-version: 17 settings-path: ${{ github.workspace }} # location for the settings.xml file distribution: temurin - - name: Setup JDK 11 with Credentials + - name: Setup JDK 17 with Credentials if: ${{ inputs.gpg_signing_key != '' }} uses: actions/setup-java@v3 with: - java-version: 11 + java-version: 17 settings-path: ${{ github.workspace }} # location for the settings.xml file server-id: ossrh server-username: NEXUS_USERNAME diff --git a/.github/old/build.yml b/.github/old/build.yml deleted file mode 100644 index 3c6cc1b..0000000 --- a/.github/old/build.yml +++ /dev/null @@ -1,99 +0,0 @@ -name: Frontegg Android SDK - -on: - push: - branches-ignore: - - 'master' - - 'release/next' -env: - CI: true - LANG: en_US.UTF-8 - API_LEVEL: 29 - -concurrency: - group: ci-push-${{ github.ref }} - cancel-in-progress: true - -jobs: - build-and-test: - name: Build And Test - runs-on: macos-latest-xl - steps: - - name: Checkout - uses: actions/checkout@v3 -# - name: Clone Mock Server -# uses: actions/checkout@v3 -# with: -# repository: frontegg/frontegg-mock-server -# ssh-key: ${{ secrets.MOCK_SERVER_SSH_KEY }} -# ref: "master" -# path: mocker -# - name: Install Mock Server -# working-directory: mocker -# run: yarn install -# - name: Run Mock Server -# working-directory: mocker -# env: -# ANDROID_ASSOCIATED_DOMAIN_GRADLE_PATH: "${{ github.workspace }}/app/build.gradle" -# SERVER_HOSTNAME: "10.0.2.2" -# NGROCK_AUTH_TOKEN: "${{ secrets.NGROCK_AUTH_TOKEN }}" -# NGROCK_SUBDOMAIN: "frontegg-test" -# run: | -# echo "ANDROID_ASSOCIATED_DOMAIN_GRADLE_PATH: $ANDROID_ASSOCIATED_DOMAIN_GRADLE_PATH" -# echo "SERVER_HOSTNAME: $SERVER_HOSTNAME" -# echo "NGROCK_SUBDOMAIN: $NGROCK_SUBDOMAIN" -# (yarn start:mobile-mock&) -# sleep 40 - - - name: Set git config - run: | - git config --global user.name 'github-actions' - git config --global user.email 'github-actions@github.com' - - - name: Setup JDK 11 - uses: actions/setup-java@v3 - with: - java-version: 11 - settings-path: ${{ github.workspace }} # location for the settings.xml file - distribution: temurin - - - name: Gradle cache - uses: gradle/gradle-build-action@v2 - - - name: Build Libraries - run: ./gradlew :app:build --no-daemon -# - name: AVD cache -# uses: actions/cache@v3 -# id: avd-cache -# with: -# path: | -# ~/.android/avd/* -# ~/.android/adb* -# key: avd-${{env.API_LEVEL}} -# -# - name: Create AVD and generate snapshot for caching -# if: steps.avd-cache.outputs.cache-hit != 'true' -# uses: reactivecircus/android-emulator-runner@v2 -# with: -# avd-name: "AndroidEmulator" -# api-level: ${{env.API_LEVEL}} -# force-avd-creation: false -# emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -# disable-animations: true -# script: echo "Generated AVD snapshot for caching." - -# - name: Run tests -# uses: reactivecircus/android-emulator-runner@v2 -# with: -# api-level: ${{env.API_LEVEL}} -# force-avd-creation: false -# emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -# disable-animations: true -# script: ./run-tests.sh -# -# - name: Upload test results -# if: always() -# uses: actions/upload-artifact@v2 -# with: -# name: test-results -# path: app/build/reports/androidTests/connected/ diff --git a/.github/test-jks/debug.keystore b/.github/test-jks/debug.keystore new file mode 100644 index 0000000..f7db5ba Binary files /dev/null and b/.github/test-jks/debug.keystore differ diff --git a/.github/test-jks/release.keystore b/.github/test-jks/release.keystore new file mode 100644 index 0000000..eaa1ec2 Binary files /dev/null and b/.github/test-jks/release.keystore differ diff --git a/.github/workflows/onPublishAlpha.yml b/.github/workflows/onPublishAlpha.yml index 7da99b9..728b4d5 100644 --- a/.github/workflows/onPublishAlpha.yml +++ b/.github/workflows/onPublishAlpha.yml @@ -5,7 +5,7 @@ on: env: CI: true LANG: en_US.UTF-8 - API_LEVEL: 29 + API_LEVEL: 34 concurrency: group: ci-publish-alpha-${{ github.ref }} @@ -17,7 +17,7 @@ jobs: runs-on: macos-latest-xl steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4.2.0 with: fetch-depth: "0" diff --git a/.github/workflows/onPullRequestMerged.yaml b/.github/workflows/onPullRequestMerged.yaml index dad9e89..b1d5d17 100644 --- a/.github/workflows/onPullRequestMerged.yaml +++ b/.github/workflows/onPullRequestMerged.yaml @@ -7,7 +7,7 @@ on: env: CI: true LANG: en_US.UTF-8 - API_LEVEL: 29 + API_LEVEL: 34 jobs: createReleasePullRequest: @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4.2.0 - name: Setup uses: ./.github/actions/setup diff --git a/.github/workflows/onPush.yml b/.github/workflows/onPush.yml index 0a3d9ff..1cab2ec 100644 --- a/.github/workflows/onPush.yml +++ b/.github/workflows/onPush.yml @@ -8,7 +8,7 @@ on: env: CI: true LANG: en_US.UTF-8 - API_LEVEL: 29 + API_LEVEL: 34 concurrency: group: ci-push-${{ github.ref }} @@ -20,7 +20,7 @@ jobs: runs-on: macos-latest-xl steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4.2.0 - name: Setup uses: ./.github/actions/setup @@ -30,19 +30,3 @@ jobs: - name: Build Libraries shell: bash run: ./gradlew :app:build --no-daemon - - - name: Set Alpha Version - id: incremented-alpha-version - uses: ./.github/actions/update-gradle-version - with: - type: alpha - - - name: prepare release - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - NEXUS_USERNAME: ${{ secrets.NEXUS_USERNAME }} - NEXUS_PASSWORD: ${{ secrets.NEXUS_PASSWORD }} - GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} - GPG_PRIVATE_KEY: ${{ secrets.GPG_SIGNING_KEY }} - run: ./gradlew publish --no-daemon --no-parallel - diff --git a/.github/workflows/onReleaseMerged.yaml b/.github/workflows/onReleaseMerged.yaml index 2fe2cc0..64ea941 100644 --- a/.github/workflows/onReleaseMerged.yaml +++ b/.github/workflows/onReleaseMerged.yaml @@ -16,7 +16,7 @@ jobs: runs-on: macos-12 steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4.2.0 - name: Setup uses: ./.github/actions/setup @@ -93,7 +93,7 @@ jobs: - - uses: actions/github-script@0.8.0 + - uses: actions/github-script@v7 with: github-token: ${{secrets.GITHUB_TOKEN}} script: | diff --git a/.github/workflows/onTestWorkflow.yml b/.github/workflows/onTestWorkflow.yml new file mode 100644 index 0000000..b306caa --- /dev/null +++ b/.github/workflows/onTestWorkflow.yml @@ -0,0 +1,75 @@ +name: "(▶) E2E Test" +on: + push: + +env: + CI: true + LANG: en_US.UTF-8 + API_LEVEL: 34 + +concurrency: + group: ci-e2e-test-${{ github.ref }} + cancel-in-progress: true + +jobs: + uploadApk: + name: 'Upload apk' + runs-on: macos-latest-xl + steps: + - name: Checkout + uses: actions/checkout@v4.2.0 + with: + fetch-depth: "0" + + - name: Setup + uses: ./.github/actions/setup + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + + + - name: Config root certificate for testing + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const path = require('path'); + const securityConfig = `\n \n \n \n \n \n \n \n \n` + fs.mkdirSync(path.join(process.env.GITHUB_WORKSPACE, 'embedded/src/main/res/xml'), { recursive: true }); + fs.writeFileSync(path.join(process.env.GITHUB_WORKSPACE, 'embedded/src/main/res/xml/network_security_config.xml'), securityConfig, 'utf8'); + + let manifest = fs.readFileSync(path.join(process.env.GITHUB_WORKSPACE, 'embedded/src/main/AndroidManifest.xml'), 'utf8'); + manifest = manifest.replace(/ 0))') + + echo "artifact urls: $apk_urls" \ No newline at end of file diff --git a/README.md b/README.md index b97f395..70dbe12 100644 --- a/README.md +++ b/README.md @@ -486,13 +486,13 @@ class App : Application() { listOf( RegionConfig( "eu", - "auth.davidantoon.me", + "autheu.davidantoon.me", "b6adfe4c-d695-4c04-b95f-3ec9fd0c6cca" ), RegionConfig( "us", - "davidprod.frontegg.com", - "d7d07347-2c57-4450-8418-0ec7ee6e096b" + "authus.frontegg.com", + "6903cab0-9809-4a2e-97dd-b8c0f966c813" ) ), this diff --git a/android/build.gradle b/android/build.gradle index 714832c..3f8c896 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -35,7 +35,7 @@ android { kotlinOptions { jvmTarget = '1.8' } - buildToolsVersion '30.0.3' + buildToolsVersion '34.0.0' publishing { singleVariant("release") { @@ -51,16 +51,20 @@ dependencies { implementation 'io.reactivex.rxjava3:rxkotlin:3.0.1' implementation 'com.squareup.okhttp3:okhttp:4.12.0' implementation 'com.google.code.gson:gson:2.10.1' - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3" - implementation "androidx.browser:browser:1.8.0" - implementation("androidx.security:security-crypto:1.1.0-alpha06") { + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3' + implementation 'androidx.browser:browser:1.8.0' + implementation ('androidx.security:security-crypto:1.1.0-alpha06') { exclude group: 'com.google.crypto.tink', module: 'tink-android' } - implementation "com.google.crypto.tink:tink-android:1.9.0" + implementation 'com.google.crypto.tink:tink-android:1.9.0' implementation 'com.google.androidbrowserhelper:androidbrowserhelper:2.5.0' - implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" - implementation 'androidx.lifecycle:lifecycle-process:2.6.2' + implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' + implementation 'androidx.webkit:webkit:1.12.1' + implementation 'androidx.lifecycle:lifecycle-process:2.8.6' + // optional - needed for credentials support from play services, for devices running Android 13 and below. + implementation "androidx.credentials:credentials-play-services-auth:1.3.0" + implementation 'androidx.credentials:credentials:1.3.0' } afterEvaluate { diff --git a/android/consumer-rules.pro b/android/consumer-rules.pro index 4689556..d4f8671 100644 --- a/android/consumer-rules.pro +++ b/android/consumer-rules.pro @@ -20,4 +20,12 @@ -keep class com.frontegg.android.models.** { *; } # Retain Tink classes used for shared preferences encryption --keep class com.google.crypto.tink.** { *; } \ No newline at end of file +-keep class com.google.crypto.tink.** { *; } + +-if class androidx.credentials.CredentialManager +-keep class androidx.credentials.playservices.** { + *; +} + +-keep public class android.net.http.SslError +-keep public class android.webkit.WebViewClient diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index 1dd27e5..93df43f 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -4,6 +4,8 @@ + + diff --git a/android/src/main/java/com/frontegg/android/EmbeddedAuthActivity.kt b/android/src/main/java/com/frontegg/android/EmbeddedAuthActivity.kt index 4dbebfc..d293b24 100644 --- a/android/src/main/java/com/frontegg/android/EmbeddedAuthActivity.kt +++ b/android/src/main/java/com/frontegg/android/EmbeddedAuthActivity.kt @@ -186,12 +186,17 @@ class EmbeddedAuthActivity : Activity() { var onAuthFinishedCallback: (() -> Unit)? = null // Store callback - fun authenticate(activity: Activity, loginHint: String? = null) { + fun authenticate( + activity: Activity, + loginHint: String? = null, + callback: (() -> Unit)? = null + ) { val intent = Intent(activity, EmbeddedAuthActivity::class.java) - val authorizeUri = AuthorizeUrlGenerator().generate(loginHint=loginHint) + val authorizeUri = AuthorizeUrlGenerator().generate(loginHint = loginHint) intent.putExtra(AUTH_LAUNCHED, true) intent.putExtra(AUTHORIZE_URI, authorizeUri.first) + onAuthFinishedCallback = callback activity.startActivityForResult(intent, OAUTH_LOGIN_REQUEST) } diff --git a/android/src/main/java/com/frontegg/android/FronteggAuth.kt b/android/src/main/java/com/frontegg/android/FronteggAuth.kt index db5004d..839e693 100644 --- a/android/src/main/java/com/frontegg/android/FronteggAuth.kt +++ b/android/src/main/java/com/frontegg/android/FronteggAuth.kt @@ -230,11 +230,11 @@ class FronteggAuth( private fun cancelLastTimer() { Log.d(TAG, "Cancel Last Timer") - if(timerTask!= null){ + if (timerTask != null) { timerTask?.cancel() timerTask = null } - if(refreshTokenJob!= null) { + if (refreshTokenJob != null) { val context = FronteggApp.getInstance().context val jobScheduler = context.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler @@ -245,15 +245,20 @@ class FronteggAuth( fun scheduleTimer(offset: Long) { FronteggApp.getInstance().lastJobStart = Instant.now().toEpochMilli() - if(FronteggApp.getInstance().appInForeground){ + if (FronteggApp.getInstance().appInForeground) { Log.d(TAG, "[foreground] Start Timer task (${offset} ms)") this.timerTask = Timer().schedule(offset) { - Log.d(TAG, "[foreground] Job started, (${Instant.now().toEpochMilli()-FronteggApp.getInstance().lastJobStart} ms)") + Log.d( + TAG, + "[foreground] Job started, (${ + Instant.now().toEpochMilli() - FronteggApp.getInstance().lastJobStart + } ms)" + ) refreshTokenIfNeeded() } - }else { + } else { Log.d(TAG, "[background] Start Job Scheduler task (${offset} ms)") val context = FronteggApp.getInstance().context val jobScheduler = @@ -347,15 +352,20 @@ class FronteggAuth( } - fun login(activity: Activity, loginHint: String? = null) { + fun login(activity: Activity, loginHint: String? = null, callback: (() -> Unit)? = null) { if (FronteggApp.getInstance().isEmbeddedMode) { - EmbeddedAuthActivity.authenticate(activity, loginHint) + EmbeddedAuthActivity.authenticate(activity, loginHint, callback) } else { AuthenticationActivity.authenticate(activity, loginHint) } } - fun directLoginAction(activity: Activity, type: String, data: String, callback: (() -> Unit)? = null) { + fun directLoginAction( + activity: Activity, + type: String, + data: String, + callback: (() -> Unit)? = null + ) { if (FronteggApp.getInstance().isEmbeddedMode) { EmbeddedAuthActivity.directLoginAction(activity, type, data, callback) } else { diff --git a/android/src/main/java/com/frontegg/android/embedded/CredentialManagerHandler.kt b/android/src/main/java/com/frontegg/android/embedded/CredentialManagerHandler.kt new file mode 100644 index 0000000..4e70af1 --- /dev/null +++ b/android/src/main/java/com/frontegg/android/embedded/CredentialManagerHandler.kt @@ -0,0 +1,69 @@ +package com.frontegg.android.embedded + + +import android.app.Activity +import android.util.Log +import androidx.credentials.* +import androidx.credentials.exceptions.* +import org.json.JSONObject + +/** + * A class that encapsulates the credential manager object and provides simplified APIs for + * creating and retrieving public key credentials. For other types of credentials follow the + * documentation https://developer.android.com/training/sign-in/passkeys + */ +class CredentialManagerHandler(private val activity: Activity) { + + private val mCredMan = CredentialManager.create(activity.applicationContext) + private val TAG = "CredentialManagerHandler" + + /** + * Encapsulates the create passkey API for credential manager in a less error-prone manner. + * + * @param request a create public key credential request JSON required by [CreatePublicKeyCredentialRequest]. + * @return [CreatePublicKeyCredentialResponse] containing the result of the credential creation. + */ + suspend fun createPasskey(requestStr: String): CreatePublicKeyCredentialResponse { + + val json = JSONObject(requestStr) + val authenticatorSelection = json.getJSONObject("authenticatorSelection"); + authenticatorSelection.put("residentKey", "preferred") + authenticatorSelection.put("userVerification", "required") + authenticatorSelection.put("authenticatorAttachment", "platform") + authenticatorSelection.put("requireResidentKey", false) + json.put("authenticatorSelection", authenticatorSelection) + val request = json.toString() + + val createRequest = CreatePublicKeyCredentialRequest(request, null, true) + try { + return mCredMan.createCredential( + activity, + createRequest + ) as CreatePublicKeyCredentialResponse + } catch (e: CreateCredentialException) { + // For error handling use guidance from https://developer.android.com/training/sign-in/passkeys + Log.i( + TAG, + "Error creating credential: ErrMessage: ${e.errorMessage}, ErrType: ${e.type}" + ) + throw e + } + } + + /** + * Encapsulates the get passkey API for credential manager in a less error-prone manner. + * + * @param request a get public key credential request JSON required by [GetCredentialRequest]. + * @return [GetCredentialResponse] containing the result of the credential retrieval. + */ + suspend fun getPasskey(request: String): GetCredentialResponse { + val getRequest = GetCredentialRequest(listOf(GetPublicKeyCredentialOption(request, null))) + try { + return mCredMan.getCredential(activity, getRequest) + } catch (e: GetCredentialException) { + // For error handling use guidance from https://developer.android.com/training/sign-in/passkeys + Log.i(TAG, "Error retrieving credential: ${e.message}") + throw e + } + } +} diff --git a/android/src/main/java/com/frontegg/android/embedded/FronteggNativeBridge.kt b/android/src/main/java/com/frontegg/android/embedded/FronteggNativeBridge.kt index 96adcd7..2377a06 100644 --- a/android/src/main/java/com/frontegg/android/embedded/FronteggNativeBridge.kt +++ b/android/src/main/java/com/frontegg/android/embedded/FronteggNativeBridge.kt @@ -11,7 +11,6 @@ import androidx.browser.customtabs.CustomTabsIntent import com.frontegg.android.EmbeddedAuthActivity import com.frontegg.android.FronteggApp import com.frontegg.android.utils.AuthorizeUrlGenerator -import com.google.androidbrowserhelper.trusted.LauncherActivity import org.json.JSONException import org.json.JSONObject @@ -30,11 +29,15 @@ class FronteggNativeBridge(val context: Context) { companion object { - fun directLoginWithContext(context: Context, directLogin: Map, preserveCodeVerifier:Boolean) { + fun directLoginWithContext( + context: Context, + directLogin: Map, + preserveCodeVerifier: Boolean + ) { val generatedUrl = try { val jsonData = JSONObject(directLogin).toString().toByteArray(Charsets.UTF_8) - val jsonString = Base64.encodeToString(jsonData, Base64.NO_WRAP ) + val jsonString = Base64.encodeToString(jsonData, Base64.NO_WRAP) AuthorizeUrlGenerator().generate(null, jsonString, preserveCodeVerifier) } catch (e: JSONException) { AuthorizeUrlGenerator().generate(null, null, preserveCodeVerifier) diff --git a/android/src/main/java/com/frontegg/android/embedded/FronteggWebClient.kt b/android/src/main/java/com/frontegg/android/embedded/FronteggWebClient.kt index a4d25f5..f0e9810 100644 --- a/android/src/main/java/com/frontegg/android/embedded/FronteggWebClient.kt +++ b/android/src/main/java/com/frontegg/android/embedded/FronteggWebClient.kt @@ -33,7 +33,8 @@ import okhttp3.Request import org.json.JSONObject -class FronteggWebClient(val context: Context) : WebViewClient() { +class FronteggWebClient(val context: Context, val passkeyWebListener: PasskeyWebListener) : + WebViewClient() { companion object { private val TAG = FronteggWebClient::class.java.simpleName } @@ -45,6 +46,9 @@ class FronteggWebClient(val context: Context) : WebViewClient() { super.onPageStarted(view, url, favicon) Log.d(TAG, "onPageStarted $url") FronteggAuth.instance.isLoading.value = true + + passkeyWebListener.onPageStarted(); + view?.evaluateJavascript(PasskeyWebListener.INJECTED_VAL, null) } override fun onPageFinished(view: WebView?, url: String?) { @@ -158,7 +162,6 @@ class FronteggWebClient(val context: Context) : WebViewClient() { } - var json = JsonParser.parseString(text) while (!json.isJsonObject) { text = json.asString diff --git a/android/src/main/java/com/frontegg/android/embedded/FronteggWebView.kt b/android/src/main/java/com/frontegg/android/embedded/FronteggWebView.kt index 2fd933a..a0b61d1 100644 --- a/android/src/main/java/com/frontegg/android/embedded/FronteggWebView.kt +++ b/android/src/main/java/com/frontegg/android/embedded/FronteggWebView.kt @@ -1,15 +1,16 @@ package com.frontegg.android.embedded import android.annotation.SuppressLint +import android.app.Activity import android.content.Context import android.content.pm.PackageManager import android.util.AttributeSet import android.webkit.CookieManager import android.webkit.WebView +import androidx.webkit.WebViewCompat +import androidx.webkit.WebViewFeature import com.frontegg.android.FronteggApp -import com.frontegg.android.utils.AuthorizeUrlGenerator -import okhttp3.internal.userAgent -import java.util.* +import kotlinx.coroutines.MainScope open class FronteggWebView : WebView { @@ -60,14 +61,23 @@ open class FronteggWebView : WebView { settings.userAgentString = userAgent } - webViewClient = FronteggWebClient(context) + val scope = MainScope() + val credentialManagerHandler = CredentialManagerHandler(context as Activity) + val passkeyWebListener = PasskeyWebListener(context, scope, credentialManagerHandler) + + webViewClient = FronteggWebClient(context, passkeyWebListener) CookieManager.getInstance().setAcceptThirdPartyCookies(this, true) this.addJavascriptInterface(FronteggNativeBridge(context), "FronteggNativeBridge") + val rules = setOf("*") + if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) { + WebViewCompat.addWebMessageListener( + this, PasskeyWebListener.INTERFACE_NAME, rules, passkeyWebListener + ) + } } - override fun onDetachedFromWindow() { super.onDetachedFromWindow() clearWebView() diff --git a/android/src/main/java/com/frontegg/android/embedded/PasskeyWebListener.kt b/android/src/main/java/com/frontegg/android/embedded/PasskeyWebListener.kt new file mode 100644 index 0000000..4aec8be --- /dev/null +++ b/android/src/main/java/com/frontegg/android/embedded/PasskeyWebListener.kt @@ -0,0 +1,276 @@ +package com.frontegg.android.embedded + +import android.app.Activity +import android.net.Uri +import android.util.Log +import android.webkit.WebView +import android.widget.Toast +import androidx.annotation.UiThread +import androidx.credentials.PublicKeyCredential +import androidx.credentials.exceptions.CreateCredentialException +import androidx.credentials.exceptions.GetCredentialException +import androidx.credentials.exceptions.NoCredentialException +import androidx.webkit.JavaScriptReplyProxy +import androidx.webkit.WebMessageCompat +import androidx.webkit.WebViewCompat +import com.frontegg.android.FronteggAuth +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import org.json.JSONArray +import org.json.JSONObject + + +/** +This web listener looks for the 'postMessage()' call on the javascript web code, and when it +receives it, it will handle it in the manner dictated in this local codebase. This allows for +javascript on the web to interact with the local setup on device that contains more complex logic. + +The embedded javascript can be found in CredentialManagerWebView/javascript/encode.js. +It can be modified depending on the use case. If you wish to minify, please use the following command +to call the toptal minifier API. +``` +cat encode.js | grep -v '^let __webauthn_interface__;$' | \ +curl -X POST --data-urlencode input@- \ +https://www.toptal.com/developers/javascript-minifier/api/raw | tr '"' "'" | pbcopy +``` +pbpaste should output the proper minimized code. In linux, you may have to alias as follows: +``` +alias pbcopy='xclip -selection clipboard' +alias pbpaste='xclip -selection clipboard -o' +``` +in your bashrc. + */ +class PasskeyWebListener( + private val activity: Activity, + private val coroutineScope: CoroutineScope, + private val credentialManagerHandler: CredentialManagerHandler +) : WebViewCompat.WebMessageListener { + + /** havePendingRequest is true if there is an outstanding WebAuthn request. There is only ever + one request outstanding at a time.*/ + private var havePendingRequest = false + + /** pendingRequestIsDoomed is true if the WebView has navigated since starting a request. The + fido module cannot be cancelled, but the response will never be delivered in this case.*/ + private var pendingRequestIsDoomed = false + + /** replyChannel is the port that the page is listening for a response on. It + is valid if `havePendingRequest` is true.*/ + private var replyChannel: ReplyChannel? = null + + /** Called by the page when it wants to do a WebAuthn `get` or 'post' request. */ + @UiThread + override fun onPostMessage( + view: WebView, + message: WebMessageCompat, + sourceOrigin: Uri, + isMainFrame: Boolean, + replyProxy: JavaScriptReplyProxy, + ) { + Log.i(TAG, "In Post Message : $message source: $sourceOrigin"); + val messageData = message.data ?: return + onRequest(messageData, sourceOrigin, isMainFrame, JavaScriptReplyChannel(replyProxy)) + } + + private fun onRequest( + msg: String, + sourceOrigin: Uri, + isMainFrame: Boolean, + reply: ReplyChannel, + ) { + msg.let { + val jsonObj = JSONObject(msg); + val type = jsonObj.getString(TYPE_KEY) + val message = jsonObj.getString(REQUEST_KEY) + + if (havePendingRequest) { + postErrorMessage(reply, "Request already in progress", type) + return + } + replyChannel = reply + if (!isMainFrame) { + reportFailure("Requests from SubFrames are not supported", type) + return + } + + val originScheme = sourceOrigin.scheme + if (originScheme == null || originScheme.lowercase() != "https") { + reportFailure("WebAuthn not permitted for current URL", type) + return + } + + // Verify that origin belongs to your website, + // it's because the unknown origin may gain credential info. + if (isUnknownOrigin(sourceOrigin)) { + reportFailure("WebAuthn not permitted for current URL", type) + return + } + + havePendingRequest = true + pendingRequestIsDoomed = false + + // Let’s use a temporary “replyCurrent” variable to send the data back, while resetting + // the main “replyChannel” variable to null so it’s ready for the next request. + val replyCurrent = replyChannel + if (replyCurrent == null) { + Log.i(TAG, "reply channel was null, cannot continue") + return; + } + + when (type) { + CREATE_UNIQUE_KEY -> + this.coroutineScope.launch { + handleCreateFlow(credentialManagerHandler, message, replyCurrent) + } + + GET_UNIQUE_KEY -> this.coroutineScope.launch { + handleGetFlow(credentialManagerHandler, message, replyCurrent) + } + + else -> Log.i(TAG, "Incorrect request json") + } + } + } + + private fun isUnknownOrigin(sourceOrigin: Uri): Boolean { + val baseUrlHost = Uri.parse(FronteggAuth.instance.baseUrl).host + return sourceOrigin.host != baseUrlHost + } + + // Handles the get flow in a less error-prone way + private suspend fun handleGetFlow( + credentialManagerHandler: CredentialManagerHandler, + message: String, + reply: ReplyChannel, + ) { + try { + havePendingRequest = false + pendingRequestIsDoomed = false + val r = credentialManagerHandler.getPasskey(message) + val successArray = ArrayList(); + successArray.add("success"); + successArray.add( + JSONObject( + (r.credential as PublicKeyCredential).authenticationResponseJson + ) + ) + successArray.add(GET_UNIQUE_KEY); + reply.send(JSONArray(successArray).toString()) + replyChannel = null // setting initial replyChannel for next request given temp 'reply' + } catch (e: GetCredentialException) { + reportFailure("Error: ${e.errorMessage} w type: ${e.type} w obj: $e", GET_UNIQUE_KEY, e) + } catch (t: Throwable) { + reportFailure("Error: ${t.message}", GET_UNIQUE_KEY, Exception(t)) + } + } + + // handles the create flow in a less error prone way + private suspend fun handleCreateFlow( + credentialManagerHandler: CredentialManagerHandler, + message: String, + reply: ReplyChannel, + ) { + try { + havePendingRequest = false + pendingRequestIsDoomed = false + val response = credentialManagerHandler.createPasskey(message) + val successArray = ArrayList(); + successArray.add("success"); + successArray.add(JSONObject(response.registrationResponseJson)); + successArray.add(CREATE_UNIQUE_KEY); + reply.send(JSONArray(successArray).toString()) + replyChannel = null // setting initial replyChannel for next request given temp 'reply' + } catch (e: CreateCredentialException) { + reportFailure( + "Error: ${e.errorMessage} w type: ${e.type} w obj: $e", + CREATE_UNIQUE_KEY, + e + ) + } catch (t: Throwable) { + reportFailure("Error: ${t.message}", CREATE_UNIQUE_KEY, t as Exception) + } + } + + /** Invalidates any current request. */ + fun onPageStarted() { + if (havePendingRequest) { + pendingRequestIsDoomed = true + } + } + + /** Sends an error result to the page. */ + private fun reportFailure(message: String, type: String, e: Exception? = null) { + havePendingRequest = false + pendingRequestIsDoomed = false + val reply: ReplyChannel = replyChannel!! // verifies non null by throwing NPE + replyChannel = null + postErrorMessage(reply, message, type, e) + } + + private fun postErrorMessage( + reply: ReplyChannel, + errorMessage: String, + type: String, + e: Exception? = null + ) { + Log.i(TAG, "Sending error message back to the page via replyChannel $errorMessage"); + val array: MutableList = ArrayList() + array.add("error") + array.add(errorMessage) + array.add(type) + reply.send(JSONArray(array).toString()) + + if (e is NoCredentialException) { + Toast.makeText( + this.activity.applicationContext, + "No passkeys found. Try another sign-in option.", + Toast.LENGTH_LONG + ).show() + } else if (e == null) { + Toast.makeText(this.activity.applicationContext, errorMessage, Toast.LENGTH_SHORT) + .show() + } else { + Toast.makeText( + this.activity.applicationContext, + "Unknown Error Occurred", + Toast.LENGTH_SHORT + ).show() + + } + } + + private class JavaScriptReplyChannel(private val reply: JavaScriptReplyProxy) : + ReplyChannel { + override fun send(message: String?) { + try { + reply.postMessage(message!!) + } catch (t: Throwable) { + Log.i(TAG, "Reply failure due to: " + t.message); + } + } + } + + /** ReplyChannel is the interface over which replies to the embedded site are sent. This allows + for testing because AndroidX bans mocking its objects.*/ + interface ReplyChannel { + fun send(message: String?) + } + + companion object { + /** INTERFACE_NAME is the name of the MessagePort that must be injected into pages. */ + const val INTERFACE_NAME = "__webauthn_interface__" + + const val CREATE_UNIQUE_KEY = "create" + const val GET_UNIQUE_KEY = "get" + const val TYPE_KEY = "type" + const val REQUEST_KEY = "request" + + /** INJECTED_VAL is the minified version of the JavaScript code described at this class + * heading. The non minified form is found at credmanweb/javascript/encode.js.*/ + const val INJECTED_VAL = """ + var __webauthn_interface__,__webauthn_hooks__;!function(e){console.log("In the hook."),__webauthn_interface__.addEventListener("message",function e(n){var r=JSON.parse(n.data),t=r[2];"get"===t?o(r):"create"===t?u(r):console.log("Incorrect response format for reply")});var n=null,r=null,t=null,a=null;function o(e){if(null!==n&&null!==t){if("success"!=e[0]){var r=t;n=null,t=null,r(new DOMException(e[1],"NotAllowedError"));return}var a=i(e[1]),o=n;n=null,t=null,o(a)}}function l(e){var n=e.length%4;return Uint8Array.from(atob(e.replace(/-/g,"+").replace(/_/g,"/").padEnd(e.length+(0===n?0:4-n),"=")),function(e){return e.charCodeAt(0)}).buffer}function s(e){return btoa(Array.from(new Uint8Array(e),function(e){return String.fromCharCode(e)}).join("")).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+${'$'}/,"")}function u(e){if(null===r||null===a){console.log("Here: "+r+" and reject: "+a);return}if(console.log("Output back: "+e),"success"!=e[0]){var n=a;r=null,a=null,n(new DOMException(e[1],"NotAllowedError"));return}var t=i(e[1]),o=r;r=null,a=null,o(t)}function i(e){return console.log("Here is the response from credential manager: "+e),e.rawId=l(e.rawId),e.response.clientDataJSON=l(e.response.clientDataJSON),e.response.hasOwnProperty("attestationObject")&&(e.response.attestationObject=l(e.response.attestationObject)),e.response.hasOwnProperty("authenticatorData")&&(e.response.authenticatorData=l(e.response.authenticatorData)),e.response.hasOwnProperty("signature")&&(e.response.signature=l(e.response.signature)),e.response.hasOwnProperty("userHandle")&&(e.response.userHandle=l(e.response.userHandle)),e.getClientExtensionResults=function e(){return{}},e}e.create=function n(t){if(!("publicKey"in t))return e.originalCreateFunction(t);var o=new Promise(function(e,n){r=e,a=n}),l=t.publicKey;if(l.hasOwnProperty("challenge")){var u=s(l.challenge);l.challenge=u}if(l.hasOwnProperty("user")&&l.user.hasOwnProperty("id")){var i=s(l.user.id);l.user.id=i}var c=JSON.stringify({type:"create",request:l});return __webauthn_interface__.postMessage(c),o},e.get=function r(a){if(!("publicKey"in a))return e.originalGetFunction(a);var o=new Promise(function(e,r){n=e,t=r}),l=a.publicKey;if(l.hasOwnProperty("challenge")){var u=s(l.challenge);l.challenge=u}var i=JSON.stringify({type:"get",request:l});return __webauthn_interface__.postMessage(i),o},e.onReplyGet=o,e.CM_base64url_decode=l,e.CM_base64url_encode=s,e.onReplyCreate=u}(__webauthn_hooks__||(__webauthn_hooks__={})),__webauthn_hooks__.originalGetFunction=navigator.credentials.get,__webauthn_hooks__.originalCreateFunction=navigator.credentials.create,navigator.credentials.get=__webauthn_hooks__.get,navigator.credentials.create=__webauthn_hooks__.create,window.PublicKeyCredential=function(){},window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable=function(){return Promise.resolve(!1)}; + """ + const val TAG = "PasskeyWebListener" + } + +} \ No newline at end of file diff --git a/android/src/main/java/com/frontegg/android/exceptions/FronteggException.kt b/android/src/main/java/com/frontegg/android/exceptions/FronteggException.kt index 1d05c20..23d874e 100644 --- a/android/src/main/java/com/frontegg/android/exceptions/FronteggException.kt +++ b/android/src/main/java/com/frontegg/android/exceptions/FronteggException.kt @@ -5,8 +5,11 @@ public open class FronteggException(message: String, cause: Throwable? = null) : public companion object { public const val UNKNOWN_ERROR: String = "frontegg.error.unknown" - public const val FRONTEGG_APP_MUST_BE_INITIALIZED: String = "frontegg.error.app_must_be_initialized" - public const val FRONTEGG_DOMAIN_MUST_NOT_START_WITH_HTTPS: String = "frontegg.error.domain_must_not_start_with_https" - public const val KEY_NOT_FOUND_SHARED_PREFERENCES_ERROR: String = "frontegg.error.key_not_found_shared_preferences" + public const val FRONTEGG_APP_MUST_BE_INITIALIZED: String = + "frontegg.error.app_must_be_initialized" + public const val FRONTEGG_DOMAIN_MUST_NOT_START_WITH_HTTPS: String = + "frontegg.error.domain_must_not_start_with_https" + public const val KEY_NOT_FOUND_SHARED_PREFERENCES_ERROR: String = + "frontegg.error.key_not_found_shared_preferences" } } \ No newline at end of file diff --git a/android/src/main/java/com/frontegg/android/exceptions/KeyNotFoundException.kt b/android/src/main/java/com/frontegg/android/exceptions/KeyNotFoundException.kt index 31853af..d68862a 100644 --- a/android/src/main/java/com/frontegg/android/exceptions/KeyNotFoundException.kt +++ b/android/src/main/java/com/frontegg/android/exceptions/KeyNotFoundException.kt @@ -3,4 +3,5 @@ package com.frontegg.android.exceptions /** * Exception that represents key not found when trying to get value from Shared Preference */ -public class KeyNotFoundException(cause: Throwable) : FronteggException(KEY_NOT_FOUND_SHARED_PREFERENCES_ERROR, cause) +public class KeyNotFoundException(cause: Throwable) : + FronteggException(KEY_NOT_FOUND_SHARED_PREFERENCES_ERROR, cause) diff --git a/android/src/main/java/com/frontegg/android/services/RefreshTokenService.kt b/android/src/main/java/com/frontegg/android/services/RefreshTokenService.kt index 80f44fb..3ee9efa 100644 --- a/android/src/main/java/com/frontegg/android/services/RefreshTokenService.kt +++ b/android/src/main/java/com/frontegg/android/services/RefreshTokenService.kt @@ -16,7 +16,12 @@ class RefreshTokenService : JobService() { } override fun onStartJob(params: JobParameters?): Boolean { - Log.d(TAG, "Job started, (${Instant.now().toEpochMilli() - FronteggApp.getInstance().lastJobStart} ms)") + Log.d( + TAG, + "Job started, (${ + Instant.now().toEpochMilli() - FronteggApp.getInstance().lastJobStart + } ms)" + ) performBackgroundTask(params) return true } diff --git a/android/src/main/java/com/frontegg/android/utils/ObservableValue.kt b/android/src/main/java/com/frontegg/android/utils/ObservableValue.kt index 521edd6..ce6c668 100644 --- a/android/src/main/java/com/frontegg/android/utils/ObservableValue.kt +++ b/android/src/main/java/com/frontegg/android/utils/ObservableValue.kt @@ -1,7 +1,5 @@ package com.frontegg.android.utils -import android.os.Handler -import android.os.Looper import io.reactivex.rxjava3.disposables.Disposable import io.reactivex.rxjava3.functions.Consumer import io.reactivex.rxjava3.subjects.PublishSubject diff --git a/app/build.gradle b/app/build.gradle index 5564db3..3ae4575 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -3,7 +3,7 @@ plugins { id 'org.jetbrains.kotlin.android' } -def fronteggDomain = "auth.davidantoon.me" +def fronteggDomain = "autheu.davidantoon.me" def fronteggClientId = "b6adfe4c-d695-4c04-b95f-3ec9fd0c6cca" def fronteggApplicationId = "16407b9a-5b6c-43de-9f58-6a1d1e0077f8" @@ -14,7 +14,7 @@ android { defaultConfig { applicationId "com.frontegg.demo" minSdk 26 - targetSdk 33 + targetSdk 34 versionCode 1 versionName "1.0" @@ -46,6 +46,7 @@ android { } buildFeatures { viewBinding true + buildConfig true } buildToolsVersion buildToolsVersion diff --git a/build.gradle b/build.gradle index ba9ac0d..76a3d70 100644 --- a/build.gradle +++ b/build.gradle @@ -1,14 +1,14 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - id 'com.android.application' version '7.4.2' apply false - id 'com.android.library' version '7.4.2' apply false + id 'com.android.application' version '8.5.2' apply false + id 'com.android.library' version '8.5.2' apply false id 'org.jetbrains.kotlin.android' version '1.9.10' apply false } ext { kotlinVersion = "1.7.10" - buildToolsVersion = "32.0.0" + buildToolsVersion = "35.0.0" androidxAnnotationVersion = "1.5.0" extTruthVersion = "1.6.0-alpha01" coreVersion = "1.6.0-alpha01" diff --git a/embedded/build.gradle b/embedded/build.gradle index d2bcd2a..07019ee 100644 --- a/embedded/build.gradle +++ b/embedded/build.gradle @@ -3,9 +3,14 @@ plugins { id 'org.jetbrains.kotlin.android' } -def fronteggDomain = "auth.davidantoon.me" + +def fronteggDomain = "autheu.davidantoon.me" def fronteggClientId = "b6adfe4c-d695-4c04-b95f-3ec9fd0c6cca" +//def fronteggDomain = "auth.davidantoon.me" +//def fronteggClientId = "04ae2174-d8d9-4a90-8bab-2548e210a508" + + android { namespace 'com.frontegg.demo' compileSdk 34 @@ -30,11 +35,29 @@ android { } + signingConfigs { + release { + storeFile file(project.property("RELEASE_STORE_FILE")) + storePassword project.property("RELEASE_STORE_PASSWORD") + keyAlias project.property("RELEASE_KEY_ALIAS") + keyPassword project.property("RELEASE_KEY_PASSWORD") + } + debug { + storeFile file(project.property("DEBUG_STORE_FILE")) + storePassword project.property("DEBUG_STORE_PASSWORD") + keyAlias project.property("DEBUG_KEY_ALIAS") + keyPassword project.property("DEBUG_KEY_PASSWORD") + } + } buildTypes { release { minifyEnabled true shrinkResources true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + signingConfig signingConfigs.release + } + debug { + signingConfig signingConfigs.debug } } compileOptions { @@ -46,6 +69,7 @@ android { } buildFeatures { viewBinding true + buildConfig true } buildToolsVersion buildToolsVersion @@ -56,14 +80,14 @@ dependencies { implementation 'androidx.core:core-ktx:1.10.0' implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'io.reactivex.rxjava3:rxkotlin:3.0.1' - implementation 'com.google.android.material:material:1.8.0' + implementation 'com.google.android.material:material:1.12.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' - implementation 'androidx.navigation:navigation-fragment-ktx:2.5.3' - implementation 'androidx.navigation:navigation-ui-ktx:2.5.3' - implementation 'androidx.tracing:tracing:1.1.0' + implementation 'androidx.navigation:navigation-fragment-ktx:2.8.1' + implementation 'androidx.navigation:navigation-ui-ktx:2.8.1' + implementation 'androidx.tracing:tracing:1.2.0' implementation project(path: ':android') - implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.2' - implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2' + implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.8.6' + implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.6' implementation 'de.hdodenhof:circleimageview:3.1.0' implementation 'com.github.bumptech.glide:glide:4.12.0' diff --git a/embedded/proguard-rules.pro b/embedded/proguard-rules.pro index 481bb43..f1b4245 100644 --- a/embedded/proguard-rules.pro +++ b/embedded/proguard-rules.pro @@ -18,4 +18,4 @@ # If you keep the line number information, uncomment this to # hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file +#-renamesourcefileattribute SourceFile diff --git a/embedded/src/main/java/com/frontegg/demo/AuthFragment.kt b/embedded/src/main/java/com/frontegg/demo/AuthFragment.kt index 2c36c66..1ad1bfa 100644 --- a/embedded/src/main/java/com/frontegg/demo/AuthFragment.kt +++ b/embedded/src/main/java/com/frontegg/demo/AuthFragment.kt @@ -36,7 +36,9 @@ class AuthFragment : Fragment() { super.onViewCreated(view, savedInstanceState) binding.loginButton.setOnClickListener { - FronteggAuth.instance.login(requireActivity()) + FronteggAuth.instance.login(requireActivity()) { + Log.d("AuthFragment", "Login callback") + } } binding.googleLoginButton.setOnClickListener { diff --git a/gradle.properties b/gradle.properties index 8ea0209..e174f9e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -21,4 +21,18 @@ kotlin.code.style=official # resources declared in the library itself and none from the library's dependencies, # thereby reducing the size of the R class for that library android.nonTransitiveRClass=true +android.suppressUnsupportedCompileSdk=35 + + +# Test release gradle settings +RELEASE_STORE_FILE=../.github/test-jks/release.keystore +RELEASE_STORE_PASSWORD=android +RELEASE_KEY_ALIAS=androidreleasekey +RELEASE_KEY_PASSWORD=android + +# Test debug gradle settings +DEBUG_STORE_FILE=../.github/test-jks/debug.keystore +DEBUG_STORE_PASSWORD=android +DEBUG_KEY_ALIAS=androiddebugkey +DEBUG_KEY_PASSWORD=android diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index df250ba..8556058 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Sat Jul 08 01:29:18 IDT 2023 +#Tue Oct 01 17:33:30 IDT 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/multi-region/build.gradle b/multi-region/build.gradle index 367ec05..ae4c8c9 100644 --- a/multi-region/build.gradle +++ b/multi-region/build.gradle @@ -4,9 +4,11 @@ plugins { } -def fronteggDomain = "auth.davidantoon.me" +def fronteggDomain = "autheu.davidantoon.me" def fronteggClientId = "b6adfe4c-d695-4c04-b95f-3ec9fd0c6cca" +def fronteggDomain2 = "authus.davidantoon.me" + android { namespace 'com.frontegg.demo' compileSdk 34 @@ -23,7 +25,8 @@ android { manifestPlaceholders = [ "package_name" : applicationId, "frontegg_domain" : fronteggDomain, - "frontegg_client_id": fronteggClientId + "frontegg_client_id": fronteggClientId, + "frontegg_domain_2" : fronteggDomain2, ] vectorDrawables { useSupportLibrary true diff --git a/multi-region/src/main/AndroidManifest.xml b/multi-region/src/main/AndroidManifest.xml index 12ac938..4902a40 100644 --- a/multi-region/src/main/AndroidManifest.xml +++ b/multi-region/src/main/AndroidManifest.xml @@ -41,16 +41,16 @@ @@ -66,11 +66,19 @@ + + + + + + + + diff --git a/multi-region/src/main/java/com/frontegg/demo/App.kt b/multi-region/src/main/java/com/frontegg/demo/App.kt index 8faff08..b0bf6e7 100644 --- a/multi-region/src/main/java/com/frontegg/demo/App.kt +++ b/multi-region/src/main/java/com/frontegg/demo/App.kt @@ -18,13 +18,13 @@ class App : Application() { listOf( RegionConfig( "eu", - "auth.davidantoon.me", + "autheu.davidantoon.me", "b6adfe4c-d695-4c04-b95f-3ec9fd0c6cca" ), RegionConfig( "us", - "davidprod.frontegg.com", - "d7d07347-2c57-4450-8418-0ec7ee6e096b" + "authus.davidantoon.me", + "6903cab0-9809-4a2e-97dd-b8c0f966c813" ) ), this diff --git a/multi-region/src/main/res/layout/activity_region_selection.xml b/multi-region/src/main/res/layout/activity_region_selection.xml index 47895b8..65571a2 100644 --- a/multi-region/src/main/res/layout/activity_region_selection.xml +++ b/multi-region/src/main/res/layout/activity_region_selection.xml @@ -26,7 +26,7 @@ @@ -51,7 +51,7 @@