Skip to content

Commit aad8a58

Browse files
authored
Merge pull request #130 from MetaMask/session-persistence
feat: Session persistence v2
2 parents 1c91e8b + 27942f7 commit aad8a58

File tree

8 files changed

+134
-40
lines changed

8 files changed

+134
-40
lines changed

app/src/main/java/com/metamask/dapp/AppModule.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ internal object AppModule {
2525

2626
@Provides // Add SDKOptions(infuraAPIKey="supply_your_key_here") to Ethereum constructor for read-only calls
2727
fun provideEthereum(@ApplicationContext context: Context, dappMetadata: DappMetadata, logger: Logger): Ethereum {
28-
return Ethereum(context, dappMetadata, SDKOptions(infuraAPIKey = "#####"), logger)
28+
return Ethereum(context, dappMetadata, null, logger)
2929
}
3030

3131
@Provides

app/src/main/java/com/metamask/dapp/Setup.kt

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,13 @@ fun Setup(ethereumViewModel: EthereumFlowViewModel, screenViewModel: ScreenViewM
2020
var isConnecting by remember { mutableStateOf(false) }
2121
var isConnectSigning by remember { mutableStateOf(false) }
2222
var connectResult by remember { mutableStateOf<Result>(Result.Success.Item("")) }
23-
var signMessage by remember { mutableStateOf("") }
23+
var account by remember { mutableStateOf(ethereumState.selectedAddress) }
24+
25+
LaunchedEffect(ethereumState.selectedAddress) {
26+
if (ethereumState.selectedAddress.isNotEmpty()) {
27+
screenViewModel.setScreen(ACTIONS)
28+
}
29+
}
2430

2531
// Connect
2632
LaunchedEffect(isConnecting) {
@@ -38,7 +44,7 @@ fun Setup(ethereumViewModel: EthereumFlowViewModel, screenViewModel: ScreenViewM
3844
}
3945
}
4046

41-
NavHost(navController = navController, startDestination = CONNECT.name) {
47+
NavHost(navController = navController, startDestination = if (account.isNotEmpty()) { DappScreen.ACTIONS.name } else { DappScreen.CONNECT.name }) {
4248
composable(CONNECT.name) {
4349
ConnectScreen(
4450
ethereumState = ethereumState,

metamask-android-sdk/src/main/java/io/metamask/androidsdk/CommunicationClient.kt

Lines changed: 29 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,14 @@ import kotlinx.serialization.Serializable
1616
import org.json.JSONObject
1717
import java.lang.ref.WeakReference
1818

19-
internal class CommunicationClient(context: Context, callback: EthereumEventCallback?, private val logger: Logger = DefaultLogger) {
19+
class CommunicationClient(
20+
context: Context,
21+
callback: EthereumEventCallback?,
22+
private val sessionManager: SessionManager,
23+
private val keyExchange: KeyExchange,
24+
private val logger: Logger = DefaultLogger) {
2025

2126
var sessionId: String = ""
22-
private val keyExchange: KeyExchange = KeyExchange()
2327

2428
var dappMetadata: DappMetadata? = null
2529
var isServiceConnected = false
@@ -35,20 +39,23 @@ internal class CommunicationClient(context: Context, callback: EthereumEventCall
3539
private var submittedRequests: MutableMap<String, SubmittedRequest> = mutableMapOf()
3640
private var queuedRequests: MutableMap<String, SubmittedRequest> = mutableMapOf()
3741

38-
private var sessionManager: SessionManager
39-
4042
private var isMetaMaskReady = false
4143
private var sentOriginatorInfo = false
4244
private var requestedBindService = false
4345

46+
var hasSubmittedRequests: Boolean = submittedRequests.isEmpty()
47+
var hasRequestJobs: Boolean = requestJobs.isEmpty()
48+
var hasQueuedRequests: Boolean = queuedRequests.isEmpty()
49+
4450
var enableDebug: Boolean = false
4551
set(value) {
4652
field = value
4753
tracker.enableDebug = value
4854
}
4955

5056
init {
51-
sessionManager = SessionManager(KeyStorage(context))
57+
sessionId = sessionManager.sessionId
58+
// in case not yet initialised in SessionManager
5259
sessionManager.onInitialized = {
5360
sessionId = sessionManager.sessionId
5461
}
@@ -142,7 +149,7 @@ internal class CommunicationClient(context: Context, callback: EthereumEventCall
142149
sentOriginatorInfo = false
143150
}
144151

145-
private fun handleMessage(message: String) {
152+
fun handleMessage(message: String) {
146153
val jsonString = keyExchange.decrypt(message)
147154
val json = JSONObject(jsonString)
148155

@@ -187,7 +194,7 @@ internal class CommunicationClient(context: Context, callback: EthereumEventCall
187194
}
188195
}
189196

190-
private fun resumeRequestJobs() {
197+
fun resumeRequestJobs() {
191198
logger.log("CommunicationClient:: Resuming jobs")
192199

193200
while (requestJobs.isNotEmpty()) {
@@ -196,12 +203,12 @@ internal class CommunicationClient(context: Context, callback: EthereumEventCall
196203
}
197204
}
198205

199-
private fun queueRequestJob(job: () -> Unit) {
206+
fun queueRequestJob(job: () -> Unit) {
200207
requestJobs.add(job)
201208
logger.log("CommunicationClient:: Queued job")
202209
}
203210

204-
private fun clearPendingRequests() {
211+
fun clearPendingRequests() {
205212
queuedRequests = mutableMapOf()
206213
requestJobs = mutableListOf()
207214
submittedRequests = mutableMapOf()
@@ -311,7 +318,7 @@ internal class CommunicationClient(context: Context, callback: EthereumEventCall
311318
}
312319
}
313320

314-
private fun handleError(error: String, id: String): Boolean {
321+
fun handleError(error: String, id: String): Boolean {
315322
if (error.isEmpty()) {
316323
return false
317324
}
@@ -329,7 +336,7 @@ internal class CommunicationClient(context: Context, callback: EthereumEventCall
329336
return true
330337
}
331338

332-
private fun completeRequest(id: String, result: Result) {
339+
fun completeRequest(id: String, result: Result) {
333340
if (queuedRequests[id] != null) {
334341
queuedRequests[id]?.callback?.invoke(result)
335342
queuedRequests.remove(id)
@@ -338,13 +345,13 @@ internal class CommunicationClient(context: Context, callback: EthereumEventCall
338345
submittedRequests.remove(id)
339346
}
340347

341-
private fun handleEvent(event: JSONObject) {
348+
fun handleEvent(event: JSONObject) {
342349
when (event.optString("method")) {
343350
EthereumMethod.METAMASK_ACCOUNTS_CHANGED.value -> {
344351
val accountsJson = event.optString("params")
345352
val accounts: List<String> = Gson().fromJson(accountsJson, object : TypeToken<List<String>>() {}.type)
346353
accounts.getOrNull(0)?.let { account ->
347-
logger.error("CommunicationClient:: Event Updated to account $account")
354+
logger.log("CommunicationClient:: Event Updated to account $account")
348355
updateAccount(account)
349356
}
350357
}
@@ -362,17 +369,17 @@ internal class CommunicationClient(context: Context, callback: EthereumEventCall
362369
}
363370
}
364371

365-
private fun updateAccount(account: String) {
372+
fun updateAccount(account: String) {
366373
val callback = ethereumEventCallbackRef.get()
367374
callback?.updateAccount(account)
368375
}
369376

370-
private fun updateChainId(chainId: String) {
377+
fun updateChainId(chainId: String) {
371378
val callback = ethereumEventCallbackRef.get()
372379
callback?.updateChainId(chainId)
373380
}
374381

375-
private fun handleKeyExchange(message: String) {
382+
fun handleKeyExchange(message: String) {
376383
val json = JSONObject(message)
377384

378385
val keyExchangeStep = json.optString(KeyExchange.TYPE, KeyExchangeMessageType.KEY_HANDSHAKE_SYN.name)
@@ -396,7 +403,7 @@ internal class CommunicationClient(context: Context, callback: EthereumEventCall
396403
}
397404
}
398405

399-
private fun sendMessage(message: String) {
406+
fun sendMessage(message: String) {
400407
val bundle = Bundle().apply {
401408
putString(MESSAGE, message)
402409
}
@@ -439,7 +446,7 @@ internal class CommunicationClient(context: Context, callback: EthereumEventCall
439446
}
440447
}
441448

442-
private fun processRequest(request: RpcRequest, callback: (Result) -> Unit) {
449+
fun processRequest(request: RpcRequest, callback: (Result) -> Unit) {
443450
logger.log("CommunicationClient:: sending request $request")
444451
if (queuedRequests[request.id] != null) {
445452
queuedRequests.remove(request.id)
@@ -455,7 +462,7 @@ internal class CommunicationClient(context: Context, callback: EthereumEventCall
455462
sendMessage(messageJson)
456463
}
457464

458-
private fun sendOriginatorInfo() {
465+
fun sendOriginatorInfo() {
459466
if (sentOriginatorInfo) { return }
460467
sentOriginatorInfo = true
461468

@@ -479,7 +486,7 @@ internal class CommunicationClient(context: Context, callback: EthereumEventCall
479486
sendMessage(messageJson)
480487
}
481488

482-
private fun isQA(): Boolean {
489+
fun isQA(): Boolean {
483490
if (Build.VERSION.SDK_INT < 33 ) { // i.e Build.VERSION_CODES.TIRAMISU
484491
return false
485492
}
@@ -494,7 +501,7 @@ internal class CommunicationClient(context: Context, callback: EthereumEventCall
494501
}
495502
}
496503

497-
private fun bindService() {
504+
fun bindService() {
498505
logger.log("CommunicationClient:: Binding service")
499506
requestedBindService = true
500507

@@ -538,7 +545,7 @@ internal class CommunicationClient(context: Context, callback: EthereumEventCall
538545
sendKeyExchangeMesage(keyExchange.toString())
539546
}
540547

541-
private fun sendKeyExchangeMesage(message: String) {
548+
fun sendKeyExchangeMesage(message: String) {
542549
val bundle = Bundle().apply {
543550
putString(KEY_EXCHANGE, message)
544551
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package io.metamask.androidsdk
2+
3+
import android.content.Context
4+
5+
class CommunicationClientModule(private val context: Context): CommunicationClientModuleInterface {
6+
override fun provideKeyStorage(): KeyStorage {
7+
return KeyStorage(context)
8+
}
9+
10+
override fun provideSessionManager(keyStorage: SecureStorage): SessionManager {
11+
return SessionManager(keyStorage)
12+
}
13+
14+
override fun provideKeyExchange(): KeyExchange {
15+
return KeyExchange()
16+
}
17+
18+
override fun provideLogger(): Logger {
19+
return DefaultLogger
20+
}
21+
22+
override fun provideCommunicationClient(callback: EthereumEventCallback?): CommunicationClient {
23+
val keyStorage = provideKeyStorage()
24+
val sessionManager = provideSessionManager(keyStorage)
25+
val keyExchange = provideKeyExchange()
26+
val logger = provideLogger()
27+
return CommunicationClient(context, callback, sessionManager, keyExchange, logger)
28+
}
29+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package io.metamask.androidsdk
2+
3+
interface CommunicationClientModuleInterface {
4+
fun provideKeyStorage(): SecureStorage
5+
fun provideSessionManager(keyStorage: SecureStorage): SessionManager
6+
fun provideKeyExchange(): KeyExchange
7+
fun provideLogger(): Logger
8+
fun provideCommunicationClient(callback: EthereumEventCallback?): CommunicationClient
9+
}

metamask-android-sdk/src/main/java/io/metamask/androidsdk/Ethereum.kt

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,31 @@ import android.content.Intent
55
import android.net.Uri
66
import androidx.lifecycle.LiveData
77
import androidx.lifecycle.MutableLiveData
8+
import kotlinx.coroutines.CoroutineScope
9+
import kotlinx.coroutines.Dispatchers
10+
import kotlinx.coroutines.SupervisorJob
11+
import kotlinx.coroutines.launch
812
import java.lang.ref.WeakReference
913

1014
private const val METAMASK_DEEPLINK = "https://metamask.app.link"
1115
private const val METAMASK_BIND_DEEPLINK = "$METAMASK_DEEPLINK/bind"
12-
private const val DEFAULT_SESSION_DURATION: Long = 30 * 24 * 3600 // 30 days default
1316

1417
class Ethereum (
1518
private val context: Context,
1619
private val dappMetadata: DappMetadata,
1720
sdkOptions: SDKOptions? = null,
18-
private val logger: Logger = DefaultLogger
21+
private val logger: Logger = DefaultLogger,
22+
private val communicationClientModule: CommunicationClientModule = CommunicationClientModule(context)
1923
): EthereumEventCallback {
2024
private var connectRequestSent = false
25+
2126
private val communicationClient: CommunicationClient? by lazy {
22-
CommunicationClient(context, null)
27+
communicationClientModule.provideCommunicationClient(this)
2328
}
2429

30+
private val storage = communicationClientModule.provideKeyStorage()
31+
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
32+
2533
private val infuraProvider: InfuraProvider? = sdkOptions?.let {
2634
if (it.infuraAPIKey.isNotEmpty()) {
2735
InfuraProvider(it.infuraAPIKey)
@@ -51,13 +59,31 @@ class Ethereum (
5159

5260
init {
5361
updateSessionDuration()
62+
initializeEthereumState()
63+
}
64+
65+
private fun initializeEthereumState() {
66+
coroutineScope.launch(Dispatchers.IO) {
67+
try {
68+
val account = storage.getValue(key = SessionManager.SESSION_ACCOUNT_KEY, file = SessionManager.SESSION_CONFIG_FILE)
69+
val chainId = storage.getValue(key = SessionManager.SESSION_CHAIN_ID_KEY, file = SessionManager.SESSION_CONFIG_FILE)
70+
_ethereumState.postValue(
71+
currentEthereumState.copy(
72+
selectedAddress = account ?: "",
73+
chainId = chainId ?: ""
74+
)
75+
)
76+
} catch (e: Exception) {
77+
logger.error(e.localizedMessage)
78+
}
79+
}
5480
}
5581

5682
fun enableDebug(enable: Boolean) = apply {
5783
this.enableDebug = enable
5884
}
5985

60-
private var sessionDuration: Long = DEFAULT_SESSION_DURATION
86+
private var sessionDuration: Long = SessionManager.DEFAULT_SESSION_DURATION
6187

6288
override fun updateAccount(account: String) {
6389
logger.log("Ethereum:: Selected account changed: $account")
@@ -67,6 +93,9 @@ class Ethereum (
6793
sessionId = communicationClient?.sessionId ?: ""
6894
)
6995
)
96+
if (account.isNotEmpty()) {
97+
storage.putValue(account, key = SessionManager.SESSION_ACCOUNT_KEY, SessionManager.SESSION_CONFIG_FILE)
98+
}
7099
}
71100

72101
override fun updateChainId(newChainId: String) {
@@ -77,17 +106,21 @@ class Ethereum (
77106
sessionId = communicationClient?.sessionId ?: ""
78107
)
79108
)
109+
if (newChainId.isNotEmpty()) {
110+
storage.putValue(newChainId, key = SessionManager.SESSION_CHAIN_ID_KEY, SessionManager.SESSION_CONFIG_FILE)
111+
}
80112
}
81113

82114
// Set session duration in seconds
83-
fun updateSessionDuration(duration: Long = DEFAULT_SESSION_DURATION) = apply {
115+
fun updateSessionDuration(duration: Long = SessionManager.DEFAULT_SESSION_DURATION) = apply {
84116
sessionDuration = duration
85117
communicationClient?.updateSessionDuration(duration)
86118
}
87119

88120
// Clear persisted session. Subsequent MetaMask connection request will need approval
89121
fun clearSession() {
90122
disconnect(true)
123+
storage.clear(SessionManager.SESSION_CONFIG_FILE)
91124
}
92125

93126
fun connect(callback: ((Result) -> Unit)? = null) {
@@ -308,7 +341,7 @@ class Ethereum (
308341
fun sendRequest(request: RpcRequest, callback: ((Result) -> Unit)? = null) {
309342
logger.log("Ethereum:: Sending request $request")
310343

311-
if (!connectRequestSent) {
344+
if (!connectRequestSent && selectedAddress.isEmpty()) {
312345
requestAccounts {
313346
sendRequest(request, callback)
314347
}

metamask-android-sdk/src/main/java/io/metamask/androidsdk/EthereumFlow.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,10 @@ constructor(
180180
ethereumRequest(method = EthereumMethod.SWITCH_ETHEREUM_CHAIN, params = listOf(mapOf("chainId" to targetChainId)))
181181

182182
override fun disconnect(clearSession: Boolean) {
183-
ethereum.disconnect(clearSession)
183+
if (clearSession) {
184+
ethereum.clearSession()
185+
} else {
186+
ethereum.disconnect()
187+
}
184188
}
185189
}

0 commit comments

Comments
 (0)