-
Notifications
You must be signed in to change notification settings - Fork 514
Payments app design fixes #1375
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Developing-Gamer
wants to merge
49
commits into
dev
Choose a base branch
from
Payments-app-design-fixes
base: dev
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
49 commits
Select commit
Hold shift + click to select a range
d60b0b4
added roids skill
Developing-Gamer 6d87ff6
auth methods page redesigned (1/2)
Developing-Gamer 18f9d24
auth methods page redesigned (2/2)
Developing-Gamer 6bfd71c
Sign-up rules page redesigned (1/2)
Developing-Gamer 8c026a9
updated dialogs
Developing-Gamer ed24d9f
add reusable DesignDialog base component
Developing-Gamer cad76f8
export DesignDialog primitives from dashboard ui package
Developing-Gamer 6251bd2
document DesignDialog as default dashboard modal
Developing-Gamer b8a164a
migrate trigger history modal to DesignDialog shell
Developing-Gamer 8e9c228
add DesignDialog demos to design language catalog
Developing-Gamer d23fbbd
add interactive DesignDialog controls in playground
Developing-Gamer 6d1dc2f
fix(ui): set pointer-events-auto on Popover content
Developing-Gamer 1e96fb6
fix(ui): align dropdown and select item focus and highlight styles
Developing-Gamer 19a7b73
refactor(dashboard): update RepeatingInput component
Developing-Gamer da1fc82
refactor(payments): update shared ItemDialog
Developing-Gamer d344c6e
refactor(payments): update create product line dialog
Developing-Gamer f61093c
refactor(payments): update included item dialog
Developing-Gamer 5513a7b
refactor(payments): update price edit dialog
Developing-Gamer 072a1c2
refactor(payments): update new product page and pricing section
Developing-Gamer c9979eb
Restyle payments Stripe connection status with DesignCard
Developing-Gamer e70a9fd
Restyle payments test mode card with DesignCard and badges
Developing-Gamer 9dd6c99
Restyle payment methods settings with DesignCard and actions
Developing-Gamer dd7726e
Add checkout controls card for block-new-purchases on payments settings
Developing-Gamer 26b52e8
Merge branch 'dev' into Payments-app-design-fixes
Developing-Gamer 3164522
refactor/fix: remove default prod creation
nams1570 d3d0a0b
Implement migration to remove legacy `include-by-default` price senti…
mantrakp04 dca5096
Remove legacy invoice entries from dummy data seeding in `seed-dummy-…
mantrakp04 a7dab36
fix(payments): improve price validation and handling in product dialogs
mantrakp04 cb24516
fix(payments): drop dead include-by-default comparison in validate-code
mantrakp04 360c50e
fix(payments): enhance product price validation and subscription swit…
mantrakp04 65dd2d1
fix(payments): enhance subscription switching logic for free plans
mantrakp04 5279a49
chore(payments): drop dead include-by-default branches in dev-merged …
nams1570 6cbb3de
test(migrations): cover include-by-default snapshot rewrite
nams1570 c88bca3
refactor: error handling on poor product saves
nams1570 3d49b92
chore: clear up comments
nams1570 5286ce9
refactor: payments dashboard consistency changes
nams1570 07f38c0
fix: schema protections against non usd currencies
nams1570 51cbe5f
feat(schema-fuzzer): enhance price validation and add test for config…
mantrakp04 acdb502
feat(payments-demo): implement payments demo page and related API end…
mantrakp04 a256924
Merge branch 'dev' into remove-default-prod-support
mantrakp04 b798f43
Merge branch 'dev' into remove-default-prod-support
nams1570 ff32fc4
chore: switch schema tests away from default prrice
nams1570 9ee7f39
feat: add alert on products page for invalid products
nams1570 f4de807
Merge branch 'dev' into Payments-app-design-fixes
mantrakp04 7759dc3
Merge branch 'remove-default-prod-support' into Payments-app-design-f…
mantrakp04 cdc26f8
Enhance seed process and product switching logic
mantrakp04 3928ab3
Enhance PageClient with customizable dialog features
mantrakp04 c609dd2
Update app-card component to include PlayCircle icon
mantrakp04 0645bb4
Enhance UI components with layout and styling improvements
mantrakp04 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| --- | ||
| name: roids | ||
| description: Use when presenting more than one comparable UI direction, or proactively whenever you ship multiple layout options for the user to choose—do not wait for the user to mention Roids or type /roids. Use for comparison, preview-first, or pick-a-winner flows before cleanup. Default to a small set of variants (~2–5) unless the user specifies how many. | ||
| metadata: | ||
| version: "1.0.0" | ||
| --- | ||
|
|
||
| # Roids | ||
|
|
||
| Compare variants in the browser with the hosted runtime and markup contract. | ||
|
|
||
| ## How it works | ||
|
|
||
| 1. Fetch the source URL below (documentation only—never load it as `<script src>`). | ||
| 2. Follow `SKILL.txt` exactly—wire `https://tryroids.com/roid-tool.js` and `data-roid-*` when showing multiple variants. | ||
| 3. Do not use custom tabs, selects, or one-off pickers for those variants. | ||
|
|
||
| ## Source | ||
|
|
||
| `https://tryroids.com/SKILL.txt` | ||
34 changes: 34 additions & 0 deletions
34
.../backend/prisma/migrations/20260421000000_drop_include_by_default_snapshots/migration.sql
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| -- Rewrite legacy `include-by-default` price sentinel in historical product JSON | ||
| -- snapshots to an empty price map, and coalesce any missing `includedItems` to | ||
| -- an empty record so downstream readers (e.g. mapProductSnapshotToInlineProduct) | ||
| -- don't throw on legacy snapshots. Include-by-default was deprecated in the | ||
| -- bulldozer payments rework and is no longer supported. | ||
| -- | ||
| -- Scale note: prod has ~5 products affected at the time of writing, so a | ||
| -- single-statement UPDATE inside Prisma's default migration transaction is fine. | ||
| -- If this ever needs to run against a larger affected row set, batch it or | ||
| -- split the migration so it runs outside a transaction. | ||
|
|
||
| UPDATE "Subscription" | ||
| SET "product" = jsonb_set( | ||
| jsonb_set("product"::jsonb, '{prices}', '{}'::jsonb), | ||
| '{includedItems}', | ||
| COALESCE("product"::jsonb->'includedItems', '{}'::jsonb) | ||
| )::json | ||
| WHERE "product"->>'prices' = 'include-by-default'; | ||
|
|
||
| UPDATE "OneTimePurchase" | ||
| SET "product" = jsonb_set( | ||
| jsonb_set("product"::jsonb, '{prices}', '{}'::jsonb), | ||
| '{includedItems}', | ||
| COALESCE("product"::jsonb->'includedItems', '{}'::jsonb) | ||
| )::json | ||
| WHERE "product"->>'prices' = 'include-by-default'; | ||
|
|
||
| UPDATE "ProductVersion" | ||
| SET "productJson" = jsonb_set( | ||
| jsonb_set("productJson"::jsonb, '{prices}', '{}'::jsonb), | ||
| '{includedItems}', | ||
| COALESCE("productJson"::jsonb->'includedItems', '{}'::jsonb) | ||
| )::json | ||
| WHERE "productJson"->>'prices' = 'include-by-default'; |
270 changes: 270 additions & 0 deletions
270
...ma/migrations/20260421000000_drop_include_by_default_snapshots/tests/rewrite-snapshots.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,270 @@ | ||
| import { randomUUID } from 'crypto'; | ||
| import type { Sql } from 'postgres'; | ||
| import { expect } from 'vitest'; | ||
|
|
||
| /** | ||
| * Migration-level test for `20260421000000_drop_include_by_default_snapshots`. | ||
| * | ||
| * The migration's job is to rewrite historical product JSON snapshots in | ||
| * three tables (`Subscription`, `OneTimePurchase`, `ProductVersion`) so that | ||
| * the legacy `"include-by-default"` price sentinel is replaced with an empty | ||
| * price record, and any missing `includedItems` field is filled in with `{}` | ||
| * (downstream readers like `mapProductSnapshotToInlineProduct` assume both | ||
| * fields exist as records). | ||
| * | ||
| * Edge cases covered: | ||
| * 1. `Subscription`: sentinel + missing `includedItems` → prices `{}`, items `{}`. | ||
| * 2. `Subscription`: sentinel + existing `includedItems` → items preserved. | ||
| * 3. `Subscription`: NO sentinel (real prices) → row untouched. | ||
| * 4. `OneTimePurchase`: sentinel → migrated identically to Subscription. | ||
| * 5. `ProductVersion`: sentinel (in `productJson` not `product`) → migrated. | ||
| * | ||
| * `tenancyId` on these tables is a UUID column without an enforced FK to | ||
| * `Tenancy`, so we can use random UUIDs without seeding the parent rows. | ||
| */ | ||
|
|
||
| type Ctx = { | ||
| // Subscription IDs | ||
| subSentinelMissingItemsId: string, | ||
| subSentinelWithItemsId: string, | ||
| subRealPricesId: string, | ||
| subSentinelMissingItemsTenancy: string, | ||
| subSentinelWithItemsTenancy: string, | ||
| subRealPricesTenancy: string, | ||
| // OneTimePurchase | ||
| otpId: string, | ||
| otpTenancy: string, | ||
| // ProductVersion | ||
| pvProductVersionId: string, | ||
| pvTenancy: string, | ||
| }; | ||
|
|
||
| export const preMigration = async (sql: Sql): Promise<Ctx> => { | ||
| const ctx: Ctx = { | ||
| subSentinelMissingItemsId: randomUUID(), | ||
| subSentinelWithItemsId: randomUUID(), | ||
| subRealPricesId: randomUUID(), | ||
| subSentinelMissingItemsTenancy: randomUUID(), | ||
| subSentinelWithItemsTenancy: randomUUID(), | ||
| subRealPricesTenancy: randomUUID(), | ||
| otpId: randomUUID(), | ||
| otpTenancy: randomUUID(), | ||
| pvProductVersionId: `pv-${randomUUID()}`, | ||
| pvTenancy: randomUUID(), | ||
| }; | ||
|
|
||
| // Case 1: Subscription with sentinel + no includedItems field at all. | ||
| // `updatedAt` must be set explicitly — Prisma's `@updatedAt` annotation is | ||
| // client-side, raw SQL inserts skip it and the column is NOT NULL. | ||
| await sql` | ||
| INSERT INTO "Subscription" ( | ||
| "id", "tenancyId", "customerId", "customerType", | ||
| "productId", "priceId", "product", "quantity", | ||
| "status", "currentPeriodStart", "currentPeriodEnd", | ||
| "cancelAtPeriodEnd", "creationSource", "updatedAt" | ||
| ) VALUES ( | ||
| ${ctx.subSentinelMissingItemsId}::uuid, | ||
| ${ctx.subSentinelMissingItemsTenancy}::uuid, | ||
| 'customer-1', 'TEAM', | ||
| 'legacy-default', NULL, | ||
| ${sql.json({ | ||
| displayName: 'Legacy Default', | ||
| customerType: 'team', | ||
| prices: 'include-by-default', | ||
| })}, | ||
| 1, | ||
| 'active'::"SubscriptionStatus", | ||
| NOW(), | ||
| NOW() + interval '30 days', | ||
| false, | ||
| 'API_GRANT'::"PurchaseCreationSource", | ||
| NOW() | ||
| ) | ||
| `; | ||
|
|
||
| // Case 2: Subscription with sentinel + already-populated includedItems. | ||
| // The migration must NOT overwrite this — it only fills in when missing. | ||
| await sql` | ||
| INSERT INTO "Subscription" ( | ||
| "id", "tenancyId", "customerId", "customerType", | ||
| "productId", "priceId", "product", "quantity", | ||
| "status", "currentPeriodStart", "currentPeriodEnd", | ||
| "cancelAtPeriodEnd", "creationSource", "updatedAt" | ||
| ) VALUES ( | ||
| ${ctx.subSentinelWithItemsId}::uuid, | ||
| ${ctx.subSentinelWithItemsTenancy}::uuid, | ||
| 'customer-2', 'TEAM', | ||
| 'legacy-default-2', NULL, | ||
| ${sql.json({ | ||
| displayName: 'Legacy Default With Items', | ||
| customerType: 'team', | ||
| prices: 'include-by-default', | ||
| includedItems: { | ||
| 'item-a': { quantity: 5, repeat: 'never', expires: 'never' }, | ||
| }, | ||
| })}, | ||
| 1, | ||
| 'active'::"SubscriptionStatus", | ||
| NOW(), | ||
| NOW() + interval '30 days', | ||
| false, | ||
| 'API_GRANT'::"PurchaseCreationSource", | ||
| NOW() | ||
| ) | ||
| `; | ||
|
|
||
| // Case 3: Subscription with REAL prices — must remain untouched. | ||
| await sql` | ||
| INSERT INTO "Subscription" ( | ||
| "id", "tenancyId", "customerId", "customerType", | ||
| "productId", "priceId", "product", "quantity", | ||
| "status", "currentPeriodStart", "currentPeriodEnd", | ||
| "cancelAtPeriodEnd", "creationSource", "updatedAt" | ||
| ) VALUES ( | ||
| ${ctx.subRealPricesId}::uuid, | ||
| ${ctx.subRealPricesTenancy}::uuid, | ||
| 'customer-3', 'USER', | ||
| 'paid-plan', 'monthly', | ||
| ${sql.json({ | ||
| displayName: 'Paid Plan', | ||
| customerType: 'user', | ||
| prices: { | ||
| monthly: { USD: '10.00', interval: [1, 'month'], serverOnly: false }, | ||
| }, | ||
| includedItems: {}, | ||
| })}, | ||
| 1, | ||
| 'active'::"SubscriptionStatus", | ||
| NOW(), | ||
| NOW() + interval '30 days', | ||
| false, | ||
| 'PURCHASE_PAGE'::"PurchaseCreationSource", | ||
| NOW() | ||
| ) | ||
| `; | ||
|
|
||
| // Case 4: OneTimePurchase with sentinel. | ||
| await sql` | ||
| INSERT INTO "OneTimePurchase" ( | ||
| "id", "tenancyId", "customerId", "customerType", | ||
| "productId", "priceId", "product", "quantity", | ||
| "creationSource" | ||
| ) VALUES ( | ||
| ${ctx.otpId}::uuid, | ||
| ${ctx.otpTenancy}::uuid, | ||
| 'customer-4', 'USER', | ||
| 'legacy-otp', NULL, | ||
| ${sql.json({ | ||
| displayName: 'Legacy OTP', | ||
| customerType: 'user', | ||
| prices: 'include-by-default', | ||
| })}, | ||
| 1, | ||
| 'API_GRANT'::"PurchaseCreationSource" | ||
| ) | ||
| `; | ||
|
|
||
| // Case 5: ProductVersion with sentinel (note: column is `productJson`, not `product`). | ||
| await sql` | ||
| INSERT INTO "ProductVersion" ( | ||
| "tenancyId", "productVersionId", "productId", "productJson" | ||
| ) VALUES ( | ||
| ${ctx.pvTenancy}::uuid, | ||
| ${ctx.pvProductVersionId}, | ||
| 'legacy-pv', | ||
| ${sql.json({ | ||
| displayName: 'Legacy PV', | ||
| customerType: 'team', | ||
| prices: 'include-by-default', | ||
| })} | ||
| ) | ||
| `; | ||
|
|
||
| return ctx; | ||
| }; | ||
|
|
||
| export const postMigration = async (sql: Sql, ctx: Ctx) => { | ||
| // ---- Case 1 ---- | ||
| const sub1 = await sql<Array<{ product: unknown }>>` | ||
| SELECT "product" FROM "Subscription" | ||
| WHERE "id" = ${ctx.subSentinelMissingItemsId}::uuid | ||
| `; | ||
| expect(sub1).toHaveLength(1); | ||
| expect(sub1[0].product).toEqual({ | ||
| displayName: 'Legacy Default', | ||
| customerType: 'team', | ||
| prices: {}, | ||
| includedItems: {}, | ||
| }); | ||
|
|
||
| // ---- Case 2 ---- | ||
| const sub2 = await sql<Array<{ product: unknown }>>` | ||
| SELECT "product" FROM "Subscription" | ||
| WHERE "id" = ${ctx.subSentinelWithItemsId}::uuid | ||
| `; | ||
| expect(sub2).toHaveLength(1); | ||
| expect(sub2[0].product).toEqual({ | ||
| displayName: 'Legacy Default With Items', | ||
| customerType: 'team', | ||
| prices: {}, | ||
| includedItems: { | ||
| 'item-a': { quantity: 5, repeat: 'never', expires: 'never' }, | ||
| }, | ||
| }); | ||
|
|
||
| // ---- Case 3 (regression guard: don't touch real-price rows) ---- | ||
| const sub3 = await sql<Array<{ product: unknown }>>` | ||
| SELECT "product" FROM "Subscription" | ||
| WHERE "id" = ${ctx.subRealPricesId}::uuid | ||
| `; | ||
| expect(sub3).toHaveLength(1); | ||
| expect(sub3[0].product).toEqual({ | ||
| displayName: 'Paid Plan', | ||
| customerType: 'user', | ||
| prices: { | ||
| monthly: { USD: '10.00', interval: [1, 'month'], serverOnly: false }, | ||
| }, | ||
| includedItems: {}, | ||
| }); | ||
|
|
||
| // ---- Case 4 ---- | ||
| const otp = await sql<Array<{ product: unknown }>>` | ||
| SELECT "product" FROM "OneTimePurchase" | ||
| WHERE "id" = ${ctx.otpId}::uuid | ||
| `; | ||
| expect(otp).toHaveLength(1); | ||
| expect(otp[0].product).toEqual({ | ||
| displayName: 'Legacy OTP', | ||
| customerType: 'user', | ||
| prices: {}, | ||
| includedItems: {}, | ||
| }); | ||
|
|
||
| // ---- Case 5 ---- | ||
| const pv = await sql<Array<{ productJson: unknown }>>` | ||
| SELECT "productJson" FROM "ProductVersion" | ||
| WHERE "tenancyId" = ${ctx.pvTenancy}::uuid | ||
| AND "productVersionId" = ${ctx.pvProductVersionId} | ||
| `; | ||
| expect(pv).toHaveLength(1); | ||
| expect(pv[0].productJson).toEqual({ | ||
| displayName: 'Legacy PV', | ||
| customerType: 'team', | ||
| prices: {}, | ||
| includedItems: {}, | ||
| }); | ||
|
|
||
| // ---- Cross-table sanity: no row anywhere still has the sentinel ---- | ||
| const remainingSubs = await sql` | ||
| SELECT 1 FROM "Subscription" WHERE "product"->>'prices' = 'include-by-default' | ||
| `; | ||
| const remainingOtps = await sql` | ||
| SELECT 1 FROM "OneTimePurchase" WHERE "product"->>'prices' = 'include-by-default' | ||
| `; | ||
| const remainingPvs = await sql` | ||
| SELECT 1 FROM "ProductVersion" WHERE "productJson"->>'prices' = 'include-by-default' | ||
| `; | ||
| expect(remainingSubs).toHaveLength(0); | ||
| expect(remainingOtps).toHaveLength(0); | ||
| expect(remainingPvs).toHaveLength(0); | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file looks out-of-scope for the PR’s stated objective.
SKILL.mdunder.agents/skills/roids/does not appear related to payments UI/design fixes in the dashboard product code. Please confirm this is intentionally bundled here; otherwise it should be moved to a separate PR to keep review/risk boundaries clean.🤖 Prompt for AI Agents