Skip to main content

Payments (Stripe) — Technical Reference

← Back to Payments (Stripe)

Where this lives

Backend

Frontend

The old apps/frontend-nextjs/src/page-components/PaymentsPage.tsx is no longer wired to any route (/payments redirects instead of rendering it) — it has been superseded by the two settings/ pages above.

Technical implementation checklist

  • addPaymentMethod — resolver wired in payment-methods.resolvers.js; called from PaymentMethodsPage.tsx
  • myPaymentMethods — query wired; PaymentMethodsPage.tsx lists cards via Apollo useQuery
  • purchaseCoinsWithPayment — resolver wired in coin-purchase.resolver.js, backed by the Stripe PaymentIntent service; called from StripeCheckout.tsx
  • myPaymentTransactions / paymentTransactionStats — resolvers wired in payment-transaction.resolver.js; queried by PaymentHistoryPage.tsx at /settings/payment-history, distinct from the coin-purchase-only history shown by CoinTransactionsPage.tsx / myCoinPurchases
  • removePaymentMethod — resolver wired; called from PaymentMethodsPage.tsx; dispatches to PayPal's delete-payment-token API for provider: 'paypal' methods
  • createPaypalVaultSetupToken / confirmPaypalVaultSetup — save a PayPal account as a reusable payment method; wired in payment-methods.resolvers.js + PaymentMethodsPage.tsx's "Connect PayPal" flow

PaymentTransaction model

FieldDescription
providerAlways stripe
providerPaymentIntentIdStripe PaymentIntent ID
typecoin_purchase / subscription / tip / other
purposeHuman-readable purpose
amount / currencyAmount and currency
feeAmount / netAmountStripe fee and net received
statuspending / processing / completed / failed / refunded / cancelled
failureCode / failureMessageFailure details
refundAmount / refundReasonRefund info

Payment method types

enum PaymentMethodType {
credit_card · debit_card · paypal · stripe · apple_pay · google_pay
}

Queries

myPaymentTransactions returns the authenticated user's payment history. Filter by status (e.g. completed) or purpose to narrow results. Use this to power an "Order history" screen.

paymentTransaction fetches a single transaction by ID — useful for a post-purchase confirmation screen or support flows where failureMessage needs to be surfaced.

paymentTransactionStats returns aggregate counters without paginating through every record. Use it for the "total spent" summary on the billing page.

query MyPaymentTransactions($status: String, $purpose: String, $limit: Int, $offset: Int) {
myPaymentTransactions(status: $status, purpose: $purpose, limit: $limit, offset: $offset) {
id type purpose amount currency status createdAt
providerPaymentIntentId
}
}

query PaymentTransaction($id: ID!) {
paymentTransaction(transactionId: $id) { id amount status failureMessage }
}

query PaymentStats {
paymentTransactionStats {
totalTransactions totalAmount completedTransactions failedTransactions
}
}

Payment methods

Users can save Stripe payment methods for faster future purchases. The billingAddress block is required by Stripe for card verification.

myPaymentMethods lists all saved methods. Pass includeInactive: true to also show expired or removed cards (useful in support flows).

addPaymentMethod attaches a Stripe PaymentMethod ID (from the client-side Stripe.js flow) to the user's account. Pass setAsDefault: true to make it the default immediately.

setDefaultPaymentMethod changes which saved card is charged by default on future purchases.

removePaymentMethod detaches the method from both Closegram and Stripe. Can't remove the default method if others exist — set a new default first.

syncPaymentMethods resolves drift between the local database and Stripe's records. Call it if a user reports a card showing as valid when it was deleted in Stripe.

query MyPaymentMethods($includeInactive: Boolean) {
myPaymentMethods(includeInactive: $includeInactive) {
id type cardBrand cardLast4 cardExpMonth cardExpYear
isDefault isActive billingName billingEmail
billingAddress { line1 line2 city state postalCode country }
}
}

mutation AddPaymentMethod($paymentMethodId: String!, $setAsDefault: Boolean) {
addPaymentMethod(paymentMethodId: $paymentMethodId, setAsDefault: $setAsDefault) {
success message paymentMethod { id cardBrand cardLast4 isDefault }
}
}
mutation SetDefaultPaymentMethod($paymentMethodId: ID!) {
setDefaultPaymentMethod(paymentMethodId: $paymentMethodId) { success }
}
mutation RemovePaymentMethod($paymentMethodId: ID!) {
removePaymentMethod(paymentMethodId: $paymentMethodId) { success message }
}
# Sync stored methods with Stripe (resolves drift between local DB and Stripe)
mutation SyncPaymentMethods { syncPaymentMethods { success count } }

PayPal as a saved payment method (Vault API v3)

myPaymentMethods isn't Stripe-only: a user can also save a PayPal account (type: paypal), using PayPal's Vault API v3 instead of Stripe's payment-methods API. It reuses the same PaymentMethod/PaymentCustomer models — provider: 'paypal' on both — so myPaymentMethods, setDefaultPaymentMethod, and removePaymentMethod all work on a saved PayPal account exactly like they do on a card. removePaymentMethod dispatches by provider: for a PayPal method it calls PayPal's delete-payment-token endpoint instead of Stripe's detach.

Saving one is a two-step flow, driven from PaymentMethodsPage.tsx's "Connect PayPal" button:

  1. createPaypalVaultSetupToken — starts a PayPal setup token (no purchase attached). The frontend passes its setupTokenId into <PayPalButtons createVaultSetupToken={...}> (@paypal/react-paypal-js), which opens the approval popup.
  2. confirmPaypalVaultSetup(setupTokenId, setAsDefault) — called from onApprove once the buyer approves; exchanges the setup token for a permanent PayPal payment token and creates the PaymentMethod row (billingEmail from PayPal's response).
mutation CreatePaypalVaultSetupToken {
createPaypalVaultSetupToken { setupTokenId }
}

mutation ConfirmPaypalVaultSetup($setupTokenId: String!, $setAsDefault: Boolean) {
confirmPaypalVaultSetup(setupTokenId: $setupTokenId, setAsDefault: $setAsDefault) {
success message
paymentMethod { id type isDefault isActive billingEmail }
}
}

Once saved, it can be charged directly — see Coins & Tips → Alternative payment methods for purchaseCoinsWithSavedPaypal, which charges the vaulted token server-side with no popup.

No new environment variables: this reuses PAYPAL_CLIENT_ID / PAYPAL_CLIENT_SECRET / PAYPAL_API_BASE / NEXT_PUBLIC_PAYPAL_CLIENT_ID from the one-time PayPal checkout setup — see apps/backend/docs/PAYMENTS_WALLETS_PAYPAL.md.

Coin purchase flow

1. User selects a coin package on /settings/get-coins
2. Frontend calls createPaymentIntent
3. Backend creates a Stripe PaymentIntent → returns clientSecret
4. Frontend renders <Elements> with the clientSecret
5. User enters card details and confirms
6. Stripe confirms payment → webhook notifies backend
7. Backend receives payment_intent.succeeded
8. Backend credits coins (CoinPurchase + CoinTransaction records)
9. Apollo cache updates myCoinBalance

Security

  • Card data never passes through Closegram servers — Stripe handles it directly.
  • STRIPE_SECRET_KEY lives only on the backend.
  • NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY is the only key exposed to the frontend.
  • Purchases are validated via a webhook signed with STRIPE_WEBHOOK_SECRET.

Frontend libraries

PackagePurpose
@stripe/stripe-jsLoads Stripe.js asynchronously
@stripe/react-stripe-js<Elements>, <CardElement>, useStripe()