@dodopayments/opencode-plugin 0.6.1 → 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/package.json +1 -1
  2. package/skills/billing-sdk/SKILL.md +8 -193
  3. package/skills/billing-sdk/references/pricing-page.md +197 -0
  4. package/skills/checkout-integration/SKILL.md +14 -449
  5. package/skills/checkout-integration/references/overlay-checkout.md +148 -0
  6. package/skills/checkout-integration/references/server-examples.md +317 -0
  7. package/skills/credit-based-billing/SKILL.md +5 -151
  8. package/skills/credit-based-billing/references/ai-token-example.md +108 -0
  9. package/skills/credit-based-billing/references/credit-webhooks.md +51 -0
  10. package/skills/framework-adapters/SKILL.md +38 -527
  11. package/skills/framework-adapters/references/astro.md +52 -0
  12. package/skills/framework-adapters/references/bun.md +45 -0
  13. package/skills/framework-adapters/references/convex.md +91 -0
  14. package/skills/framework-adapters/references/express.md +72 -0
  15. package/skills/framework-adapters/references/fastify.md +54 -0
  16. package/skills/framework-adapters/references/hono.md +63 -0
  17. package/skills/framework-adapters/references/nuxt.md +53 -0
  18. package/skills/framework-adapters/references/remix.md +65 -0
  19. package/skills/framework-adapters/references/sveltekit.md +49 -0
  20. package/skills/framework-adapters/references/tanstack-start.md +31 -0
  21. package/skills/license-keys/SKILL.md +8 -303
  22. package/skills/license-keys/references/cli-tool.md +143 -0
  23. package/skills/license-keys/references/desktop-app.md +170 -0
  24. package/skills/mobile-checkout/SKILL.md +25 -313
  25. package/skills/mobile-checkout/references/android.md +69 -0
  26. package/skills/mobile-checkout/references/flutter.md +97 -0
  27. package/skills/mobile-checkout/references/ios.md +66 -0
  28. package/skills/mobile-checkout/references/react-native.md +100 -0
  29. package/skills/refunds-and-disputes/SKILL.md +1 -1
  30. package/skills/subscription-integration/SKILL.md +3 -77
  31. package/skills/subscription-integration/references/webhook-events.md +82 -0
  32. package/skills/usage-based-billing/SKILL.md +5 -114
  33. package/skills/usage-based-billing/references/instrumentation.md +119 -0
  34. package/skills/webhook-integration/SKILL.md +28 -277
  35. package/skills/webhook-integration/references/common-mistakes.md +146 -0
  36. package/skills/webhook-integration/references/event-catalog.md +103 -0
  37. package/skills/webhook-integration/references/local-testing.md +42 -0
@@ -209,467 +209,31 @@ const session = await client.checkoutSessions.create({
209
209
 
210
210
  ## Next.js App Router
211
211
 
212
- ### API Route
212
+ Full guide: [references/server-examples.md](references/server-examples.md).
213
213
 
214
- ```typescript
215
- // app/api/checkout/route.ts
216
- import { NextResponse } from 'next/server';
217
- import DodoPayments from 'dodopayments';
218
- import { getCurrentUser } from '@/lib/auth';
219
-
220
- const client = new DodoPayments({
221
- bearerToken: process.env.DODO_PAYMENTS_API_KEY!,
222
- environment: process.env.NODE_ENV === 'production' ? 'live_mode' : 'test_mode'
223
- });
224
-
225
- type PlanId = 'starter' | 'pro';
226
-
227
- const PRODUCT_IDS: Record<PlanId, string> = {
228
- starter: 'pdt_starter_monthly',
229
- pro: 'pdt_pro_monthly'
230
- };
231
-
232
- function productIdForPlan(value: unknown): string | null {
233
- if (typeof value !== 'string') return null;
234
- return PRODUCT_IDS[value as PlanId] ?? null;
235
- }
236
-
237
- export async function POST(request: Request) {
238
- const user = await getCurrentUser();
239
- if (!user) {
240
- return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
241
- }
242
-
243
- let body: unknown;
244
- try {
245
- body = await request.json();
246
- } catch {
247
- return NextResponse.json({ error: 'Invalid request body' }, { status: 400 });
248
- }
249
-
250
- if (!body || typeof body !== 'object') {
251
- return NextResponse.json({ error: 'Invalid request body' }, { status: 400 });
252
- }
253
-
254
- const input = body as { planId?: unknown; quantity?: unknown };
255
- const productId = productIdForPlan(input.planId);
256
- const quantity = input.quantity ?? 1;
257
-
258
- if (!productId) {
259
- return NextResponse.json({ error: 'Unknown plan' }, { status: 400 });
260
- }
261
-
262
- if (!Number.isInteger(quantity) || (quantity as number) < 1) {
263
- return NextResponse.json(
264
- { error: 'Quantity must be a positive integer' },
265
- { status: 400 }
266
- );
267
- }
268
-
269
- try {
270
- const session = await client.checkoutSessions.create({
271
- product_cart: [{ product_id: productId, quantity: quantity as number }],
272
- customer: { email: user.email, name: user.name },
273
- metadata: { app_user_id: user.id },
274
- return_url: `${process.env.NEXT_PUBLIC_APP_URL}/checkout/success`
275
- });
276
-
277
- return NextResponse.json({
278
- checkoutUrl: session.checkout_url,
279
- sessionId: session.session_id
280
- });
281
- } catch (error) {
282
- console.error('Checkout error:', error);
283
- return NextResponse.json(
284
- { error: 'Failed to create checkout' },
285
- { status: 500 }
286
- );
287
- }
288
- }
289
- ```
290
-
291
- ### Client Component
292
-
293
- ```typescript
294
- // components/CheckoutButton.tsx
295
- 'use client';
296
-
297
- import { useState } from 'react';
298
-
299
- interface CheckoutButtonProps {
300
- planId: 'starter' | 'pro';
301
- quantity?: number;
302
- children: React.ReactNode;
303
- }
304
-
305
- export function CheckoutButton({ planId, quantity = 1, children }: CheckoutButtonProps) {
306
- const [loading, setLoading] = useState(false);
307
-
308
- const handleCheckout = async () => {
309
- setLoading(true);
310
- try {
311
- const response = await fetch('/api/checkout', {
312
- method: 'POST',
313
- headers: { 'Content-Type': 'application/json' },
314
- body: JSON.stringify({ planId, quantity })
315
- });
316
-
317
- if (!response.ok) {
318
- throw new Error('Failed to create checkout');
319
- }
320
-
321
- const data = (await response.json()) as { checkoutUrl?: string };
322
- if (!data.checkoutUrl) throw new Error('Checkout URL was not returned');
323
- window.location.href = data.checkoutUrl;
324
- } catch (error) {
325
- console.error('Checkout error:', error);
326
- alert('Failed to start checkout. Please try again.');
327
- } finally {
328
- setLoading(false);
329
- }
330
- };
331
-
332
- return (
333
- <button onClick={handleCheckout} disabled={loading}>
334
- {loading ? 'Loading...' : children}
335
- </button>
336
- );
337
- }
338
- ```
339
-
340
- ### Success Page
341
-
342
- ```typescript
343
- // app/checkout/success/page.tsx
344
- import { Suspense } from 'react';
345
-
346
- function SuccessContent() {
347
- return (
348
- <div className="text-center py-20">
349
- <h1 className="text-3xl font-bold">Payment Successful</h1>
350
- <p className="mt-4 text-gray-600">
351
- Thank you for your purchase. Check your email for a confirmation.
352
- </p>
353
- <a href="/" className="mt-8 inline-block text-blue-600 hover:underline">
354
- Return to Home
355
- </a>
356
- </div>
357
- );
358
- }
359
-
360
- export default function SuccessPage() {
361
- return (
362
- <Suspense fallback={<div>Loading...</div>}>
363
- <SuccessContent />
364
- </Suspense>
365
- );
366
- }
367
- ```
214
+ Covers:
368
215
 
369
- ---
216
+ - API Route
217
+ - Client Component
218
+ - Success Page
370
219
 
371
220
  ## Express.js
372
221
 
373
- ```typescript
374
- import express from 'express';
375
- import DodoPayments from 'dodopayments';
376
- import { getCurrentUser } from './auth';
377
- import { fulfillOneTimePurchase, grantSubscriptionAccess } from './entitlements';
378
-
379
- const app = express();
380
-
381
- const client = new DodoPayments({
382
- bearerToken: process.env.DODO_PAYMENTS_API_KEY!,
383
- environment: process.env.NODE_ENV === 'production' ? 'live_mode' : 'test_mode'
384
- });
385
-
386
- // Mount the webhook before express.json() so verification receives signed bytes.
387
- app.post('/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
388
- try {
389
- const event = client.webhooks.unwrap(req.body.toString('utf8'), {
390
- headers: {
391
- 'webhook-id': req.headers['webhook-id'] as string,
392
- 'webhook-signature': req.headers['webhook-signature'] as string,
393
- 'webhook-timestamp': req.headers['webhook-timestamp'] as string
394
- }
395
- });
396
-
397
- if (event.type === 'payment.succeeded') {
398
- // This branch fulfills one-time purchases only.
399
- await fulfillOneTimePurchase(event.data.customer.customer_id);
400
- } else if (event.type === 'subscription.active') {
401
- // Subscription access starts only after this verified event.
402
- await grantSubscriptionAccess(event.data);
403
- }
404
-
405
- res.json({ received: true });
406
- } catch (error) {
407
- console.error('Webhook verification failed:', error);
408
- res.status(401).json({ error: 'Invalid signature' });
409
- }
410
- });
411
-
412
- app.use(express.json());
413
-
414
- type PlanId = 'starter' | 'pro';
415
-
416
- const PRODUCT_IDS: Record<PlanId, string> = {
417
- starter: 'pdt_starter_monthly',
418
- pro: 'pdt_pro_monthly'
419
- };
420
-
421
- function productIdForPlan(value: unknown): string | null {
422
- if (typeof value !== 'string') return null;
423
- return PRODUCT_IDS[value as PlanId] ?? null;
424
- }
425
-
426
- app.post('/api/checkout', async (req, res) => {
427
- const user = await getCurrentUser(req);
428
- if (!user) {
429
- res.status(401).json({ error: 'Not authenticated' });
430
- return;
431
- }
432
-
433
- const { planId, quantity = 1 } = req.body as {
434
- planId?: unknown;
435
- quantity?: unknown;
436
- };
437
- const productId = productIdForPlan(planId);
438
-
439
- if (!productId) {
440
- res.status(400).json({ error: 'Unknown plan' });
441
- return;
442
- }
443
-
444
- if (!Number.isInteger(quantity) || (quantity as number) < 1) {
445
- res.status(400).json({ error: 'Quantity must be a positive integer' });
446
- return;
447
- }
448
-
449
- try {
450
- const session = await client.checkoutSessions.create({
451
- product_cart: [{ product_id: productId, quantity: quantity as number }],
452
- customer: { email: user.email, name: user.name },
453
- metadata: { app_user_id: user.id },
454
- return_url: `${process.env.APP_URL}/success`
455
- });
456
-
457
- res.json({ checkoutUrl: session.checkout_url });
458
- } catch (error) {
459
- console.error('Checkout error:', error);
460
- res.status(500).json({ error: 'Failed to create checkout' });
461
- }
462
- });
463
-
464
- app.get('/success', (req, res) => {
465
- res.send('Payment successful!');
466
- });
467
- ```
468
-
469
- ---
222
+ Full guide: [references/server-examples.md](references/server-examples.md).
470
223
 
471
224
  ## Python (FastAPI)
472
225
 
473
- ```python
474
- import logging
475
- import os
476
- from typing import Annotated, Literal
477
-
478
- from dodopayments import DodoPayments
479
- from fastapi import Depends, FastAPI, HTTPException
480
- from pydantic import BaseModel, Field
481
-
482
- from app.auth import User, get_current_user
483
-
484
- app = FastAPI()
485
- logger = logging.getLogger(__name__)
486
- client = DodoPayments(
487
- bearer_token=os.environ["DODO_PAYMENTS_API_KEY"],
488
- environment="test_mode"
489
- )
490
-
491
- PRODUCT_IDS = {
492
- "starter": "pdt_starter_monthly",
493
- "pro": "pdt_pro_monthly",
494
- }
495
-
496
- class CheckoutRequest(BaseModel):
497
- plan_id: Literal["starter", "pro"]
498
- quantity: int = Field(default=1, gt=0)
499
-
500
- @app.post("/api/checkout")
501
- async def create_checkout(
502
- request: CheckoutRequest,
503
- user: Annotated[User, Depends(get_current_user)],
504
- ):
505
- try:
506
- session = client.checkout_sessions.create(
507
- product_cart=[{
508
- "product_id": PRODUCT_IDS[request.plan_id],
509
- "quantity": request.quantity
510
- }],
511
- customer={
512
- "email": user.email,
513
- "name": user.name
514
- },
515
- metadata={"app_user_id": user.id},
516
- return_url=f"{os.environ['APP_URL']}/success"
517
- )
518
- return {"checkout_url": session.checkout_url}
519
- except Exception as error:
520
- logger.exception("Checkout session creation failed")
521
- raise HTTPException(status_code=500, detail="Failed to create checkout") from error
522
- ```
523
-
524
- ---
226
+ Full guide: [references/server-examples.md](references/server-examples.md).
525
227
 
526
228
  ## Overlay Checkout
527
229
 
528
- Use the `dodopayments-checkout` package for overlay or inline checkout that stays on your site.
230
+ Full guide: [references/overlay-checkout.md](references/overlay-checkout.md).
529
231
 
530
- ### Installation
232
+ Covers:
531
233
 
532
- ```bash
533
- npm install dodopayments-checkout
534
- ```
535
-
536
- ### Server Route
537
-
538
- Create the Checkout Session on the server. The browser sends a public plan slug, while the server derives
539
- the customer from the authenticated user and maps the slug to an allowlisted product id.
540
-
541
- ```typescript
542
- // app/api/overlay-checkout/route.ts
543
- import { NextResponse } from 'next/server';
544
- import DodoPayments from 'dodopayments';
545
- import { getCurrentUser } from '@/lib/auth';
546
-
547
- const client = new DodoPayments({
548
- bearerToken: process.env.DODO_PAYMENTS_API_KEY!,
549
- environment: process.env.NODE_ENV === 'production' ? 'live_mode' : 'test_mode'
550
- });
551
-
552
- type PlanId = 'starter' | 'pro';
553
-
554
- const PRODUCT_IDS: Record<PlanId, string> = {
555
- starter: 'pdt_starter_monthly',
556
- pro: 'pdt_pro_monthly'
557
- };
558
-
559
- export async function POST(request: Request) {
560
- const user = await getCurrentUser();
561
- if (!user) {
562
- return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
563
- }
564
-
565
- let body: unknown;
566
- try {
567
- body = await request.json();
568
- } catch {
569
- return NextResponse.json({ error: 'Invalid request body' }, { status: 400 });
570
- }
571
-
572
- if (!body || typeof body !== 'object') {
573
- return NextResponse.json({ error: 'Invalid request body' }, { status: 400 });
574
- }
575
-
576
- const { planId, quantity = 1 } = body as {
577
- planId?: unknown;
578
- quantity?: unknown;
579
- };
580
- const productId = typeof planId === 'string'
581
- ? PRODUCT_IDS[planId as PlanId]
582
- : undefined;
583
-
584
- if (!productId) {
585
- return NextResponse.json({ error: 'Unknown plan' }, { status: 400 });
586
- }
587
-
588
- if (!Number.isInteger(quantity) || (quantity as number) < 1) {
589
- return NextResponse.json(
590
- { error: 'Quantity must be a positive integer' },
591
- { status: 400 }
592
- );
593
- }
594
-
595
- try {
596
- const session = await client.checkoutSessions.create({
597
- product_cart: [{ product_id: productId, quantity: quantity as number }],
598
- customer: { email: user.email, name: user.name },
599
- metadata: { app_user_id: user.id },
600
- return_url: `${process.env.NEXT_PUBLIC_APP_URL}/checkout/success`
601
- });
602
-
603
- if (!session.checkout_url) {
604
- return NextResponse.json({ error: 'Checkout URL was not returned' }, { status: 502 });
605
- }
606
-
607
- return NextResponse.json({ checkoutUrl: session.checkout_url });
608
- } catch (error) {
609
- console.error('Overlay checkout error:', error);
610
- return NextResponse.json({ error: 'Failed to create checkout' }, { status: 500 });
611
- }
612
- }
613
- ```
614
-
615
- ### Browser Overlay and Inline Modes
616
-
617
- Browser code calls the server route and receives only the checkout URL. The Dodo Payments API bearer token
618
- stays in the server route and is never sent to the browser.
619
-
620
- ```typescript
621
- // lib/open-checkout.ts
622
- import { DodoPayments } from 'dodopayments-checkout';
623
-
624
- type PlanId = 'starter' | 'pro';
625
-
626
- async function createCheckoutUrl(planId: PlanId, quantity = 1): Promise<string> {
627
- const response = await fetch('/api/overlay-checkout', {
628
- method: 'POST',
629
- headers: { 'Content-Type': 'application/json' },
630
- body: JSON.stringify({ planId, quantity })
631
- });
632
-
633
- if (!response.ok) throw new Error('Failed to create checkout');
634
-
635
- const data = (await response.json()) as { checkoutUrl?: string };
636
- if (!data.checkoutUrl) throw new Error('Checkout URL was not returned');
637
- return data.checkoutUrl;
638
- }
639
-
640
- // Call ONCE when the app loads (e.g. in your root component or entry point),
641
- // not before every checkout. Pick the display type your page uses.
642
- export function initCheckout(displayType: 'overlay' | 'inline'): void {
643
- DodoPayments.Initialize({
644
- mode: 'test', // or 'live'
645
- displayType,
646
- onEvent: (event) => {
647
- console.log('Checkout event:', event);
648
- }
649
- });
650
- }
651
-
652
- // Requires initCheckout('overlay') to have run at app load.
653
- export async function openOverlayCheckout(planId: PlanId): Promise<void> {
654
- const checkoutUrl = await createCheckoutUrl(planId);
655
- DodoPayments.Checkout.open({ checkoutUrl });
656
- }
657
-
658
- // Requires initCheckout('inline') to have run at app load.
659
- export async function openInlineCheckout(planId: PlanId): Promise<void> {
660
- const checkoutUrl = await createCheckoutUrl(planId);
661
- DodoPayments.Checkout.open({
662
- checkoutUrl,
663
- elementId: 'dodo-inline-checkout'
664
- });
665
- }
666
- ```
667
-
668
- ```html
669
- <div id="dodo-inline-checkout"></div>
670
- ```
671
-
672
- ---
234
+ - Installation
235
+ - Server Route
236
+ - Browser Overlay and Inline Modes
673
237
 
674
238
  ## Static Payment Links
675
239
 
@@ -804,7 +368,8 @@ There is no `session_id` parameter. Treat `processing`/missing status as "unknow
804
368
 
805
369
  ### Verify Payment Server-Side
806
370
 
807
- Do not trust the browser redirect. Use the Express webhook route above: it receives the raw signed body
371
+ Do not trust the browser redirect. Use the Express webhook route in
372
+ [references/server-examples.md](references/server-examples.md#expressjs): it receives the raw signed body
808
373
  before `express.json()`, fulfills one-time purchases on verified `payment.succeeded` using
809
374
  `event.data.customer.customer_id`, and grants subscription access only on verified `subscription.active`.
810
375
 
@@ -0,0 +1,148 @@
1
+ # Dodo Payments Checkout Integration: Overlay Checkout
2
+
3
+
4
+ Use the `dodopayments-checkout` package for overlay or inline checkout that stays on your site.
5
+
6
+ ### Installation
7
+
8
+ ```bash
9
+ npm install dodopayments-checkout
10
+ ```
11
+
12
+ ### Server Route
13
+
14
+ Create the Checkout Session on the server. The browser sends a public plan slug, while the server derives
15
+ the customer from the authenticated user and maps the slug to an allowlisted product id.
16
+
17
+ ```typescript
18
+ // app/api/overlay-checkout/route.ts
19
+ import { NextResponse } from 'next/server';
20
+ import DodoPayments from 'dodopayments';
21
+ import { getCurrentUser } from '@/lib/auth';
22
+
23
+ const client = new DodoPayments({
24
+ bearerToken: process.env.DODO_PAYMENTS_API_KEY!,
25
+ environment: process.env.NODE_ENV === 'production' ? 'live_mode' : 'test_mode'
26
+ });
27
+
28
+ type PlanId = 'starter' | 'pro';
29
+
30
+ const PRODUCT_IDS: Record<PlanId, string> = {
31
+ starter: 'pdt_starter_monthly',
32
+ pro: 'pdt_pro_monthly'
33
+ };
34
+
35
+ export async function POST(request: Request) {
36
+ const user = await getCurrentUser();
37
+ if (!user) {
38
+ return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
39
+ }
40
+
41
+ let body: unknown;
42
+ try {
43
+ body = await request.json();
44
+ } catch {
45
+ return NextResponse.json({ error: 'Invalid request body' }, { status: 400 });
46
+ }
47
+
48
+ if (!body || typeof body !== 'object') {
49
+ return NextResponse.json({ error: 'Invalid request body' }, { status: 400 });
50
+ }
51
+
52
+ const { planId, quantity = 1 } = body as {
53
+ planId?: unknown;
54
+ quantity?: unknown;
55
+ };
56
+ const productId = typeof planId === 'string'
57
+ ? PRODUCT_IDS[planId as PlanId]
58
+ : undefined;
59
+
60
+ if (!productId) {
61
+ return NextResponse.json({ error: 'Unknown plan' }, { status: 400 });
62
+ }
63
+
64
+ if (!Number.isInteger(quantity) || (quantity as number) < 1) {
65
+ return NextResponse.json(
66
+ { error: 'Quantity must be a positive integer' },
67
+ { status: 400 }
68
+ );
69
+ }
70
+
71
+ try {
72
+ const session = await client.checkoutSessions.create({
73
+ product_cart: [{ product_id: productId, quantity: quantity as number }],
74
+ customer: { email: user.email, name: user.name },
75
+ metadata: { app_user_id: user.id },
76
+ return_url: `${process.env.NEXT_PUBLIC_APP_URL}/checkout/success`
77
+ });
78
+
79
+ if (!session.checkout_url) {
80
+ return NextResponse.json({ error: 'Checkout URL was not returned' }, { status: 502 });
81
+ }
82
+
83
+ return NextResponse.json({ checkoutUrl: session.checkout_url });
84
+ } catch (error) {
85
+ console.error('Overlay checkout error:', error);
86
+ return NextResponse.json({ error: 'Failed to create checkout' }, { status: 500 });
87
+ }
88
+ }
89
+ ```
90
+
91
+ ### Browser Overlay and Inline Modes
92
+
93
+ Browser code calls the server route and receives only the checkout URL. The Dodo Payments API bearer token
94
+ stays in the server route and is never sent to the browser.
95
+
96
+ ```typescript
97
+ // lib/open-checkout.ts
98
+ import { DodoPayments } from 'dodopayments-checkout';
99
+
100
+ type PlanId = 'starter' | 'pro';
101
+
102
+ async function createCheckoutUrl(planId: PlanId, quantity = 1): Promise<string> {
103
+ const response = await fetch('/api/overlay-checkout', {
104
+ method: 'POST',
105
+ headers: { 'Content-Type': 'application/json' },
106
+ body: JSON.stringify({ planId, quantity })
107
+ });
108
+
109
+ if (!response.ok) throw new Error('Failed to create checkout');
110
+
111
+ const data = (await response.json()) as { checkoutUrl?: string };
112
+ if (!data.checkoutUrl) throw new Error('Checkout URL was not returned');
113
+ return data.checkoutUrl;
114
+ }
115
+
116
+ // Call ONCE when the app loads (e.g. in your root component or entry point),
117
+ // not before every checkout. Pick the display type your page uses.
118
+ export function initCheckout(displayType: 'overlay' | 'inline'): void {
119
+ DodoPayments.Initialize({
120
+ mode: 'test', // or 'live'
121
+ displayType,
122
+ onEvent: (event) => {
123
+ console.log('Checkout event:', event);
124
+ }
125
+ });
126
+ }
127
+
128
+ // Requires initCheckout('overlay') to have run at app load.
129
+ export async function openOverlayCheckout(planId: PlanId): Promise<void> {
130
+ const checkoutUrl = await createCheckoutUrl(planId);
131
+ DodoPayments.Checkout.open({ checkoutUrl });
132
+ }
133
+
134
+ // Requires initCheckout('inline') to have run at app load.
135
+ export async function openInlineCheckout(planId: PlanId): Promise<void> {
136
+ const checkoutUrl = await createCheckoutUrl(planId);
137
+ DodoPayments.Checkout.open({
138
+ checkoutUrl,
139
+ elementId: 'dodo-inline-checkout'
140
+ });
141
+ }
142
+ ```
143
+
144
+ ```html
145
+ <div id="dodo-inline-checkout"></div>
146
+ ```
147
+
148
+ ---