Skip to content

Commit 3c88aed

Browse files
committed
feat(currencycreator): support required fees when creating a currency
Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
1 parent b927305 commit 3c88aed

13 files changed

Lines changed: 185 additions & 67 deletions

File tree

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<!--
2+
~ Copyright (C) 2026 The Android Open Source Project
3+
~
4+
~ Licensed under the Apache License, Version 2.0 (the "License");
5+
~ you may not use this file except in compliance with the License.
6+
~ You may obtain a copy of the License at
7+
~
8+
~ http://www.apache.org/licenses/LICENSE-2.0
9+
~
10+
~ Unless required by applicable law or agreed to in writing, software
11+
~ distributed under the License is distributed on an "AS IS" BASIS,
12+
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
~ See the License for the specific language governing permissions and
14+
~ limitations under the License.
15+
-->
16+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
17+
android:width="28dp"
18+
android:height="28dp"
19+
android:viewportWidth="28"
20+
android:viewportHeight="28">
21+
<path
22+
android:pathData="M7.292,5.153C7.292,3.596 8.554,2.333 10.111,2.333C11.717,2.333 13.137,3.128 14,4.345C14.863,3.128 16.283,2.333 17.889,2.333C19.446,2.333 20.708,3.596 20.708,5.153C20.708,6.296 20.306,7.346 19.634,8.167H24.5V13.125H14.875V8.167H15.944C17.609,8.167 18.958,6.817 18.958,5.153C18.958,4.562 18.479,4.083 17.889,4.083C16.224,4.083 14.875,5.433 14.875,7.097V8.167H13.125V7.097C13.125,5.433 11.776,4.083 10.111,4.083C9.52,4.083 9.042,4.562 9.042,5.153C9.042,6.817 10.391,8.167 12.056,8.167H13.125V13.125H3.5V8.167H8.366C7.695,7.346 7.292,6.296 7.292,5.153Z"
23+
android:fillColor="#ffffff"/>
24+
<path
25+
android:pathData="M14.875,14.875H23.333V24.5H14.875V14.875Z"
26+
android:fillColor="#ffffff"/>
27+
<path
28+
android:pathData="M13.125,14.875H4.667V24.5H13.125V14.875Z"
29+
android:fillColor="#ffffff"/>
30+
</vector>

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -375,8 +375,10 @@
375375
<string name="subtitle_currencyCreatorStepDescription">Describe what your currency is about</string>
376376
<string name="title_currencyCreatorStepDesign">Cash Design</string>
377377
<string name="subtitle_currencyCreatorStepDesign">Customize the look of your cash</string>
378-
<string name="title_currencyCreatorStepPurchase">Purchase %1$s</string>
379-
<string name="subtitle_currencyCreatorStepPurchase">Pay for the first %1$s to create your currency</string>
378+
<string name="title_currencyCreatorStepPurchase">Pay %1$s Fee</string>
379+
<string name="subtitle_currencyCreatorStepPurchase">Pay to create your currency</string>
380+
<string name="title_currencyCreatorStepPurchaseFreeGift">Limited Time: Get %1$s Free</string>
381+
<string name="subtitle_currencyCreatorStepPurchaseFreeGift">Get the first %1$s of your currency</string>
380382
<string name="action_getStarted">Get Started</string>
381383
<string name="title_currencyCreatorNameSelection">What do you want to call your currency?</string>
382384
<string name="hint_currencyName">Currency Name</string>
@@ -390,7 +392,7 @@
390392
<string name="label_remaining">remaining</string>
391393
<string name="placeholder_currencyName">Currency Name</string>
392394
<string name="placeholder_currencyTicker">$BadBoys</string>
393-
<string name="action_buyFirstToCreate">Pay for the First %1$s to Create</string>
395+
<string name="action_buyFirstToCreate">Pay %1$s to Create</string>
394396
<string name="title_creatingCurrency">Creating %1$s</string>
395397
<string name="title_currencyIsLive">%1$s Is Live</string>
396398
<string name="subtitle_currencyIsLive">Your currency is ready to receive and use</string>
@@ -600,5 +602,5 @@
600602
<string name="error_description_buyNewCurrencyFailed">We couldn\'t complete your initial purchase. Your currency is on hold for the next 30 minutes.</string>
601603

602604
<string name="subtitle_processingYourNewCurrencyTransaction">This transaction typically takes a few minutes. You may leave the app while it completes</string>
603-
<string name="action_receiveNewCurrency">Receive My %1$s</string>
605+
<string name="action_receiveNewCurrency">Get the First %1$s of Your Currency Free</string>
604606
</resources>

apps/flipcash/features/currency-creator/src/main/kotlin/com/flipcash/app/currencycreator/CurrencyCreatorFlowScreen.kt

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,9 @@ import com.getcode.navigation.flow.PreviewFlowNavigator
5858
import com.getcode.navigation.flow.deliverFlowResult
5959
import com.getcode.navigation.results.NavResultOrCanceled
6060
import com.getcode.navigation.results.NavResultStateRegistry
61+
import com.getcode.opencode.model.financial.Fiat
6162
import com.getcode.opencode.model.financial.LocalFiat
63+
import com.getcode.opencode.model.financial.toFiat
6264
import com.getcode.theme.CodeTheme
6365
import com.getcode.ui.core.unboundedClickable
6466

@@ -83,6 +85,15 @@ fun CurrencyCreatorFlowScreen(
8385
CurrencyCreatorTopBar(
8486
controller = topBarController,
8587
mainContent = when (state.currentStep) {
88+
is CurrencyCreatorStep.Info -> {
89+
{
90+
Text(
91+
text = stringResource(R.string.title_createYourCurrency),
92+
style = CodeTheme.typography.textLarge,
93+
color = CodeTheme.colors.textMain,
94+
)
95+
}
96+
}
8697
is CurrencyCreatorStep.Processing -> {
8798
{
8899
val text = if (state.processingState.success) {
@@ -97,7 +108,6 @@ fun CurrencyCreatorFlowScreen(
97108
}
98109

99110
Text(
100-
modifier = Modifier.fillMaxWidth(),
101111
text = text,
102112
style = CodeTheme.typography.textLarge,
103113
color = CodeTheme.colors.textMain,
@@ -143,15 +153,19 @@ fun CurrencyCreatorFlowScreen(
143153
)
144154

145155
if (result is CurrencyCreatorResult.Success) {
146-
val token = state.launchedToken
147-
if (token != null) {
148-
val bill = Bill.Cash(
149-
token = token,
150-
amount = LocalFiat(usdf = state.purchaseAmount),
151-
didReceive = true,
152-
)
153-
outerNavigator.hide()
154-
session?.showBill(bill)
156+
if (state.purchaseAmount > Fiat.Zero) {
157+
val token = state.launchedToken
158+
if (token != null) {
159+
val bill = Bill.Cash(
160+
token = token,
161+
amount = LocalFiat(usdf = state.purchaseAmount),
162+
didReceive = true,
163+
)
164+
outerNavigator.hide()
165+
session?.showBill(bill)
166+
}
167+
} else {
168+
outerNavigator.pop()
155169
}
156170
} else {
157171
outerNavigator.pop()
@@ -203,24 +217,39 @@ private fun SyncTopBar(step: CurrencyCreatorStep) {
203217
}
204218

205219
@Composable
206-
private fun CurrencyCreatorPreview(content: @Composable (state: CurrencyCreatorViewModel.State) -> Unit) {
220+
private fun CurrencyCreatorPreview(
221+
feeAmount: Fiat = 15.toFiat(),
222+
content: @Composable (state: CurrencyCreatorViewModel.State) -> Unit
223+
) {
207224
FlipcashPreview(showBackground = true) {
208225
CompositionLocalProvider(
209226
LocalFlowNavigator provides PreviewFlowNavigator<CurrencyCreatorStep, CurrencyCreatorResult>(),
210227
LocalCurrencyCreatorTopBar provides remember { CurrencyCreatorTopBarController() },
211228
) {
212-
val state = CurrencyCreatorViewModel.State()
229+
val state = CurrencyCreatorViewModel.State(feeAmount = feeAmount)
213230
content(state)
214231
}
215232
}
216233
}
217234

218235
@Preview
219236
@Composable
220-
private fun Preview_Info() {
237+
private fun Preview_Info_15Fee() {
221238
CurrencyCreatorPreview { InfoScreenContent(it) }
222239
}
223240

241+
@Preview
242+
@Composable
243+
private fun Preview_Info_NoFee() {
244+
CurrencyCreatorPreview(feeAmount = Fiat.Zero) { InfoScreenContent(it) }
245+
}
246+
247+
@Preview
248+
@Composable
249+
private fun Preview_Info_5Fee() {
250+
CurrencyCreatorPreview(feeAmount = 5.toFiat()) { InfoScreenContent(it) }
251+
}
252+
224253
@Preview
225254
@Composable
226255
private fun Preview_Name() {

apps/flipcash/features/currency-creator/src/main/kotlin/com/flipcash/app/currencycreator/internal/CurrencyCreatorViewModel.kt

Lines changed: 50 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ import com.getcode.opencode.model.financial.MintMetadata
4545
import com.getcode.opencode.model.financial.Token
4646
import com.getcode.opencode.model.financial.TokenCreateRequest
4747
import com.getcode.opencode.model.financial.fromLaunch
48+
import com.getcode.opencode.model.financial.minus
49+
import com.getcode.opencode.model.financial.orZero
50+
import com.getcode.opencode.model.financial.plus
4851
import com.getcode.opencode.model.moderation.ModerationAttestation
4952
import com.getcode.opencode.model.transactions.SwapFundingSource
5053
import com.getcode.solana.keys.Mint
@@ -107,7 +110,8 @@ internal class CurrencyCreatorViewModel @Inject constructor(
107110
val bill: Bill? = null,
108111
val createdMint: Mint? = null,
109112
val launchedToken: Token? = null,
110-
val purchaseAmount: Fiat = 20.toFiat(),
113+
val purchaseAmount: Fiat = 5.toFiat(),
114+
val feeAmount: Fiat? = null,
111115
val processingState: LoadingSuccessState = LoadingSuccessState(),
112116
val attestations: ModerationAttestations = ModerationAttestations(),
113117
) {
@@ -132,6 +136,12 @@ internal class CurrencyCreatorViewModel @Inject constructor(
132136
return (index + 1).toFloat() / PROGRESS_STEPS.size
133137
}
134138

139+
val totalCost: Fiat
140+
get() {
141+
val fee = feeAmount.orZero()
142+
return purchaseAmount + fee
143+
}
144+
135145
private companion object {
136146
private const val MAX_DESCRIPTION = 500
137147

@@ -159,7 +169,7 @@ internal class CurrencyCreatorViewModel @Inject constructor(
159169
data class OnIconSelected(val image: Uri) : Event
160170
data class OnIconCached(val image: Uri) : Event
161171

162-
data class OnPurchaseAmountChanged(val amount: Fiat) : Event
172+
data class OnPurchaseAmountChanged(val amount: Fiat, val feeAmount: Fiat) : Event
163173

164174
data class OnBillConfirmed(val bill: Bill?) : Event
165175
data class UpdateProcessingState(
@@ -172,24 +182,28 @@ internal class CurrencyCreatorViewModel @Inject constructor(
172182
data class LaunchToken(val method: PurchaseMethod) : Event
173183
data class OnTokenMinted(val mint: Mint): Event
174184
data object Purchase : Event
175-
data class PurchaseWithReserves(val token: Token, val amount: Fiat) : Event
176-
data class PurchaseWithPhantom(val token: Token, val amount: Fiat) : Event
177-
data class PurchaseWithGooglePay(val token: Token, val amount: Fiat) : Event
185+
data class PurchaseWithReserves(val context: LaunchedContext) : Event
186+
data class PurchaseWithPhantom(val context: LaunchedContext) : Event
187+
data class PurchaseWithGooglePay(val context: LaunchedContext) : Event
178188

179189
data class PurchaseSubmitted(val swapId: SwapId, val mint: Mint) : Event
180190
data class PurchaseCompleted(val token: Token): Event
181191
}
182192

183-
private data class LaunchedContext(
193+
data class LaunchedContext(
184194
val method: PurchaseMethod,
185195
val token: Token,
186196
val amount: Fiat,
197+
val feeAmount: Fiat?,
187198
)
188199

189200
init {
190201
userFlags.resolvedFlags
191-
.map { it.newCurrencyPurchaseAmount.effectiveValue }
192-
.onEach { dispatchEvent(Event.OnPurchaseAmountChanged(it)) }
202+
.onEach { flags ->
203+
val purchaseAmount = flags.newCurrencyPurchaseAmount.effectiveValue
204+
val feeAmount = flags.newCurrencyFeeAmount.effectiveValue
205+
dispatchEvent(Event.OnPurchaseAmountChanged(purchaseAmount, feeAmount))
206+
}
193207
.launchIn(viewModelScope)
194208

195209
// Debounced draft persistence — save state 300ms after changes.
@@ -243,11 +257,11 @@ internal class CurrencyCreatorViewModel @Inject constructor(
243257
.filterIsInstance<Event.CheckName>()
244258
.map { stateFlow.value.nameFieldState.text.toString() }
245259
.onEach { dispatchEvent(Event.UpdateProcessingState(loading = true)) }
246-
.map { moderationController.moderateText(it) }
260+
.map { moderationController.moderateText(it.trim()) }
247261
.flatMapResult { result ->
248262
when (result.flaggedCategory) {
249263
ModerationResult.FlaggedCategory.NONE -> {
250-
currencyController.checkTokenAvailability(result.text)
264+
currencyController.checkTokenAvailability(result.text.trim())
251265
.map { result.attestation }
252266
}
253267

@@ -346,7 +360,7 @@ internal class CurrencyCreatorViewModel @Inject constructor(
346360
.filterIsInstance<Event.CheckDescription>()
347361
.map { stateFlow.value.descriptionFieldState.text.toString() }
348362
.onEach { dispatchEvent(Event.UpdateProcessingState(loading = true)) }
349-
.map { moderationController.moderateText(it) }
363+
.map { moderationController.moderateText(it.trim()) }
350364
.flatMapResult { result ->
351365
when (result.flaggedCategory) {
352366
ModerationResult.FlaggedCategory.NONE -> Result.success(result.attestation)
@@ -389,7 +403,8 @@ internal class CurrencyCreatorViewModel @Inject constructor(
389403
.onEach {
390404
val metadata = PurchaseMethodMetadata(
391405
mint = null,
392-
purchaseAmount = stateFlow.value.purchaseAmount,
406+
purchaseAmount = stateFlow.value.totalCost,
407+
feeAmount = stateFlow.value.feeAmount,
393408
paymentAction = PaymentAction.Pay,
394409
)
395410
purchaseMethodController.present(metadata)
@@ -408,11 +423,11 @@ internal class CurrencyCreatorViewModel @Inject constructor(
408423
println("customizations=${stateFlow.value.customizations}")
409424
val request = TokenCreateRequest(
410425
name = ModerationAttestation.Text(
411-
text = stateFlow.value.nameFieldState.text.toString(),
426+
text = stateFlow.value.nameFieldState.text.trim().toString(),
412427
attestation = stateFlow.value.attestations.name.rawValue,
413428
),
414429
description = ModerationAttestation.Text(
415-
text = stateFlow.value.descriptionFieldState.text.toString(),
430+
text = stateFlow.value.descriptionFieldState.text.trim().toString(),
416431
attestation = stateFlow.value.attestations.description.rawValue
417432
),
418433
icon = ModerationAttestation.Image(
@@ -452,20 +467,25 @@ internal class CurrencyCreatorViewModel @Inject constructor(
452467
request = request,
453468
owner = accountCluster.authorityPublicKey,
454469
)
455-
LaunchedContext(method, token, stateFlow.value.purchaseAmount)
470+
LaunchedContext(
471+
method = method,
472+
token = token,
473+
amount = stateFlow.value.totalCost,
474+
feeAmount = stateFlow.value.feeAmount,
475+
)
456476
}
457477
}
458478
.onResult(
459479
onSuccess = { ctx ->
460480
when (ctx.method) {
461481
is PurchaseMethod.CashReserves ->
462-
dispatchEvent(Event.PurchaseWithReserves(ctx.token, ctx.amount))
482+
dispatchEvent(Event.PurchaseWithReserves(ctx))
463483

464484
PurchaseMethod.PhantomWallet ->
465-
dispatchEvent(Event.PurchaseWithPhantom(ctx.token, ctx.amount))
485+
dispatchEvent(Event.PurchaseWithPhantom(ctx))
466486

467487
PurchaseMethod.CoinbaseOnRamp -> {
468-
dispatchEvent(Event.PurchaseWithGooglePay(ctx.token, ctx.amount))
488+
dispatchEvent(Event.PurchaseWithGooglePay(ctx))
469489
}
470490
}
471491
},
@@ -487,26 +507,30 @@ internal class CurrencyCreatorViewModel @Inject constructor(
487507
AppRoute.Token.CurrencyCreator,
488508
OnRampProvider.Phantom
489509
)
490-
externalWalletController.setAmount(LocalFiat(usdf = event.amount))
491-
externalWalletController.setTokenToPurchase(event.token)
510+
val totalAmount = LocalFiat(usdf = event.context.amount)
511+
println("total amount ${totalAmount.underlyingTokenAmount}")
512+
val feeAmount = event.context.feeAmount?.let { LocalFiat(usdf = it) }
513+
externalWalletController.setAmount(amount = totalAmount, feeAmount = feeAmount)
514+
externalWalletController.setTokenToPurchase(event.context.token)
492515
}
493516
.launchIn(viewModelScope)
494517

495518
eventFlow
496519
.filterIsInstance<Event.PurchaseWithReserves>()
497520
.mapNotNull { event ->
498521
val owner = userManager.accountCluster ?: return@mapNotNull null
499-
Triple(owner, event.token, event.amount)
522+
Pair(owner, event.context)
500523
}
501524
.onEach { dispatchEvent(Event.UpdateProcessingState(loading = true)) }
502-
.map { (owner, token, amount) ->
525+
.map { (owner, context) ->
503526
transactionController.buy(
504527
owner = owner,
505-
amount = LocalFiat(usdf = amount),
506-
of = token,
528+
amount = LocalFiat(usdf = context.amount),
529+
feeAmount = context.feeAmount?.let { LocalFiat(usdf = it) },
530+
of = context.token,
507531
source = SwapFundingSource.SubmitIntent(),
508532
fund = null,
509-
).map { swapId -> swapId to token.address }
533+
).map { swapId -> swapId to context.token.address }
510534
}
511535
.onResult(
512536
onSuccess = { (swapId, mint) ->
@@ -600,7 +624,7 @@ internal class CurrencyCreatorViewModel @Inject constructor(
600624
}
601625

602626
is Event.OnPurchaseAmountChanged -> { state ->
603-
state.copy(purchaseAmount = event.amount)
627+
state.copy(purchaseAmount = event.amount, feeAmount = event.feeAmount)
604628
}
605629

606630
is Event.UpdateProcessingState -> { state ->

0 commit comments

Comments
 (0)