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
30 changes: 26 additions & 4 deletions apps/flipcash/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -187,11 +187,33 @@
</intent-filter>

<!--
No `/chat/.*` App Link. AppRouter deliberately does not classify chat links
(the direct-send entry point they opened was removed), so claiming the URL
here would capture it from the browser and dead-end the user on the home
screen. Restore this filter only alongside routing in AppRouter.
Group chat invites: `app.flipcash.com/chat/{uuid}` (Linkify.groupChatInvite).
Claimed here because AppRouter now classifies them — the two go together, and
the path was left unclaimed while it did not.

`.*` rather than the UUID shape: `pathPattern` has no charset form, and
`pathAdvancedPattern` is ignored below API 31, so the shape check would not hold
on every version anyway. AppRouter makes it instead. A `/chat/` link that is not
an invite is therefore captured and does nothing — the same as any other path on
an app-owned host that AppRouter has no route for (DeeplinkAction.None). Only the
apex is handed back to a browser, because only its path space is shared with the
website.

No assetlinks change: App Links verification is per-host, and `app.flipcash.com`
is already verified by the filters above. Adding a path to a verified host does
not re-open verification.
-->
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />

<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />

<data
android:host="app.flipcash.com"
android:pathPattern="/chat/.*"
android:scheme="https" />
</intent-filter>

<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ import com.flipcash.app.balance.WalletScreen
import com.flipcash.app.cash.CashScreen
import com.flipcash.app.contact.verification.VerificationFlowScreen
import com.flipcash.app.currencycreator.CurrencyCreatorFlowScreen
import com.flipcash.app.tipping.FindByUsernameScreen
import com.flipcash.app.tipping.NewChatScreen
import com.flipcash.app.tipping.NewGroupFlowScreen
import com.flipcash.app.tipping.TippingFlowScreen
import com.flipcash.shared.transactionhistory.ActivityHistoryScreen
import com.flipcash.app.core.AppRoute
Expand Down Expand Up @@ -112,6 +114,10 @@ fun appEntryProvider(
ChatFlowScreen(route = key, resultStateRegistry = resultStateRegistry)
}
annotatedEntry<AppRoute.Messaging.NewChat> { NewChatScreen() }
annotatedEntry<AppRoute.Messaging.FindByUsername> { FindByUsernameScreen() }
annotatedEntry<AppRoute.Messaging.NewGroup> { key ->
NewGroupFlowScreen(route = key, resultStateRegistry = resultStateRegistry)
}

// Tokens
annotatedEntry<AppRoute.Token.Info>(testTag = "token_info_screen") { key ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.os.Parcelable
import androidx.navigation3.runtime.NavKey
import com.flipcash.app.core.chat.ChatIdentifier
import com.flipcash.app.core.chat.ChatStep
import com.flipcash.app.core.chat.NewGroupStep
import com.flipcash.app.core.deposit.DepositResult
import com.flipcash.app.core.deposit.DepositStep
import com.flipcash.app.core.onboarding.OnboardingStep
Expand Down Expand Up @@ -329,16 +330,34 @@ sealed interface AppRoute : NavKey, Parcelable {
}

/**
* Starting a chat by typing someone's `@handle` (node 9442:5825), reached from the "+" on
* the Chats list.
* Node 10127:117987 — the two ways to start a chat, reached from the "+" on the Chats list.
*
* A top-level route rather than a step of the tipping flow, even though the Chats list it
* is reached from is one: the flow is a tab home, so a step pushed inside it keeps the tab
* bar. This covers it, the way [Chat] does.
* bar. This covers it, the way [Chat] does. [FindByUsername] and [NewGroup] are pushed on
* top of it for the same reason.
*/
@Serializable
@Parcelize
data object NewChat : Messaging

/** Node 9442:5825 — starting a chat by typing someone's `@handle`. */
@Serializable
@Parcelize
data object FindByUsername : Messaging

/**
* Node 10153:22901 — creating a public group, from the empty form to its invite link.
*
* The three screens share one draft, so they are steps of a flow rather than routes; see
* [NewGroupStep].
*/
@Serializable
@Parcelize
data object NewGroup : Messaging, FlowRoute {
override val initialStack: List<NavKey>
get() = listOf(NewGroupStep.Form)
}
}

@Serializable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,18 @@ sealed interface ChatStep : FlowStep, Parcelable {
data object InitPayment :
ChatStep, NavigationRetVal<ChatSendResult>, Sheet, WrapContentSheet

/**
* Node 10127:118315 — the two ways to hand out a group's invite link.
*
* A [WrapContentSheet] over the conversation rather than a screen of its own: it is two rows,
* and inviting is something you do *from* the group, not a place you go. Reached from the empty
* transcript's CTA and from the group's profile, which is why it hangs off the chat flow rather
* than the create flow that used to end on it.
*/
@Parcelize
@Serializable
data object InviteToGroup : ChatStep, Sheet, WrapContentSheet

@Parcelize
@Serializable
data class Profile(val contact: ChatParticipant): ChatStep
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.flipcash.app.core.chat

import android.os.Parcelable
import com.getcode.navigation.Sheet
import com.getcode.navigation.flow.FlowStep
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable

/**
* The steps of creating a public group — section 10153:22901.
*
* A flow rather than three pushed routes because all three read and write one draft: the title, the
* picture, the mint and the amount are entered on [Form], and the mint and the amount are changed
* on [SelectCurrency] and [CustomAmount]. A shared view model
* ([com.getcode.navigation.flow.flowSharedViewModel]) holds that draft for the flow's lifetime,
* which is also what keeps one create attempt's idempotency key alive across a retry.
*
* Creating the chat ends the flow. The invite link is handed out from inside the group
* ([ChatStep.InviteToGroup]), so it is not a step here.
*/
@Serializable
sealed interface NewGroupStep : FlowStep, Parcelable {

/** Node 10127:118014 — name, picture, and the balance rule. */
@Parcelize
@Serializable
data object Form : NewGroupStep

/**
* Node 10127:118100 — which mint the balance requirement is denominated in.
*
* A [Sheet] so the form stays on screen behind it: the picker is a change to one field of the
* draft, and dismissing it leaves the rest of the form exactly as it was.
*/
@Parcelize
@Serializable
data object SelectCurrency : NewGroupStep, Sheet

/**
* The `…` preset — the balance requirement as a free amount rather than one of the three chips.
*
* The design names the chip but not where it lands, so this is the app's standard amount entry
* ([com.flipcash.shared.amountentry.AmountEntryScreen]), the same keypad the minimum-to-chat fee
* is set on. A [Sheet] for the same reason as [SelectCurrency]: it edits one field of the draft
* the form is still holding.
*/
@Parcelize
@Serializable
data object CustomAmount : NewGroupStep, Sheet
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,17 @@ sealed interface DeeplinkType: Parcelable {

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

/**
* A group chat invite — `app.flipcash.com/chat/{uuid}`.
*
* Carries a [ChatId] rather than a [ChatIdentifier] because a group is only ever addressed by
* its own id: there is no counterparty to resolve it through, the way a DM has.
*
* The chat it opens may be one the viewer has not joined. That is the normal case for an
* invite, and the chat screen is already the gated preview for it — see `GroupAccess`.
*/
@Serializable data class GroupChatInvite(val chatId: ChatId): DeeplinkType, Navigatable

/**
* A tip card addressed by account id — `flipcash.com/{uuid}`, or the older
* `app.flipcash.com/tip/{uuid}` that links already shared still carry.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.flipcash.app.core.tokens

import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import com.flipcash.core.R
import com.getcode.opencode.model.financial.Token
import com.getcode.solana.keys.Mint
import com.getcode.util.resources.ResourceHelper

/**
* Whether this token is the reserve — the one holding the app counts as plain dollars.
*
* Worth a name because the reserve is written differently wherever it is named, and the mint
* comparison on its own does not say why a screen is asking.
*/
val Token.isReserve: Boolean
get() = address == Mint.usdf

/**
* What the user calls this token.
*
* The reserve is branded Dollars everywhere they meet it; [Token.name] off the wire is "USDF", which
* is the mint, not the thing they hold. Every other token is called what it is named — the name, not
* the ticker, which is what the balance list and the currency picker show.
*/
fun Token.brandedName(resources: ResourceHelper): String = when {
isReserve -> resources.getString(R.string.displayName_dollars)
else -> name
}

/** [brandedName] where a composable has the resources to hand rather than an injected [ResourceHelper]. */
@Composable
fun Token.brandedName(): String = when {
isReserve -> stringResource(R.string.displayName_dollars)
else -> name
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,24 @@ object Linkify {
fun tokenInfo(token: Token): String = tokenInfo(token.address)
fun tokenInfo(mint: Mint): String = "https://app.flipcash.com/token/${mint.base58()}"
fun tipChatById(chatId: ChatId): String = "https://app.flipcash.com/tip/chat/${chatId.bytes.encodeBase64(urlSafe = true)}"

/**
* A group chat's invite link — `app.flipcash.com/chat/{uuid}`.
*
* The id is written as a dashed UUID rather than as the base64url [tipChatById] uses, because
* `common.v1.ChatId.value` is a 16-byte UUID for a group and that is its canonical text form.
* Nothing mints this link server-side: there is no invite RPC in flipcash2, so the link is the
* chat id and the client builds and parses both ends of it.
*
* Note the `app.` host, unlike [tipcard]'s bare one. The apex shares its path space with the
* website and can only be claimed by path *shape*, which a UUID would satisfy — an invite on
* the apex would be indistinguishable from a tip card addressed by account id. The `app.` host
* belongs to the app whole, so `/chat/` can be claimed there outright.
*
* Null for a chat that has no UUID form: a DM's id is a 32-byte hash, and no invite exists for
* one. Callers hold a group id by construction, so this is unreachable rather than a case to
* render.
*/
fun groupChatInvite(chatId: ChatId): String? =
chatId.bytes.toList().uuid?.let { "https://app.flipcash.com/chat/$it" }
}
14 changes: 14 additions & 0 deletions apps/flipcash/core/src/main/res/drawable/ic_at.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!-- IconAt — "Find by Username" on New Chat (node 10127:118008) and "Send Invite Link" on the
invite screen (node 10127:118328). Full white; tinted at the call site. -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M16.7368 19.6541C15.361 20.5073 13.738 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12C21 13.9262 20.0428 15.9154 17.8101 15.7125C15.9733 15.5455 14.6512 13.8737 14.9121 12.0479L15.4274 8.5M14.8581 12.4675C14.559 14.596 12.8066 16.1093 10.9442 15.8476C9.08175 15.5858 7.81444 13.6481 8.11358 11.5196C8.41272 9.39109 10.165 7.87778 12.0275 8.13953C13.8899 8.40128 15.1573 10.339 14.8581 12.4675Z"
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"
android:strokeWidth="2"
android:strokeLineCap="round" />
</vector>
22 changes: 22 additions & 0 deletions apps/flipcash/core/src/main/res/drawable/ic_camera.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<!-- IconCamera1 — the empty avatar tile on the New Public Group form (node 10127:118273). Full
white; the tile tints it. -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="40"
android:viewportHeight="40">
<path
android:pathData="M8.33333 11.6669H10.716C11.8306 11.6669 12.8713 11.1099 13.4895 10.1825L14.8438 8.1512C15.462 7.22387 16.5028 6.66687 17.6173 6.66687H22.3827C23.4972 6.66687 24.538 7.22387 25.1562 8.1512L26.5105 10.1825C27.1287 11.1099 28.1695 11.6669 29.284 11.6669H31.6667C33.5077 11.6669 35 13.1593 35 15.0002V30.0002C35 31.8412 33.5077 33.3335 31.6667 33.3335H8.33333C6.49238 33.3335 5 31.8412 5 30.0002V15.0002C5 13.1593 6.49238 11.6669 8.33333 11.6669Z"
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"
android:strokeWidth="3.33333"
android:strokeLineCap="square"
android:strokeLineJoin="round" />
<path
android:pathData="M25 21.6669C25 24.4284 22.7615 26.6669 20 26.6669C17.2385 26.6669 15 24.4284 15 21.6669C15 18.9054 17.2385 16.6669 20 16.6669C22.7615 16.6669 25 18.9054 25 21.6669Z"
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"
android:strokeWidth="3.33333"
android:strokeLineCap="square"
android:strokeLineJoin="round" />
</vector>
16 changes: 16 additions & 0 deletions apps/flipcash/core/src/main/res/drawable/ic_chevron_down_small.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!-- IconChevronDownSmall — the mint selector on the New Public Group form (node 10127:118258).
A 24dp stroked chevron rather than the 16dp filled ic_chevron_down_medium, which reads much
heavier once scaled to sit beside 16sp text. -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M8 10L12 14L16 10"
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"
android:strokeWidth="1.5"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
37 changes: 37 additions & 0 deletions apps/flipcash/core/src/main/res/drawable/ic_group_3.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<!-- IconGroup3 — the "Create a Public Group" row on New Chat (node 10127:118001). Exported at 50%
stroke opacity; drawn at full white here so the call site's tint decides the colour, as
ic_people_circle does. -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M11 7C11 8.65685 9.65685 10 8 10C6.34315 10 5 8.65685 5 7C5 5.34315 6.34315 4 8 4C9.65685 4 11 5.34315 11 7Z"
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M19.5 7.5C19.5 8.88071 18.3807 10 17 10C15.6193 10 14.5 8.88071 14.5 7.5C14.5 6.11929 15.6193 5 17 5C18.3807 5 19.5 6.11929 19.5 7.5Z"
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M2 20H14C13.3421 10.6667 2.65792 10.6667 2 20Z"
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M17.9999 19H22.1455C21.8245 14.4468 18.7198 12.4851 16 13.1147"
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
Loading
Loading