Skip to content

Commit 774e7fb

Browse files
authored
Merge branch 'code/cash' into dependabot/gradle/code/cash/ksp-2.3.6
2 parents eacee5e + b4be216 commit 774e7fb

27 files changed

Lines changed: 333 additions & 368 deletions

File tree

.github/workflows/ci.yml

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,19 +38,16 @@ jobs:
3838
with:
3939
fileName: google-services.json
4040
fileDir: ./apps/flipcash/app/src
41-
encodedString: ${{ secrets.FLIPCASH_GOOGLE_SERVICES_JSON }}
41+
encodedString: ${{ secrets.FLIPCASH2_GOOGLE_SERVICES_JSON }}
4242

4343
- name: Setup BugSnag API Key
4444
run: echo BUGSNAG_API_KEY=\"${{ secrets.FLIPCASH_BUGSNAG_API_KEY }}\" > ./local.properties
45-
46-
- name: Setup Fingerprint API Key
47-
run: echo FINGERPRINT_API_KEY=${{ secrets.FINGERPRINT_API_KEY }} >> ./local.properties
48-
45+
4946
- name: Setup Google Cloud Project Number
5047
run: echo GOOGLE_CLOUD_PROJECT_NUMBER=${{ secrets.GOOGLE_CLOUD_PROJECT_NUMBER }} >> ./local.properties
5148

5249
- name: Setup Mixpanel API Key
5350
run: echo MIXPANEL_API_KEY=\"${{ secrets.FLIPCASH_MIXPANEL_API_KEY }}\" >> ./local.properties
5451

55-
- name: Run Code tests
52+
- name: Run Flipcash tests
5653
run: bundle exec fastlane android flipcash_tests

CLAUDE.md

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
Code/Flipcash is a mobile wallet app for instant, global, private payments using self-custodial blockchain (Solana/Kin) technology. The Android app is a multi-module Gradle project with 100+ modules.
8+
9+
## Build Commands
10+
11+
```bash
12+
# Build debug APK
13+
./gradlew :apps:flipcash:app:assembleDebug
14+
15+
# Build release bundle
16+
./gradlew :apps:flipcash:app:bundleRelease
17+
18+
# Run unit tests (all modules)
19+
./gradlew test
20+
21+
# Run unit tests for a specific module
22+
./gradlew :apps:flipcash:features:<feature>:test
23+
24+
# Run instrumented tests
25+
./gradlew connectedAndroidTest
26+
27+
# Run tests via Fastlane (used in CI)
28+
bundle exec fastlane android flipcash_tests
29+
```
30+
31+
**Requirements**: Java 21 (Corretto), `google-services.json` in `apps/flipcash/app/src/`, API keys in `local.properties` (BUGSNAG_API_KEY, FINGERPRINT_API_KEY, GOOGLE_CLOUD_PROJECT_NUMBER, MIXPANEL_API_KEY).
32+
33+
## Module Structure
34+
35+
```
36+
apps/flipcash/
37+
app/ — Main application entry point (FlipcashApp, MainActivity)
38+
core/ — Base Compose utilities, payment models, billing state, cache policies
39+
features/ — 20+ isolated feature modules (login, cash, balance, etc.)
40+
shared/ — 30+ shared modules (auth, tokens, payments, notifications, etc.)
41+
42+
services/
43+
flipcash/ — Flipcash gRPC services and models
44+
opencode/ — Open Code Protocol gRPC services
45+
*-compose/ — Compose wrappers for services
46+
47+
definitions/
48+
flipcash/ — Protobuf definitions for Flipcash
49+
opencode/ — Protobuf definitions for OCP
50+
51+
libs/ — 20+ internal libraries
52+
crypto/ — Solana, Kin, Ed25519, encryption, key management
53+
network/ — Connectivity, JWT, exchange rates, Coinbase
54+
models, messaging, logging, etc.
55+
56+
ui/ — Shared UI layer
57+
core/, components/, theme/, resources/
58+
navigation/, scanner/, biometrics/, analytics/
59+
60+
vendor/ — Third-party: Kik scanner, TipKit, OpenCV
61+
build-logic/ — Convention plugins for consistent module setup
62+
```
63+
64+
## Convention Plugins (build-logic)
65+
66+
All modules use convention plugins applied via `build.gradle.kts`:
67+
- `flipcash.android.library` — Base Android library config (compile SDK 36, min SDK 29, Java 21)
68+
- `flipcash.android.library.compose` — Adds Jetpack Compose support
69+
- `flipcash.android.feature` — Full feature module: Compose + Hilt + KSP + Parcelize + common UI dependencies
70+
71+
The feature plugin automatically includes `:libs:logging`, `:ui:core`, `:ui:components`, `:ui:navigation`, `:ui:resources`, `:ui:theme`, and `:apps:flipcash:core`.
72+
73+
## Architecture
74+
75+
- **Pattern**: MVI/MVVM hybrid with Compose-driven UI and reactive state
76+
- **DI**: Hilt — all feature modules get Hilt via the convention plugin
77+
- **Navigation**: Jetpack Navigation + Voyager for compose-based screens; custom `Router` controller
78+
- **Networking**: gRPC with Protobuf for backend services; Retrofit/OkHttp for REST
79+
- **Async**: Kotlin Coroutines + RxJava 3 (both coexist)
80+
- **Persistence**: Room (encrypted with SQLCipher), DataStore for preferences
81+
- **Crypto**: Libsodium, Ed25519, Solana/Kin SDK for on-chain operations
82+
83+
## Key Patterns
84+
85+
- **CompositionLocal injection**: `MainActivity` provides dozens of controllers/services via `CompositionLocalProvider` — features access dependencies through `Local*` composition locals rather than direct injection
86+
- **Feature modules are self-contained**: Each has its own state, controllers, and UI; communicates via shared modules
87+
- **Protobuf models**: Backend models are generated from `.proto` files in `definitions/`; don't hand-edit generated code
88+
- **Dark mode only**: App forces `MODE_NIGHT_YES`
89+
90+
## Namespaces
91+
92+
- App: `com.flipcash.app.android` (debug: `com.flipcash.app.android.dev`)
93+
- Legacy/shared: `com.getcode`
94+
- Features: `com.flipcash.features.*`
95+
- Shared modules: `com.flipcash.shared.*`
96+
- UI: `com.getcode.ui.core`
97+
98+
## Git Conventions
99+
100+
- Conventional commits: `feat:`, `fix:`, `chore:`, with optional scope in parens (e.g., `feat(oc):`, `fix(tokens):`)
101+
- Main branch: `main`
102+
- CI runs on all PRs (tests via Fastlane)

apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/navigation/AppScreenContent.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ internal fun AppScreenContent(content: @Composable () -> Unit) {
7777
}
7878

7979
register<AppRoute.Main.Give> {
80-
CashScreen(it.mint)
80+
CashScreen(it.mint, it.fromTokenInfo)
8181
}
8282

8383
register<AppRoute.Token.Info> {

apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/AppRoute.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ sealed interface AppRoute : ScreenProvider, Parcelable {
4141
// TODO: is there a better place for this to live?
4242
data class RegionSelection(val kind: RegionSelectionKind) : Main
4343

44-
data class Give(val mint: Mint? = null) : Main
44+
data class Give(val mint: Mint? = null, val fromTokenInfo: Boolean = false) : Main
4545
}
4646

4747
@Parcelize

apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/ui/TokenImageWithName.kt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,7 @@ fun TokenIcon(
9898
.data(imageUrl)
9999
.crossfade(false)
100100
.error(R.drawable.ic_placeholder_user)
101-
.memoryCacheKey(symbol)
102101
.memoryCachePolicy(CachePolicy.ENABLED)
103-
.diskCacheKey(symbol)
104102
.diskCachePolicy(CachePolicy.ENABLED)
105103
.build(),
106104
contentDescription = null,

apps/flipcash/features/cash/src/main/kotlin/com/flipcash/app/cash/CashScreen.kt

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ import kotlinx.parcelize.Parcelize
3434

3535
@Parcelize
3636
class CashScreen(
37-
private val selectedMint: Mint?
37+
private val selectedMint: Mint?,
38+
private val fromTokenInfo: Boolean,
3839
) : ModalScreen, Parcelable {
3940

4041
@IgnoredOnParcel
@@ -80,8 +81,15 @@ class CashScreen(
8081
)
8182
}
8283
},
84+
leftIcon = {
85+
if (fromTokenInfo) {
86+
AppBarDefaults.UpNavigation { navigator.pop() }
87+
}
88+
},
8389
rightContents = {
84-
AppBarDefaults.Close { navigator.hide() }
90+
if (!fromTokenInfo) {
91+
AppBarDefaults.Close { navigator.hide() }
92+
}
8593
}
8694
)
8795
GiveScreenContent(viewModel)

apps/flipcash/features/deposit/src/main/kotlin/com/flipcash/app/deposit/internal/DepositViewModel.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import com.flipcash.app.core.extensions.setText
77
import com.flipcash.features.deposit.R
88
import com.flipcash.services.user.UserManager
99
import com.getcode.manager.BottomBarManager
10-
import com.getcode.opencode.controllers.TokenController
10+
import com.getcode.opencode.providers.TokenMetadataProvider
1111
import com.getcode.solana.keys.Mint
1212
import com.getcode.solana.keys.base58
1313
import com.getcode.util.resources.ResourceHelper
@@ -24,7 +24,7 @@ import kotlin.time.Duration.Companion.seconds
2424
@HiltViewModel
2525
internal class DepositViewModel @Inject constructor(
2626
userManager: UserManager,
27-
tokenController: TokenController,
27+
tokenController: TokenMetadataProvider,
2828
clipboardManager: ClipboardManager,
2929
resources: ResourceHelper,
3030
) : BaseViewModel2<DepositViewModel.State, DepositViewModel.Event>(

apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/internal/TokenInfoScreen.kt

Lines changed: 32 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -282,46 +282,44 @@ private fun BottomBarButtons(
282282
verticalAlignment = Alignment.CenterVertically,
283283
horizontalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x2),
284284
) {
285-
if (!state.isCashReserve) {
286-
if (state.cashReservesEnabled) {
287-
CodeButton(
288-
modifier = Modifier.weight(1f),
289-
buttonState = ButtonState.Filled,
290-
text = stringResource(R.string.action_buy),
291-
) {
292-
dispatch(TokenInfoViewModel.Event.OpenPurchaseMethods(forNeededFunds = isForNeededFunds))
293-
}
285+
if (state.isCashReserve) return@Row
286+
val canGive = state.balance.nativeAmount.isPositive
287+
if (canGive) {
288+
CodeButton(
289+
modifier = Modifier.weight(1f),
290+
buttonState = ButtonState.Filled,
291+
text = stringResource(R.string.action_give),
292+
) {
293+
dispatch(
294+
TokenInfoViewModel.Event.OpenScreen(
295+
AppRoute.Main.Give(mint = loadable.data.address, fromTokenInfo = true)
296+
)
297+
)
298+
}
299+
}
294300

295-
if (state.canSell) {
296-
CodeButton(
297-
modifier = Modifier
298-
.weight(1f),
299-
buttonState = ButtonState.Filled20,
300-
text = stringResource(R.string.action_sell),
301-
) {
302-
analytics.buttonTapped(Button.TokenSell)
303-
dispatch(
304-
TokenInfoViewModel.Event.OpenScreen(
305-
AppRoute.Token.SwapTransact(
306-
purpose = TokenSwapPurpose.Sell(loadable.data.address),
307-
)
308-
)
309-
)
310-
}
311-
}
312-
} else {
301+
if (state.cashReservesEnabled) {
302+
CodeButton(
303+
modifier = Modifier.weight(1f),
304+
buttonState = if (canGive) ButtonState.Filled20 else ButtonState.Filled,
305+
text = stringResource(R.string.action_buy),
306+
) {
307+
dispatch(TokenInfoViewModel.Event.OpenPurchaseMethods(forNeededFunds = isForNeededFunds))
308+
}
309+
310+
if (state.canSell) {
313311
CodeButton(
314312
modifier = Modifier
315-
.fillMaxWidth()
316-
.padding(horizontal = CodeTheme.dimens.inset)
317-
.padding(bottom = CodeTheme.dimens.grid.x3)
318-
.navigationBarsPadding(),
319-
buttonState = ButtonState.Filled,
320-
text = stringResource(R.string.action_give),
313+
.weight(1f),
314+
buttonState = ButtonState.Filled20,
315+
text = stringResource(R.string.action_sell),
321316
) {
317+
analytics.buttonTapped(Button.TokenSell)
322318
dispatch(
323319
TokenInfoViewModel.Event.OpenScreen(
324-
AppRoute.Main.Give(mint = loadable.data.address)
320+
AppRoute.Token.SwapTransact(
321+
purpose = TokenSwapPurpose.Sell(loadable.data.address),
322+
)
325323
)
326324
)
327325
}

apps/flipcash/shared/session/src/main/kotlin/com/flipcash/app/session/internal/RealSessionController.kt

Lines changed: 1 addition & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import com.flipcash.app.core.internal.bill.BillController
1414
import com.flipcash.app.core.internal.errors.showNetworkError
1515
import com.flipcash.app.core.internal.updater.ProfileUpdater
1616
import com.flipcash.app.core.navigation.DeeplinkType
17-
import com.flipcash.app.tokens.TokenUpdater
1817
import com.flipcash.app.featureflags.FeatureFlag
1918
import com.flipcash.app.featureflags.FeatureFlagController
2019
import com.flipcash.app.session.BillDeterminationResult
@@ -29,6 +28,7 @@ import com.flipcash.app.shareable.ShareSheetController
2928
import com.flipcash.app.shareable.Shareable
3029
import com.flipcash.app.shareable.ShareableConfirmationController
3130
import com.flipcash.app.tokens.TokenCoordinator
31+
import com.flipcash.app.tokens.TokenUpdater
3232
import com.flipcash.core.R
3333
import com.flipcash.services.controllers.AccountController
3434
import com.flipcash.services.user.AuthState
@@ -44,9 +44,6 @@ import com.getcode.opencode.model.core.PayloadKind
4444
import com.getcode.opencode.model.financial.LocalFiat
4545
import com.getcode.opencode.model.financial.Token
4646
import com.getcode.opencode.model.financial.sum
47-
import com.getcode.opencode.model.financial.usdf
48-
import com.getcode.opencode.model.transactions.AirdropType
49-
import com.getcode.opencode.utils.nonce
5047
import com.getcode.ui.core.RestrictionType
5148
import com.getcode.util.permissions.PermissionResult
5249
import com.getcode.util.resources.ResourceHelper
@@ -72,7 +69,6 @@ import kotlinx.coroutines.flow.launchIn
7269
import kotlinx.coroutines.flow.map
7370
import kotlinx.coroutines.flow.mapNotNull
7471
import kotlinx.coroutines.flow.onEach
75-
import kotlinx.coroutines.flow.scan
7672
import kotlinx.coroutines.flow.update
7773
import kotlinx.coroutines.launch
7874
import kotlinx.coroutines.suspendCancellableCoroutine
@@ -188,18 +184,6 @@ class RealSessionController @Inject constructor(
188184
.onEach { tokens ->
189185
_state.update { it.copy(tokens = tokens) }
190186
}.launchIn(scope)
191-
192-
state
193-
.map { it.isCameraUp }
194-
.distinctUntilChanged() // Emit only when value changes
195-
.scan(Pair(null as Boolean?, null as Boolean?)) { previousPair, current ->
196-
Pair(previousPair.second, current)
197-
}
198-
.mapNotNull { (previous, current) ->
199-
if (previous == null && current != null) current else null
200-
}
201-
.onEach { checkForAirdrops() }
202-
.launchIn(scope)
203187
}
204188

205189
/**
@@ -274,56 +258,6 @@ class RealSessionController @Inject constructor(
274258
}
275259
}
276260

277-
private fun checkForAirdrops() {
278-
if (userManager.authState.canAccessAuthenticatedApis) {
279-
scope.launch {
280-
userManager.accountCluster?.let {
281-
transactionController.airdrop(
282-
type = AirdropType.WelcomeBonus,
283-
destination = it.authority.keyPair
284-
).onSuccess { amount ->
285-
presentWelcomeBonus(amount)
286-
}
287-
}
288-
}
289-
}
290-
}
291-
292-
private fun presentWelcomeBonus(amount: LocalFiat) {
293-
scope.launch {
294-
val presentWithBill = featureFlagController.get(FeatureFlag.WelcomeBonusBill)
295-
toastController.enqueue(
296-
amount = amount,
297-
isDeposit = true,
298-
initialDelay = if (presentWithBill) ToastController.INITIAL_DELAY else AIRDROP_INITIAL_DELAY
299-
)
300-
301-
if (!presentWithBill) {
302-
// consume the toast immediately
303-
toastController.consumeQueue()
304-
return@launch
305-
}
306-
307-
delay(1.seconds)
308-
val payloadInfo = OpenCodePayload(
309-
kind = PayloadKind.Cash,
310-
value = amount.nativeAmount,
311-
nonce = nonce
312-
)
313-
314-
val bill = Bill.Cash(
315-
token = Token.usdf,
316-
data = payloadInfo.codeData.toList(),
317-
amount = amount,
318-
didReceive = true,
319-
confirmationDelay = 500.milliseconds
320-
)
321-
322-
presentBillToUser(data = payloadInfo.codeData.toList(), bill = bill)
323-
feedCoordinator.fetchSinceLatest()
324-
}
325-
}
326-
327261
private fun checkPendingItemsInFeed() {
328262
if (userManager.authState.canAccessAuthenticatedApis) {
329263
scope.launch {

0 commit comments

Comments
 (0)