Skip to content
Open
Show file tree
Hide file tree
Changes from 67 commits
Commits
Show all changes
94 commits
Select commit Hold shift + click to select a range
f8ccfdb
test: add entry point for testing onboarding flow
lealobanov Oct 9, 2025
e9f8b25
feat: extending bridge for existing onboarding flow
lealobanov Oct 20, 2025
27b7188
pull from dev
lealobanov Oct 20, 2025
8a7649d
feat: onboarding ui
lealobanov Oct 21, 2025
f821df9
feat: onboarding ui
lealobanov Oct 21, 2025
0e4cb3f
feat: onboarding ui entrypoint
lealobanov Oct 21, 2025
991dea6
feat: onboarding ui entrypoint
lealobanov Oct 21, 2025
ab02a54
feat: bump wallet kit
lealobanov Oct 30, 2025
f36e286
feat(android): add saveMnemonic bridge method for EOA accounts
lealobanov Oct 30, 2025
0a58b2e
chore: remove unused bridge methods
lealobanov Oct 30, 2025
e06ee29
feat: add entrypoint for onboarding flow
lealobanov Nov 6, 2025
542ea8e
feat: add entrypoint for onboarding flow
lealobanov Nov 6, 2025
c1a9461
feat: add entrypoint for onboarding flow
lealobanov Nov 6, 2025
0225369
Merge dev branch and resolve conflicts
lealobanov Nov 6, 2025
9199ff1
feat: add entrypoint for onboarding flow
lealobanov Nov 6, 2025
a682720
feat: add entrypoint for onboarding flow
lealobanov Nov 6, 2025
7045625
feat: add entrypoint for onboarding flow
lealobanov Nov 6, 2025
f6dc181
feat: add entrypoint for onboarding flow
lealobanov Nov 10, 2025
b07d564
fix: add missing entry point and bridge methods
lealobanov Nov 10, 2025
68d7451
chore: pull from dev
lealobanov Nov 10, 2025
6baab43
fix: add missing entry point and bridge methods
lealobanov Nov 10, 2025
c7a8c25
fix: add missing entry point and bridge methods
lealobanov Nov 10, 2025
b2c29b7
fix: merge conflicts
lealobanov Nov 10, 2025
911d807
fix: feedback
lealobanov Nov 10, 2025
4cd3556
fix: feedback
lealobanov Nov 10, 2025
47bc372
fix: feedback
lealobanov Nov 10, 2025
c40407a
fix: feedback
lealobanov Nov 11, 2025
561e02f
fix: feedback
lealobanov Nov 11, 2025
a3d3fab
fix: save mnemonic
lealobanov Nov 11, 2025
acd4a61
fix: save mnemonic
lealobanov Nov 11, 2025
d25d9da
chore: clean up
lealobanov Nov 11, 2025
a4d8d7d
chore: clean up
lealobanov Nov 11, 2025
07686dd
chore: clean up
lealobanov Nov 11, 2025
5555213
chore: clean up
lealobanov Nov 11, 2025
c3ea40c
fix: feedback
lealobanov Nov 12, 2025
210d3d0
fix: feedback
lealobanov Nov 12, 2025
288683f
fix: feedback
lealobanov Nov 12, 2025
3b19ee4
fix: feedback
lealobanov Nov 12, 2025
0b4a973
fix: feedback
lealobanov Nov 12, 2025
33fdf27
fix: update method name
lealobanov Nov 13, 2025
7e5bba0
fix: remove mnemonic generation from Secure Enclave account creation
lealobanov Nov 13, 2025
1a32ead
fix: remove EOA chip for Secure Enclave/COA accounts
lealobanov Nov 13, 2025
8b739f7
feat: add createLinkedCOAAccount method for Recovery Phrase flow
lealobanov Nov 13, 2025
f14774e
feat: add bridge methods for keys
lealobanov Nov 13, 2025
bea92a0
feat: add bridge methods for keys
lealobanov Nov 13, 2025
dd23c28
feat: add bridge methods for keys
lealobanov Nov 13, 2025
4f20ae8
feat: remove cast to lowercase during register outblock call (#2121)
lealobanov Nov 13, 2025
cbb91ac
chore: refactoring + feedback
lealobanov Nov 14, 2025
ad2a01d
Merge branch 'onboarding-entrypoint' of https://github.com/onflow/FRW…
lealobanov Nov 14, 2025
ee760e6
chore: refactoring + feedback
lealobanov Nov 14, 2025
e5bdf98
chore: refactoring + feedback
lealobanov Nov 14, 2025
7f1001d
chore: refactoring + feedback
lealobanov Nov 17, 2025
7ca9a1d
chore: refactoring + feedback
lealobanov Nov 17, 2025
e4346e7
chore: refactoring + feedback
lealobanov Nov 17, 2025
1fb087e
chore: refactoring + feedback
lealobanov Nov 17, 2025
5809e17
chore: refactoring + feedback
lealobanov Nov 17, 2025
4544a82
chore: refactoring + feedback
lealobanov Nov 17, 2025
5cdab40
Merge dev branch - resolve conflicts in UserRegisterUtils
lealobanov Nov 17, 2025
74330ce
chore: regenerate BridgeModels with InitialRoute and SPResponse
lealobanov Nov 17, 2025
8594aae
chore: remove debug logs and comments
lealobanov Nov 17, 2025
4eaf0a3
fix: normalize public keys
lealobanov Nov 17, 2025
08d0100
fix: feedback
lealobanov Nov 19, 2025
a99ef81
chore: pull from dev
lealobanov Nov 20, 2025
b6e76db
fix: feedback
lealobanov Nov 21, 2025
319e60c
Merge branch 'dev' into onboarding-entrypoint
lealobanov Nov 24, 2025
6ea6d0b
fix: feedback - rename method
lealobanov Nov 24, 2025
c80b808
fix: feedback - rename method
lealobanov Nov 24, 2025
ae2adcf
refactor(bridge): rename linkCOAAccountOnChain to registerAccountWith…
lealobanov Nov 25, 2025
020aa4d
Merge branch 'dev' into onboarding-entrypoint
lealobanov Nov 25, 2025
abc3245
security(bridge): use in-memory storage for unconfirmed seed phrases
lealobanov Nov 25, 2025
774a3a3
fix: feedback
lealobanov Nov 25, 2025
6e290ad
fix: feedback
lealobanov Nov 25, 2025
bbf1127
Merge branch 'dev' of https://github.com/onflow/FRW-Android into onbo…
lealobanov Nov 26, 2025
45f14cd
feat(android): route Add Account and Create Profile to RN onboarding
lealobanov Nov 26, 2025
525b802
chore(android): remove unused WalletUnregisteredFragment
lealobanov Nov 26, 2025
d73cb9c
fix: feedback
lealobanov Nov 26, 2025
18ca7dc
fix(android): merge dev branch with proper two-button drawer layout
lealobanov Nov 26, 2025
05deb4a
fix(android): merge dev branch with proper two-button drawer layout
lealobanov Nov 26, 2025
639e57d
feat(android): connect drawer add profile button to RN onboarding
lealobanov Nov 26, 2025
0d91256
fix(android): resolve merge conflict - keep RN onboarding integration
lealobanov Nov 26, 2025
096919b
fix: navigate to ProfileTypeSelection instead of GetStarted
lealobanov Nov 26, 2025
a60adaa
fix(android): use launchWithRoute for existing user entry points
lealobanov Nov 26, 2025
0f69b66
fix: feedback
lealobanov Nov 27, 2025
7d37d3f
chore(android): remove auto-generated React Native assets from git
lealobanov Nov 27, 2025
7f36889
fix: register v3
lealobanov Nov 27, 2025
cd50ff3
Merge branch 'dev' into onboarding-entrypoint
lealobanov Nov 27, 2025
09dbc03
chore: restore FirebaseAuth.kt to dev branch version
lealobanov Nov 27, 2025
b5461e5
fix: register v3
lealobanov Nov 27, 2025
c5eafcd
fix: align with dev
lealobanov Nov 27, 2025
4eb1325
fix: align with dev
lealobanov Nov 27, 2025
6abb33a
fix: align with dev
lealobanov Nov 27, 2025
c1c0a6c
fix: align with dev
lealobanov Nov 27, 2025
10748f6
fix: align with dev
lealobanov Nov 27, 2025
628a13e
fix: fix the secure enclave flow
lealobanov Nov 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,9 @@ dependencies {
implementation('com.trustwallet:wallet-core:4.3.2') {
exclude group: 'com.google.protobuf', module: 'protobuf-java'
}
// Add wallet-core-proto separately to avoid conflicts
// Note: wallet-core-proto may require protobuf-java, so we allow it for this dependency only
implementation('com.trustwallet:wallet-core-proto:4.3.2')

// room https://developer.android.google.cn/training/data-storage/room?hl=zh-cn
implementation "androidx.room:room-runtime:2.6.1"
Expand Down
5 changes: 2 additions & 3 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,11 @@
<activity
android:name=".reactnative.ReactNativeActivity"
android:exported="false"
android:launchMode="standard"
android:launchMode="singleTop"
android:theme="@style/AppTheme"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode"
android:windowSoftInputMode="adjustResize"
android:excludeFromRecents="true"
android:noHistory="true"/>
android:excludeFromRecents="false"/>

<activity
android:name=".manager.drive.GoogleDriveAuthActivity"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,18 +146,33 @@ object AccountCacheManager{
}

private fun cacheSync(data: List<Account>) {
val str = Json.encodeToString(ListSerializer(Account.serializer()), data)

// Validate JSON before writing
try {
Json.decodeFromString(ListSerializer(Account.serializer()), str)
val str = Json.encodeToString(ListSerializer(Account.serializer()), data)

// Validate JSON before writing
try {
Json.decodeFromString(ListSerializer(Account.serializer()), str)
} catch (e: Exception) {
loge(TAG, "Generated invalid JSON, not writing to cache: $e")
return
}

str.saveToFile(file)
logd(TAG, "Successfully cached ${data.size} accounts")
} catch (e: Exception) {
loge(TAG, "Generated invalid JSON, not writing to cache: $e")
return
loge(TAG, "Error during cacheSync: $e")
loge(TAG, "Exception type: ${e.javaClass.name}")
e.printStackTrace()
// Log account structure for debugging
if (data.isNotEmpty()) {
val firstAccount = data.first()
loge(TAG, "First account structure: userInfo=${firstAccount.userInfo.javaClass.name}, " +
"wallet=${firstAccount.wallet?.javaClass?.name}, " +
"walletEmojiList=${firstAccount.walletEmojiList?.javaClass?.name}, " +
"evmAddressData=${firstAccount.evmAddressData?.javaClass?.name}")
}
throw e
}

str.saveToFile(file)
logd(TAG, "Successfully cached ${data.size} accounts")
}

fun clearCache() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.flowfoundation.wallet.firebase.messaging.getFirebaseMessagingToken
import com.flowfoundation.wallet.network.clearUserCache
import com.flowfoundation.wallet.utils.ioScope
import com.flowfoundation.wallet.utils.logd
import com.flowfoundation.wallet.utils.loge
import com.flowfoundation.wallet.utils.uiScope
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
Expand All @@ -29,14 +30,26 @@ fun firebaseCustomLogin(token: String, onComplete: FirebaseAuthCallback) {
logd(TAG, "=== firebaseCustomLogin START ===")
val auth = Firebase.auth
val currentUser = auth.currentUser
logd(TAG, "Current Firebase user: ${currentUser?.uid ?: "null"}")

if (currentUser != null) {
logd(TAG, "User already signed in, UID: ${currentUser.uid}, isAnonymous: ${currentUser.isAnonymous}")
onComplete.invoke(true, null)
logd(TAG, "Current Firebase user: ${currentUser?.uid ?: "null"}, isAnonymous: ${currentUser?.isAnonymous ?: false}")

// If user exists and is NOT anonymous, we need to sign out first to replace with custom token user
// If user is anonymous, we can proceed to sign in with custom token (Firebase will replace the user)
if (currentUser != null && !currentUser.isAnonymous) {
logd(TAG, "Non-anonymous user already signed in (UID: ${currentUser.uid}), signing out first...")
auth.signOut()
// Wait a moment for sign out to complete, then proceed
android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
signInWithCustomTokenInternal(token, onComplete)
}, 100)
return
}

// If anonymous or no user, proceed directly to sign in with custom token
signInWithCustomTokenInternal(token, onComplete)
}

private fun signInWithCustomTokenInternal(token: String, onComplete: FirebaseAuthCallback) {
val auth = Firebase.auth
logd(TAG, "Attempting to sign in with custom token (length: ${token.length})")
auth.signInWithCustomToken(token).addOnCompleteListener { task ->
logd(TAG, "signInWithCustomToken completed - success: ${task.isSuccessful}")
Expand All @@ -52,7 +65,12 @@ fun firebaseCustomLogin(token: String, onComplete: FirebaseAuthCallback) {
logd(TAG, "Requesting ID token refresh")

newUser?.getIdToken(true)?.addOnSuccessListener { result ->
logd(TAG, "ID token obtained successfully")
val token = result.token
if (token != null) {
logd(TAG, "ID token obtained successfully (length: ${token.length})")
} else {
logd(TAG, "ID token obtained but token is null")
}
uiScope {
onComplete.invoke(true, null)
}
Expand All @@ -77,23 +95,102 @@ fun firebaseCustomLogin(token: String, onComplete: FirebaseAuthCallback) {
fun firebaseUid() = Firebase.auth.currentUser?.uid

suspend fun getFirebaseJwt(forceRefresh: Boolean = false) = suspendCoroutine { continuation ->
ioScope {
val auth = Firebase.auth
if (auth.currentUser == null) {
signInAnonymously()
}

val user = auth.currentUser
if (user == null) {
continuation.resume("")
return@ioScope
}

val auth = Firebase.auth
val user = auth.currentUser

if (user != null) {
// User exists, get ID token from existing user
logd(TAG, "User exists, getting ID token: ${user.uid}, isAnonymous: ${user.isAnonymous}")
user.getIdToken(forceRefresh).addOnCompleteListener { task ->
if (task.isSuccessful) {
continuation.resume(task.result.token.orEmpty())
if (task.isSuccessful && task.result != null) {
val token = task.result.token
if (token.isNullOrEmpty()) {
loge(TAG, "ID token is null or empty")
continuation.resume("")
} else {
logd(TAG, "ID token obtained successfully (length: ${token.length})")
continuation.resume(token)
}
} else {
val exception = task.exception
val errorMessage = exception?.message ?: "Unknown error"
loge(TAG, "Failed to get ID token: $errorMessage")

// Check if it's a network error
if (errorMessage.contains("network", ignoreCase = true) ||
errorMessage.contains("unreachable", ignoreCase = true) ||
errorMessage.contains("timeout", ignoreCase = true)) {
loge(TAG, "Network error detected - Firebase services may be unreachable")
}

continuation.resume("")
}
}
} else {
// No user exists, sign in anonymously first (matching extension behavior)
logd(TAG, "No Firebase user found, signing in anonymously...")
auth.signInAnonymously().addOnCompleteListener { signInTask ->
if (!signInTask.isSuccessful) {
val exception = signInTask.exception
val errorMessage = exception?.message ?: "Unknown error"
loge(TAG, "Failed to sign in anonymously: $errorMessage")

// Check if it's a network error
val isNetworkError = errorMessage.contains("network", ignoreCase = true) ||
errorMessage.contains("unreachable", ignoreCase = true) ||
errorMessage.contains("timeout", ignoreCase = true) ||
errorMessage.contains("No address associated", ignoreCase = true) ||
errorMessage.contains("Unable to resolve host", ignoreCase = true) ||
exception?.javaClass?.simpleName?.contains("Network", ignoreCase = true) == true

if (isNetworkError) {
val detailedError = "Network error preventing anonymous sign-in - Firebase services unreachable. " +
"Error: $errorMessage. " +
"Troubleshooting: " +
"1. Check device/emulator internet connectivity " +
"2. Verify Firebase configuration (google-services.json) " +
"3. For emulators, ensure DNS is configured (use 8.8.8.8) " +
"4. Check firewall/proxy settings"
loge(TAG, detailedError)
}

continuation.resume("")
return@addOnCompleteListener
}

// Get user from task result (more reliable than auth.currentUser)
val anonymousUser = signInTask.result?.user ?: auth.currentUser
if (anonymousUser == null) {
loge(TAG, "Sign in succeeded but user is null")
continuation.resume("")
return@addOnCompleteListener
}

logd(TAG, "Anonymous sign-in successful, user: ${anonymousUser.uid}")
anonymousUser.getIdToken(forceRefresh).addOnCompleteListener { tokenTask ->
if (tokenTask.isSuccessful && tokenTask.result != null) {
val token = tokenTask.result.token
if (token.isNullOrEmpty()) {
loge(TAG, "Anonymous user ID token is null or empty")
continuation.resume("")
} else {
logd(TAG, "Anonymous user ID token obtained successfully (length: ${token.length})")
continuation.resume(token)
}
} else {
val exception = tokenTask.exception
val errorMessage = exception?.message ?: "Unknown error"
loge(TAG, "Failed to get ID token from anonymous user: $errorMessage")

// Check if it's a network error
if (errorMessage.contains("network", ignoreCase = true) ||
errorMessage.contains("unreachable", ignoreCase = true) ||
errorMessage.contains("timeout", ignoreCase = true)) {
loge(TAG, "Network error preventing token retrieval - Firebase services unreachable")
}

continuation.resume("")
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ fun Account.getFlowAddress(networkName: String, logTag: String = "AccountExtensi
walletData.blockchain?.asSequence() ?: emptySequence()
}
?.find { blockchainData ->
val matches = blockchainData.chainId == networkName

// Compare case-insensitively since chainId from backend might be "mainnet" but networkName might be "Mainnet"
val matches = blockchainData.chainId?.lowercase() == networkName.lowercase()
matches
}?.address
logd(logTag, " Returning address: $foundAddress for ${this.userInfo.username} on $networkName")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package com.flowfoundation.wallet.manager.account

import android.content.Intent
import android.widget.Toast
import com.google.gson.annotations.SerializedName
import com.flowfoundation.wallet.R
import com.flowfoundation.wallet.cache.AccountCacheManager
import com.flowfoundation.wallet.cache.UserPrefixCacheManager
Expand Down Expand Up @@ -43,8 +42,11 @@ import com.flowfoundation.wallet.wallet.Wallet
import com.google.firebase.auth.ktx.auth
import com.google.firebase.ktx.Firebase
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerialName
import kotlinx.serialization.json.Json
import kotlinx.serialization.encodeToString
import kotlinx.serialization.decodeFromString
import java.lang.ref.WeakReference
import java.util.concurrent.CopyOnWriteArrayList
import com.flowfoundation.wallet.manager.wallet.walletAddress
Expand Down Expand Up @@ -692,7 +694,7 @@ object AccountManager {
val passwordMap = getPasswordMap()
if (passwordMap.containsKey(accountId)) {
passwordMap.remove(accountId)
storeWalletPassword(Gson().toJson(passwordMap))
storeWalletPassword(Json.encodeToString(passwordMap))
logd(TAG, "Successfully removed password for account ID: $accountId")
} else {
logd(TAG, "No password found for account ID: $accountId")
Expand All @@ -705,8 +707,7 @@ object AccountManager {
HashMap()
} else {
runCatching {
Gson().fromJson(pref, object : TypeToken<HashMap<String, String>>() {}.type)
?: HashMap<String, String>()
Json.decodeFromString<Map<String, String>>(pref).toMap(HashMap())
}.getOrElse { HashMap() }
}
}
Expand Down Expand Up @@ -830,27 +831,19 @@ fun username() = AccountManager.get()!!.userInfo.username

@Serializable
data class Account(
@SerializedName("username")
@SerialName("username")
var userInfo: UserInfoData,
@SerializedName("isActive")
var isActive: Boolean = false,
@SerializedName("wallet")
var wallet: WalletListData? = null,
@SerializedName("prefix")
var prefix: String? = null,
@SerializedName("evmAddressData")
var evmAddressData: EVMAddressData? = null,
@SerializedName("walletEmojiList")
var walletEmojiList: List<WalletEmojiInfo>? = null,
@SerializedName("keyStoreInfo")
var keyStoreInfo: String? = null
)

@Serializable
data class UserPrefix(
@SerializedName("userId")
val userId: String,
@SerializedName("prefix")
var prefix: String
)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
package com.flowfoundation.wallet.manager.emoji.model

import com.google.gson.annotations.SerializedName
import kotlinx.serialization.Serializable

@Serializable
data class WalletEmojiInfo(
@SerializedName("address")
val address: String,
@SerializedName("emojiId")
val emojiId: Int,
@SerializedName("emojiName")
val emojiName: String
)
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import com.flowfoundation.wallet.wallet.toAddress
import com.google.gson.annotations.SerializedName
import org.onflow.flow.models.TransactionStatus
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerialName
import org.onflow.flow.ChainId
import org.web3j.crypto.Keys
import java.math.BigDecimal
Expand Down Expand Up @@ -118,7 +119,13 @@ object EVMWalletManager {
}

evmAddressMap[networkAddress] = formattedAddress
AccountManager.updateEVMAddressInfo(evmAddressMap.toMutableMap())

// Preserve existing entries from account's evmAddressData (e.g., EOA address with empty string key)
// Merge with internal evmAddressMap to avoid overwriting EOA addresses
val existingMap = AccountManager.evmAddressData()?.evmAddressMap?.toMutableMap() ?: mutableMapOf()
existingMap.putAll(evmAddressMap)
AccountManager.updateEVMAddressInfo(existingMap)

callback?.invoke(true)
} else {
ErrorReporter.reportWithMixpanel(EVMError.QUERY_EVM_ADDRESS_FAILED, getCurrentCodeLocation())
Expand Down Expand Up @@ -658,6 +665,7 @@ object EVMWalletManager {

@Serializable
data class EVMAddressData(
@SerialName("evmAddressMap")
@SerializedName("evmAddressMap")
var evmAddressMap: Map<String, String>? = null
)
Loading