Commit cc1a1c0
feat(website): live Stripe Payment Links + post-checkout spinner + webhook provisioning (#1178)
* feat(website): wire live Stripe Payment Links + post-checkout UX + webhook provisioning
End-to-end Stripe checkout now works against the live account:
**Pricing page (src/pages/pricing.astro)**
When the user is signed in at click time, the Subscribe buttons append
?client_reference_id=<user.id>&prefilled_email=<user.email>
to the Stripe Payment Link URL. The stripe-webhook edge function reads
client_reference_id first and falls back to customer_email so the
webhook can bind the Stripe customer to the right Supabase profile
regardless of whether the customer signed up before or after paying.
**Post-checkout provisioning UX (src/pages/account/licenses/index.astro)**
Stripe's success_url redirects to /account/licenses/?new=true. The page
now detects that query param and renders a spinner + 'Issuing your
license...' banner instead of the default loading state. A 1-second
polling loop runs for up to 30 seconds waiting for the stripe-webhook
edge function's license_keys INSERT to land; the moment the row
appears the banner flips to a green checkmark and the new license
card renders in the list. If the webhook is still pending at the 30s
deadline, the banner shows an amber warning with a support email.
The license-list rendering was factored out of loadLicenses() into a
shared renderLicenses() helper so the polling path and the normal path
use identical markup + setup-instructions logic. The history.replaceState
call clears ?new=true from the URL so a browser refresh doesn't retrigger
the spinner after the license is already visible.
**Webhook handler rewrite (supabase/functions/stripe-webhook/index.ts)**
- Inserts now include product='aidotnet' so they pass the NOT NULL
constraint added by migration 20260419000000.
- user_id resolution falls back through client_reference_id →
customer_details.email; failures log loudly so admins can reconcile.
- Idempotent: if the user already holds an active license at the same
(product, tier), a retried checkout.session.completed event no-ops.
- Secrets read lazily inside the request handler so deploy doesn't
throw when STRIPE_SECRET_KEY isn't yet set on a fresh project.
- Uses the async constructEventAsync signature variant for Deno.
- Tier max_activations centralized in TIER_MAX_ACTIVATIONS map.
The function is already deployed to prod; secrets STRIPE_SECRET_KEY +
STRIPE_WEBHOOK_SECRET need to be set on the Supabase project's Edge
Functions → Secrets dashboard before real payments will process.
Verify:
- Local `npx astro build`: 78 pages, 0 errors.
- DNS probe: MX + SPF propagated (sales@aidotnet.dev → cheatcountry@gmail.com).
- 4 live Payment Links created in Stripe (Pro monthly/yearly, Ent monthly/yearly).
- Stripe webhook endpoint created (events: checkout.session.completed,
invoice.paid, customer.subscription.{created,updated,deleted},
invoice.payment_failed).
- 4 PUBLIC_STRIPE_*_LINK + STRIPE_SECRET_KEY + STRIPE_WEBHOOK_SECRET
GitHub secrets set on ooples/AiDotNet.
* fix(website): CodeRabbit review on PR #1178 — 7 review comments addressed
account/licenses/index.astro (3 comments):
- Document p99 webhook latency + rationale for the 1s/30s poll cadence
so future maintainers know when to revisit these values.
- Replace innerHTML assignment for the support-email subtitle with
textContent + document.createElement to close the 'hardcoded today,
interpolated tomorrow' XSS vector. The mailto link is built as a DOM
node instead of a string.
- Flip the polling loop so it CHECKS first and sleeps between retries.
If the Stripe webhook landed while the initial loadLicenses() fetch
was in flight, the first poll wins immediately and we skip the
gratuitous 1-second spinner pause on the happy path.
stripe-webhook/index.ts (4 comments):
- Derive the allowed-tier set from TIER_MAX_ACTIVATIONS instead of
hardcoding a parallel [professional, enterprise] list. Adding a tier
becomes a one-line change on the map.
- handleCheckoutCompleted: profile update no longer silently ignores
errors. On failure, log a warning and continue (license insert is
the customer-critical path; profile is recoverable via admin).
- handleSubscriptionUpdated: same pattern — warn-and-continue on
profile update failures so RLS/permission regressions surface in
logs instead of hiding behind silent success.
- handleSubscriptionDeleted: same pattern on the profile downgrade.
License row is the source of truth for access; a stale profile
only affects the billing page's tier display.
Verify: local astro build clean (78 pages, 0 errors).
* fix(website): CodeRabbit round 2 on PR #1178 — 3 review comments
1. (Nit) Spinner icon still used innerHTML for its state swap, inconsistent
with the XSS-safe subtitle builder. Replaced the three-state swap with
three pre-rendered SVGs in the markup (loading, success, warning) that
toggle via hidden class. No innerHTML sink remains on the status-display
code path; future state additions are just another SVG + classList
toggle.
2. (Nit) Polling loop selected id rows and then counted .length, fetching
every license row on every iteration. Switched to
.select('*', { count: 'exact', head: true })
which asks PostgREST for only the Content-Range header (no row data).
Poll bandwidth is now O(1) regardless of how many licenses the user
owns.
3. (Nit) handleSubscriptionUpdated assumed single-item subscriptions when
extracting tier. Stripe permits multi-item subs (plan + add-on, etc.).
Replaced subscription.items.data[0] lookup with a TIER_RANK-ordered
walk: collect every known tier advertised in price metadata, pick the
highest-ranked one. Enterprise wins over professional; unknown tiers
are ignored rather than trusted. Adding a new tier requires appending
it to TIER_RANK so precedence stays explicit.
Verify: local astro build clean (78 pages, 0 errors).
---------
Co-authored-by: franklinic <franklin@ivorycloud.com>1 parent 075b15b commit cc1a1c0
3 files changed
Lines changed: 440 additions & 119 deletions
File tree
- website
- src/pages
- account/licenses
- supabase/functions/stripe-webhook
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
42 | 42 | | |
43 | 43 | | |
44 | 44 | | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
45 | 81 | | |
46 | 82 | | |
47 | 83 | | |
| |||
93 | 129 | | |
94 | 130 | | |
95 | 131 | | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
96 | 162 | | |
97 | 163 | | |
98 | 164 | | |
99 | 165 | | |
100 | 166 | | |
| 167 | + | |
101 | 168 | | |
102 | 169 | | |
103 | 170 | | |
104 | | - | |
| 171 | + | |
105 | 172 | | |
106 | 173 | | |
107 | 174 | | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
108 | 183 | | |
109 | 184 | | |
110 | 185 | | |
111 | 186 | | |
112 | 187 | | |
113 | 188 | | |
114 | | - | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
115 | 251 | | |
116 | 252 | | |
117 | 253 | | |
| |||
122 | 258 | | |
123 | 259 | | |
124 | 260 | | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
125 | 308 | | |
126 | 309 | | |
| 310 | + | |
| 311 | + | |
127 | 312 | | |
128 | 313 | | |
129 | 314 | | |
130 | | - | |
131 | | - | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
132 | 318 | | |
| 319 | + | |
133 | 320 | | |
134 | 321 | | |
135 | 322 | | |
136 | 323 | | |
137 | | - | |
| 324 | + | |
138 | 325 | | |
139 | 326 | | |
140 | 327 | | |
| |||
155 | 342 | | |
156 | 343 | | |
157 | 344 | | |
158 | | - | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
159 | 348 | | |
160 | | - | |
161 | | - | |
| 349 | + | |
162 | 350 | | |
163 | 351 | | |
164 | 352 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
313 | 313 | | |
314 | 314 | | |
315 | 315 | | |
316 | | - | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
317 | 324 | | |
318 | 325 | | |
319 | 326 | | |
320 | | - | |
321 | | - | |
322 | | - | |
323 | | - | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
324 | 345 | | |
| 346 | + | |
325 | 347 | | |
326 | 348 | | |
327 | 349 | | |
| |||
0 commit comments