feat: code redemption in billing settings#42377
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
4 Skipped Deployments
|
|
This pull request has been ignored for the connected project Preview Branches by Supabase. |
WalkthroughThis PR introduces credit code redemption functionality to the organization billing interface. It adds a new modal component for redeeming credit codes with form validation and HCaptcha verification, implements supporting backend API endpoints for code preview and redemption, and refactors the CreditTopUp component structure. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant Modal as CreditCodeRedemption<br/>Modal
participant Form as Form Validation<br/>(zod)
participant HCaptcha as HCaptcha<br/>Service
participant API as Platform API
participant Cache as React Query<br/>Cache
User->>Modal: Opens redemption modal
User->>Modal: Enters credit code
Modal->>API: Preview code (debounced)
API->>Modal: Returns preview data (amount, expiry)
Modal->>Modal: Displays new balance calculation
User->>Modal: Clicks redeem button
Modal->>Form: Validates code input
Form-->>Modal: ✓ Valid
Modal->>HCaptcha: Request invisible token
HCaptcha-->>Modal: Returns captcha token
Modal->>API: POST redeem with code & token
API->>API: Validates and processes redemption
API-->>Modal: Success response
Modal->>Cache: Invalidate queries<br/>(customerProfile, orgSubscription,<br/>previewCreditCode)
Cache-->>Modal: ✓ Queries refreshed
Modal->>User: Close modal + show toast
Modal->>HCaptcha: Reset captcha
Suggested reviewers
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 8
🤖 Fix all issues with AI agents
In
`@apps/studio/components/interfaces/Organization/BillingSettings/CreditBalance.tsx`:
- Around line 11-31: The file has excessive blank lines in the imports/spacing
of the CreditBalance component causing Prettier to fail; open
apps/studio/components/interfaces/Organization/BillingSettings/CreditBalance.tsx,
collapse the extra empty lines around the import block (where
CreditCodeRedemption and CreditTopUp are imported) and run `prettier --write`
(or apply your project's Prettier rules) to reformat the file so it matches
linting/Prettier expectations.
In
`@apps/studio/components/interfaces/Organization/BillingSettings/CreditCodeRedemption.tsx`:
- Around line 96-105: The useEffect in CreditCodeRedemption reads router and
calls form.setValue but form is missing from the dependency array; update the
effect dependencies to include either the form object or the stable setter
(extract setValue from form and include setValue) so the effect watches the
setter correctly (i.e., reference useEffect that sets form.setValue — add form
or form.setValue/setValue to the dependency array to satisfy ESLint and ensure
correct behavior).
- Around line 152-154: The useEffect in CreditCodeRedemption calls initHcaptcha
but doesn't list it in the dependency array causing an ESLint
react-hooks/exhaustive-deps failure; fix by adding initHcaptcha to the
dependency array of the useEffect (useEffect(() => { initHcaptcha() },
[codeRedemptionModalVisible, captchaRef, initHcaptcha])) and refactor the
initHcaptcha function into a stable reference by wrapping it with useCallback
inside the component so it doesn’t trigger infinite re-renders.
In `@apps/studio/data/organizations/keys.ts`:
- Line 1: Remove the unused import previewCreditCode from this module: the file
defines its own inline key factory for preview credit codes, so delete the
import statement "import { previewCreditCode } from
'./organization-credit-code-preview-query'" to avoid an unused symbol; ensure no
other references to previewCreditCode remain in this file after removal.
In `@apps/studio/data/organizations/organization-credit-code-preview-query.ts`:
- Around line 69-138: Remove the duplicate/type-conflicting block: delete the
OrganizationCreditCodeRedemptionVariables type, previewCode function,
OrganizationCreditCodePreviewData type alias, and the
useOrganizationCreditCodeRedemptionMutation hook from this file to avoid
clashing with the canonical implementations in
organization-credit-code-redemption-mutation.ts; if the captcha preview behavior
is truly required, rename previewCode to a distinct name (e.g.,
previewCreditCodeWithCaptcha) and relocate it to a separate file so it does not
shadow the existing mutation hook, ensuring the canonical
useOrganizationCreditCodeRedemptionMutation (the one that invalidates queries)
remains the single source of truth.
- Line 59: Remove the stray debug console.log statement that prints {
canApplyCreditCode, slug } in organization-credit-code-preview-query.ts: locate
the line with console.log({ canApplyCreditCode, slug }) and delete it (or
replace it with a proper logger call such as processLogger.debug(...) if you
need retained debug info), ensuring no plain console.log remains in the
codebase.
In
`@apps/studio/data/organizations/organization-credit-code-redemption-mutation.ts`:
- Line 5: Remove the unused import orgSSOKeys from this module: locate the
import statement "import { orgSSOKeys } from '../sso/keys'" and delete it so
there are no unused imports in organization-credit-code-redemption-mutation.ts;
verify no other references to orgSSOKeys exist in that file and run the
linter/tests to confirm the cleanup.
- Line 45: The type alias OrganizationTaxIdUpdateData is misnamed and should be
renamed to reflect credit code redemption (e.g.,
OrganizationCreditCodeRedemptionData or RedeemCodeResult) since it equals
Awaited<ReturnType<typeof redeemCode>>; rename the alias and then update all
local references/usages of OrganizationTaxIdUpdateData in this module (places
that consume the redeemCode return type) to the new name so the type meaning
matches redeemCode.
🧹 Nitpick comments (4)
apps/studio/components/interfaces/Organization/BillingSettings/CreditTopUp.tsx (1)
119-121: Consider addinginitHcaptchato the dependency array.The
useEffectcallsinitHcaptchabut it's not in the dependency array. While the current pattern works becauseinitHcaptchais redefined on each render and captures the necessary state, adding it to deps would make the intent clearer. This is a minor observation and the current implementation is functional.apps/studio/components/interfaces/Organization/BillingSettings/CreditCodeRedemption.tsx (1)
156-171: Use the destructuredcodeparameter instead ofform.getValues.The
codevalue is already available from the destructured parameter. Usingform.getValues('code')is redundant and could introduce subtle timing issues.♻️ Proposed fix
const onSubmit: SubmitHandler<CreditCodeRedemptionForm> = async ({ code }) => { const token = await initHcaptcha() await redeemCode( { slug, - code: form.getValues('code'), + code, hcaptchaToken: token, }, { onSuccess: (data) => { resetCaptcha() }, } ) }apps/studio/data/organizations/organization-credit-code-redemption-mutation.ts (2)
9-13: Duplicate type definition.
OrganizationCreditCodeRedemptionVariablesis also defined inorganization-credit-code-preview-query.ts(lines 69-73). Consider exporting from a single location to avoid drift.
67-74: Consider parallel invalidation for better performance.The sequential
awaitcalls could be parallelized since they're independent operations.♻️ Proposed fix
async onSuccess(data, variables, context) { const { slug, code } = variables - await queryClient.invalidateQueries({ queryKey: organizationKeys.customerProfile(slug) }) - await queryClient.invalidateQueries({ queryKey: subscriptionKeys.orgSubscription(slug) }) - await queryClient.invalidateQueries({ - queryKey: organizationKeys.previewCreditCode(slug, code), - }) + await Promise.all([ + queryClient.invalidateQueries({ queryKey: organizationKeys.customerProfile(slug) }), + queryClient.invalidateQueries({ queryKey: subscriptionKeys.orgSubscription(slug) }), + queryClient.invalidateQueries({ queryKey: organizationKeys.previewCreditCode(slug, code) }), + ]) await onSuccess?.(data, variables, context) },
apps/studio/components/interfaces/Organization/BillingSettings/CreditBalance.tsx
Outdated
Show resolved
Hide resolved
apps/studio/components/interfaces/Organization/BillingSettings/CreditCodeRedemption.tsx
Outdated
Show resolved
Hide resolved
apps/studio/components/interfaces/Organization/BillingSettings/CreditCodeRedemption.tsx
Outdated
Show resolved
Hide resolved
apps/studio/data/organizations/organization-credit-code-preview-query.ts
Outdated
Show resolved
Hide resolved
apps/studio/data/organizations/organization-credit-code-preview-query.ts
Outdated
Show resolved
Hide resolved
apps/studio/data/organizations/organization-credit-code-redemption-mutation.ts
Outdated
Show resolved
Hide resolved
apps/studio/data/organizations/organization-credit-code-redemption-mutation.ts
Outdated
Show resolved
Hide resolved
🎭 Playwright Test ResultsDetails
Flaky testsFeatures › table-editor.spec.ts › table editor › can create and remove foreign key with column selection Skipped testsFeatures › sql-editor.spec.ts › SQL Editor › snippet favourite works as expected |
There was a problem hiding this comment.
The UX of making a request whenever the coupon code changes is very nice and snappy - but I think that a coupon form is not the best place to use it. We don't want users to be encouraged to easily test a list of coupons, in my opinion. It's a minor thing, though - and it's not like the codes are guessable.
But in the spirit of reducing the amount of code, I would like to simplify it to just check validity on "Redeem"
apps/studio/components/interfaces/Organization/BillingSettings/CreditCodeRedemption.tsx
Outdated
Show resolved
Hide resolved
apps/studio/components/interfaces/Organization/BillingSettings/CreditCodeRedemption.tsx
Outdated
Show resolved
Hide resolved
|
@ignaciodob in that case, how would you give the user a preview of how many credits are going to be applied, when they expire etc. Only as a follow up once applied in another modal? There are also some pretty hard rate limits (10ish per hour) on the validation/redemption in the API, so customers cannot really easily brute-force |
I didn't know about the rate limits, that's pretty good already! My preference would be:
|
0d17ef3 to
2565e6a
Compare
There was a problem hiding this comment.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/studio/components/interfaces/Organization/BillingSettings/CreditTopUp.tsx (1)
167-174:⚠️ Potential issue | 🟡 MinorClear hCaptcha state when closing the dialog.
Otherwise a stale token can be reused after reopening the modal and trigger a failed top‑up.🧹 Proposed fix
const onTopUpDialogVisibilityChange = (visible: boolean) => { setTopUpModalVisible(visible) if (!visible) { + resetCaptcha() setCaptchaRef(null) setPaymentIntentConfirmation(undefined) setPaymentIntentSecret('') } }
🧹 Nitpick comments (3)
apps/studio/components/interfaces/Organization/BillingSettings/CreditTopUp.tsx (1)
100-121: MakeinitHcaptchaan effect-safe stable callback.
It reads component state but is invoked fromuseEffectwithout a stable reference, which can trigger exhaustive‑deps warnings and stale closures.As per coding guidelines: Use `useStaticEffectEvent` hook when you need to read latest state/props inside an Effect without re-triggering it.♻️ Suggested update
-import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { useStaticEffectEvent } from 'hooks/misc/useStaticEffectEvent' @@ - const initHcaptcha = async () => { + const initHcaptcha = useStaticEffectEvent(async () => { if (topUpModalVisible && captchaRef) { let token = captchaToken @@ - } + }) useEffect(() => { initHcaptcha() - }, [topUpModalVisible, captchaRef]) + }, [topUpModalVisible, captchaRef, initHcaptcha])apps/studio/components/interfaces/Organization/BillingSettings/CreditCodeRedemption.tsx (2)
107-129: PreferuseStaticEffectEventoveruseLatestfor effect callbacks.
This keeps the callback stable while always reading fresh state, and aligns with the studio hook guideline.As per coding guidelines: Use `useStaticEffectEvent` hook when you need to read latest state/props inside an Effect without re-triggering it.♻️ Suggested update
-import useLatest from '@/hooks/misc/useLatest' +import { useStaticEffectEvent } from '@/hooks/misc/useStaticEffectEvent' @@ - const initHcaptcha = async () => { + const initHcaptcha = useStaticEffectEvent(async () => { let token = captchaTokenRef.current @@ - } - const initHcaptchaRef = useLatest(initHcaptcha) + }) useEffect(() => { if (codeRedemptionModalVisible) { - initHcaptchaRef.current() + initHcaptcha() } - }, [codeRedemptionModalVisible, initHcaptchaRef]) + }, [codeRedemptionModalVisible, initHcaptcha])
74-76: Gate the customer profile query when the feature flag is off.
This avoids extra API calls in production when redemption is disabled.💡 Suggested update
- const { data: customerProfile, isLoading: isCustomerProfileLoading } = - useOrganizationCustomerProfileQuery({ slug }) + const { data: customerProfile, isLoading: isCustomerProfileLoading } = + useOrganizationCustomerProfileQuery( + { slug }, + { enabled: redeemCodeEnabled && typeof slug !== 'undefined' } + )
Braintrust eval reportEvals in progress... ⌛ |
Adds a new code redemption button for the new credit system. Feature Flag toggled off on production for the time being.
Summary by CodeRabbit