@01.software/init 0.10.0 → 0.10.1
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.
- package/dist/ai-docs.js +0 -0
- package/dist/chunk-4LHYICUL.js +0 -0
- package/dist/chunk-NJ4X7VNK.js +0 -0
- package/dist/chunk-Q6MSORYN.js +0 -0
- package/dist/chunk-STM4DKVZ.js +0 -0
- package/dist/chunk-WDWJ73KP.js +0 -0
- package/dist/create-app-templates/ecommerce/CHANGELOG.md +18 -0
- package/dist/create-app-templates/ecommerce/README.md +31 -16
- package/dist/create-app-templates/ecommerce/components/cart/cart-content.tsx +23 -13
- package/dist/create-app-templates/ecommerce/components/checkout/checkout-form.tsx +145 -105
- package/dist/create-app-templates/ecommerce/lib/cart/server-cart.ts +45 -6
- package/dist/create-app-templates/ecommerce/lib/checkout/parse-checkout-payload.ts +49 -39
- package/dist/create-app-templates/ecommerce/lib/checkout/start-checkout.ts +12 -2
- package/dist/create-app-templates/ecommerce/lib/checkout/types.ts +5 -2
- package/dist/create-app-templates/ecommerce/lib/commerce/adapters/mock.ts +134 -124
- package/dist/create-app-templates/ecommerce/lib/commerce/adapters/software.ts +69 -52
- package/dist/create-app-templates/ecommerce/lib/commerce/types.ts +139 -137
- package/dist/create-app-templates/ecommerce/package.json +2 -2
- package/dist/create-app-templates/ecommerce/tests/customer-cart.test.ts +18 -9
- package/dist/create-app-templates/ecommerce/tests/domain.test.ts +107 -12
- package/dist/file-ops.js +0 -0
- package/dist/init.js +0 -0
- package/dist/templates.js +0 -0
- package/package.json +15 -16
package/dist/ai-docs.js
CHANGED
|
File without changes
|
package/dist/chunk-4LHYICUL.js
CHANGED
|
File without changes
|
package/dist/chunk-NJ4X7VNK.js
CHANGED
|
File without changes
|
package/dist/chunk-Q6MSORYN.js
CHANGED
|
File without changes
|
package/dist/chunk-STM4DKVZ.js
CHANGED
|
File without changes
|
package/dist/chunk-WDWJ73KP.js
CHANGED
|
File without changes
|
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# ecommerce
|
|
2
2
|
|
|
3
|
+
## 0.1.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [81ef105]
|
|
8
|
+
- Updated dependencies [9789528]
|
|
9
|
+
- Updated dependencies [9b7afab]
|
|
10
|
+
- Updated dependencies [db8bd4b]
|
|
11
|
+
- Updated dependencies [b95f6ac]
|
|
12
|
+
- Updated dependencies [c60ec7f]
|
|
13
|
+
- Updated dependencies [6c71a71]
|
|
14
|
+
- Updated dependencies [3aa6239]
|
|
15
|
+
- Updated dependencies [5d7b26f]
|
|
16
|
+
- Updated dependencies [294b7d1]
|
|
17
|
+
- Updated dependencies [f87fe09]
|
|
18
|
+
- Updated dependencies [4362b14]
|
|
19
|
+
- @01.software/sdk@0.43.0
|
|
20
|
+
|
|
3
21
|
## 0.1.1
|
|
4
22
|
|
|
5
23
|
### Patch Changes
|
|
@@ -21,7 +21,13 @@ provider for a generated app is recorded in `app-config.ts` and its keys live in
|
|
|
21
21
|
TossPayments payment REST API on the server.
|
|
22
22
|
- A theming seam: a CSS-variable token layer in `app/globals.css` plus Tailwind.
|
|
23
23
|
- Server-side order pricing, stock checks, and payment sync invariants.
|
|
24
|
-
-
|
|
24
|
+
- Server-authoritative cart operations through the template's own route handlers,
|
|
25
|
+
backed by `commerce.cart.*`.
|
|
26
|
+
- An HttpOnly cart cookie containing only the unguessable `cartToken`; client
|
|
27
|
+
JavaScript receives rendered cart views but never owns the authoritative cart
|
|
28
|
+
or token, and no cart contents are persisted in localStorage.
|
|
29
|
+
- Checkout through `orders.checkout({ cartId })`, with orders resolved after
|
|
30
|
+
payment by `orders.getByPaymentId`.
|
|
25
31
|
|
|
26
32
|
## Scaffolding vs. this in-repo source
|
|
27
33
|
|
|
@@ -44,13 +50,13 @@ pnpm dev
|
|
|
44
50
|
Without SDK or payment credentials, the app uses demo providers:
|
|
45
51
|
|
|
46
52
|
```bash
|
|
47
|
-
# 01.software keys omitted -> demo catalog
|
|
53
|
+
# 01.software keys omitted -> demo catalog plus in-memory cart/checkout
|
|
48
54
|
# payment keys omitted -> local demo payment completion
|
|
49
55
|
```
|
|
50
56
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
57
|
+
The mock adapter emulates the server cart and checkout flow in memory for
|
|
58
|
+
zero-backend demos. Redirect and success routes resolve the same in-process
|
|
59
|
+
checkout model without writing a local order index.
|
|
54
60
|
|
|
55
61
|
To use the Console ecommerce SDK adapter, set both SDK keys:
|
|
56
62
|
|
|
@@ -63,10 +69,11 @@ SOFTWARE_FREE_SHIPPING_ABOVE_AMOUNT=100000
|
|
|
63
69
|
```
|
|
64
70
|
|
|
65
71
|
`SOFTWARE_API_URL`, `SOFTWARE_SHIPPING_AMOUNT`, and
|
|
66
|
-
`SOFTWARE_FREE_SHIPPING_ABOVE_AMOUNT` are optional. The SDK adapter
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
72
|
+
`SOFTWARE_FREE_SHIPPING_ABOVE_AMOUNT` are optional. The SDK adapter uses Console
|
|
73
|
+
cart and order APIs for the full server-authoritative flow: route handlers call
|
|
74
|
+
`commerce.cart.*`, checkout calls `orders.checkout({ cartId })`, and
|
|
75
|
+
return/webhook/success reconciliation resolves orders by payment id through
|
|
76
|
+
`orders.getByPaymentId`.
|
|
70
77
|
|
|
71
78
|
## Payment provider keys
|
|
72
79
|
|
|
@@ -96,9 +103,12 @@ NEXT_PUBLIC_TOSSPAYMENTS_CUSTOMER_KEY=customer_...
|
|
|
96
103
|
|
|
97
104
|
`NEXT_PUBLIC_TOSSPAYMENTS_CUSTOMER_KEY` is optional. If omitted, the browser SDK
|
|
98
105
|
uses TossPayments' anonymous customer key. TossPayments redirects back with
|
|
99
|
-
`paymentKey`, `orderId`, and `amount`;
|
|
100
|
-
|
|
101
|
-
|
|
106
|
+
`paymentKey`, `orderId`, and `amount`; `/api/checkout/payment-return` re-fetches
|
|
107
|
+
the provider payment, rejects a tampered redirect amount, captures with the
|
|
108
|
+
PG-verified amount, then redirects to the success page. Success-page
|
|
109
|
+
reconciliation re-fetches the provider payment again and places the Console
|
|
110
|
+
checkout through `orders.confirmPayment`, where the open checkout quote remains
|
|
111
|
+
the authoritative amount check.
|
|
102
112
|
`TOSSPAYMENTS_API_BASE_URL` is optional and defaults to
|
|
103
113
|
`https://api.tosspayments.com/v1`.
|
|
104
114
|
|
|
@@ -133,7 +143,12 @@ pnpm build
|
|
|
133
143
|
|
|
134
144
|
## Important boundary
|
|
135
145
|
|
|
136
|
-
The
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
146
|
+
The browser never owns the authoritative cart or cart capability tokens. It
|
|
147
|
+
talks to the template's cart route handlers, which keep the cart
|
|
148
|
+
server-authoritative and store only an HttpOnly cart cookie in the browser.
|
|
149
|
+
Client components may keep rendered cart views in memory, but cart mutations and
|
|
150
|
+
checkout always return to the server cart. Checkout attaches customer and
|
|
151
|
+
shipping details to that server cart, converts it with
|
|
152
|
+
`orders.checkout({ cartId })`, and then synchronizes payment state. After
|
|
153
|
+
payment, order lookup uses `orders.getByPaymentId`, so the template does not
|
|
154
|
+
need a file-backed order index.
|
|
@@ -1,22 +1,25 @@
|
|
|
1
|
-
|
|
1
|
+
'use client'
|
|
2
2
|
|
|
3
|
-
import Image from
|
|
4
|
-
import Link from
|
|
5
|
-
import { useCart } from
|
|
6
|
-
import { formatMoney } from
|
|
3
|
+
import Image from 'next/image'
|
|
4
|
+
import Link from 'next/link'
|
|
5
|
+
import { useCart } from '@/lib/cart/use-cart'
|
|
6
|
+
import { formatMoney } from '@/lib/format'
|
|
7
7
|
|
|
8
8
|
export function CartContent() {
|
|
9
|
-
const { cart, loading, updateItem, removeItem } = useCart()
|
|
9
|
+
const { cart, loading, updateItem, removeItem } = useCart()
|
|
10
10
|
|
|
11
11
|
if (loading && !cart) {
|
|
12
12
|
return (
|
|
13
13
|
<section>
|
|
14
14
|
<h1>Loading your cart…</h1>
|
|
15
15
|
</section>
|
|
16
|
-
)
|
|
16
|
+
)
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
const items = cart?.items ?? []
|
|
19
|
+
const items = cart?.items ?? []
|
|
20
|
+
const hasShippableItems = items.some(
|
|
21
|
+
(item) => item.requiresShipping !== false,
|
|
22
|
+
)
|
|
20
23
|
|
|
21
24
|
if (items.length === 0) {
|
|
22
25
|
return (
|
|
@@ -25,7 +28,7 @@ export function CartContent() {
|
|
|
25
28
|
<p>Add products before starting a payment check.</p>
|
|
26
29
|
<Link href="/products">Browse products</Link>
|
|
27
30
|
</section>
|
|
28
|
-
)
|
|
31
|
+
)
|
|
29
32
|
}
|
|
30
33
|
|
|
31
34
|
return (
|
|
@@ -104,16 +107,23 @@ export function CartContent() {
|
|
|
104
107
|
<dd>
|
|
105
108
|
<output>{formatMoney(cart?.subtotalAmount ?? 0)}</output>
|
|
106
109
|
</dd>
|
|
107
|
-
|
|
108
|
-
|
|
110
|
+
{hasShippableItems ? (
|
|
111
|
+
<>
|
|
112
|
+
<dt>Shipping</dt>
|
|
113
|
+
<dd>{formatMoney(cart?.shippingAmount ?? 0)}</dd>
|
|
114
|
+
</>
|
|
115
|
+
) : null}
|
|
109
116
|
<dt>Total</dt>
|
|
110
117
|
<dd>
|
|
111
118
|
<output>{formatMoney(cart?.totalAmount ?? 0)}</output>
|
|
112
119
|
</dd>
|
|
113
120
|
</dl>
|
|
114
|
-
<p>
|
|
121
|
+
<p>
|
|
122
|
+
Totals are computed by the server cart and confirmed again at
|
|
123
|
+
checkout.
|
|
124
|
+
</p>
|
|
115
125
|
<Link href="/checkout">Checkout</Link>
|
|
116
126
|
</aside>
|
|
117
127
|
</div>
|
|
118
|
-
)
|
|
128
|
+
)
|
|
119
129
|
}
|
|
@@ -1,91 +1,99 @@
|
|
|
1
|
-
|
|
1
|
+
'use client'
|
|
2
2
|
|
|
3
|
-
import Link from
|
|
4
|
-
import { useRouter } from
|
|
5
|
-
import { useState } from
|
|
6
|
-
import { paymentProviderLabel } from
|
|
7
|
-
import { useCart } from
|
|
8
|
-
import type { CustomerSnapshot, ShippingAddress } from
|
|
9
|
-
import type { ClientPaymentRequest } from
|
|
3
|
+
import Link from 'next/link'
|
|
4
|
+
import { useRouter } from 'next/navigation'
|
|
5
|
+
import { useState } from 'react'
|
|
6
|
+
import { paymentProviderLabel } from '@/app-config'
|
|
7
|
+
import { useCart } from '@/lib/cart/use-cart'
|
|
8
|
+
import type { CustomerSnapshot, ShippingAddress } from '@/lib/commerce/types'
|
|
9
|
+
import type { ClientPaymentRequest } from '@/lib/payment/types'
|
|
10
10
|
|
|
11
11
|
type CheckoutResponse = {
|
|
12
|
-
orderNumber: string
|
|
13
|
-
paymentId: string
|
|
14
|
-
amount: number
|
|
15
|
-
currency: string
|
|
16
|
-
redirectUrl?: string
|
|
17
|
-
clientPayment?: ClientPaymentRequest
|
|
18
|
-
}
|
|
12
|
+
orderNumber: string
|
|
13
|
+
paymentId: string
|
|
14
|
+
amount: number
|
|
15
|
+
currency: string
|
|
16
|
+
redirectUrl?: string
|
|
17
|
+
clientPayment?: ClientPaymentRequest
|
|
18
|
+
}
|
|
19
19
|
|
|
20
20
|
export function CheckoutForm() {
|
|
21
|
-
const router = useRouter()
|
|
22
|
-
const { cart, loading, reset } = useCart()
|
|
23
|
-
const [error, setError] = useState<string | null>(null)
|
|
24
|
-
const [submitting, setSubmitting] = useState(false)
|
|
25
|
-
const providerLabel = paymentProviderLabel()
|
|
21
|
+
const router = useRouter()
|
|
22
|
+
const { cart, loading, reset } = useCart()
|
|
23
|
+
const [error, setError] = useState<string | null>(null)
|
|
24
|
+
const [submitting, setSubmitting] = useState(false)
|
|
25
|
+
const providerLabel = paymentProviderLabel()
|
|
26
|
+
const hasShippableItems = !cart
|
|
27
|
+
? true
|
|
28
|
+
: cart.items.some((item) => item.requiresShipping !== false)
|
|
26
29
|
|
|
27
30
|
async function submit(formData: FormData) {
|
|
28
|
-
setSubmitting(true)
|
|
29
|
-
setError(null)
|
|
31
|
+
setSubmitting(true)
|
|
32
|
+
setError(null)
|
|
30
33
|
|
|
31
34
|
const customerSnapshot: CustomerSnapshot = {
|
|
32
|
-
name: value(formData,
|
|
33
|
-
email: value(formData,
|
|
34
|
-
phone: value(formData,
|
|
35
|
-
}
|
|
36
|
-
const shippingAddress: ShippingAddress =
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
35
|
+
name: value(formData, 'customerName'),
|
|
36
|
+
email: value(formData, 'email'),
|
|
37
|
+
phone: value(formData, 'phone'),
|
|
38
|
+
}
|
|
39
|
+
const shippingAddress: ShippingAddress | undefined = hasShippableItems
|
|
40
|
+
? {
|
|
41
|
+
recipientName: value(formData, 'recipientName'),
|
|
42
|
+
phone: value(formData, 'shippingPhone'),
|
|
43
|
+
postalCode: value(formData, 'postalCode'),
|
|
44
|
+
address: value(formData, 'address'),
|
|
45
|
+
detailAddress: value(formData, 'detailAddress'),
|
|
46
|
+
deliveryMessage: value(formData, 'deliveryMessage'),
|
|
47
|
+
}
|
|
48
|
+
: undefined
|
|
44
49
|
|
|
45
|
-
const response = await fetch(
|
|
46
|
-
method:
|
|
47
|
-
headers: {
|
|
48
|
-
body: JSON.stringify({
|
|
49
|
-
|
|
50
|
+
const response = await fetch('/api/checkout', {
|
|
51
|
+
method: 'POST',
|
|
52
|
+
headers: { 'Content-Type': 'application/json' },
|
|
53
|
+
body: JSON.stringify({
|
|
54
|
+
customerSnapshot,
|
|
55
|
+
...(shippingAddress ? { shippingAddress } : {}),
|
|
56
|
+
}),
|
|
57
|
+
})
|
|
50
58
|
|
|
51
59
|
if (!response.ok) {
|
|
52
|
-
const payload = (await response.json()) as { message?: string }
|
|
53
|
-
setError(payload.message ??
|
|
54
|
-
setSubmitting(false)
|
|
55
|
-
return
|
|
60
|
+
const payload = (await response.json()) as { message?: string }
|
|
61
|
+
setError(payload.message ?? 'Checkout failed')
|
|
62
|
+
setSubmitting(false)
|
|
63
|
+
return
|
|
56
64
|
}
|
|
57
65
|
|
|
58
|
-
const payload = (await response.json()) as CheckoutResponse
|
|
66
|
+
const payload = (await response.json()) as CheckoutResponse
|
|
59
67
|
// scaffold:provider:portone:start
|
|
60
|
-
if (payload.clientPayment?.provider ===
|
|
68
|
+
if (payload.clientPayment?.provider === 'portone') {
|
|
61
69
|
try {
|
|
62
|
-
await requestPortOnePayment(payload.clientPayment)
|
|
63
|
-
reset()
|
|
70
|
+
await requestPortOnePayment(payload.clientPayment)
|
|
71
|
+
reset()
|
|
64
72
|
} catch (error) {
|
|
65
|
-
setError(error instanceof Error ? error.message :
|
|
66
|
-
setSubmitting(false)
|
|
73
|
+
setError(error instanceof Error ? error.message : 'Payment failed')
|
|
74
|
+
setSubmitting(false)
|
|
67
75
|
}
|
|
68
|
-
return
|
|
76
|
+
return
|
|
69
77
|
}
|
|
70
78
|
// scaffold:provider:portone:end
|
|
71
79
|
// scaffold:provider:tosspayments:start
|
|
72
|
-
if (payload.clientPayment?.provider ===
|
|
80
|
+
if (payload.clientPayment?.provider === 'tosspayments') {
|
|
73
81
|
try {
|
|
74
|
-
await requestTossPayments(payload.clientPayment)
|
|
75
|
-
reset()
|
|
82
|
+
await requestTossPayments(payload.clientPayment)
|
|
83
|
+
reset()
|
|
76
84
|
} catch (error) {
|
|
77
|
-
setError(error instanceof Error ? error.message :
|
|
78
|
-
setSubmitting(false)
|
|
85
|
+
setError(error instanceof Error ? error.message : 'Payment failed')
|
|
86
|
+
setSubmitting(false)
|
|
79
87
|
}
|
|
80
|
-
return
|
|
88
|
+
return
|
|
81
89
|
}
|
|
82
90
|
// scaffold:provider:tosspayments:end
|
|
83
91
|
|
|
84
|
-
reset()
|
|
92
|
+
reset()
|
|
85
93
|
router.push(
|
|
86
94
|
payload.redirectUrl ??
|
|
87
95
|
`/checkout/success?paymentId=${encodeURIComponent(payload.paymentId)}&orderNumber=${encodeURIComponent(payload.orderNumber)}`,
|
|
88
|
-
)
|
|
96
|
+
)
|
|
89
97
|
}
|
|
90
98
|
|
|
91
99
|
if (!loading && (!cart || cart.items.length === 0)) {
|
|
@@ -95,7 +103,7 @@ export function CheckoutForm() {
|
|
|
95
103
|
<p>Add at least one item before checkout.</p>
|
|
96
104
|
<Link href="/products">Browse products</Link>
|
|
97
105
|
</section>
|
|
98
|
-
)
|
|
106
|
+
)
|
|
99
107
|
}
|
|
100
108
|
|
|
101
109
|
return (
|
|
@@ -103,31 +111,57 @@ export function CheckoutForm() {
|
|
|
103
111
|
<section aria-labelledby="checkout-title">
|
|
104
112
|
<h1 id="checkout-title">Review order details</h1>
|
|
105
113
|
<p id="checkout-summary">
|
|
106
|
-
Customer
|
|
114
|
+
Customer details are collected before payment handoff.
|
|
107
115
|
</p>
|
|
108
116
|
|
|
109
117
|
<fieldset>
|
|
110
118
|
<legend>Customer</legend>
|
|
111
|
-
<Field
|
|
119
|
+
<Field
|
|
120
|
+
name="customerName"
|
|
121
|
+
label="Customer name"
|
|
122
|
+
autoComplete="name"
|
|
123
|
+
/>
|
|
112
124
|
<Field name="email" label="Email" type="email" autoComplete="email" />
|
|
113
125
|
<Field name="phone" label="Phone" autoComplete="tel" />
|
|
114
126
|
</fieldset>
|
|
115
127
|
|
|
116
|
-
|
|
117
|
-
<
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
128
|
+
{hasShippableItems ? (
|
|
129
|
+
<fieldset>
|
|
130
|
+
<legend>Shipping</legend>
|
|
131
|
+
<Field
|
|
132
|
+
name="recipientName"
|
|
133
|
+
label="Recipient name"
|
|
134
|
+
autoComplete="name"
|
|
135
|
+
/>
|
|
136
|
+
<Field
|
|
137
|
+
name="shippingPhone"
|
|
138
|
+
label="Shipping phone"
|
|
139
|
+
autoComplete="tel"
|
|
140
|
+
/>
|
|
141
|
+
<Field
|
|
142
|
+
name="postalCode"
|
|
143
|
+
label="Postal code"
|
|
144
|
+
autoComplete="postal-code"
|
|
145
|
+
/>
|
|
146
|
+
<Field
|
|
147
|
+
name="address"
|
|
148
|
+
label="Address"
|
|
149
|
+
autoComplete="street-address"
|
|
150
|
+
/>
|
|
151
|
+
<Field name="detailAddress" label="Address detail" />
|
|
152
|
+
<Field
|
|
153
|
+
name="deliveryMessage"
|
|
154
|
+
label="Shipping message"
|
|
155
|
+
required={false}
|
|
156
|
+
/>
|
|
157
|
+
</fieldset>
|
|
158
|
+
) : null}
|
|
125
159
|
</section>
|
|
126
160
|
|
|
127
161
|
<aside>
|
|
128
162
|
<h2>Payment handoff</h2>
|
|
129
163
|
<p>
|
|
130
|
-
Local demo payments complete immediately. Configured {providerLabel}{
|
|
164
|
+
Local demo payments complete immediately. Configured {providerLabel}{' '}
|
|
131
165
|
credentials start the provider handoff after the order is created.
|
|
132
166
|
</p>
|
|
133
167
|
<dl>
|
|
@@ -146,20 +180,27 @@ export function CheckoutForm() {
|
|
|
146
180
|
</dl>
|
|
147
181
|
{error ? <p role="alert">{error}</p> : null}
|
|
148
182
|
<button type="submit" disabled={submitting}>
|
|
149
|
-
{submitting ?
|
|
183
|
+
{submitting ? 'Creating order...' : 'Start payment'}
|
|
150
184
|
</button>
|
|
151
|
-
<p aria-live="polite">
|
|
185
|
+
<p aria-live="polite">
|
|
186
|
+
{submitting ? 'Creating order and payment request.' : null}
|
|
187
|
+
</p>
|
|
152
188
|
</aside>
|
|
153
189
|
</form>
|
|
154
|
-
)
|
|
190
|
+
)
|
|
155
191
|
}
|
|
156
192
|
|
|
157
193
|
// scaffold:provider:portone:start
|
|
158
|
-
async function requestPortOnePayment(
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
redirectUrl
|
|
194
|
+
async function requestPortOnePayment(
|
|
195
|
+
payment: Extract<ClientPaymentRequest, { provider: 'portone' }>,
|
|
196
|
+
) {
|
|
197
|
+
const PortOne = await import('@portone/browser-sdk/v2')
|
|
198
|
+
const redirectUrl = new URL(
|
|
199
|
+
'/api/checkout/payment-return',
|
|
200
|
+
window.location.origin,
|
|
201
|
+
)
|
|
202
|
+
redirectUrl.searchParams.set('paymentId', payment.paymentId)
|
|
203
|
+
redirectUrl.searchParams.set('orderNumber', payment.orderNumber)
|
|
163
204
|
|
|
164
205
|
const result = await PortOne.requestPayment({
|
|
165
206
|
storeId: payment.storeId,
|
|
@@ -180,43 +221,42 @@ async function requestPortOnePayment(payment: Extract<ClientPaymentRequest, { pr
|
|
|
180
221
|
// a webhook arriving before/instead of this redirect can resolve the open
|
|
181
222
|
// checkout via a server re-fetch (#1544 / I6). No local index.
|
|
182
223
|
customData: { orderNumber: payment.orderNumber },
|
|
183
|
-
} as Parameters<typeof PortOne.requestPayment>[0])
|
|
224
|
+
} as Parameters<typeof PortOne.requestPayment>[0])
|
|
184
225
|
|
|
185
226
|
if (result?.code) {
|
|
186
|
-
throw new Error(result.message ?? result.code)
|
|
227
|
+
throw new Error(result.message ?? result.code)
|
|
187
228
|
}
|
|
188
229
|
|
|
189
230
|
if (result?.paymentId) {
|
|
190
|
-
window.location.assign(redirectUrl.toString())
|
|
231
|
+
window.location.assign(redirectUrl.toString())
|
|
191
232
|
}
|
|
192
233
|
}
|
|
193
234
|
// scaffold:provider:portone:end
|
|
194
235
|
|
|
195
236
|
// scaffold:provider:tosspayments:start
|
|
196
237
|
async function requestTossPayments(
|
|
197
|
-
payment: Extract<ClientPaymentRequest, { provider:
|
|
238
|
+
payment: Extract<ClientPaymentRequest, { provider: 'tosspayments' }>,
|
|
198
239
|
) {
|
|
199
|
-
const { ANONYMOUS, loadTossPayments } =
|
|
200
|
-
|
|
201
|
-
)
|
|
202
|
-
const tossPayments = await loadTossPayments(payment.clientKey);
|
|
240
|
+
const { ANONYMOUS, loadTossPayments } =
|
|
241
|
+
await import('@tosspayments/tosspayments-sdk')
|
|
242
|
+
const tossPayments = await loadTossPayments(payment.clientKey)
|
|
203
243
|
const checkoutReturnUrl = new URL(
|
|
204
|
-
|
|
244
|
+
'/api/checkout/payment-return',
|
|
205
245
|
window.location.origin,
|
|
206
|
-
)
|
|
207
|
-
checkoutReturnUrl.searchParams.set(
|
|
208
|
-
checkoutReturnUrl.searchParams.set(
|
|
246
|
+
)
|
|
247
|
+
checkoutReturnUrl.searchParams.set('paymentId', payment.paymentId)
|
|
248
|
+
checkoutReturnUrl.searchParams.set('orderNumber', payment.orderNumber)
|
|
209
249
|
|
|
210
|
-
const failUrl = new URL(
|
|
211
|
-
failUrl.searchParams.set(
|
|
212
|
-
failUrl.searchParams.set(
|
|
250
|
+
const failUrl = new URL('/checkout', window.location.origin)
|
|
251
|
+
failUrl.searchParams.set('paymentId', payment.paymentId)
|
|
252
|
+
failUrl.searchParams.set('orderNumber', payment.orderNumber)
|
|
213
253
|
|
|
214
254
|
const paymentClient = tossPayments.payment({
|
|
215
255
|
customerKey: payment.customerKey ?? ANONYMOUS,
|
|
216
|
-
})
|
|
256
|
+
})
|
|
217
257
|
|
|
218
258
|
await paymentClient.requestPayment({
|
|
219
|
-
method:
|
|
259
|
+
method: 'CARD',
|
|
220
260
|
amount: {
|
|
221
261
|
currency: payment.currency,
|
|
222
262
|
value: payment.amount,
|
|
@@ -232,22 +272,22 @@ async function requestTossPayments(
|
|
|
232
272
|
// a webhook arriving before/instead of this redirect can resolve the open
|
|
233
273
|
// checkout via a server re-fetch (#1544 / I6). No local index.
|
|
234
274
|
metadata: { orderNumber: payment.orderNumber },
|
|
235
|
-
})
|
|
275
|
+
})
|
|
236
276
|
}
|
|
237
277
|
// scaffold:provider:tosspayments:end
|
|
238
278
|
|
|
239
279
|
function Field({
|
|
240
280
|
label,
|
|
241
281
|
name,
|
|
242
|
-
type =
|
|
282
|
+
type = 'text',
|
|
243
283
|
autoComplete,
|
|
244
284
|
required = true,
|
|
245
285
|
}: {
|
|
246
|
-
label: string
|
|
247
|
-
name: string
|
|
248
|
-
type?: string
|
|
249
|
-
autoComplete?: string
|
|
250
|
-
required?: boolean
|
|
286
|
+
label: string
|
|
287
|
+
name: string
|
|
288
|
+
type?: string
|
|
289
|
+
autoComplete?: string
|
|
290
|
+
required?: boolean
|
|
251
291
|
}) {
|
|
252
292
|
return (
|
|
253
293
|
<label>
|
|
@@ -259,9 +299,9 @@ function Field({
|
|
|
259
299
|
required={required}
|
|
260
300
|
/>
|
|
261
301
|
</label>
|
|
262
|
-
)
|
|
302
|
+
)
|
|
263
303
|
}
|
|
264
304
|
|
|
265
305
|
function value(formData: FormData, key: string): string {
|
|
266
|
-
return String(formData.get(key) ??
|
|
306
|
+
return String(formData.get(key) ?? '').trim()
|
|
267
307
|
}
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import "server-only";
|
|
2
2
|
import type { CartItemRef, CartView } from "../commerce/types.ts";
|
|
3
|
+
// scaffold:customer:start
|
|
4
|
+
import { resolveCustomerCartToken } from "../customer/cart-sync.ts";
|
|
5
|
+
import { createCustomerCommerceClient } from "../customer/client.server.ts";
|
|
6
|
+
import { getSessionToken } from "../customer/session.ts";
|
|
7
|
+
// scaffold:customer:end
|
|
3
8
|
import { clearCartToken, readCartToken, writeCartToken } from "./cookie.ts";
|
|
4
9
|
import { resolveCartProviderForRequest } from "./select-provider.ts";
|
|
5
10
|
|
|
@@ -40,16 +45,25 @@ export async function addItemToCart(item: CartItemRef): Promise<CartView> {
|
|
|
40
45
|
const provider = await cartProvider();
|
|
41
46
|
let token = await readCartToken();
|
|
42
47
|
|
|
48
|
+
// scaffold:customer:start
|
|
49
|
+
const sessionToken = await getSessionToken();
|
|
50
|
+
if (token && sessionToken) {
|
|
51
|
+
token = (await recoverCustomerCartToken(token, sessionToken)) ?? token;
|
|
52
|
+
}
|
|
53
|
+
// scaffold:customer:end
|
|
54
|
+
|
|
43
55
|
if (token && !(await provider.getCart(token))) {
|
|
44
|
-
|
|
56
|
+
let nextToken: string | null = null;
|
|
57
|
+
// scaffold:customer:start
|
|
58
|
+
if (sessionToken) {
|
|
59
|
+
nextToken = await recoverCustomerCartToken(token, sessionToken);
|
|
60
|
+
}
|
|
61
|
+
// scaffold:customer:end
|
|
62
|
+
token = nextToken && (await provider.getCart(nextToken)) ? nextToken : null;
|
|
45
63
|
}
|
|
46
64
|
if (!token) {
|
|
47
65
|
// Minted through the resolved provider: for a logged-in customer that is the
|
|
48
|
-
// customer client, so create() is customer-bound.
|
|
49
|
-
// failed best-effort and left a guest token, the logged-in customer keeps
|
|
50
|
-
// operating that same-identity guest cart until the next successful sync —
|
|
51
|
-
// never another identity's cart, since the customer client re-verifies
|
|
52
|
-
// ownership.)
|
|
66
|
+
// customer client, so create() is customer-bound.
|
|
53
67
|
const created = await provider.createCart();
|
|
54
68
|
token = created.cartToken;
|
|
55
69
|
await writeCartToken(token);
|
|
@@ -94,3 +108,28 @@ async function requireCartToken(): Promise<string> {
|
|
|
94
108
|
if (!token) throw new CartNotFoundError();
|
|
95
109
|
return token;
|
|
96
110
|
}
|
|
111
|
+
|
|
112
|
+
// scaffold:customer:start
|
|
113
|
+
async function recoverCustomerCartToken(
|
|
114
|
+
staleCartToken: string,
|
|
115
|
+
sessionToken: string,
|
|
116
|
+
): Promise<string | null> {
|
|
117
|
+
const client = await createCustomerCommerceClient(sessionToken);
|
|
118
|
+
if (!client) return null;
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const { cartToken } = await resolveCustomerCartToken(
|
|
122
|
+
client.commerce,
|
|
123
|
+
staleCartToken,
|
|
124
|
+
);
|
|
125
|
+
if (cartToken) {
|
|
126
|
+
await writeCartToken(cartToken);
|
|
127
|
+
} else {
|
|
128
|
+
await clearCartToken();
|
|
129
|
+
}
|
|
130
|
+
return cartToken;
|
|
131
|
+
} catch {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// scaffold:customer:end
|