1+ /*
2+ * Copyright (c) 2026-present, salesforce.com, inc.
3+ * All rights reserved.
4+ * Redistribution and use of this software in source and binary forms, with or
5+ * without modification, are permitted provided that the following conditions
6+ * are met:
7+ * - Redistributions of source code must retain the above copyright notice, this
8+ * list of conditions and the following disclaimer.
9+ * - Redistributions in binary form must reproduce the above copyright notice,
10+ * this list of conditions and the following disclaimer in the documentation
11+ * and/or other materials provided with the distribution.
12+ * - Neither the name of salesforce.com, inc. nor the names of its contributors
13+ * may be used to endorse or promote products derived from this software without
14+ * specific prior written permission of salesforce.com, inc.
15+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
19+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
20+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
21+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
22+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
23+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
24+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
25+ * POSSIBILITY OF SUCH DAMAGE.
26+ */
27+ package com.salesforce.androidsdk.accounts
28+
29+ import android.content.Intent
30+ import com.salesforce.androidsdk.accounts.UserAccountManager.getInstance
31+ import com.salesforce.androidsdk.app.SalesforceSDKManager
32+ import com.salesforce.androidsdk.config.OAuthConfig
33+ import com.salesforce.androidsdk.ui.TokenMigrationActivity
34+ import com.salesforce.androidsdk.util.SalesforceSDKLogger
35+ import java.util.UUID
36+
37+ const val TAG = " UserAccountManager"
38+
39+ /* *
40+ * Attempts to migrate the [userAccount] to the provided Connected App or
41+ * External Client Application [appConfig].
42+ *
43+ * This might cause the approve/deny screen to be presented to the user to authorize the
44+ * new app. If successful a new set of credentials (refresh token, access token) are obtained
45+ * and replace the existing credentials for the user.
46+ */
47+ @Suppress(" UnusedReceiverParameter" )
48+ fun UserAccountManager.migrateRefreshToken (
49+ userAccount : UserAccount ? = getInstance().currentUser,
50+ appConfig : OAuthConfig ,
51+ onMigrationSuccess : (userAccount: UserAccount ) -> Unit ,
52+ onMigrationError : (error: String , errorDesc: String? , e: Throwable ? ) -> Unit ,
53+ ) {
54+ val loggedOnSuccess: (userAccount: UserAccount ) -> Unit = { user ->
55+ SalesforceSDKLogger .i(TAG , " Token Migration Successful \n\n User ${user.username} " +
56+ " (${user.instanceServer} ) successfully migrated to: \n $appConfig ." )
57+ onMigrationSuccess.invoke(user)
58+ }
59+ val userId = userAccount?.userId
60+ val orgId = userAccount?.orgId
61+
62+ if (userId == null || orgId == null ) {
63+ val message = " User account, userId or orgId is null."
64+ SalesforceSDKLogger .e(TAG , message)
65+ onMigrationError(message, null , null )
66+ return
67+ }
68+
69+ val callbackKey = MigrationCallbackRegistry .register(
70+ callbacks = MigrationCallbackRegistry .MigrationCallbacks (
71+ onMigrationSuccess = loggedOnSuccess,
72+ onMigrationError = onMigrationError,
73+ )
74+ )
75+
76+ with (SalesforceSDKManager .getInstance().appContext) {
77+ startActivity(
78+ Intent (/* packageContext = */ this , TokenMigrationActivity ::class .java).apply {
79+ addFlags(Intent .FLAG_ACTIVITY_NEW_TASK )
80+ putExtra(TokenMigrationActivity .EXTRA_ORG_ID , orgId)
81+ putExtra(TokenMigrationActivity .EXTRA_USER_ID , userId)
82+ putExtra(TokenMigrationActivity .EXTRA_OAUTH_CONFIG , appConfig)
83+ putExtra(TokenMigrationActivity .EXTRA_CALLBACK_ID , callbackKey)
84+ }
85+ )
86+ }
87+ }
88+
89+ /*
90+ This mechanism is used to pass a _string_ id to the Activity to retrieve callback functions.
91+
92+ Lambda functions may appear Parcelable/Serializable but since we cannot guarantee the
93+ content are they should not be passed. For instance, if the lambda function contains
94+ compose state an exception will be thrown.
95+ */
96+ internal object MigrationCallbackRegistry {
97+ private val callbacks = mutableMapOf<String , MigrationCallbacks >()
98+
99+ data class MigrationCallbacks (
100+ val onMigrationSuccess : (UserAccount ) -> Unit ,
101+ val onMigrationError : (String , String? , Throwable ? ) -> Unit
102+ )
103+
104+ fun register (callbacks : MigrationCallbacks ): String {
105+ val key = UUID .randomUUID().toString()
106+ this .callbacks[key] = callbacks
107+ return key
108+ }
109+
110+ fun consume (key : String ): MigrationCallbacks ? = callbacks.remove(key)
111+ }
0 commit comments