Skip to content

Commit cf90793

Browse files
authored
Merge branch 'dependabot/gradle/code/cash/com.patrykandpatrick.vico-compose-3.0.2' into fix/vico-3-migration
2 parents 55add54 + 4638d35 commit cf90793

62 files changed

Lines changed: 2593 additions & 352 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.well-known/release-manifest.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"updated": "2026-05-29T14:37:59Z",
2+
"updated": "2026-06-01T17:47:43Z",
33
"tracks": {
44
"production": {
55
"versionCode": 3740,
@@ -8,8 +8,8 @@
88
"beta": null,
99
"alpha": null,
1010
"internal": {
11-
"versionCode": 3740,
12-
"versionName": "2026.5.6"
11+
"versionCode": 3783,
12+
"versionName": "2026.6.1"
1313
}
1414
}
1515
}

apps/flipcash/app/build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,4 +271,7 @@ dependencies {
271271

272272
implementation(libs.timber)
273273
implementation(libs.bugsnag)
274+
275+
testImplementation(libs.junit)
276+
testImplementation(libs.kotlin.test.junit)
274277
}

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

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -234,11 +234,14 @@ internal fun App(
234234
}
235235

236236
val emailCodeChannel = LocalEmailCodeChannel.current
237-
LaunchedEffect(deepLink) {
237+
val currentRoute = codeNavigator.currentRouteKey
238+
LaunchedEffect(deepLink, currentRoute) {
238239
val link = deepLink ?: return@LaunchedEffect
239240

240-
if (codeNavigator.currentRouteKey is AppRoute.Loading) {
241-
// Cold start — MainRoot handles it via the deepLink lambda
241+
if (currentRoute is AppRoute.Loading) {
242+
// Cold start — MainRoot handles Navigate actions;
243+
// other actions (OpenCashLink, Login) wait until
244+
// navigation leaves Loading.
242245
return@LaunchedEffect
243246
}
244247

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

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ import com.flipcash.app.core.extensions.navigateAll
2929
import com.flipcash.app.core.extensions.resolveRoutes
3030
import com.flipcash.app.router.LocalRouter
3131
import com.flipcash.app.router.Router
32-
import com.flipcash.services.models.UserFlags
3332
import com.flipcash.services.user.AuthState
3433
import com.getcode.navigation.core.CodeNavigator
3534
import com.getcode.navigation.core.LocalCodeNavigator
@@ -88,9 +87,9 @@ internal fun MainRoot(deepLink: () -> DeepLink?) {
8887

8988
LaunchedEffect(userManager) {
9089
userManager.state
91-
.map { it.authState to it.flags }
90+
.map { it.authState }
9291
.distinctUntilChanged()
93-
.onEach { (state, flags) ->
92+
.onEach { state ->
9493
trace(
9594
tag = "AuthStateRouter",
9695
message = "Handling auth state change during app launch => $state",
@@ -100,18 +99,17 @@ internal fun MainRoot(deepLink: () -> DeepLink?) {
10099
)
101100
val launch = buildNavGraphForLaunch(
102101
state = state,
103-
userFlags = flags,
104102
router = router,
105103
deepLink = deepLink
106104
)
107105

108106
when (state) {
109-
AuthState.LoggedInAwaitingUser -> {
107+
AuthState.Authenticating -> {
110108
delay(0.5.seconds)
111109
showLoading = true
112110
}
113111

114-
AuthState.LoggedInWithUser -> {
112+
AuthState.Ready -> {
115113
showLogo = false
116114
}
117115

@@ -143,7 +141,7 @@ internal fun MainRoot(deepLink: () -> DeepLink?) {
143141
* [deeplinkRoutes] are applied via navigateTo, which handles sheet wrapping
144142
* so deeplinks targeting screens inside sheets render correctly.
145143
*/
146-
private data class LaunchNavGraph(
144+
internal data class LaunchNavGraph(
147145
val baseRoutes: List<NavKey>,
148146
val deeplinkRoutes: List<AppRoute> = emptyList(),
149147
) {
@@ -171,33 +169,31 @@ private fun List<NavKey>.startsWith(prefix: List<NavKey>): Boolean {
171169
return prefix.indices.all { i -> this[i]::class == prefix[i]::class }
172170
}
173171

174-
private fun buildNavGraphForLaunch(
172+
internal fun buildNavGraphForLaunch(
175173
state: AuthState,
176-
userFlags: UserFlags?,
177174
router: Router,
178175
deepLink: () -> DeepLink?,
179176
): LaunchNavGraph? {
180177
return when (state) {
181-
is AuthState.Registered -> {
182-
val resumePoint = when {
183-
!state.seenAccessKey -> AppRoute.OnboardingFlow.ResumePoint.AccessKey
184-
userFlags?.requiresIapForRegistration == true ->
185-
AppRoute.OnboardingFlow.ResumePoint.AccessKeyThenPurchase
186-
else -> AppRoute.OnboardingFlow.ResumePoint.PostAccessKey
178+
is AuthState.Onboarding -> {
179+
val resumePoint = when (state.resumePoint) {
180+
AuthState.ResumePoint.AccessKey -> AppRoute.OnboardingFlow.ResumePoint.AccessKey
181+
AuthState.ResumePoint.AccessKeyThenPurchase -> AppRoute.OnboardingFlow.ResumePoint.AccessKeyThenPurchase
182+
AuthState.ResumePoint.PostAccessKey -> AppRoute.OnboardingFlow.ResumePoint.PostAccessKey
187183
}
188184
LaunchNavGraph(listOf(AppRoute.OnboardingFlow(resumeAt = resumePoint)))
189185
}
190186

191-
AuthState.LoggedInWithUser -> {
187+
AuthState.Ready -> {
192188
val link = deepLink()
193189
if (link != null) {
194190
when (val action = router.dispatch(link)) {
195191
is DeeplinkAction.Navigate -> LaunchNavGraph(
196192
baseRoutes = listOf(AppRoute.Main.Scanner),
197193
deeplinkRoutes = action.routes,
198194
)
199-
// ExternalWallet/Login/OpenCashLink can't be handled on cold start
200-
// (encryption state lost, no session yet) — fall through to Scanner
195+
// OpenCashLink/Login/ExternalWallet are handled by App.kt's
196+
// LaunchedEffect(deepLink, currentRoute) once we leave Loading.
201197
else -> LaunchNavGraph(listOf(AppRoute.Main.Scanner))
202198
}
203199
} else {
@@ -218,6 +214,6 @@ private fun buildNavGraphForLaunch(
218214
}
219215
}
220216

221-
AuthState.LoggedInAwaitingUser -> null
217+
AuthState.Authenticating -> null
222218
}
223219
}
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
package com.flipcash.app.internal.ui.navigation
2+
3+
import com.flipcash.app.core.AppRoute
4+
import com.flipcash.app.core.navigation.DeeplinkAction
5+
import com.flipcash.app.core.navigation.DeeplinkType
6+
import com.flipcash.app.router.Router
7+
import com.flipcash.services.user.AuthState
8+
import dev.theolm.rinku.DeepLink
9+
import kotlin.test.Test
10+
import kotlin.test.assertEquals
11+
import kotlin.test.assertIs
12+
import kotlin.test.assertNull
13+
import kotlin.test.assertTrue
14+
15+
class BuildNavGraphForLaunchTest {
16+
17+
// -- Helpers --
18+
19+
/** Router that returns a fixed action for any deeplink. */
20+
private class FakeRouter(private val action: DeeplinkAction) : Router {
21+
override fun dispatch(deepLink: DeepLink): DeeplinkAction = action
22+
override fun classify(deepLink: DeepLink): DeeplinkType? = null
23+
}
24+
25+
private val dummyLink = DeepLink("https://send.flipcash.com/c/e=testEntropy")
26+
27+
private fun build(
28+
state: AuthState,
29+
action: DeeplinkAction = DeeplinkAction.None,
30+
deepLink: DeepLink? = null,
31+
): LaunchNavGraph? = buildNavGraphForLaunch(
32+
state = state,
33+
router = FakeRouter(action),
34+
deepLink = { deepLink },
35+
)
36+
37+
// -- Ready --
38+
39+
@Test
40+
fun `logged in without deeplink navigates to Scanner`() {
41+
val result = build(AuthState.Ready)!!
42+
assertEquals(listOf(AppRoute.Main.Scanner), result.baseRoutes)
43+
assertTrue(result.deeplinkRoutes.isEmpty())
44+
}
45+
46+
@Test
47+
fun `logged in with Navigate deeplink includes deeplink routes`() {
48+
val routes = listOf(AppRoute.Main.Scanner)
49+
val result = build(
50+
state = AuthState.Ready,
51+
action = DeeplinkAction.Navigate(routes),
52+
deepLink = dummyLink,
53+
)!!
54+
assertEquals(listOf(AppRoute.Main.Scanner), result.baseRoutes)
55+
assertEquals(routes, result.deeplinkRoutes)
56+
}
57+
58+
@Test
59+
fun `logged in with OpenCashLink defers to App for dispatch`() {
60+
val result = build(
61+
state = AuthState.Ready,
62+
action = DeeplinkAction.OpenCashLink("testEntropy"),
63+
deepLink = dummyLink,
64+
)!!
65+
assertEquals(listOf(AppRoute.Main.Scanner), result.baseRoutes)
66+
assertTrue(result.deeplinkRoutes.isEmpty(), "OpenCashLink must not be consumed by MainRoot")
67+
}
68+
69+
@Test
70+
fun `logged in with Login action defers to App for dispatch`() {
71+
val result = build(
72+
state = AuthState.Ready,
73+
action = DeeplinkAction.Login("seed"),
74+
deepLink = dummyLink,
75+
)!!
76+
assertEquals(listOf(AppRoute.Main.Scanner), result.baseRoutes)
77+
assertTrue(result.deeplinkRoutes.isEmpty(), "Login must not be consumed by MainRoot")
78+
}
79+
80+
@Test
81+
fun `logged in with None action navigates to Scanner without deeplink routes`() {
82+
val result = build(
83+
state = AuthState.Ready,
84+
action = DeeplinkAction.None,
85+
deepLink = dummyLink,
86+
)!!
87+
assertEquals(listOf(AppRoute.Main.Scanner), result.baseRoutes)
88+
assertTrue(result.deeplinkRoutes.isEmpty())
89+
}
90+
91+
// -- LoggedOut / Unknown --
92+
93+
@Test
94+
fun `logged out without deeplink navigates to OnboardingFlow`() {
95+
val result = build(AuthState.LoggedOut)!!
96+
assertIs<AppRoute.OnboardingFlow>(result.baseRoutes.single())
97+
}
98+
99+
@Test
100+
fun `logged out with Navigate deeplink uses action routes`() {
101+
val routes = listOf(AppRoute.OnboardingFlow(seed = "abc"))
102+
val result = build(
103+
state = AuthState.LoggedOut,
104+
action = DeeplinkAction.Navigate(routes),
105+
deepLink = dummyLink,
106+
)!!
107+
assertEquals(routes, result.baseRoutes)
108+
}
109+
110+
@Test
111+
fun `logged out with OpenCashLink falls back to OnboardingFlow`() {
112+
val result = build(
113+
state = AuthState.LoggedOut,
114+
action = DeeplinkAction.OpenCashLink("entropy"),
115+
deepLink = dummyLink,
116+
)!!
117+
assertIs<AppRoute.OnboardingFlow>(result.baseRoutes.single())
118+
}
119+
120+
@Test
121+
fun `unknown auth state without deeplink navigates to OnboardingFlow`() {
122+
val result = build(AuthState.Unknown)!!
123+
assertIs<AppRoute.OnboardingFlow>(result.baseRoutes.single())
124+
}
125+
126+
// -- Onboarding --
127+
128+
@Test
129+
fun `onboarding at AccessKey resume point routes to AccessKey`() {
130+
val result = build(AuthState.Onboarding(AuthState.ResumePoint.AccessKey))!!
131+
val route = assertIs<AppRoute.OnboardingFlow>(result.baseRoutes.single())
132+
assertEquals(AppRoute.OnboardingFlow.ResumePoint.AccessKey, route.resumeAt)
133+
}
134+
135+
@Test
136+
fun `onboarding at PostAccessKey resume point routes to PostAccessKey`() {
137+
val result = build(AuthState.Onboarding(AuthState.ResumePoint.PostAccessKey))!!
138+
val route = assertIs<AppRoute.OnboardingFlow>(result.baseRoutes.single())
139+
assertEquals(AppRoute.OnboardingFlow.ResumePoint.PostAccessKey, route.resumeAt)
140+
}
141+
142+
@Test
143+
fun `onboarding at AccessKeyThenPurchase resume point routes to AccessKeyThenPurchase`() {
144+
val result = build(AuthState.Onboarding(AuthState.ResumePoint.AccessKeyThenPurchase))!!
145+
val route = assertIs<AppRoute.OnboardingFlow>(result.baseRoutes.single())
146+
assertEquals(AppRoute.OnboardingFlow.ResumePoint.AccessKeyThenPurchase, route.resumeAt)
147+
}
148+
149+
// -- Authenticating --
150+
151+
@Test
152+
fun `authenticating returns null`() {
153+
assertNull(build(AuthState.Authenticating))
154+
}
155+
}

apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/send/SendStep.kt

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.flipcash.app.core.send
22

33
import android.os.Parcelable
44
import com.getcode.navigation.flow.FlowStep
5+
import com.getcode.opencode.model.financial.Fiat
56
import kotlinx.parcelize.Parcelize
67
import kotlinx.serialization.Serializable
78

@@ -22,7 +23,18 @@ sealed interface SendStep : FlowStep, Parcelable {
2223
@Parcelize
2324
@Serializable
2425
data object ContactList : SendStep
26+
27+
@Parcelize
28+
@Serializable
29+
data class AmountEntry(
30+
val e164: String,
31+
val displayName: String,
32+
) : SendStep
2533
}
2634

2735
@Serializable
28-
sealed interface SendResult : Parcelable
36+
sealed interface SendResult : Parcelable {
37+
@Parcelize
38+
@Serializable
39+
data class Sent(val amount: Fiat) : SendResult
40+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.flipcash.app.core.ui
2+
3+
/** Determines the confirmation widget shown on amount entry screens. */
4+
enum class ConfirmationStyle {
5+
/** Standard tap-to-confirm button (CodeButton). */
6+
Button,
7+
/** Swipe-to-confirm slider (SlideToConfirm). */
8+
Slide,
9+
}

apps/flipcash/core/src/main/res/values/strings.xml

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@
5252
<string name="subtitle_giveCashHint">Enter up to %1$s</string>
5353
<string name="subtitle_giveCashHintLimitExceeded">You can only give up to %1$s</string>
5454

55+
<string name="subtitle_sendHint">Enter up to %1$s</string>
56+
<string name="subtitle_sendHintLimitExceeded">You can only send up to %1$s</string>
57+
5558
<string name="error_title_sendLimitReached">Transaction Limit Reached</string>
5659
<string name="error_description_sendLimitReached">Flipcash is designed for small, every day transactions. Send limits reset daily</string>
5760

@@ -153,7 +156,7 @@
153156
<string name="title_shareDownloadLink">Get the Flipcash App</string>
154157
<string name="title_shareCashLink">Send %1$s</string>
155158
<string name="title_inviteContact">Invite to Flipcash</string>
156-
<string name="message_invite_contact">Download Flipcash so I can send you money\n\n%1$s</string>
159+
<string name="message_invite_contact">You should download Flipcash, a new way to send cash\n\n%1$s</string>
157160

158161
<string name="title_enterAddress">Enter Solana address</string>
159162
<string name="subtitle_whereWithdrawTo">Where would you like to withdraw your %1$s to?</string>
@@ -679,6 +682,9 @@
679682
<string name="error_title_staleRates">Rate Unavailable</string>
680683
<string name="error_description_staleRates">Couldn\'t get a fresh rate. Please try again.</string>
681684

685+
<string name="error_title_amountTooSmall">Amount Too Small</string>
686+
<string name="error_description_amountTooSmall">The amount you entered is too small to transfer\nPlease enter a larger amount</string>
687+
682688
<string name="title_settings">Settings</string>
683689

684690
<string name="title_withdrawUsdfAsUsdc">Withdraw as USDC</string>
@@ -728,4 +734,14 @@
728734
<item quantity="other">%1$s Contacts Already On Flipcash</item>
729735
</plurals>
730736
<string name="prompt_description_contactsAlreadyOnFlipcash">Send them money, or invite other contacts to sign up for Flipcash</string>
737+
738+
<string name="error_title_contactNotOnFlipcash">Something Went Wrong</string>
739+
<string name="error_description_contactNotOnFlipcash">This contact isn\'t on Flipcash. Pick someone else to send cash</string>
740+
741+
<string name="prompt_title_fundsSentToContact">Cash Sent Successfully</string>
742+
<string name="prompt_description_fundsSentToContact">%1$s is on its way to %2$s</string>
743+
744+
<string name="error_title_cashFailedToSend">Something Went Wrong</string>
745+
<string name="error_description_cashFailedToSend">We were unable to send your cash. Please try again</string>
746+
<string name="action_swipeToSend">Swipe to Send</string>
731747
</resources>

apps/flipcash/features/balance/src/main/kotlin/com/flipcash/app/balance/internal/BalanceViewModel.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ internal class BalanceViewModel @Inject constructor(
4242

4343
init {
4444
userManager.state
45-
.filter { it.authState is AuthState.LoggedInWithUser }
45+
.filter { it.authState is AuthState.Ready }
4646
.flatMapLatest { userFlags.resolvedFlags }
4747
.mapNotNull { it.preferredOnRampProvider.effectiveValue }
4848
.filterIsInstance<OnRampProvider.Defined>()

0 commit comments

Comments
 (0)