Skip to content

Commit 26213df

Browse files
authored
Merge branch 'code/cash' into feat/usd-on-flipcash-deposit
2 parents 75aefa1 + fa9a873 commit 26213df

30 files changed

Lines changed: 1002 additions & 298 deletions

File tree

.claude/skills/release-notes/SKILL.md

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,19 @@ Use the Agent tool with `model: "haiku"`. Pass the raw changelog output with thi
5555
> - If no user-facing changes, output: Bug fixes and performance improvements.
5656
> - Output ONLY the section markdown, no title or fences
5757
58-
### 3. Assemble final notes
58+
### 3. Generate App Store "What's New"
59+
60+
Using the same changelog, write a short "What's New" blurb suitable for the Google Play Store listing. Rules:
61+
- 3–5 bullet lines max, plain dashes (`- `)
62+
- Lead with the most impactful user-facing features
63+
- End with "Bug fixes and performance improvements" if there are fixes/chores
64+
- No markdown bold, no sections headers — just a flat list
65+
- Keep each line under ~60 characters
66+
- If there are no notable user-facing changes, output only: `- Bug fixes and performance improvements`
67+
68+
You can derive this yourself from the changelog without a subagent call.
69+
70+
### 4. Assemble final notes
5971

6072
Combine the agent's output with the changelog compare link:
6173

@@ -67,11 +79,11 @@ Combine the agent's output with the changelog compare link:
6779

6880
The release title/name is the version number without the `fcash/` prefix (e.g., `2026.4.11`).
6981

70-
### 4. Review gate
82+
### 5. Review gate
7183

72-
Show the assembled release notes to the user for approval. Do NOT create the release until the user explicitly confirms.
84+
Show the assembled GitHub release notes **and** the App Store "What's New" to the user for approval. Do NOT create the release until the user explicitly confirms.
7385

74-
### 5. Publish
86+
### 6. Publish
7587

7688
After user confirms, create the release:
7789
```bash

.well-known/release-manifest.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
{
2-
"updated": "2026-05-13T22:30:06Z",
2+
"updated": "2026-05-15T18:05:45Z",
33
"tracks": {
44
"production": {
5-
"versionCode": 3508,
6-
"versionName": "2026.5.2"
5+
"versionCode": 3599,
6+
"versionName": "2026.5.4"
77
},
88
"beta": null,
99
"alpha": null,
1010
"internal": {
11-
"versionCode": 3584,
12-
"versionName": "2026.5.3"
11+
"versionCode": 3599,
12+
"versionName": "2026.5.4"
1313
}
1414
}
1515
}

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

Lines changed: 74 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@ import androidx.compose.runtime.CompositionLocalProvider
1515
import androidx.compose.runtime.LaunchedEffect
1616
import androidx.compose.runtime.collectAsState
1717
import androidx.compose.runtime.getValue
18+
import androidx.compose.runtime.mutableLongStateOf
1819
import androidx.compose.runtime.mutableStateOf
1920
import androidx.compose.runtime.remember
2021
import androidx.compose.runtime.setValue
22+
import androidx.compose.runtime.snapshotFlow
2123
import androidx.compose.ui.Modifier
2224
import androidx.compose.ui.res.stringResource
2325
import androidx.compose.ui.semantics.semantics
@@ -32,45 +34,50 @@ import androidx.navigation3.scene.SinglePaneSceneStrategy
3234
import com.flipcash.app.analytics.rememberAnalytics
3335
import com.flipcash.app.android.BuildConfig
3436
import com.flipcash.app.bill.customization.BillPlaygroundScaffold
35-
import com.flipcash.app.core.LocalUserManager
3637
import com.flipcash.app.core.AppRoute
37-
import com.flipcash.app.core.verification.email.LocalEmailCodeChannel
38+
import com.flipcash.app.core.LocalUserManager
39+
import com.flipcash.app.core.extensions.navigateTo
3840
import com.flipcash.app.core.navigation.DeeplinkAction
41+
import com.flipcash.app.core.verification.email.LocalEmailCodeChannel
42+
import com.flipcash.app.featureflags.FeatureFlag
43+
import com.flipcash.app.featureflags.LocalFeatureFlags
44+
import com.flipcash.app.featureflags.model.BackgroundResetTimeout
3945
import com.flipcash.app.internal.ui.navigation.appEntryProvider
4046
import com.flipcash.app.internal.ui.navigation.decorators.rememberNavBlockingOverlayEntryDecorator
4147
import com.flipcash.app.internal.ui.navigation.decorators.rememberNavMessagingEntryDecorator
4248
import com.flipcash.app.onramp.CoinbaseOnRampHandler
4349
import com.flipcash.app.onramp.ExternalWalletOnRampHandler
4450
import com.flipcash.app.onramp.LocalExternalWalletOnRampController
45-
import com.flipcash.app.onramp.LocalCoinbaseOnRampController
4651
import com.flipcash.app.router.LocalRouter
4752
import com.flipcash.app.session.LocalSessionController
4853
import com.flipcash.app.theme.FlipcashTheme
4954
import com.flipcash.features.shareapp.R
5055
import com.flipcash.services.user.AuthState
56+
import com.getcode.animation.LocalSharedTransitionScope
5157
import com.getcode.libs.biometrics.BiometricsError
5258
import com.getcode.libs.qr.rememberQrBitmapPainter
5359
import com.getcode.navigation.AppNavHost
60+
import com.getcode.navigation.Sheet
61+
import com.getcode.navigation.core.CodeNavigator
5462
import com.getcode.navigation.core.LocalCodeNavigator
5563
import com.getcode.navigation.core.rememberCodeNavigator
5664
import com.getcode.navigation.extensions.getActivityScopedViewModel
5765
import com.getcode.navigation.results.rememberNavResultStateRegistry
5866
import com.getcode.navigation.scenes.ModalBottomSheetSceneStrategy
67+
import com.getcode.navigation.scrim.LocalScrimController
68+
import com.getcode.navigation.scrim.ScrimController
69+
import com.getcode.navigation.scrim.ScrimOverlay
5970
import com.getcode.theme.CodeTheme
6071
import com.getcode.ui.biometrics.LocalBiometricsState
6172
import com.getcode.ui.biometrics.rememberBiometricsState
6273
import com.getcode.ui.components.OnLifecycleEvent
6374
import com.getcode.ui.components.bars.rememberBarManager
6475
import com.getcode.ui.core.RestrictionType
65-
import com.flipcash.app.core.extensions.navigateTo
66-
import com.getcode.animation.LocalSharedTransitionScope
67-
import com.getcode.navigation.scrim.LocalScrimController
68-
import com.getcode.navigation.scrim.ScrimController
69-
import com.getcode.navigation.scrim.ScrimOverlay
7076
import dev.bmcreations.tipkit.TipScaffold
7177
import dev.bmcreations.tipkit.engines.TipsEngine
7278
import dev.theolm.rinku.DeepLink
7379
import dev.theolm.rinku.compose.ext.DeepLinkListener
80+
import kotlinx.coroutines.flow.first
7481

7582
@Composable
7683
internal fun App(
@@ -96,6 +103,7 @@ internal fun App(
96103
}
97104

98105
var deepLink by remember { mutableStateOf<DeepLink?>(null) }
106+
var deeplinkHandled by remember { mutableStateOf(false) }
99107
val userManager = LocalUserManager.current!!
100108
DeepLinkListener {
101109
analytics.deeplinkOpened(it.data)
@@ -239,7 +247,9 @@ internal fun App(
239247
return@LaunchedEffect
240248
}
241249

242-
when (val action = router.dispatch(link)) {
250+
val action = router.dispatch(link)
251+
deeplinkHandled = action != DeeplinkAction.None
252+
when (action) {
243253
is DeeplinkAction.Navigate -> {
244254
// If a verification code targets a screen already open,
245255
// deliver via side-channel and skip navigation.
@@ -323,6 +333,13 @@ internal fun App(
323333
else -> Unit
324334
}
325335
}
336+
337+
BackgroundResetEffect(
338+
navigator = codeNavigator,
339+
deepLink = { deepLink },
340+
deeplinkHandled = { deeplinkHandled },
341+
onReset = { deeplinkHandled = false },
342+
)
326343
}
327344
}
328345
}
@@ -331,3 +348,51 @@ internal fun App(
331348
}
332349
}
333350

351+
@Composable
352+
private fun BackgroundResetEffect(
353+
navigator: CodeNavigator,
354+
deepLink: () -> DeepLink?,
355+
deeplinkHandled: () -> Boolean,
356+
onReset: () -> Unit,
357+
) {
358+
val featureFlags = LocalFeatureFlags.current
359+
val option by featureFlags.getOption(FeatureFlag.BackgroundReset)
360+
.collectAsStateWithLifecycle()
361+
362+
var pendingReset by remember { mutableStateOf(false) }
363+
var backgroundedAt by remember { mutableLongStateOf(0L) }
364+
365+
OnLifecycleEvent { _, event ->
366+
when (event) {
367+
Lifecycle.Event.ON_STOP -> {
368+
backgroundedAt = System.currentTimeMillis()
369+
}
370+
Lifecycle.Event.ON_RESUME -> {
371+
val timeout = runCatching { BackgroundResetTimeout.valueOf(option) }
372+
.getOrNull()
373+
?.duration
374+
375+
if (timeout != null && backgroundedAt > 0L) {
376+
val elapsed = System.currentTimeMillis() - backgroundedAt
377+
if (elapsed >= timeout.inWholeMilliseconds) {
378+
pendingReset = true
379+
}
380+
}
381+
backgroundedAt = 0L
382+
}
383+
else -> Unit
384+
}
385+
}
386+
387+
LaunchedEffect(pendingReset) {
388+
if (!pendingReset) return@LaunchedEffect
389+
// Wait for any pending deeplink to be consumed before deciding
390+
snapshotFlow { deepLink() }.first { it == null }
391+
if (!deeplinkHandled()) {
392+
navigator.popUntil { it !is Sheet }
393+
}
394+
onReset()
395+
pendingReset = false
396+
}
397+
}
398+

apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/tokens/TokenSwapPurpose.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import kotlinx.serialization.Serializable
1818
enum class FundingSource {
1919
Flexible,
2020
Phantom,
21+
Coinbase,
2122
}
2223

2324
@Serializable

apps/flipcash/core/src/main/res/drawable/ic_phantom_connected.xml

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,22 @@
1414
~ limitations under the License.
1515
-->
1616
<vector xmlns:android="http://schemas.android.com/apk/res/android"
17-
android:width="111dp"
18-
android:height="112dp"
19-
android:viewportWidth="111"
20-
android:viewportHeight="112">
17+
android:width="122dp"
18+
android:height="124dp"
19+
android:viewportWidth="122"
20+
android:viewportHeight="124">
2121
<path
22-
android:pathData="M50,100C77.71,100 100,77.71 100,50C100,22.29 77.71,0 50,0C22.29,0 0,22.29 0,50C0,77.71 22.29,100 50,100Z"
22+
android:pathData="M61,112C88.71,112 111,89.71 111,62C111,34.29 88.71,12 61,12C33.29,12 11,34.29 11,62C11,89.71 33.29,112 61,112Z"
2323
android:fillColor="#AB9FF1"/>
2424
<path
25-
android:pathData="M26.77,75.76C34.63,75.76 40.53,68.93 44.06,63.53C43.63,64.72 43.39,65.92 43.39,67.07C43.39,70.22 45.2,72.47 48.77,72.47C53.67,72.47 58.91,68.17 61.63,63.53C61.44,64.2 61.34,64.82 61.34,65.39C61.34,67.59 62.58,68.98 65.1,68.98C73.05,68.98 81.05,54.88 81.05,42.55C81.05,32.95 76.2,24.49 64.01,24.49C42.58,24.49 19.49,50.68 19.49,67.59C19.49,74.23 23.06,75.76 26.77,75.76ZM56.63,41.5C56.63,39.11 57.96,37.44 59.91,37.44C61.82,37.44 63.15,39.11 63.15,41.5C63.15,43.89 61.82,45.61 59.91,45.61C57.96,45.61 56.63,43.89 56.63,41.5ZM66.82,41.5C66.82,39.11 68.15,37.44 70.1,37.44C72.01,37.44 73.34,39.11 73.34,41.5C73.34,43.89 72.01,45.61 70.1,45.61C68.15,45.61 66.82,43.89 66.82,41.5Z"
25+
android:pathData="M37.77,87.76C45.63,87.76 51.53,80.93 55.06,75.53C54.63,76.72 54.39,77.92 54.39,79.07C54.39,82.22 56.2,84.47 59.77,84.47C64.67,84.47 69.91,80.17 72.63,75.53C72.44,76.2 72.34,76.82 72.34,77.39C72.34,79.59 73.58,80.98 76.1,80.98C84.05,80.98 92.05,66.88 92.05,54.55C92.05,44.95 87.2,36.49 75.01,36.49C53.58,36.49 30.49,62.68 30.49,79.59C30.49,86.23 34.06,87.76 37.77,87.76ZM67.63,53.5C67.63,51.11 68.96,49.44 70.91,49.44C72.82,49.44 74.15,51.11 74.15,53.5C74.15,55.89 72.82,57.61 70.91,57.61C68.96,57.61 67.63,55.89 67.63,53.5ZM77.82,53.5C77.82,51.11 79.15,49.44 81.1,49.44C83.01,49.44 84.34,51.11 84.34,53.5C84.34,55.89 83.01,57.61 81.1,57.61C79.15,57.61 77.82,55.89 77.82,53.5Z"
2626
android:fillColor="#ffffff"/>
2727
<path
28-
android:pathData="M91.73,92.76m-16.98,0a16.98,16.98 0,1 1,33.96 0a16.98,16.98 0,1 1,-33.96 0"
28+
android:pathData="M102.73,104.76m-16.98,0a16.98,16.98 0,1 1,33.96 0a16.98,16.98 0,1 1,-33.96 0"
2929
android:strokeWidth="3.95626"
3030
android:fillColor="#ffffff"
3131
android:strokeColor="#19191A"/>
3232
<path
33-
android:pathData="M99.3,88.29L89.72,99.05L84.69,94.11L86.15,92.63L89.62,96.03L97.75,86.9L99.3,88.29Z"
33+
android:pathData="M110.3,100.29L100.72,111.05L95.69,106.11L97.15,104.63L100.62,108.03L108.75,98.9L110.3,100.29Z"
3434
android:fillColor="#19191A"/>
3535
</vector>

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -594,6 +594,10 @@
594594
<string name="error_title_onrampGooglePayNotReady">Google Pay Not Ready</string>
595595
<string name="error_description_onrampGooglePayNotReady">Please add a payment method to Google Pay and try again</string>
596596

597+
<string name="error_title_onrampNonStableWebView">Unsupported WebView</string>
598+
<string name="error_description_onrampNonStableWebView">Your device is using a %1$s version of Android System WebView, which may cause issues with purchases. Google recommends switching to the stable channel before continuing with your purchase</string>
599+
<string name="action_continueAnyway">Continue Anyway</string>
600+
597601
<string name="error_title_onrampUnknownFailure">Something Went Wrong</string>
598602
<string name="error_description_onrampUnknownFailure">Please contact support@flipcash.com</string>
599603

apps/flipcash/features/bill-customization/src/main/kotlin/com/flipcash/app/bill/customization/components/BillPlayground.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ internal fun Modifier.presenceBorder(
217217
)
218218

219219
private object PreviewFeatureFlagController : FeatureFlagController by NoOpFeatureFlagController {
220-
override fun observe(flag: FeatureFlag): StateFlow<Boolean> = MutableStateFlow(true)
220+
override fun observe(flag: FeatureFlag<*>): StateFlow<Boolean> = MutableStateFlow(true)
221221
}
222222

223223
@Composable

apps/flipcash/features/lab/src/main/kotlin/com/flipcash/app/lab/internal/LabsScreenContent.kt

Lines changed: 62 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import androidx.compose.ui.unit.dp
2525
import androidx.lifecycle.compose.collectAsStateWithLifecycle
2626
import com.flipcash.app.core.AppRoute
2727
import com.flipcash.app.core.extensions.navigateTo
28+
import com.flipcash.app.featureflags.FlagOption
2829
import com.flipcash.app.featureflags.LocalFeatureFlags
2930
import com.flipcash.app.featureflags.message
3031
import com.flipcash.app.featureflags.title
@@ -35,6 +36,7 @@ import com.getcode.ui.components.ListItem
3536
import com.getcode.ui.components.SettingsSwitchRow
3637
import com.getcode.ui.components.text.SectionHeader
3738
import com.getcode.ui.core.verticalScrollStateGradient
39+
import com.getcode.ui.theme.CodeSegmentedControl
3840
import com.getcode.ui.utils.sheetResignmentBehavior
3941

4042
@Composable
@@ -59,12 +61,24 @@ internal fun LabsScreenContent(viewModel: LabsScreenViewModel) {
5961
SectionHeader(stringResource(R.string.title_settingsSectionFeatures))
6062
}
6163
items(betaFlags, key = { it.flag.key }) { feature ->
62-
SettingsSwitchRow(
63-
title = feature.flag.title,
64-
subtitle = feature.flag.message,
65-
checked = feature.enabled
66-
) {
67-
betaFlagsController.set(feature.flag, !feature.enabled)
64+
if (feature.flag.isOptionFlag) {
65+
SettingsOptionRow(
66+
title = feature.flag.title,
67+
subtitle = feature.flag.message,
68+
options = feature.flag.options,
69+
selectedOption = feature.selectedOption ?: feature.flag.defaultOption,
70+
onOptionSelected = { optionKey ->
71+
betaFlagsController.setOption(feature.flag, optionKey)
72+
},
73+
)
74+
} else {
75+
SettingsSwitchRow(
76+
title = feature.flag.title,
77+
subtitle = feature.flag.message,
78+
checked = feature.enabled
79+
) {
80+
betaFlagsController.set(feature.flag, !feature.enabled)
81+
}
6882
}
6983

7084
HorizontalDivider(
@@ -133,4 +147,46 @@ internal fun LabsScreenContent(viewModel: LabsScreenViewModel) {
133147
}
134148
}
135149
}
150+
}
151+
152+
@Composable
153+
private fun SettingsOptionRow(
154+
title: String,
155+
subtitle: String?,
156+
options: List<FlagOption>,
157+
selectedOption: String,
158+
onOptionSelected: (String) -> Unit,
159+
) {
160+
Column(
161+
modifier = Modifier
162+
.fillMaxWidth()
163+
.padding(horizontal = CodeTheme.dimens.grid.x3)
164+
.padding(vertical = CodeTheme.dimens.grid.x3),
165+
) {
166+
Text(
167+
text = title,
168+
color = CodeTheme.colors.textMain,
169+
style = CodeTheme.typography.textMedium,
170+
)
171+
if (!subtitle.isNullOrEmpty()) {
172+
Text(
173+
text = subtitle,
174+
style = CodeTheme.typography.textSmall,
175+
color = CodeTheme.colors.textSecondary,
176+
)
177+
}
178+
CodeSegmentedControl(
179+
options = options,
180+
selected = options.find { it.key == selectedOption },
181+
modifier = Modifier
182+
.fillMaxWidth()
183+
.padding(top = CodeTheme.dimens.grid.x2),
184+
mapper = { option ->
185+
Text(text = option.label)
186+
},
187+
onSelectionChanged = { option ->
188+
onOptionSelected(option.key)
189+
},
190+
)
191+
}
136192
}

apps/flipcash/features/menu/src/main/kotlin/com/flipcash/app/menu/internal/MenuItems.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ internal data object SwitchAccount : StaffMenuItem<MenuScreenViewModel.Event>()
5050
override val name: String
5151
@Composable get() = stringResource(R.string.title_switchAccounts)
5252
override val action: MenuScreenViewModel.Event = MenuScreenViewModel.Event.OnSwitchAccountsClicked
53-
override val featureFlag: FeatureFlag = FeatureFlag.CredentialManager
53+
override val featureFlag: FeatureFlag<*> = FeatureFlag.CredentialManager
5454
}
5555

5656
internal data object Labs : StaffMenuItem<MenuScreenViewModel.Event>() {
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.flipcash.app.featureflags
22

33
data class BetaFeature(
4-
val flag: FeatureFlag,
4+
val flag: FeatureFlag<*>,
55
val enabled: Boolean,
6+
val selectedOption: String? = null,
67
)

0 commit comments

Comments
 (0)