Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 0 additions & 1 deletion apps/flipcash/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,6 @@ dependencies {
implementation(project(":apps:flipcash:features:transactions"))
implementation(project(":apps:flipcash:features:bill-customization"))
implementation(project(":apps:flipcash:features:currency-creator"))
implementation(project(":apps:flipcash:features:direct-send"))
implementation(project(":apps:flipcash:features:messenger"))
implementation(project(":apps:flipcash:features:invite"))
implementation(project(":apps:flipcash:features:discovery"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,12 +192,12 @@ class MainActivity : FragmentActivity() {

/**
* Test-only: enable beta flags passed as a launch argument, so flag-gated features
* (tipping, blocklist, …) can be exercised in UI tests without toggling them in the
* (blocklist, …) can be exercised in UI tests without toggling them in the
* Labs UI. Mirrors iOS's `--beta-flags`. Debug/UI-test builds only.
*
* launchApp:
* arguments:
* betaFlags: "tipping_enabled,blocklist_enabled"
* betaFlags: "blocklist_enabled"
*
* The value is a comma-separated list of [FeatureFlag.key]s.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ import com.flipcash.app.core.AppRoute
import com.flipcash.app.core.navigation.DeeplinkAction
import com.flipcash.app.currency.RegionSelectionScreen
import com.flipcash.app.deposit.DepositFlowScreen
import com.flipcash.app.directsend.SendFlowScreen
import com.flipcash.app.invite.InviteContactScreen
import com.flipcash.app.messenger.ChatFlowScreen
import com.flipcash.app.discovery.TokenDiscoveryScreen
Expand Down Expand Up @@ -94,7 +93,6 @@ fun appEntryProvider(
// Sheets (inner content — wrapped in Main.Sheet by navigateTo())
// Route type is `Give` but the screen is the Cash/Give screen the flows call cash_screen.
annotatedEntry<AppRoute.Sheets.Give>(testTag = "cash_screen") { key -> CashScreen(key.mint, key.fromTokenInfo) }
annotatedEntry<AppRoute.Sheets.Send> { SendFlowScreen(resultStateRegistry = resultStateRegistry) }
annotatedEntry<AppRoute.Sheets.Tips> { key ->
TippingFlowScreen(route = key, resultStateRegistry = resultStateRegistry)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,16 +160,6 @@ sealed interface AppRoute : NavKey, Parcelable {
@Serializable
data class Give(val mint: Mint? = null, val fromTokenInfo: Boolean = false) : Sheets

/**
* Direct send flow — phone-verified user picks a contact and sends funds.
*
* @param resumed `true` when the flow is re-entered after an interrupting gate
* (e.g. phone verification). A distinct value produces a new route instance so
* Nav3 treats `replaceAll` as a forward push instead of a pop.
*/
@Serializable
data class Send(val resumed: Boolean = false): Sheets

@Serializable
data class Tips(val resumed: Boolean = false): Sheets {
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ sealed interface DeeplinkType: Parcelable {

@Serializable data class TokenInfo(val mint: Mint): DeeplinkType, Navigatable

@Serializable data class Chat(val identifier: ChatIdentifier): DeeplinkType, Navigatable

@Serializable data class TipChat(val identifier: ChatIdentifier): DeeplinkType, Navigatable

@Serializable data class Tipcard(val userId: ID): DeeplinkType
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,10 @@ enum class NavBarButton {
Give,
Wallet,
Discover,
Send,
Tips,
;

companion object {
val defaultOrder = listOf(Discover, Give, Send, Tips, Wallet,)
val defaultOrder = listOf(Discover, Give, Tips, Wallet,)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -147,15 +147,6 @@ fun NavigationBar(
onClick = { onButtonClick(NavBarButton.Discover) }
)

NavBarButton.Send -> BottomBarAction(
modifier = buttonModifier,
label = stringResource(R.string.action_send),
badgeCount = state.contactDmUnreadCount,
painter = painterResource(R.drawable.ic_send_outlined),
imageSize = imageSize,
onClick = { onButtonClick(NavBarButton.Send) }
)

NavBarButton.Tips -> BottomBarAction(
modifier = buttonModifier,
label = stringResource(R.string.action_tips),
Expand Down Expand Up @@ -320,14 +311,3 @@ private fun NavigationBarPreview() {
state = NavigationBarState(contactDmUnreadCount = 100),
)
}
@Preview
@PreviewWrapper(FlipcashThemeWrapper::class)
@Composable
private fun SendActionPreview() {
BottomBarAction(
painter = painterResource(R.drawable.ic_send_outlined),
label = "Send",
badgeCount = 100,
onClick = null,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,5 @@ object Linkify {
fun tweet(message: String): String = "https://www.twitter.com/intent/tweet?text=${message.urlEncode()}"
fun tokenInfo(token: Token): String = tokenInfo(token.address)
fun tokenInfo(mint: Mint): String = "https://app.flipcash.com/token/${mint.base58()}"
fun chatById(chatId: ChatId): String = "https://app.flipcash.com/chat/${chatId.bytes.encodeBase64(urlSafe = true)}"
fun tipChatById(chatId: ChatId): String = "https://app.flipcash.com/tip/chat/${chatId.bytes.encodeBase64(urlSafe = true)}"
fun chatByPhone(phoneNumber: String): String = "https://app.flipcash.com/chat/${phoneNumber.urlEncode()}"
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class NavBarConfigTest {
@Test
fun `round-trips a full, current order`() {
val config = NavBarConfig(
order = listOf(NavBarButton.Wallet, NavBarButton.Give, NavBarButton.Discover, NavBarButton.Send, NavBarButton.Tips),
order = listOf(NavBarButton.Wallet, NavBarButton.Give, NavBarButton.Discover, NavBarButton.Tips),
giveButtonLabel = GiveButtonLabel.Cash,
)
assertEquals(config, NavBarConfig.deserialize(config.serialize()))
Expand All @@ -23,39 +23,50 @@ class NavBarConfigTest {
@Test
fun `back-fills a button added after the order was persisted, at its default position`() {
// A nav bar order persisted before Tips was added to the enum/defaultOrder.
val legacy = "Discover,Give,Send,Wallet|Cash"
val legacy = "Discover,Give,Wallet|Cash"

val order = NavBarConfig.deserialize(legacy).order

assertTrue(NavBarButton.Tips in order, "Tips should be back-filled into a legacy order")
// Inserted at its defaultOrder position (between Send and Wallet), not appended.
// Inserted at its defaultOrder position (between Give and Wallet), not appended.
assertEquals(
listOf(NavBarButton.Discover, NavBarButton.Give, NavBarButton.Send, NavBarButton.Tips, NavBarButton.Wallet),
listOf(NavBarButton.Discover, NavBarButton.Give, NavBarButton.Tips, NavBarButton.Wallet),
order,
)
}

@Test
fun `back-fill preserves a user's custom ordering of existing buttons`() {
val legacy = "Wallet,Send,Give,Discover|Cash"
val legacy = "Wallet,Give,Discover|Cash"

val order = NavBarConfig.deserialize(legacy).order

assertTrue(NavBarButton.Tips in order)
// Existing buttons keep the user's reversed order; only the missing one is added.
assertEquals(
listOf(NavBarButton.Wallet, NavBarButton.Send, NavBarButton.Give, NavBarButton.Discover),
listOf(NavBarButton.Wallet, NavBarButton.Give, NavBarButton.Discover),
order.filterNot { it == NavBarButton.Tips },
)
}

@Test
fun `unknown button names are dropped and missing known ones back-filled`() {
val order = NavBarConfig.deserialize("Discover,Bogus,Give,Send,Wallet|Cash").order
val order = NavBarConfig.deserialize("Discover,Bogus,Give,Wallet|Cash").order

assertEquals(NavBarButton.defaultOrder, order)
}

@Test
fun `a persisted order containing the removed Send button is dropped without crashing`() {
// Users who customized their nav bar before Send was removed have "Send" persisted in
// their NavBar config. deserialize() must silently drop the now-unknown token (never
// throw on NavBarButton.valueOf) and back-fill the current default order.
val order = NavBarConfig.deserialize("Discover,Give,Send,Tips,Wallet|Cash").order

assertTrue("Send" !in order.map { it.name }, "the removed Send token must not survive")
assertEquals(NavBarButton.defaultOrder, order)
}

@Test
fun `empty order falls back to the default order`() {
val order = NavBarConfig.deserialize("|Cash").order
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,5 @@ sealed class ScannerDecorItem(val screen: AppRoute) {
data object Menu : ScannerDecorItem(AppRoute.Sheets.Menu)
data object Logo: ScannerDecorItem(AppRoute.Sheets.ShareApp)
data object Discover: ScannerDecorItem(AppRoute.Token.Discovery)
data object Send: ScannerDecorItem(AppRoute.Sheets.Send())
data object Tips: ScannerDecorItem(AppRoute.Sheets.Tips())
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,23 +31,9 @@ internal fun ScannerNavigationBar(
NavBarConfig.deserialize(navBarConfigString)
}

val effectiveConfig = remember(config, state.isTippingEnabled) {
val buttons = config.order
.filter { option ->
when (option) {
NavBarButton.Tips -> state.isTippingEnabled
else -> true
}
}

config.copy(
order = buttons,
)
}

NavigationBar(
modifier = modifier,
config = effectiveConfig,
config = config,
state = NavigationBarState(
contactDmUnreadCount = state.contactDmUnreadCount,
tipUnreadCount = state.tipsUnreadCount,
Expand All @@ -60,7 +46,6 @@ internal fun ScannerNavigationBar(
NavBarButton.Give -> ScannerDecorItem.Give
NavBarButton.Wallet -> ScannerDecorItem.Wallet
NavBarButton.Discover -> ScannerDecorItem.Discover
NavBarButton.Send -> ScannerDecorItem.Send
NavBarButton.Tips -> ScannerDecorItem.Tips
}
onAction(item)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewWrapper
import com.flipcash.app.core.AppRoute
import com.flipcash.app.core.LocalUserManager
import com.flipcash.app.core.send.SendResult
import com.flipcash.app.core.send.SendStep
import com.flipcash.app.core.tipping.TipResult
import com.flipcash.app.core.tipping.TipStep
import com.flipcash.app.theme.FlipcashThemeWrapper
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,16 +129,6 @@ sealed interface FeatureFlag<T: Any> {
override val persistLogOut: Boolean = false
}

@FeatureFlagMarker
data object Tipping: FeatureFlag<Boolean> {
override val key: String = "tipping_enabled"
override val default: Boolean = false
override val launched: Boolean = false
override val visible: Boolean = true
override val persistLogOut: Boolean = false
override val minTrack: FeatureTrack = FeatureTrack.Production
}

@FeatureFlagMarker
data object FrostedTipCard: FeatureFlag<Boolean> {
override val key: String = "frosted_tip_card_enabled"
Expand Down Expand Up @@ -180,7 +170,6 @@ val FeatureFlag<*>.title: String
FeatureFlag.NavBar -> "Navigation Bar"
FeatureFlag.GiveUsdf -> "Give/Send USDF"
FeatureFlag.ShowNetworkState -> "Network Offline Indicator"
FeatureFlag.Tipping -> "Tipping"
FeatureFlag.FrostedTipCard -> "Frosted Tip Card"
FeatureFlag.Blocklist -> "Blocklist"
}
Expand All @@ -197,7 +186,6 @@ val FeatureFlag<*>.message: String
FeatureFlag.NavBar -> "Customize the order and labels of navigation bar buttons"
FeatureFlag.GiveUsdf -> "When enabled, you'll gain the ability to send USDF directly and give it as cash"
FeatureFlag.ShowNetworkState -> "When enabled, you'll gain the ability to see the network state on the Scanner when offline"
FeatureFlag.Tipping -> "When enabled, you'll gain the ability to tip other users and set up your own tip card to receive tips"
FeatureFlag.FrostedTipCard -> "When enabled, the tip card in the scanner renders as frosted glass over a blurred snapshot of the camera instead of a solid card"
FeatureFlag.Blocklist -> "When enabled, you'll gain the ability to open a chat participant's profile, block them, and manage your blocklist from My Account"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -421,16 +421,16 @@ class NotificationService : FirebaseMessagingService(),
data = Linkify.tokenInfo(navigation.mint).toUri()
}

is NavigationTrigger.Chat.ById -> Intent(Intent.ACTION_VIEW).apply {
data = if (metadata?.chatType == ChatType.TIP_DM) {
Linkify.tipChatById(navigation.chatId).toUri()
} else {
Linkify.chatById(navigation.chatId).toUri()
// Only tip DMs deep-link (via /tip/chat/…). Non-tip chat notifications — and all
// contact/phone-addressed chats — no longer have an in-app entry point (the Send
// tab / direct-send flow was removed), so fall through to a plain launch that opens
// the app on the camera instead of firing a now-unhandled /chat/ deeplink.
is NavigationTrigger.Chat.ById -> if (metadata?.chatType == ChatType.TIP_DM) {
Intent(Intent.ACTION_VIEW).apply {
data = Linkify.tipChatById(navigation.chatId).toUri()
}
}

is NavigationTrigger.Chat.ByContact -> Intent(Intent.ACTION_VIEW).apply {
data = Linkify.chatByPhone(navigation.phoneNumber).toUri()
} else {
packageManager.getLaunchIntentForPackage(packageName)
}

else -> packageManager.getLaunchIntentForPackage(packageName)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package com.flipcash.app.router.internal
import androidx.core.net.toUri
import com.flipcash.app.core.AppRoute
import com.flipcash.app.core.chat.ChatIdentifier
import com.flipcash.app.core.contacts.DeviceContact
import com.flipcash.app.core.navigation.DeeplinkAction
import com.flipcash.app.core.navigation.DeeplinkType
import com.flipcash.services.models.chat.ChatId
Expand Down Expand Up @@ -64,9 +63,6 @@ internal class AppRouter(
)

is DeeplinkType.EmailVerification -> resolveEmailVerification(type)
is DeeplinkType.Chat -> DeeplinkAction.Navigate(
listOf(AppRoute.Sheets.Send(), AppRoute.Messaging.Chat(type.identifier))
)

is DeeplinkType.TipChat -> DeeplinkAction.Navigate(
listOf(AppRoute.Sheets.Tips(), AppRoute.Messaging.Chat(type.identifier))
Expand All @@ -82,9 +78,12 @@ internal class AppRouter(
deepLink.isCashLink() -> deepLink.handleCashLink()
deepLink.isToken() -> deepLink.handleTokenLink()
deepLink.isEmailVerification() -> deepLink.handleEmailVerification()
deepLink.isChat() -> deepLink.handleChat()
deepLink.isTipChat() -> deepLink.handleTipChat()
deepLink.isTipCard() -> deepLink.handleTipCard()
// `/chat/{id}` links are intentionally NOT handled: the Send tab / direct-send
// flow they opened was removed, so they fall through to `null` and the app lands
// on the camera. Do not re-add chat routing here without restoring that entry point.
// (Tip DMs use `/tip/chat/{id}` — handled above via isTipChat.)
else -> null
}
}
Expand Down Expand Up @@ -138,8 +137,6 @@ private fun DeepLink.isToken(): Boolean = token.contains(pathSegments.getOrNull(
private fun DeepLink.isEmailVerification(): Boolean = verification.contains(pathSegments.getOrNull(0))
&& data.toUri().getQueryParameter("email") != null

private fun DeepLink.isChat(): Boolean = chat.contains(pathSegments.getOrNull(0))

// https://app.flipcash.com/tip/chat/{url encoded chatId}
private fun DeepLink.isTipChat(): Boolean =
tip.contains(pathSegments.getOrNull(0)) && chat.contains(pathSegments.getOrNull(1))
Expand Down Expand Up @@ -170,24 +167,6 @@ private fun DeepLink.handleTokenLink(): DeeplinkType.TokenInfo? {
return DeeplinkType.TokenInfo(Mint(mint))
}

// https://app.flipcash.com/chat/{url encoded chatId}
// https://app.flipcash.com/chat/{url encoded e164}
private fun DeepLink.handleChat(): DeeplinkType.Chat? {
val uri = data.toUri()
// pathSegments already percent-decodes; do NOT urlDecode() again
// because URLDecoder treats '+' as a space, mangling phone numbers.
val chatTarget = uri.pathSegments.getOrNull(1) ?: return null

val identifier = if (chatTarget.startsWith("+")) {
ChatIdentifier.ByContact(DeviceContact.unknownContact(e164 = chatTarget))
} else {
val chatId = ChatId(chatTarget.decodeBase64UrlSafe().toList())
ChatIdentifier.ByChatId(chatId)
}

return DeeplinkType.Chat(identifier)
}

// https://app.flipcash.com/tip/chat/{url encoded chatId}
private fun DeepLink.handleTipChat(): DeeplinkType.TipChat? {
val uri = data.toUri()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ data class SessionState(
val contactDmUnreadCount: Int = 0,
val tipsUnreadCount: Int = 0,
val tokens: List<Token> = emptyList(),
val isTippingEnabled: Boolean = false,
)

val LocalSessionController = staticCompositionLocalOf<SessionController?> { null }
Original file line number Diff line number Diff line change
Expand Up @@ -273,10 +273,6 @@ class RealSessionController @Inject constructor(
stateHolder.update { it.copy(tokens = tokens) }
}.launchIn(scope)

featureFlagController.observe(FeatureFlag.Tipping)
.onEach { enabled -> stateHolder.update { it.copy(isTippingEnabled = enabled) } }
.launchIn(scope)

// Retry updateUserFlags when network is restored
networkObserver.state
.map { it.connected }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,15 @@ class SessionStateHolder @Inject constructor() {
* Those flag observers are hot [StateFlow]s that only re-emit on a *value change*.
* A blanket `SessionState()` reset would clobber these fields to their defaults, and
* the observer would not re-push its unchanged current value to repopulate them —
* leaving the UI desynced from the still-persisted flag (e.g. Tipping flag on, but the
* scanner Tips tab gone). Account/token/settings-derived fields are safe to reset:
* leaving the UI desynced from the still-persisted flag (e.g. vibrate-on-scan on, but
* the setting reads as off). Account/token/settings-derived fields are safe to reset:
* their sources re-emit when the account changes, so they self-heal.
*/
fun reset() {
_state.update { prev ->
SessionState(
vibrateOnScan = prev.vibrateOnScan,
showNetworkOffline = prev.showNetworkOffline,
isTippingEnabled = prev.isTippingEnabled,
)
}
}
Expand Down
Loading
Loading