Skip to content

feat: implement subscription checkout flow with plan tracking - #25

Open
HyperRanger wants to merge 2 commits into
mainfrom
backend-pr
Open

feat: implement subscription checkout flow with plan tracking#25
HyperRanger wants to merge 2 commits into
mainfrom
backend-pr

Conversation

@HyperRanger

@HyperRanger HyperRanger commented Jun 12, 2026

Copy link
Copy Markdown
Collaborator
  • Add subscription checkout page with vendor validation and plan selection
  • Create subscription payment request endpoint (POST /subscriptions/payment-request)
  • Add subscription utilities and types for API communication
  • Track vendor subscription plan in database (free/pro)
  • Display subscription status in vendor profile
  • Update pricing component with 1% fee for Pro plan
  • Add PriceTier component to home page for non-authenticated users
  • Register subscriptions router in main FastAPI app

Summary by CodeRabbit

  • New Features
    • Subscription checkout page allows vendors to upgrade and manage subscription plans
    • Vendor products analytics dashboard displays revenue, sales metrics, and performance insights
    • Subscription plan status indicator in vendor profile shows current plan level (Free or Pro)
    • Pro plan transaction fee reduced from 2% to 1%
    • Updated pricing page to direct Pro plan upgrades to checkout flow

- Add subscription checkout page with vendor validation and plan selection
- Create subscription payment request endpoint (POST /subscriptions/payment-request)
- Add subscription utilities and types for API communication
- Track vendor subscription plan in database (free/pro)
- Display subscription status in vendor profile
- Update pricing component with 1% fee for Pro plan
- Add PriceTier component to home page for non-authenticated users
- Register subscriptions router in main FastAPI app
@vercel

vercel Bot commented Jun 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
proofpay-ai Error Error Jun 12, 2026 12:05pm

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR implements a complete vendor subscription upgrade system. The backend adds a new payment request API endpoint that validates vendors, creates database payment and transaction records, and returns checkout configuration. The frontend provides a checkout flow for vendors to upgrade to pro plans, includes session-aware pricing display on the home page, and adds a new product sales analytics dashboard for vendors to track performance metrics.

Changes

Subscription Feature

Layer / File(s) Summary
Backend subscription payment API
backend/app/api/v1/routes_subscriptions.py
New FastAPI router with POST /api/v1/subscriptions/payment-request endpoint that validates vendor existence, ensures subscription schema columns, creates payment and transaction database records with "pending" status, and returns checkout configuration. Includes webhook-invoked update_vendor_subscription endpoint for plan updates.
Backend vendor subscription state
backend/app/services/vendor_service.py
Vendor service adds _ensure_vendor_subscription_columns helper to backfill subscription columns on vendors table with 'free' default. Updates account shaping, vendor creation, and login flows to track and return subscription_plan field in user payloads.
Backend API registration
backend/app/main.py
Subscription router imported and registered with FastAPI application.
Frontend subscription client action
web/lib/actions/subscriptions.ts
TypeScript module exporting createSubscriptionPaymentRequest client action with request/response types and error translation to user-friendly messages.
Frontend subscription checkout page
web/app/subscription/checkout/page.tsx
New client-side checkout page for vendors with plan selection, vendor-only access gating, price display, "Proceed to Payment" button that calls API and routes to payment callback, error handling with toast notifications, and "Cancel" button returning to pricing.
Frontend home page session-aware pricing
web/app/page.tsx
Home page converted to client component with cached session via useSyncExternalStore; pricing tiers now render only when no user session is active.
Frontend pricing tier updates
web/components/home/price-tier.tsx
Pro tier transaction fee reduced from 2% to 1%; upgrade button redirects from /vendors/signup to /subscription/checkout?plan=pro.
Frontend vendor product sales dashboard
web/app/vendors/products/page.tsx
New analytics page for vendors displaying product list with revenue, request count, and completion rate metrics; includes ranked product table with TrendingUp icons and interactive BarChart visualization of sales breakdown by product using Recharts.
Frontend vendor profile enhancements
web/app/vendors/profile/page.tsx
Vendor profile header now conditionally displays "Subscription" section showing "Pro Plan" or "Free Plan" based on session.subscription_plan; vendor actions updated from single "Create new product" button to dual-button layout including "View Products" and "Create new product".

Sequence Diagram(s)

sequenceDiagram
  participant Vendor as Vendor Client
  participant PricingUI as Pricing Page
  participant CheckoutUI as Checkout Page
  participant ClientAction as createSubscriptionPaymentRequest
  participant BackendAPI as POST /subscriptions/payment-request
  participant VendorService as Vendor Service
  participant Database as Database
  participant PaymentCallback as Payment Callback Route

  Vendor->>PricingUI: View Pro tier (not logged in)
  PricingUI->>Vendor: Show "Upgrade to Pro" button
  Vendor->>CheckoutUI: Click upgrade link to /subscription/checkout?plan=pro
  CheckoutUI->>CheckoutUI: Validate session exists, user is vendor
  Vendor->>CheckoutUI: Review plan details and click "Proceed to Payment"
  CheckoutUI->>ClientAction: Call createSubscriptionPaymentRequest
  ClientAction->>BackendAPI: POST vendor_id, plan, amount_kobo, currency
  BackendAPI->>VendorService: Verify vendor exists
  BackendAPI->>Database: Ensure subscription columns exist
  BackendAPI->>Database: INSERT payment_requests row
  BackendAPI->>Database: INSERT transactions row (pending)
  BackendAPI->>Database: COMMIT transaction
  BackendAPI->>ClientAction: Return payment_request_id and checkout config
  ClientAction->>CheckoutUI: Resolve with response data
  CheckoutUI->>PaymentCallback: Navigate to /payments/callback/{payment_request_id}
  PaymentCallback->>Vendor: Initiate external payment processing
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • fisayo-dev/proofpay-ai#10: Updates vendor profile page session and auth rendering logic that overlaps with subscription status display changes in this PR.

Poem

🐰 A vendor's journey to upgrade,
From free to pro, no more delay,
With checkout flows and charts so bright,
Product sales now shine in sight,
Subscriptions bloom—let commerce take flight! 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title directly summarizes the main changes: implementing a subscription checkout flow and adding plan tracking for vendors across the backend and frontend.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch backend-pr

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
backend/app/services/vendor_service.py (1)

156-165: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Include subscription_plan in the signup response too.

login_account() now returns subscription_plan, but create_account() still drops it when shaping the vendor payload. Newly created vendor sessions will miss the new field until the user logs in again, which breaks the updated cross-layer contract for surfaced subscription state.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/services/vendor_service.py` around lines 156 - 165, The signup
response shaping in vendor_service.py omits subscription_plan causing new
accounts to lack that field; update the dict returned by the account-creation
shaping (the function that currently returns keys like "user_id", "vendor_id",
"role", "full_name", etc.) to include "subscription_plan":
vendor.get("subscription_plan") so the create_account()/vendor payload matches
login_account()'s contract and newly created sessions include the subscription
state.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/app/api/v1/routes_subscriptions.py`:
- Around line 103-130: The current flow calls conn.commit() before calling
build_checkout_config, so if build_checkout_config(...) fails the DB commit
remains and the endpoint returns 500 causing duplicate attempts; move the
conn.commit() to after you successfully construct checkout_config and the full
response payload (i.e., call build_checkout_config(...) first, then
conn.commit(), then return the dict). Ensure the except block still rolls back
on any error and reference the functions/variables payment_request_id,
build_checkout_config, checkout_config, conn.commit(), and the returned payload
to locate and reorder the operations.
- Around line 16-20: The request model CreateSubscriptionPaymentRequestBody
currently accepts amount_kobo and currency which lets clients tamper pricing;
remove amount_kobo and currency from that Pydantic model and change the
subscription payment endpoint handler that consumes it to validate plan against
an allowlist (e.g., {"free","pro"}), then compute the canonical amount_kobo and
currency server-side (lookup from your pricing table/constant) and use those
computed values when creating the checkout/persisting the payment (update any
calls in the handler that previously read amount_kobo/currency to use the
server-computed values); also apply the same change to the other handlers
referenced in the diff (the other subscription checkout/create flows around the
same file) and add/adjust tests to assert server-side pricing is enforced.
- Around line 23-38: The handler create_subscription_payment_request currently
trusts body.vendor_id and calls get_vendor_by_id; instead derive the vendor
identity from the authenticated session/token (e.g., use the existing auth
dependency like get_current_user or get_current_vendor_from_token) and use that
vendor for all operations; ignore or remove body.vendor_id, validate that
get_vendor_by_id(current_vendor.id) exists, and if the caller supplied a
different vendor_id reject with 403 (or simply ignore the supplied id and do not
allow creating payment requests for other vendors). Update the route signature
to accept the auth dependency and replace uses of body.vendor_id with the
authenticated vendor's id.

In `@backend/app/services/vendor_service.py`:
- Around line 39-41: The helper _ensure_vendor_subscription_columns currently
runs ALTER TABLE DDL during request handling; remove calls to
_ensure_vendor_subscription_columns from login/signup/subscription paths and
delete or disable that function in vendor_service.py, and instead add a
dedicated schema migration that performs the two ALTER TABLE ADD COLUMN IF NOT
EXISTS statements (setting subscription_plan TEXT DEFAULT 'free' and
subscription_started_at TEXT) with appropriate transactional/migration tooling
for your DB; after deploying the migration, ensure any code that previously
depended on _ensure_vendor_subscription_columns no longer invokes it (or falls
back safely) so runtime requests no longer execute DDL.

In `@web/app/page.tsx`:
- Around line 26-27: The PriceTier component is rendered twice for
unauthenticated users and also shown to authenticated users because of the
unconditional second render; remove the unconditional <PriceTier /> and ensure
PriceTier is only rendered inside the conditional that checks session (i.e.,
keep the {!session && <PriceTier />} usage, delete the standalone PriceTier
render) so PriceTier appears only for logged-out users.
- Around line 14-18: The subscription passed to useSyncExternalStore is a no-op
so session (from getCachedSession) never updates on login/logout; export and
implement a real subscription in web/lib/session.ts (e.g., subscribeToSession or
onSessionChange) that listens for auth/cookie/session updates and returns an
unsubscribe function, then change the useSyncExternalStore call in
web/app/page.tsx to pass that subscribe function as the first argument (keep
getCachedSession as the snapshot) so session re-renders when the session
changes.

In `@web/app/subscription/checkout/page.tsx`:
- Around line 144-147: Update the Pro plan fee text in the checkout page JSX:
locate the list item rendering the fee (the <li className="flex items-start
gap-2"> containing the span with "2% transaction fee (vs 3% for free)") in
web/app/subscription/checkout/page.tsx and change the copy to "1% transaction
fee (vs 3% for free)" so the displayed plan terms match the PR that sets Pro to
1%.
- Around line 27-38: The page currently uses planParam (raw search param) for
API requests even though plan is validated/fallback via SUBSCRIPTION_PLANS;
compute a validated plan key (e.g., validatedPlanKey = planParam in
SUBSCRIPTION_PLANS ? planParam : "pro") and use that validatedPlanKey for the
checkout POST and any other places that currently reference planParam (including
the occurrences around lines 95-99). Update uses of planParam to reference
validatedPlanKey so the API receives the normalized/validated plan key
consistent with the displayed plan (symbols to locate: planParam, plan,
SUBSCRIPTION_PLANS and the checkout POST call).

In `@web/app/vendors/products/page.tsx`:
- Around line 46-50: The route currently only redirects when session === null,
allowing authenticated non-vendor users to render the vendor page and block on
fetch; update the guards so only users with session.role === "vendor" proceed.
In the useEffect that currently checks session (the useEffect in page.tsx),
change the logic to redirect any non-vendor (session === null OR session.role
!== "vendor") to "/vendors/signup" (or the appropriate non-vendor route), and
update any data-fetching guards (the vendor products fetch/loader and any code
that uses vendor_id) to only run when session?.role === "vendor" and vendor_id
is present (e.g., session.user.vendor_id or the vendor_id prop), preventing
rendering of vendor components and fetch calls for buyers.
- Around line 142-144: The displayed completion percentage uses
product.completion_rate as a 0..1 ratio; multiply it by 100 before formatting
and appending '%' so 0.8 becomes 80.0%. Update the JSX that references
product.completion_rate (in the paragraph alongside product.paid_count) to use
(product.completion_rate * 100).toFixed(1) + '%' (or a small null-safe
expression like product.completion_rate != null ? (product.completion_rate *
100).toFixed(1) + '%' : '—') to render the correct percentage.

In `@web/components/home/price-tier.tsx`:
- Line 31: The Pro tier fee string in the PriceTier component is "1%" (symbol:
fee in web/components/home/price-tier.tsx) but the subscription checkout
included-features list (symbol: the transaction fee text in page.tsx) shows
"2%"; update one of these to match the other so messaging is consistent—either
change fee: "1%" to "2%" in the PriceTier component or update the checkout
page's transaction fee text to "1%", and ensure both the PriceTier component
(fee) and the checkout included-features text use the exact same percentage
string.

---

Outside diff comments:
In `@backend/app/services/vendor_service.py`:
- Around line 156-165: The signup response shaping in vendor_service.py omits
subscription_plan causing new accounts to lack that field; update the dict
returned by the account-creation shaping (the function that currently returns
keys like "user_id", "vendor_id", "role", "full_name", etc.) to include
"subscription_plan": vendor.get("subscription_plan") so the
create_account()/vendor payload matches login_account()'s contract and newly
created sessions include the subscription state.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1773249f-7cb8-49b5-b277-211c64ed506c

📥 Commits

Reviewing files that changed from the base of the PR and between 32c4060 and d9fb81d.

📒 Files selected for processing (9)
  • backend/app/api/v1/routes_subscriptions.py
  • backend/app/main.py
  • backend/app/services/vendor_service.py
  • web/app/page.tsx
  • web/app/subscription/checkout/page.tsx
  • web/app/vendors/products/page.tsx
  • web/app/vendors/profile/page.tsx
  • web/components/home/price-tier.tsx
  • web/lib/actions/subscriptions.ts

Comment on lines +16 to +20
class CreateSubscriptionPaymentRequestBody(BaseModel):
vendor_id: str
plan: str # "free" or "pro"
amount_kobo: int
currency: str = "NGN"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Derive plan pricing server-side instead of trusting amount_kobo from the request.

A caller can send plan="pro" with any amount_kobo and those values are persisted and used for checkout as-is. That makes the upgrade price client-tamperable. Keep only the selected plan in the request, validate it against an allowlist, and compute the amount/currency on the server.

Also applies to: 67-80, 95-100

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/api/v1/routes_subscriptions.py` around lines 16 - 20, The request
model CreateSubscriptionPaymentRequestBody currently accepts amount_kobo and
currency which lets clients tamper pricing; remove amount_kobo and currency from
that Pydantic model and change the subscription payment endpoint handler that
consumes it to validate plan against an allowlist (e.g., {"free","pro"}), then
compute the canonical amount_kobo and currency server-side (lookup from your
pricing table/constant) and use those computed values when creating the
checkout/persisting the payment (update any calls in the handler that previously
read amount_kobo/currency to use the server-computed values); also apply the
same change to the other handlers referenced in the diff (the other subscription
checkout/create flows around the same file) and add/adjust tests to assert
server-side pricing is enforced.

Comment on lines +23 to +38
@router.post("/subscriptions/payment-request", status_code=201)
def create_subscription_payment_request(body: CreateSubscriptionPaymentRequestBody):
"""
Create a subscription payment request for upgrading to Pro plan.
Similar to payment requests but specifically for subscriptions.
"""
# Verify vendor exists
vendor = get_vendor_by_id(body.vendor_id)
if not vendor:
raise HTTPException(
status_code=404,
detail={
"code": "VENDOR_NOT_FOUND",
"message": "Vendor not found.",
},
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Enforce vendor ownership on the backend, not just existence.

This route only checks that body.vendor_id exists. Any caller who knows another vendor's ID can create subscription payment rows for that account. The handler should derive the vendor from the authenticated session/token and reject mismatches instead of accepting vendor_id from the body.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/api/v1/routes_subscriptions.py` around lines 23 - 38, The handler
create_subscription_payment_request currently trusts body.vendor_id and calls
get_vendor_by_id; instead derive the vendor identity from the authenticated
session/token (e.g., use the existing auth dependency like get_current_user or
get_current_vendor_from_token) and use that vendor for all operations; ignore or
remove body.vendor_id, validate that get_vendor_by_id(current_vendor.id) exists,
and if the caller supplied a different vendor_id reject with 403 (or simply
ignore the supplied id and do not allow creating payment requests for other
vendors). Update the route signature to accept the auth dependency and replace
uses of body.vendor_id with the authenticated vendor's id.

Comment on lines +103 to +130
conn.commit()

# Build checkout config
checkout_config = build_checkout_config(
settings.kora_public_key,
get_kora_notification_url(),
kora_reference,
body.amount_kobo,
body.currency,
vendor.get("business_name") if hasattr(vendor, 'get') else vendor["business_name"],
vendor.get("email") if hasattr(vendor, 'get') else vendor.get("email"),
)

return {
"payment_request_id": payment_request_id,
"kora_reference": kora_reference,
"status": "created",
"checkout_config": checkout_config
}

except Exception as e:
conn.rollback()
raise HTTPException(
status_code=500,
detail={
"code": "SUBSCRIPTION_CREATION_FAILED",
"message": "Could not create subscription payment request.",
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Build the checkout payload before conn.commit().

If build_checkout_config(...) raises after Line 103, the endpoint returns 500 even though the payment_requests and transactions rows are already committed. A retry then creates duplicate pending subscription attempts. Commit only after the response payload is ready.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/api/v1/routes_subscriptions.py` around lines 103 - 130, The
current flow calls conn.commit() before calling build_checkout_config, so if
build_checkout_config(...) fails the DB commit remains and the endpoint returns
500 causing duplicate attempts; move the conn.commit() to after you successfully
construct checkout_config and the full response payload (i.e., call
build_checkout_config(...) first, then conn.commit(), then return the dict).
Ensure the except block still rolls back on any error and reference the
functions/variables payment_request_id, build_checkout_config, checkout_config,
conn.commit(), and the returned payload to locate and reorder the operations.

Comment on lines +39 to +41
def _ensure_vendor_subscription_columns(cursor) -> None:
cursor.execute("ALTER TABLE vendors ADD COLUMN IF NOT EXISTS subscription_plan TEXT DEFAULT 'free'", ())
cursor.execute("ALTER TABLE vendors ADD COLUMN IF NOT EXISTS subscription_started_at TEXT", ())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Move this schema change out of request handling.

ALTER TABLE ... ADD COLUMN IF NOT EXISTS is now part of login/signup/subscription flows through this helper. In Postgres that DDL takes a strong table lock and also requires schema-change privileges, so auth/checkout traffic can block or fail at runtime. This belongs in a migration, not an application request path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/services/vendor_service.py` around lines 39 - 41, The helper
_ensure_vendor_subscription_columns currently runs ALTER TABLE DDL during
request handling; remove calls to _ensure_vendor_subscription_columns from
login/signup/subscription paths and delete or disable that function in
vendor_service.py, and instead add a dedicated schema migration that performs
the two ALTER TABLE ADD COLUMN IF NOT EXISTS statements (setting
subscription_plan TEXT DEFAULT 'free' and subscription_started_at TEXT) with
appropriate transactional/migration tooling for your DB; after deploying the
migration, ensure any code that previously depended on
_ensure_vendor_subscription_columns no longer invokes it (or falls back safely)
so runtime requests no longer execute DDL.

Comment thread web/app/page.tsx
Comment on lines +14 to +18
const session = useSyncExternalStore(
() => () => {},
() => getCachedSession(),
() => null,
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether session store exposes a real subscribe API for useSyncExternalStore.
# Expected: a subscribe/listener function in web/lib/session.ts (or equivalent),
# otherwise this hook wiring is non-reactive by design.
fd -i 'session.ts' web | xargs -I{} sh -c 'echo "---- {} ----"; sed -n "1,260p" "{}" | rg -n "subscribe|listener|useSyncExternalStore|getCachedSession|getSession" -n -C 2'

Repository: fisayo-dev/proofpay-ai

Length of output: 758


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "## web/app/page.tsx: useSyncExternalStore wiring"
if [ -f web/app/page.tsx ]; then
  sed -n '1,120p' web/app/page.tsx | nl -ba | sed -n '1,120p'
  echo "----"
  rg -n "useSyncExternalStore" web/app/page.tsx || true
  rg -n "useSyncExternalStore\\(" web/app/page.tsx -n -C 3 || true
fi

echo "## web/lib/session.ts: full search for subscribe/listener APIs"
if [ -f web/lib/session.ts ]; then
  echo "wc -l web/lib/session.ts"
  wc -l web/lib/session.ts
  echo "---- matching lines ----"
  rg -n "subscribe|listener" web/lib/session.ts || true
  echo "---- matching useSyncExternalStore related names ----"
  rg -n "useSyncExternalStore|getCachedSession|getSession|SESSION_KEY" web/lib/session.ts -n || true
  echo "---- full file small preview around caching ----"
  sed -n '1,220p' web/lib/session.ts | nl -ba | sed -n '1,220p'
  echo "---- tail preview ----"
  tail -n 80 web/lib/session.ts | nl -ba
fi

echo "## find any session subscribe API in web/lib"
fd -i "session.ts" web | xargs -I{} sh -c 'echo "---- {} ----"; rg -n "subscribe|listener|getCachedSession|getSession|setSession|clearSession" "{}" || true'

Repository: fisayo-dev/proofpay-ai

Length of output: 176


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "## web/app/page.tsx: show around useSyncExternalStore call"
if [ -f web/app/page.tsx ]; then
  sed -n '1,120p' web/app/page.tsx | awk '{printf "%4d:%s\n", NR, $0}'
  echo "---- matches ----"
  rg -n "useSyncExternalStore" web/app/page.tsx || true
  echo "---- useSyncExternalStore(...) context ----"
  rg -n "useSyncExternalStore\\(" web/app/page.tsx -C 3 || true
fi

echo "## web/lib/session.ts: full search for subscribe/listener APIs"
if [ -f web/lib/session.ts ]; then
  echo "line_count=$(wc -l < web/lib/session.ts)"
  echo "---- matching subscribe/listener in file ----"
  rg -n "subscribe|listener" web/lib/session.ts || true
  echo "---- matching useSyncExternalStore/getCachedSession/getSession/SESSION_KEY/setSession/clearSession ----"
  rg -n "useSyncExternalStore|getCachedSession|getSession|SESSION_KEY|setSession|clearSession" web/lib/session.ts || true
  echo "---- show the relevant cache section ----"
  rg -n "getCachedSession" web/lib/session.ts -n -C 20 || true
fi

echo "## repo-wide: any session subscribe/listener in other session files"
fd -i "session.ts" web | xargs -I{} sh -c 'echo "---- {} ----"; rg -n "subscribe|listener" "{}" || true'

Repository: fisayo-dev/proofpay-ai

Length of output: 3441


Fix useSyncExternalStore subscription so session re-renders on login/logout (web/app/page.tsx)
web/app/page.tsx wires useSyncExternalStore with a no-op subscribe (() => () => {}), while web/lib/session.ts only reads/caches cookies (getCachedSession) and exposes no subscribe/listener API—so session can stay stale until an unrelated re-render.

Current wiring
const session = useSyncExternalStore(
  () => () => {},
  () => getCachedSession(),
  () => null,
);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/app/page.tsx` around lines 14 - 18, The subscription passed to
useSyncExternalStore is a no-op so session (from getCachedSession) never updates
on login/logout; export and implement a real subscription in web/lib/session.ts
(e.g., subscribeToSession or onSessionChange) that listens for
auth/cookie/session updates and returns an unsubscribe function, then change the
useSyncExternalStore call in web/app/page.tsx to pass that subscribe function as
the first argument (keep getCachedSession as the snapshot) so session re-renders
when the session changes.

Comment on lines +27 to +38
const planParam = (searchParams.get("plan") || "pro") as PlanKey;

const session = useSyncExternalStore(
() => () => {},
() => getCachedSession(),
() => null,
);

const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);

const plan = SUBSCRIPTION_PLANS[planParam] || SUBSCRIPTION_PLANS.pro;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use the normalized plan key for the API call.

plan falls back to SUBSCRIPTION_PLANS.pro for display, but the request still sends raw planParam. For /subscription/checkout?plan=enterprise, the page renders Pro pricing and then posts plan: "enterprise". Send the validated key instead of the unchecked search param.

Also applies to: 95-99

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/app/subscription/checkout/page.tsx` around lines 27 - 38, The page
currently uses planParam (raw search param) for API requests even though plan is
validated/fallback via SUBSCRIPTION_PLANS; compute a validated plan key (e.g.,
validatedPlanKey = planParam in SUBSCRIPTION_PLANS ? planParam : "pro") and use
that validatedPlanKey for the checkout POST and any other places that currently
reference planParam (including the occurrences around lines 95-99). Update uses
of planParam to reference validatedPlanKey so the API receives the
normalized/validated plan key consistent with the displayed plan (symbols to
locate: planParam, plan, SUBSCRIPTION_PLANS and the checkout POST call).

Comment on lines +144 to +147
<li className="flex items-start gap-2">
<span className="text-primary font-bold">✓</span>
<span>2% transaction fee (vs 3% for free)</span>
</li>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update the Pro fee copy to match this PR.

This still says “2% transaction fee,” but the PR objective changes Pro to 1%. The checkout page will advertise the wrong plan terms unless this copy is updated too.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/app/subscription/checkout/page.tsx` around lines 144 - 147, Update the
Pro plan fee text in the checkout page JSX: locate the list item rendering the
fee (the <li className="flex items-start gap-2"> containing the span with "2%
transaction fee (vs 3% for free)") in web/app/subscription/checkout/page.tsx and
change the copy to "1% transaction fee (vs 3% for free)" so the displayed plan
terms match the PR that sets Pro to 1%.

Comment on lines +46 to +50
useEffect(() => {
if (session === null) {
router.push("/vendors/signup");
}
}, [session, router]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Gate this route to vendors before rendering.

Right now only session === null is redirected. An authenticated buyer session bypasses that check, never fetches because vendor_id is missing, and stays on this vendor-only page showing a perpetual “Loading your products...” state plus vendor CTAs. Add an explicit session.role === "vendor" guard before rendering/fetching.

Suggested fix
   useEffect(() => {
-    if (session === null) {
-      router.push("/vendors/signup");
+    if (session === null || session.role !== "vendor") {
+      router.replace("/vendors/signup");
     }
   }, [session, router]);

   useEffect(() => {
-    if (!session?.vendor_id) return;
+    if (session?.role !== "vendor" || !session.vendor_id) {
+      setLoading(false);
+      setProducts([]);
+      return;
+    }

     let ignore = false;
     setLoading(true);

Also applies to: 52-78, 78-100

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/app/vendors/products/page.tsx` around lines 46 - 50, The route currently
only redirects when session === null, allowing authenticated non-vendor users to
render the vendor page and block on fetch; update the guards so only users with
session.role === "vendor" proceed. In the useEffect that currently checks
session (the useEffect in page.tsx), change the logic to redirect any non-vendor
(session === null OR session.role !== "vendor") to "/vendors/signup" (or the
appropriate non-vendor route), and update any data-fetching guards (the vendor
products fetch/loader and any code that uses vendor_id) to only run when
session?.role === "vendor" and vendor_id is present (e.g.,
session.user.vendor_id or the vendor_id prop), preventing rendering of vendor
components and fetch calls for buyers.

Comment on lines +142 to +144
<p className="text-xs text-muted-foreground mt-1">
{product.paid_count} completed sales · {product.completion_rate.toFixed(1)}% completion rate
</p>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Multiply completion_rate before appending %.

The backend contract returns completion_rate as a ratio in the 0..1 range, so this currently renders 0.8% for an 80% product instead of 80.0%.

Suggested fix
-                            {product.paid_count} completed sales · {product.completion_rate.toFixed(1)}% completion rate
+                            {product.paid_count} completed sales · {(product.completion_rate * 100).toFixed(1)}% completion rate
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<p className="text-xs text-muted-foreground mt-1">
{product.paid_count} completed sales · {product.completion_rate.toFixed(1)}% completion rate
</p>
<p className="text-xs text-muted-foreground mt-1">
{product.paid_count} completed sales · {(product.completion_rate * 100).toFixed(1)}% completion rate
</p>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/app/vendors/products/page.tsx` around lines 142 - 144, The displayed
completion percentage uses product.completion_rate as a 0..1 ratio; multiply it
by 100 before formatting and appending '%' so 0.8 becomes 80.0%. Update the JSX
that references product.completion_rate (in the paragraph alongside
product.paid_count) to use (product.completion_rate * 100).toFixed(1) + '%' (or
a small null-safe expression like product.completion_rate != null ?
(product.completion_rate * 100).toFixed(1) + '%' : '—') to render the correct
percentage.

name: "Pro",
price: 1500,
fee: "2%",
fee: "1%",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fee messaging is now inconsistent across upgrade screens.

Line 31 shows Pro as 1%, but web/app/subscription/checkout/page.tsx still states 2% transaction fee in the included-features list. Keep these values aligned to avoid pricing confusion.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/components/home/price-tier.tsx` at line 31, The Pro tier fee string in
the PriceTier component is "1%" (symbol: fee in
web/components/home/price-tier.tsx) but the subscription checkout
included-features list (symbol: the transaction fee text in page.tsx) shows
"2%"; update one of these to match the other so messaging is consistent—either
change fee: "1%" to "2%" in the PriceTier component or update the checkout
page's transaction fee text to "1%", and ensure both the PriceTier component
(fee) and the checkout included-features text use the exact same percentage
string.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant