plans/new-checkout-flow.md
This plan covers:
features/pro/File: promoConfig.ts
- description: 'for launch month',
+ description: 'during Takeout 2 beta',
| Tier | Name | Price | SLA | Description |
|---|---|---|---|---|
chat | Chat | Included | None | Community chat room access, no SLA guarantee |
direct | Direct | $500/mo | 2 business days | 5 bug fixes/year, 2 business day response, fixes prioritized |
sponsor | Sponsor | $2,000/mo | 1 day | Unlimited priority fixes, 1 day response, monthly video call |
Base plan explicitly has NO support - just chat access to community room.
Remove: Support tab Add: ToggleGroup for support selection inline on Pro tab
┌─────────────────────────────────────────────────┐
│ [X] │
│ ┌──────────────────┬───────────────────┐ │
│ │ Pro │ FAQ │ │
│ └──────────────────┴───────────────────┘ │
│ │
│ The best cross-platform React + RN stack │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │3 Stacks │ │ Bento │ │ 1 Year │ │
│ │Takeout │ │ Pro │ │ Updates │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │Unlimited │ │ Discord │ │ Lifetime │ │
│ │Team │ │ Chat │ │ Rights │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ ┌─────────────────────────────────────────┐ │
│ │ • Per-project license (web + iOS + Android) │
│ │ • After 1 year: $300/year for updates │ │
│ │ • Buy multiple projects anytime │ │
│ └─────────────────────────────────────────┘ │
│ │
│ Support Level │
│ ┌────────┬────────┬──────────┐ │
│ │ Chat │ Direct │ Sponsor │ │
│ │ incl. │$500/mo │$2,000/mo │ │
│ └────────┴────────┴──────────┘ │
│ [Selected tier description appears here] │
│ │
│ ───────────────────────────────────────────── │
│ $̶9̶9̶9̶ $499 one-time [ Checkout → ] │
│ 50% off! during Takeout 2 beta │
│ │
│ For companies with >$1M revenue, contact us │
│ for enterprise pricing: [email protected] │
│ │
│ [License] [Policies] [Stripe logo] │
└─────────────────────────────────────────────────┘
Proposed new questions:
Can I buy licenses for multiple projects?
Yes! Each license covers one project (web domain + iOS + Android apps). You can purchase additional project licenses anytime. Update subscriptions are always $300/year per project regardless of when you buy.
What's the difference between support levels?
Chat (Included): Access to the private #takeout Discord channel. No SLA guarantee. Direct ($500/mo): 5 bug fixes per year, guaranteed response within 2 business days, your issues get prioritized. Sponsor ($2,000/mo): Unlimited top-priority bug fixes, 1 day response time, plus a monthly video call.
What about companies with significant revenue?
Companies with over $1M in annual revenue should contact us at [email protected] for enterprise pricing. The standard license is intended for bootstrapped companies, solo developers, and early-stage startups.
What support do I get in the base plan? (UPDATE existing)
The base plan includes access to the private #takeout Discord channel. We prioritize responses there over the public Discord, but there is no guaranteed SLA. For guaranteed response times and bug fix commitments, see our Direct and Sponsor support tiers.
features/
├── site/purchase/ # 21 files, all mixed together
│ ├── *ModalStore.ts # 5 store files
│ ├── *Modal.tsx # 5 modal components
│ ├── Stripe*.tsx # Stripe-dependent
│ ├── *.tsx # UI utilities
│ └── *.ts # Hooks, config
└── stripe/ # 4 files, backend config
├── products.ts
├── stripe.ts
├── tiers.ts # Legacy/unused?
└── types.ts
features/
└── pro/
├── index.ts # Public exports
│
├── stores/
│ ├── index.ts # Re-exports all stores
│ ├── purchaseModal.ts # Overview modal state
│ ├── checkoutModal.ts # Payment modal state (renamed from paymentModal)
│ ├── accountModal.ts # Account modal state
│ ├── teamModal.ts # Team member modal state
│ └── takeout.ts # Takeout-specific state
│
├── config/
│ ├── index.ts
│ ├── promo.ts # Promo config
│ ├── products.ts # Stripe product IDs (from features/stripe/)
│ ├── tiers.ts # Support tier definitions (NEW)
│ └── constants.ts # V2_LICENSE_PRICE, etc.
│
├── hooks/
│ ├── index.ts
│ ├── useSubscriptionModal.ts
│ ├── useProducts.ts
│ └── useTeamSeats.ts
│
├── components/
│ ├── index.ts
│ ├── PurchaseButton.tsx # From helpers.tsx
│ ├── PoweredByStripe.tsx
│ ├── FeatureGrid.tsx # NEW - grid of included features
│ ├── SupportTierToggle.tsx # NEW - the ToggleGroup
│ ├── PricingDisplay.tsx # NEW - price with promo
│ └── FundingNotice.tsx # NEW - "contact us for enterprise"
│
├── modals/
│ ├── index.ts # Lazy exports
│ │
│ ├── purchase/ # The overview modal (NO STRIPE JS)
│ │ ├── PurchaseModal.tsx # Main modal shell
│ │ ├── ProTab.tsx # Features + support toggle
│ │ └── FaqTab.tsx # FAQ content
│ │
│ ├── checkout/ # Payment modal (HAS STRIPE JS)
│ │ ├── CheckoutModal.tsx # Lazy-loaded, contains Stripe Elements
│ │ ├── PaymentForm.tsx # The actual form
│ │ └── OrderSummary.tsx # Right-side summary
│ │
│ ├── account/ # Account dashboard
│ │ ├── AccountModal.tsx
│ │ ├── PlanTab.tsx
│ │ ├── UpgradeTab.tsx # Support tier changes
│ │ ├── ManageTab.tsx
│ │ ├── TeamTab.tsx
│ │ └── DiscordPanel.tsx
│ │
│ └── shared/ # Shared modal pieces
│ ├── AgreementModal.tsx
│ ├── PoliciesModal.tsx
│ └── FaqContent.tsx # Reusable FAQ
│
├── utils/
│ ├── index.ts
│ ├── getProductInfo.ts
│ └── calculatePrice.ts
│
└── types.ts # All TypeScript types
Current: NewPurchaseModal.tsx lazy-imports StripePaymentModal.tsx
Problem: Still loads some Stripe types/deps at import time
New Architecture:
// features/pro/modals/purchase/PurchaseModal.tsx
// NO Stripe imports at all - just UI + stores
const CheckoutModal = lazy(() => import('../checkout/CheckoutModal'))
export function PurchaseModal() {
const store = usePurchaseStore()
const [checkoutOpen, setCheckoutOpen] = useState(false)
return (
<>
<Dialog open={store.show}>
<Button onPress={() => setCheckoutOpen(true)}>Checkout</Button>
</Dialog>
{/*
Key pattern: Mount CheckoutModal when purchase modal opens (not on checkout click)
This starts lazy-loading Stripe JS in background while user browses.
By the time they click "Checkout", Stripe is already loaded and ready.
*/}
{store.show && (
<Suspense fallback={null}>
<CheckoutModal open={checkoutOpen} onOpenChange={setCheckoutOpen} />
</Suspense>
)}
</>
)
}
// features/pro/modals/checkout/CheckoutModal.tsx
// ALL Stripe deps isolated here - loaded when purchase modal opens
import { Elements, PaymentElement } from '@stripe/react-stripe-js'
import { loadStripe } from '@stripe/stripe-js'
type Props = {
open: boolean
onOpenChange: (open: boolean) => void
}
export function CheckoutModal({ open, onOpenChange }: Props) {
// This file contains all Stripe JS
// By the time this renders, Stripe is already loading in background
return (
<Dialog open={open} onOpenChange={onOpenChange}>
</Dialog>
)
}
Benefits:
Current: Two nearly identical stores
purchaseModalStore.ts - has show, pricing fields, promo fieldspaymentModalStore.ts - has show, pricing fields, promo fields, V2 fieldsNew: Single source of truth with clear separation
// features/pro/stores/purchaseModal.ts
class PurchaseStore {
// Overview modal state
show = false
tab: 'pro' | 'faq' = 'pro'
// Selection state (shared with checkout)
supportTier: 'chat' | 'direct' | 'sponsor' = 'chat'
// Promo state
activePromo: PromoConfig | null = null
prefilledCouponCode: string | null = null
}
// features/pro/stores/checkoutModal.ts
class CheckoutStore {
show = false
// V2 project info (collected at checkout)
projectName = ''
projectDomain = ''
// Payment state
isProcessing = false
error: Error | null = null
}
Current: features/stripe/products.ts contains product IDs
New: Move to features/pro/config/products.ts (keep stripe backend client separate)
features/
├── pro/config/products.ts # Product IDs, tier prices, etc.
└── stripe/stripe.ts # Just the backend Stripe client init
promoConfig.tsFaqTabContent.tsxfeatures/pro/ directory structureCheckoutModal with all Stripe depsPurchaseModal to lazy-load itWhen user is logged in and wants to change support tier:
Simple flow:
No fancy UI needed - reuse the same CheckoutModal, just with different parameters.
FAQ content - should I show you the proposed FAQ updates before implementing?
Funding threshold - confirmed $1M revenue. Should this be:
Support tier names - Chat | Direct | Sponsor good? Or prefer:
Community | Priority | DedicatedBasic | Pro | EnterpriseCode reorg timing - do you want:
features/pro/
├── index.ts
├── stores/index.ts
├── stores/purchaseModal.ts
├── stores/checkoutModal.ts
├── stores/accountModal.ts
├── stores/teamModal.ts
├── stores/takeout.ts
├── config/index.ts
├── config/promo.ts
├── config/products.ts
├── config/tiers.ts
├── config/constants.ts
├── hooks/index.ts
├── hooks/useSubscriptionModal.ts
├── hooks/useProducts.ts
├── hooks/useTeamSeats.ts
├── components/index.ts
├── components/FeatureGrid.tsx
├── components/SupportTierToggle.tsx
├── components/PricingDisplay.tsx
├── components/FundingNotice.tsx
├── modals/index.ts
├── modals/purchase/PurchaseModal.tsx
├── modals/purchase/ProTab.tsx
├── modals/purchase/FaqTab.tsx
├── modals/checkout/CheckoutModal.tsx
├── modals/checkout/PaymentForm.tsx
├── modals/checkout/OrderSummary.tsx
├── modals/account/AccountModal.tsx
├── modals/account/PlanTab.tsx
├── modals/account/UpgradeTab.tsx
├── modals/account/ManageTab.tsx
├── modals/account/TeamTab.tsx
├── modals/account/DiscordPanel.tsx
├── modals/shared/AgreementModal.tsx
├── modals/shared/PoliciesModal.tsx
├── modals/shared/FaqContent.tsx
├── utils/index.ts
├── utils/getProductInfo.ts
├── utils/calculatePrice.ts
└── types.ts
app/(site)/_layout.tsx - Update importsfeatures/site/purchase/features/site/purchase/ (entire directory)features/stripe/tiers.ts (if unused)Based on analysis of 10+ files across the tamagui.dev codebase, these are the patterns to follow:
// Use YStack for vertical layouts, XStack for horizontal
<YStack gap="$4" p="$6">
<XStack gap="$3" items="center" justify="space-between">
$sm // small
$gtSm // > 768px
$gtMd // > 1024px
$gtLg // > 1280px
$maxMd // max-width 1024px (for mobile sheets)
// Usage
$gtMd={{ p: '$8', gap: '$6' }}
$maxMd={{ flexDirection: 'column' }}
<Dialog modal open={store.show} onOpenChange={(val) => (store.show = val)}>
<Dialog.Adapt when="maxMd">
<Sheet modal transition="quick">
<Sheet.Frame bg="$color1" p={0}>
<Sheet.ScrollView>
<Dialog.Adapt.Contents />
</Sheet.ScrollView>
</Sheet.Frame>
<Sheet.Overlay bg="$shadow4" />
</Sheet>
</Dialog.Adapt>
<Dialog.Portal zIndex={1_000_000}>
<Dialog.Overlay backdropFilter="blur(35px)" bg="$shadow2" />
<Dialog.Content bordered elevate width="90%" maxW={900} p={0}>
</Dialog.Content>
</Dialog.Portal>
</Dialog>
<Tabs
flex={1}
value={currentTab}
onValueChange={setCurrentTab}
orientation="horizontal"
flexDirection="column"
>
<Tabs.List>
<YStack width="50%" flex={1}>
<Tabs.Tab value="pro" unstyled items="center" justify="center" height={60}>
<Paragraph fontFamily="$mono" size="$7">
Pro
</Paragraph>
</Tabs.Tab>
</YStack>
</Tabs.List>
<Tabs.Content value={currentTab} forceMount flex={1} minH={550}>
</Tabs.Content>
</Tabs>
// Headers
<H3 fontFamily="$mono" size="$6">Section Title</H3>
// Body text (muted)
<Paragraph color="$color10" size="$4">Description text</Paragraph>
// Body text (strong)
<Paragraph color="$color11" size="$5">Important text</Paragraph>
// Small text
<SizableText size="$2" color="$color9">Fine print</SizableText>
// Price display
<H3 size="$11" letterSpacing={-2}>$499</H3>
// Semantic colors
$color1 // lightest background
$color3 // panel/card backgrounds
$color9-10 // muted text
$color11-12 // strong text
$borderColor // borders
// Theme wrapping for semantic colors
<Theme name="accent">
<Theme name="yellow">
<Theme name="green">
<Theme name="red">
<YStack
borderWidth={1}
borderColor="$color3"
rounded="$4"
p="$4"
gap="$3"
bg="$color1"
hoverStyle={{ bg: '$color2' }}
>
<H3 fontFamily="$mono" size="$6">
{title}
</H3>
<Paragraph color="$color10">{description}</Paragraph>
</YStack>
<XStack flexWrap="wrap" gap="$3" items="center" justify="center">
{features.map((feature) => (
<YStack
key={feature.id}
borderWidth={1}
borderColor="$color4"
rounded="$4"
p="$3"
width={140}
items="center"
gap="$2"
>
<feature.icon size={24} color="$color11" />
<Paragraph size="$3" text="center">
{feature.label}
</Paragraph>
</YStack>
))}
</XStack>
// Primary action
<Theme name="accent">
<Button rounded="$10" size="$4">
<Button.Text fontFamily="$mono">Checkout</Button.Text>
</Button>
</Theme>
// Secondary/text button
<SizableText
color="$color10"
cursor="pointer"
textDecorationLine="underline"
hoverStyle={{ color: '$color11' }}
size="$2"
onPress={handlePress}
>
License
</SizableText>
<ToggleGroup
type="single"
value={supportTier}
onValueChange={setSupportTier}
orientation="horizontal"
>
<ToggleGroup.Item value="chat" flex={1}>
<YStack items="center" gap="$1" p="$3">
<Paragraph fontWeight="600">Chat</Paragraph>
<Paragraph size="$2" color="$color9">
included
</Paragraph>
</YStack>
</ToggleGroup.Item>
<ToggleGroup.Item value="direct" flex={1}>
<YStack items="center" gap="$1" p="$3">
<Paragraph fontWeight="600">Direct</Paragraph>
<Paragraph size="$2" color="$color9">
$500/mo
</Paragraph>
</YStack>
</ToggleGroup.Item>
<ToggleGroup.Item value="sponsor" flex={1}>
<YStack items="center" gap="$1" p="$3">
<Paragraph fontWeight="600">Sponsor</Paragraph>
<Paragraph size="$2" color="$color9">
$2,000/mo
</Paragraph>
</YStack>
</ToggleGroup.Item>
</ToggleGroup>
// Dialog/Sheet animations
transition="quick"
enterStyle={{ y: -10, opacity: 0, scale: 0.975 }}
exitStyle={{ y: 10, opacity: 0, scale: 0.975 }}
// Hover states
hoverStyle={{ bg: '$backgroundHover', y: -2 }}
pressStyle={{ bg: '$backgroundPress', y: 0 }}
// CSS transition classes
className="transition all ease-in ms100"
$1 = 4px (tiny)
$2 = 8px (small gaps)
$3 = 12px (common gaps)
$4 = 16px (standard padding/gaps)
$5 = 20px (medium)
$6 = 24px (section padding)
$8 = 32px (large)
$10 = 40px (section separators)
<Theme name="yellow">
<XStack bg="$color3" rounded="$4" borderWidth={0.5} borderColor="$color8" p="$3">
<Paragraph size="$3" color="$color11">
For companies with over $1M in annual revenue,{' '}
<Link href="mailto:[email protected]">contact us</Link> for enterprise pricing.
</Paragraph>
</XStack>
</Theme>
<XStack items="baseline" gap="$2">
{activePromo && (
<H3
size="$10"
fontWeight="200"
opacity={0.5}
textDecorationLine="line-through"
color="$green10"
>
${originalPrice.toLocaleString()}
</H3>
)}
<H3 size="$11" letterSpacing={-2}>
${discountedPrice.toLocaleString()}
</H3>
</XStack>
<Paragraph color="$color9" size="$3">
{activePromo?.label}! {subscriptionMessage}
</Paragraph>
const CheckoutModal = lazy(() => import('../checkout/CheckoutModal'))
// In component:
{
store.show && (
<Suspense fallback={null}>
<CheckoutModal open={checkoutOpen} onOpenChange={setCheckoutOpen} />
</Suspense>
)
}
// Definition
class PurchaseStore {
show = false
supportTier: 'chat' | 'direct' | 'sponsor' = 'chat'
activePromo: PromoConfig | null = null
}
export const purchaseStore = createStore(PurchaseStore)
export const usePurchaseStore = createUseStore(PurchaseStore)
// Usage
const store = usePurchaseStore()
store.show = true
store.supportTier = 'direct'