Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
824ffe1
feat(key-storage): add independent key storage decoupled from Account
jaymengxy Mar 3, 2026
9ccc4f3
fix: switch current account set
jaymengxy Mar 2, 2026
62f1ba0
fix: firebase login after delete
jaymengxy Mar 4, 2026
9d95e06
fix: navigation bar padding & orientation
jaymengxy Mar 4, 2026
c1b8391
fix: key rotaion and migration after key storage migration & switch l…
jaymengxy Mar 5, 2026
734fb9a
feat: upgrade instabug
jaymengxy Mar 8, 2026
1682eff
fix: loading shimmer and hide coa switch view
jaymengxy Mar 8, 2026
9262a5c
fix: remove custom login delay
jaymengxy Mar 9, 2026
0067b60
fix: switch jwt force refresh
jaymengxy Mar 10, 2026
9ab8ccb
feat: add new account & firebase login fix
jaymengxy Mar 11, 2026
53f3a32
fix: add new account with 0 balance
jaymengxy Mar 11, 2026
2514979
fix: create wallet with eoa status
jaymengxy Mar 11, 2026
f766b5a
fix: luciq http interceptor
jaymengxy Mar 12, 2026
96476c9
Merge pull request #2830 from onflow/fix_onboarding_firebase_login
jaymengxy Mar 12, 2026
4be1611
feat: inbox entrance
jaymengxy Mar 16, 2026
8f48815
Merge pull request #2874 from onflow/feature/inbox
jaymengxy Mar 16, 2026
43dca5f
fix: screen type
jaymengxy Mar 16, 2026
3719f22
fix: display token after claim
jaymengxy Mar 17, 2026
7b21aff
feat: add new eoa account
jaymengxy Mar 18, 2026
5564bdf
feat: add inbox feature flag
jaymengxy Mar 18, 2026
92a5536
fix: eoa address management
jaymengxy Mar 19, 2026
012a14c
Merge pull request #2887 from onflow/feature/add_eoa_account
jaymengxy Mar 19, 2026
c4d8215
fix: add new account dialog ui
jaymengxy Mar 19, 2026
9a99273
fix: multi flow wallet address check
jaymengxy Mar 20, 2026
8cb3156
fix: eoa sign message with index
jaymengxy Mar 20, 2026
6faf0ce
fix: coa wallet display and emoji cache
jaymengxy Mar 20, 2026
cfa82f7
fix: account bridge & eth sign with address
jaymengxy Mar 24, 2026
44a1f05
fix: coa display in profile switch
jaymengxy Mar 25, 2026
7de765c
fix: currently account hidden status
jaymengxy Mar 25, 2026
148c439
feat: replace flowscan with backend api
jaymengxy Mar 31, 2026
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
8 changes: 4 additions & 4 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ plugins {
id "org.jetbrains.kotlin.plugin.serialization" version "1.9.0"
id 'com.google.gms.google-services'
id 'com.google.firebase.crashlytics'
// id("instabug-apm") // Disabled to fix ASM instrumentation issue
// id("luciq-apm") // Disabled to fix ASM instrumentation issue
id 'com.google.devtools.ksp'
id 'com.google.firebase.appdistribution'
// id 'jacoco'
Expand Down Expand Up @@ -352,7 +352,7 @@ dependencies {
api fileTree(dir: "libs", include: ["*.aar", "*.jar"], exclude: ["trustwalletcore.aar"])

// Fetch Flow Wallet Kit from GitHub Packages
implementation 'com.github.onflow.flow-wallet-kit:flow-wallet-android:0.2.3'
implementation 'com.github.onflow.flow-wallet-kit:flow-wallet-android:0.2.6'
implementation "org.jetbrains.kotlin:kotlin-stdlib:1.9.22"
/** Android Architecture **/
implementation 'androidx.core:core-ktx:1.13.1'
Expand Down Expand Up @@ -462,8 +462,8 @@ dependencies {
implementation 'com.google.firebase:firebase-appcheck-debug'

/** Instabug **/
implementation 'com.instabug.library:instabug:15.0.1'
implementation 'com.instabug.library:instabug-with-okhttp-interceptor:15.0.1'
implementation 'ai.luciq.library:luciq:19.3.0'
implementation 'ai.luciq.library:luciq-with-okhttp-interceptor:19.3.0'
/** firebase **/

implementation "dev.chrisbanes:insetter-ktx:0.3.1"
Expand Down
14 changes: 12 additions & 2 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
android:name=".reactnative.ReactNativeActivity"
android:exported="false"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:theme="@style/AppTheme"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode"
android:windowSoftInputMode="adjustResize"
Expand All @@ -84,21 +85,25 @@
<activity
android:name=".manager.drive.GoogleDriveAuthActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:theme="@style/AppTheme.Transparent.FullScreen"/>

<activity
android:name=".manager.dropbox.DropboxAuthActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:theme="@style/AppTheme.Transparent.FullScreen"/>

<activity
android:name=".page.walletrestore.WalletRestoreActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustResize"/>

<activity
android:name=".page.wallet.sync.WalletSyncActivity"
android:launchMode="singleTop"/>
android:launchMode="singleTop"
android:screenOrientation="portrait"/>

<activity
android:name=".page.wallet.confirm.WalletConfirmActivity"
Expand All @@ -111,7 +116,8 @@
android:name=".page.collection.CollectionActivity"
android:screenOrientation="portrait"/>
<activity
android:name=".page.nft.search.NFTSearchActivity"/>
android:name=".page.nft.search.NFTSearchActivity"
android:screenOrientation="portrait"/>

<activity
android:name=".page.profile.subpage.accountsetting.AccountSettingActivity"
Expand Down Expand Up @@ -277,11 +283,13 @@
<activity
android:name=".page.browser.subpage.filepicker.FilePickerActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:theme="@style/AppTheme.Transparent.FullScreen"/>

<activity
android:name=".page.security.biometric.BiometricActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:theme="@style/AppTheme.Transparent.FullScreen"/>

<activity
Expand Down Expand Up @@ -336,6 +344,7 @@
android:name=".page.component.deeplinking.DeepLinkingActivity"
android:exported="true"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:theme="@style/AppTheme.Transparent.FullScreen">
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW"/>
Expand Down Expand Up @@ -412,6 +421,7 @@
<activity
android:name=".page.common.NotificationDispatchActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:theme="@style/AppTheme.Transparent.FullScreen"/>

<activity
Expand Down
996 changes: 492 additions & 504 deletions app/src/main/assets/config/cadence_api.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
package com.flowfoundation.wallet.base.activity

import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.BaseContextWrappingDelegate
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
import com.flowfoundation.wallet.manager.app.ActivityManager
import java.lang.ref.WeakReference

Expand All @@ -11,10 +16,60 @@ open class BaseActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
currentActivity = WeakReference(this)
ActivityManager.setCurrentActivity(this) // Register with ActivityManager
ActivityManager.setCurrentActivity(this)
// Opt into edge-to-edge early. On API 35+ this is enforced by the system anyway.
// On API 28-34 this is needed so our insets listener is the sole source of nav bar padding.
WindowCompat.setDecorFitsSystemWindows(window, false)
super.onCreate(savedInstanceState)
}

override fun onPostCreate(savedInstanceState: Bundle?) {
super.onPostCreate(savedInstanceState)
// UltimateBarX fitWindow(true) calls in subclass onCreate may set
// setDecorFitsSystemWindows(true) after our onCreate. Reset it here so our
// insets listener is always in control of navigation bar padding.
WindowCompat.setDecorFitsSystemWindows(window, false)
applyNavigationBarInsets()
}

/**
* Override to return true when the activity/layout handles nav bar insets itself
* (e.g., via a direct ViewCompat.setOnApplyWindowInsetsListener on the root view).
* BaseActivity will skip its android.R.id.content padding to prevent double-padding.
*/
protected open fun handlesInsetsNatively(): Boolean = false

/**
* Adds bottom padding equal to the navigation bar height on android.R.id.content.
* Listener is attached to window.decorView — the top of the view hierarchy — so it
* receives insets before AppCompat's internal FitWindowsLinearLayout can consume them.
* Works for both 3-button nav (full bar height) and gesture nav (indicator height or 0).
* Status bar is intentionally excluded — handled by existing UltimateBarX calls per Activity.
*/
private fun applyNavigationBarInsets() {
if (handlesInsetsNatively()) return
val contentView = window.decorView.findViewById<View>(android.R.id.content) ?: return
ViewCompat.setOnApplyWindowInsetsListener(window.decorView) { _, insets ->
val navBar = insets.getInsets(WindowInsetsCompat.Type.navigationBars())
contentView.updatePadding(bottom = navBar.bottom)
insets // Return without consuming — let normal child dispatch continue
}
// Trigger an immediate insets dispatch now that the listener is set up and
// setDecorFitsSystemWindows(false) is in its final state.
ViewCompat.requestApplyInsets(window.decorView)
}

override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
// Re-trigger insets dispatch once the window is actually visible and has valid insets.
// requestApplyInsets() in onPostCreate fires before the window is shown (pre-WindowManager
// attach), so it may receive zero insets. Activities without UltimateBarX have no other
// insets trigger, so this guarantees the listener fires at least once with real values.
if (hasFocus) {
ViewCompat.requestApplyInsets(window.decorView)
}
}

override fun getDelegate() = BaseContextWrappingDelegate(super.getDelegate())

override fun onResume() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@ import com.flowfoundation.wallet.manager.account.Account
import com.flowfoundation.wallet.manager.account.AccountManager.walletNodes
import com.flowfoundation.wallet.manager.account.AccountWalletManager
import com.flowfoundation.wallet.manager.app.chainNetWorkString
import com.flowfoundation.wallet.manager.key.storage.KeyStorageManager
import com.flowfoundation.wallet.manager.walletdata.FlowWallet
import com.flowfoundation.wallet.utils.*
import com.flowfoundation.wallet.utils.error.AccountError
import com.flowfoundation.wallet.utils.error.ErrorReporter
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.json.Json
import java.io.File
Expand All @@ -18,6 +21,7 @@ object AccountCacheManager{
private val TAG = AccountCacheManager::class.java.simpleName
private val file by lazy { File(ACCOUNT_PATH, "${"accounts".hashCode()}") }
private val backupFile by lazy { File(ACCOUNT_PATH, "${"accounts_backup".hashCode()}") }
private val writeMutex = Mutex()

@WorkerThread
fun read(): List<Account>? {
Expand All @@ -29,11 +33,13 @@ object AccountCacheManager{
logd(TAG, "Successfully read from primary cache: ${primaryResult.size} accounts")
// Update backup if primary is good
ioScope {
try {
backupFile.writeText(file.readText())
logd(TAG, "Updated backup from validated primary cache")
} catch (e: Exception) {
loge(TAG, "Failed to update backup: $e")
writeMutex.withLock {
try {
backupFile.writeText(file.readText())
logd(TAG, "Updated backup from validated primary cache")
} catch (e: Exception) {
loge(TAG, "Failed to update backup: $e")
}
}
}
return primaryResult
Expand All @@ -44,13 +50,15 @@ object AccountCacheManager{
val backupResult = readFromFile(backupFile)
if (backupResult != null) {
logd(TAG, "Successfully recovered from backup cache: ${backupResult.size} accounts")
// Restore primary from backup
// Restore primary from backup — must go through mutex to avoid racing with cache()
ioScope {
try {
file.writeText(backupFile.readText())
logd(TAG, "Restored primary from repaired backup cache")
} catch (e: Exception) {
loge(TAG, "Failed to restore primary from backup: $e")
writeMutex.withLock {
try {
backupFile.copyTo(file, overwrite = true)
logd(TAG, "Restored primary from repaired backup cache")
} catch (e: Exception) {
loge(TAG, "Failed to restore primary from backup: $e")
}
}
}
return backupResult
Expand All @@ -66,12 +74,9 @@ object AccountCacheManager{
return null
}

var str = cacheFile.read()
val str = cacheFile.read()
logd(TAG, "Reading from ${cacheFile.name}: ${str.length} characters, isBlank=${str.isBlank()}")

// Migrate old field names to new format
str = migrateOldFieldNames(str)

if (str.isBlank()) {
logd(TAG, "Warning: Cache file ${cacheFile.name} exists but is empty")
return null
Expand All @@ -91,9 +96,14 @@ object AccountCacheManager{

// Validate account data
val validAccounts = result.filter { account ->
val uid = account.wallet?.id ?: ""
val isValid = account.userInfo.username.isNotBlank() &&
(!account.keyStoreInfo.isNullOrBlank() || !account.prefix
.isNullOrBlank() || AccountWalletManager.isHDWallet(account.wallet?.id ?: ""))
(!account.keyStoreInfo.isNullOrBlank() ||
!account.prefix.isNullOrBlank() ||
AccountWalletManager.isHDWallet(uid) ||
KeyStorageManager.hasSeedPhrase(uid) ||
KeyStorageManager.hasPrivateKey(uid) ||
KeyStorageManager.hasAndroidKeystorePrefix(uid))
if (!isValid) {
logd(TAG, "Invalid account found: ${account.userInfo.username}")
}
Expand Down Expand Up @@ -127,61 +137,37 @@ object AccountCacheManager{
}

fun cache(data: List<Account>) {
logd(TAG, "cache() called with ${data.size} accounts")
logd(TAG, "cache() called with accounts: $data")
if (data.isEmpty()) {
logd(TAG, "Warning: Caching empty accounts list")
} else {
logd(TAG, "Caching accounts with usernames: ${data.map { it.userInfo.username }}")
}
logd(TAG, "cache() called with ${data.size} accounts: ${data.map { it.userInfo.username }}")
ioScope {
try {
cacheSync(data)
// Create backup copy only after verifying main file integrity
if (file.exists() && file.length() > 0) {
// Verify the written data is valid before creating backup
writeMutex.withLock {
try {
cacheSync(data)
// Create backup copy only after verifying main file integrity
val writtenData = readFromFile(file)
if (writtenData != null && writtenData.size == data.size) {
backupFile.writeText(file.readText())
logd(TAG, "Created backup copy of validated account cache")
} else {
loge(TAG, "Main cache validation failed, not creating backup. Expected: ${data.size}, Got: ${writtenData?.size}")
}
} catch (e: Exception) {
loge(TAG, "Error caching accounts: $e")
}
} catch (e: Exception) {
loge(TAG, "Error caching accounts: $e")
}
}
}

private fun cacheSync(data: List<Account>) {
try {
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
}

val str = Json.encodeToString(ListSerializer(Account.serializer()), data)
// Atomic write: write to a temp file first, then rename to avoid partial writes
val tempFile = File(ACCOUNT_PATH, "${"accounts".hashCode()}.tmp")
tempFile.writeText(str)
if (!tempFile.renameTo(file)) {
// renameTo can fail across filesystems; fall back to direct write
tempFile.delete()
str.saveToFile(file)
logd(TAG, "Successfully cached ${data.size} accounts")
} catch (e: Exception) {
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}, " +
"walletNodes=${firstAccount.walletNodes.javaClass.name}")
}
throw e
}
logd(TAG, "Successfully cached ${data.size} accounts")
}

fun clearCache() {
Expand All @@ -196,29 +182,4 @@ object AccountCacheManager{
}
}

/**
* Migrates old field names in cached JSON to new format.
* This handles the transition from old cache format to new serialization format.
*
* Field mappings:
* - "isPrivate" -> "private" (UserInfoData field name change)
* - "chainId" -> "chain_id" (BlockchainData field name change)
*/
private fun migrateOldFieldNames(json: String): String {
var migrated = json

// Migrate UserInfoData.isPrivate to private
if (migrated.contains("\"isPrivate\":")) {
logd(TAG, "Migrating old field name: isPrivate -> private")
migrated = migrated.replace("\"isPrivate\":", "\"private\":")
}

// Migrate BlockchainData.chainId to chain_id
if (migrated.contains("\"chainId\":")) {
logd(TAG, "Migrating old field name: chainId -> chain_id")
migrated = migrated.replace("\"chainId\":", "\"chain_id\":")
}

return migrated
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.flowfoundation.wallet.firebase

import android.app.Application
import com.google.firebase.BuildConfig
import com.flowfoundation.wallet.BuildConfig
import com.google.firebase.FirebaseApp
import com.google.firebase.appcheck.FirebaseAppCheck
import com.google.firebase.appcheck.debug.DebugAppCheckProviderFactory
Expand Down Expand Up @@ -32,4 +32,4 @@ private fun setupAppCheck() {
logd(TAG, "AppCheck token: $token")
}
}
}
}
Loading
Loading