Skip to content

Token Migration#2837

Merged
brandonpage merged 10 commits intoforcedotcom:devfrom
brandonpage:token-migration
Feb 14, 2026
Merged

Token Migration#2837
brandonpage merged 10 commits intoforcedotcom:devfrom
brandonpage:token-migration

Conversation

@brandonpage
Copy link
Contributor

@brandonpage brandonpage commented Feb 7, 2026

This PR adds a migrateRefreshToken function to an new Kotlin UserAccountManagerExtention class.

Example usage:

UserAccountManager.getInstance().migrateRefreshToken(
    userAccount = user,  // defaults to current user if not set
    appConfig = config,
    onMigrationSuccess = { user ->
        // success lambda
    },
    onMigrationError = { error, errorDesc, e ->
        // error lambda
    },
)

Implementing it in the existing Java class would have required defining a new public interface just for the callback functions since lambda functions can not be directly passed as parameters. I did not want to introduce the extra public API since I expect we will convert the class to Kotlin soon. I attempted to convert the class for this PR but the static references converted to companion properties and return type nullability would have introduced minor breaking changes (that would be better left for 14.0).

migrateRefreshToken launches a new TokenMigrationActivity that does most of the work. A new MigrationCallbackRegistry object facilitates passing the callback functions from the entry point API to the activity, and a code comment explains why that is necessary. Most of the code in TokenMigrationActivity is the WebViewClient implementation that is very similar to the one in LoginActivity, but unfortunately they are a little too coupled to share. There is very little else in the activity because it utilizes the existing LoginViewModel to do most of the work.

The Activity UI is very minimal. When the WebView is loading, it and the Activity background are transparent with a 50% tint. The same progress indicator from standard login is reused, so if the app has customized it that should be reflected here as well.

No Interaction Required, Migration Failure, Interaction Required

Screenshot_20260115-154115 Screenshot_20260115_161228 Screenshot_20260115_161154

TODO:

  • More unit tests (buildAuthWebview, AuthenticationUtilities, etc)
  • UI Tests - Will send a follow-up PR.
  • More manual testing (beacon, etc)

@github-actions
Copy link

github-actions bot commented Feb 7, 2026

1 Warning
⚠️ Big PR, try to keep changes smaller if you can.

Generated by 🚫 Danger

import android.os.Parcelable
import kotlinx.parcelize.Parcelize

@Parcelize
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is Kotlin magic ✨. The compiler is able to implement the required functions to satisfy the Parcelable interface by itself since this is a data class.

We should do this for UserAccount in the future.

@github-actions
Copy link

github-actions bot commented Feb 7, 2026

25 Warnings
⚠️ libs/SalesforceSDK/AndroidManifest.xml#L0 - The project references RTL attributes, but does not explicitly enable or disable RTL support with android:supportsRtl in the manifest
⚠️ libs/SalesforceSDK/build.gradle.kts#L20 - A newer version of com.squareup.okhttp3:okhttp than 4.12.0 is available: 5.3.2
⚠️ libs/SalesforceSDK/build.gradle.kts#L21 - A newer version of com.google.firebase:firebase-messaging than 25.0.0 is available: 25.0.1
⚠️ libs/SalesforceSDK/build.gradle.kts#L22 - A newer version of androidx.core:core than 1.16.0 is available: 1.17.0
⚠️ libs/SalesforceSDK/build.gradle.kts#L23 - A newer version of androidx.browser:browser than 1.8.0 is available: 1.9.0
⚠️ libs/SalesforceSDK/build.gradle.kts#L24 - A newer version of androidx.work:work-runtime-ktx than 2.10.3 is available: 2.11.1
⚠️ libs/SalesforceSDK/build.gradle.kts#L30 - A newer version of androidx.core:core-ktx than 1.16.0 is available: 1.17.0
⚠️ libs/SalesforceSDK/build.gradle.kts#L31 - A newer version of androidx.activity:activity-ktx than 1.10.1 is available: 1.12.4
⚠️ libs/SalesforceSDK/build.gradle.kts#L32 - A newer version of androidx.activity:activity-compose than 1.10.1 is available: 1.12.4
⚠️ libs/SalesforceSDK/build.gradle.kts#L33 - A newer version of androidx.lifecycle:lifecycle-viewmodel-ktx than 2.8.7 is available: 2.10.0
⚠️ libs/SalesforceSDK/build.gradle.kts#L34 - A newer version of androidx.lifecycle:lifecycle-viewmodel-compose than 2.8.7 is available: 2.10.0
⚠️ libs/SalesforceSDK/build.gradle.kts#L35 - A newer version of androidx.lifecycle:lifecycle-viewmodel-savedstate than 2.8.7 is available: 2.10.0
⚠️ libs/SalesforceSDK/build.gradle.kts#L36 - A newer version of androidx.lifecycle:lifecycle-service than 2.8.7 is available: 2.10.0
⚠️ libs/SalesforceSDK/build.gradle.kts#L37 - A newer version of org.jetbrains.kotlinx:kotlinx-serialization-json than 1.6.3 is available: 1.10.0
⚠️ libs/SalesforceSDK/build.gradle.kts#L38 - A newer version of androidx.window:window than 1.4.0 is available: 1.5.1
⚠️ libs/SalesforceSDK/build.gradle.kts#L39 - A newer version of androidx.window:window-core than 1.4.0 is available: 1.5.1
⚠️ libs/SalesforceSDK/build.gradle.kts#L40 - A newer version of androidx.compose.material3:material3-android than 1.3.2 is available: 1.4.0
⚠️ libs/SalesforceSDK/build.gradle.kts#L41 - A newer version of androidx.compose:compose-bom than 2025.07.00 is available: 2026.02.00
⚠️ libs/SalesforceSDK/build.gradle.kts#L42 - A newer version of androidx.compose.foundation:foundation-android than 1.8.2 is available: 1.10.3
⚠️ libs/SalesforceSDK/build.gradle.kts#L43 - A newer version of androidx.compose.runtime:runtime-livedata than 1.8.2 is available: 1.10.3
⚠️ libs/SalesforceSDK/build.gradle.kts#L44 - A newer version of androidx.compose.ui:ui-tooling-preview-android than 1.8.2 is available: 1.10.3
⚠️ libs/SalesforceSDK/build.gradle.kts#L45 - A newer version of androidx.compose.material:material than 1.8.2 is available: 1.10.3
⚠️ libs/SalesforceSDK/build.gradle.kts#L47 - A newer version of androidx.compose.ui:ui-tooling than 1.8.2 is available: 1.10.3
⚠️ libs/SalesforceSDK/build.gradle.kts#L48 - A newer version of androidx.compose.ui:ui-test-manifest than 1.8.2 is available: 1.10.3
⚠️ libs/SalesforceSDK/build.gradle.kts#L56 - A newer version of io.mockk:mockk-android than 1.14.0 is available: 1.14.9

Generated by 🚫 Danger

Comment on lines 96 to 105
val callbackKey = intent.getStringExtra(EXTRA_CALLBACK_ID) ?: run {
SalesforceSDKLogger.e(TAG, "Unable to parse MigrationResult callback id.")
finish()
return
}
val resultCallback = MigrationCallbackRegistry.consume(callbackKey) ?: run {
SalesforceSDKLogger.e(TAG, "Unable to retrieve MigrationResult callback.")
finish()
return
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should never happen, but I will admit it is a scary little gap in the flow. If we somehow hit an error here we can't invoke the error callback.

If anyone has a clever idea let me know. I'd rather not call startActivityForResult since it has been deprecated for years. And it's replacement registerForActivityResult needs to be called from an Activity.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't read everything yet, but could you use this@TokenMigrationActivity since that's captured in this closure?

<!-- Token Migration Activity -->
<activity android:name="com.salesforce.androidsdk.ui.TokenMigrationActivity"
android:excludeFromRecents="true"
android:theme="@style/AccountSwitcher"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should I rename this style to something more generic?

@codecov
Copy link

codecov bot commented Feb 7, 2026

Codecov Report

❌ Patch coverage is 83.98268% with 37 lines in your changes missing coverage. Please review.
✅ Project coverage is 63.87%. Comparing base (d42772e) to head (d6d948e).
⚠️ Report is 14 commits behind head on dev.

Files with missing lines Patch % Lines
...salesforce/androidsdk/ui/TokenMigrationActivity.kt 83.73% 9 Missing and 11 partials ⚠️
...esforce/androidsdk/auth/AuthenticationUtilities.kt 80.76% 1 Missing and 4 partials ⚠️
.../src/com/salesforce/androidsdk/ui/LoginActivity.kt 42.85% 0 Missing and 4 partials ⚠️
...rce/androidsdk/ui/components/TokenMigrationView.kt 80.00% 0 Missing and 4 partials ⚠️
...src/com/salesforce/androidsdk/ui/LoginViewModel.kt 85.00% 0 Missing and 3 partials ⚠️
...alesforce/androidsdk/auth/idp/IDPAuthCodeHelper.kt 0.00% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##                dev    #2837      +/-   ##
============================================
+ Coverage     63.24%   63.87%   +0.62%     
- Complexity     2825     2856      +31     
============================================
  Files           216      219       +3     
  Lines         16982    17186     +204     
  Branches       2418     2452      +34     
============================================
+ Hits          10741    10977     +236     
+ Misses         5072     5008      -64     
- Partials       1169     1201      +32     
Components Coverage Δ
Analytics 47.92% <ø> (ø)
SalesforceSDK 57.67% <83.98%> (+1.32%) ⬆️
Hybrid 59.05% <ø> (ø)
SmartStore 78.20% <ø> (ø)
MobileSync 81.68% <ø> (ø)
React 52.36% <ø> (ø)
Files with missing lines Coverage Δ
...androidsdk/accounts/UserAccountManagerExtension.kt 100.00% <100.00%> (ø)
...SDK/src/com/salesforce/androidsdk/auth/OAuth2.java 76.94% <ø> (+0.64%) ⬆️
...rc/com/salesforce/androidsdk/config/OAuthConfig.kt 89.47% <100.00%> (+0.58%) ⬆️
...m/salesforce/androidsdk/ui/components/LoginView.kt 43.83% <ø> (ø)
...alesforce/androidsdk/auth/idp/IDPAuthCodeHelper.kt 6.84% <0.00%> (ø)
...src/com/salesforce/androidsdk/ui/LoginViewModel.kt 88.99% <85.00%> (-0.75%) ⬇️
.../src/com/salesforce/androidsdk/ui/LoginActivity.kt 43.11% <42.85%> (ø)
...rce/androidsdk/ui/components/TokenMigrationView.kt 80.00% <80.00%> (ø)
...esforce/androidsdk/auth/AuthenticationUtilities.kt 51.39% <80.76%> (+21.27%) ⬆️
...salesforce/androidsdk/ui/TokenMigrationActivity.kt 83.73% <83.73%> (ø)

... and 5 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

* and replace the existing credentials for the user.
*/
@Suppress("UnusedReceiverParameter")
fun UserAccountManager.migrateRefreshToken(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Taking the success and error callbacks as up-front parameters works so no change required. I wondered if this could benefit from returning a Result. The call would look something like this pseudo-code then:

UserAccountManager.getInstance().migrateRefreshToken(
    userAccount = user,  // defaults to current user if not set
    appConfig = config
).onSuccess { user ->
        // success lambda
}.onError { error -> // I saw there's currently three parameters, but maybe they all fold into the error here?
        // error lambda
}

Copy link
Contributor Author

@brandonpage brandonpage Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had the same thought. Originally I had a function in the AppFlowTester's RestUtils class that called migrateRefreshToken and returned a Result but I ended up just not using it.

I don't think it makes sense to change the signature to Result since the lambdas passed in flow all the way through AuthenticationUtilities. But I will take a look in case doing so lets us get rid of MigrationCallbackRegistry.

// Remove the existing Account from AccountManager so createAccount can create a fresh one
val existingAccount = userAccountManager.buildAccount(duplicateUserAccount)
if (existingAccount != null) {
SalesforceSDKManager.getInstance().clientManager.removeAccount(existingAccount)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What was the impact of not doing the removeAccount until now?

Copy link
Contributor

@wmathurin wmathurin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM - the UI tests (especially the one combining multi users + migration) will catch anything we missed.

@wmathurin
Copy link
Contributor

LGTM - the UI tests (especially the one combining multi users + migration) will catch anything we missed.

After writing that, I went and checked the UI tests on iOS and realized we don't have a test that does migration with multi users ;-)

Copy link
Contributor

@JohnsonEricAtSalesforce JohnsonEricAtSalesforce left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⭐️

…iometricAuthPolicy and handleDuplicateUserAccount. Split out TokenMigrationView and add tests.
androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0")
androidTestImplementation("androidx.test.ext:junit:1.3.0")
androidTestImplementation("androidx.arch.core:core-testing:2.2.0")
androidTestImplementation("androidx.compose.ui:ui-test-junit4:$composeVersion")
Copy link

@github-actions github-actions bot Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ A newer version of androidx.compose.ui:ui-test-junit4 than 1.8.2 is available: 1.10.3

@brandonpage brandonpage merged commit cf5704e into forcedotcom:dev Feb 14, 2026
20 checks passed
@brandonpage
Copy link
Contributor Author

I will send a follow-up PR with UI tests as this one has gotten very large.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants