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
131 changes: 131 additions & 0 deletions PLAN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# Credit Card Credits Flow Implementation Plan

## Overview

Route starter-pack credit-card purchases through the existing USD credits
deposit flow instead of creating a direct Coinflow starter-pack checkout. After
credits settle, resume the original bundle purchase automatically.

## Goals

- Always open the credits deposit flow when "Credit Card" is selected.
- Default to the Credits rail whenever the existing balance covers the purchase.
- Display the card-backed purchase amount in USD, not USDC.
- Preserve automatic purchase completion after the deposited credits become
available.

## Non-Goals

- Change Apple Pay, wallet, or direct credits behavior.
- Remove Coinflow support from the standalone credits deposit flow.

## Assumptions and Constraints

- The existing credits quote remains the authoritative bundle price.
- Existing minimum and maximum credits purchase limits remain unchanged.
- Coinflow sandbox deposits do not produce spendable credits and retain the
current warning/error behavior.

## Requirements

### Functional

- Credit-card checkout starts a Coinflow-backed credits deposit.
- The deposit amount covers the credits shortfall and respects the minimum
deposit.
- Successful settlement purchases the requested bundle with credits.
- The review total uses the USD pseudo-token and credits quote.

### Non-Functional

- Keep analytics method attribution as `coinflow` for card selection.
- Reuse the existing stale-purchase and duplicate-completion guards.

## Technical Design

### Data Model

No schema changes.

### API Design

No API changes. Reuse bundle credits quote, credits deposit, balance refresh,
and credits purchase operations.

### Architecture

`Credit Card selection -> credits deposit drawer -> credits settlement -> bundle credits purchase -> success`

### UX Flow

The review screen shows USD. Continue opens the credits amount/deposit flow with
Coinflow preferred; completion resumes the original purchase.

---

## Implementation Plan

### Serial Dependencies (Must Complete First)

#### Phase 0: Shared Calculation

**Prerequisite for:** Checkout routing

| Task | Description | Output |
| ---- | ------------------------------------------------------------ | ---------------------------- |
| 0.1 | Centralize shortfall-to-USD rounding and minimum enforcement | Tested credits top-up helper |

---

### Parallel Workstreams

#### Workstream A: Checkout Routing

**Dependencies:** Phase 0 **Can parallelize with:** Workstream B

| Task | Description | Output |
| ---- | ------------------------------------------------------------- | ------------------------ |
| A.1 | Route card purchases into credits deposit and resume purchase | Updated checkout handler |

#### Workstream B: USD Presentation

**Dependencies:** Phase 0 **Can parallelize with:** Workstream A

| Task | Description | Output |
| ---- | ----------------------------------------------------------- | ---------------------- |
| B.1 | Render card-backed review pricing with the USD pseudo-token | Updated cost breakdown |

---

### Merge Phase

#### Phase 2: Integration

**Dependencies:** Workstreams A, B

| Task | Description | Output |
| ---- | -------------------------------------------------------------------------------------- | -------------------- |
| 2.1 | Remove direct starter-pack Coinflow drawer restrictions and validate the combined flow | Focused, linted diff |

---

## Testing and Validation

- Unit-test top-up rounding, existing-balance subtraction, and minimum
enforcement.
- Run keychain unit tests, lint/format checks, and TypeScript build.

## Rollout and Migration

No migration. Ship with the next controller/keychain release; rollback is a
normal code revert.

## Risks and Mitigations

- Credits settlement delay: retain polling and stale-purchase guards.
- Sandbox cannot mint spendable credits: retain the explicit sandbox failure
message.

## Open Questions

None; the Slack thread defines the required routing and currency behavior.
28 changes: 16 additions & 12 deletions examples/next/src/components/providers/StarknetProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -169,29 +169,33 @@ const provider = jsonRpcProvider({
});

const getKeychainUrl = () => {
const configuredUrl = process.env.NEXT_PUBLIC_KEYCHAIN_FRAME_URL;

if (
process.env.NEXT_PUBLIC_VERCEL_ENV === "preview" &&
process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_REF
) {
let branchName: string;
let branchName = process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_REF.replace(
/[^a-zA-Z0-9-]/g,
"-",
);

const url = window.location.href;
const match = url.match(/git-([a-zA-Z0-9-]+)\.preview/);
// Some Vercel builds report the fallback branch name "update-ui". In
// that case, recover the branch name from the current URL.
if (branchName === "update-ui") {
const match = window.location.href.match(/git-([a-zA-Z0-9-]+)\.preview/);

if (match && match[1]) {
branchName = match[1];
} else {
branchName = process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_REF.replace(
/[^a-zA-Z0-9-]/g,
"-",
);
if (match && match[1]) {
branchName = match[1];
}
}

const keychainUrl = `https://keychain-git-${branchName}.preview.cartridge.gg/`;
const keychainPreviewLabel = `keychain-git-${branchName}`;
const keychainUrl = `https://${keychainPreviewLabel}.preview.cartridge.gg/`;

return keychainUrl;
} else {
return process.env.NEXT_PUBLIC_KEYCHAIN_FRAME_URL;
return configuredUrl;
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
import { CoinbaseDrawer } from "@/components/purchase/checkout/coinbase/drawer";
import { useCoinbase } from "@/hooks/payments/coinbase";
import { waitForCryptoPaymentConfirmation } from "@/hooks/payments/crypto";
import { useUsdcToken } from "@/hooks/payments/usdc";
import { CREDITS_TOKEN } from "@/components/purchase/review/cost";
import { MIN_CREDITS_PURCHASE_USD } from "@/utils/credits";
import { ErrorCard } from "@/components/purchase/checkout/onchain/error";
import { CoinbaseOnrampStatus } from "@/utils/api";
Expand Down Expand Up @@ -51,7 +51,6 @@ export function CoinbaseCreditsCheckout({
onChangeAmount,
}: CoinbaseCreditsCheckoutProps) {
const { isMainnet } = useConnection();
const usdcToken = useUsdcToken();
const [orderError, setOrderError] = useState<Error | null>(null);

const coinbase = useCoinbase({ onError: setOrderError });
Expand Down Expand Up @@ -217,7 +216,7 @@ export function CoinbaseCreditsCheckout({
amount={amount}
onChangeMethod={onChangeMethod}
onChangeAmount={onChangeAmount}
costToken={usdcToken}
costToken={CREDITS_TOKEN}
costValue={
coinbaseQuote ? (
<span className="text-foreground-100">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import {
type CoinflowRailContextValue,
} from "@/components/purchase/checkout/rails";
import { CoinflowDrawer } from "@/components/purchase/checkout/coinflow/drawer";
import { convertCentsToDollars } from "@/components/purchase/review/cost";
import { useUsdcToken } from "@/hooks/payments/usdc";
import {
CREDITS_TOKEN,
convertCentsToDollars,
} from "@/components/purchase/review/cost";
import {
useCoinflowCreditsPayment,
useCoinflowIsMainnet,
Expand Down Expand Up @@ -47,7 +49,6 @@ export function CoinflowCreditsCheckout({
onChangeAmount,
}: CoinflowCreditsCheckoutProps) {
const { createIntent, env, isLoading, error } = useCoinflowCreditsPayment();
const usdcToken = useUsdcToken();
const { isCoinflowSandbox } = useCoinflowIsMainnet();
const [intent, setIntent] = useState<CoinflowIntent>();
const { phase, verifying, handleContinue, backToReview } =
Expand Down Expand Up @@ -112,7 +113,7 @@ export function CoinflowCreditsCheckout({
amount={amount}
onChangeMethod={onChangeMethod}
onChangeAmount={onChangeAmount}
costToken={usdcToken}
costToken={CREDITS_TOKEN}
costValue={
intent ? (
<span className="text-foreground-100">
Expand Down
Loading
Loading