@hanzo/commerce 7.5.0 → 7.6.0

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/billing.ts ADDED
@@ -0,0 +1,2 @@
1
+ /** @hanzo/commerce/billing — re-exports from @hanzo/commerce/client */
2
+ export * from './client'
package/client.ts ADDED
@@ -0,0 +1,723 @@
1
+ /**
2
+ * @hanzo/commerce/client
3
+ *
4
+ * Universal TypeScript client for the Hanzo Commerce API.
5
+ * Works in browser, Node.js, and edge runtimes — no backend required.
6
+ * Covers billing, subscriptions, payments, checkout, coupons,
7
+ * referrals, affiliates, usage, and plans.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * import { Commerce } from '@hanzo/commerce/client'
12
+ *
13
+ * const commerce = new Commerce({ baseUrl: 'https://api.hanzo.ai', token: iamToken })
14
+ *
15
+ * // Validate a coupon before checkout
16
+ * const coupon = await commerce.validateCoupon('LAUNCH50')
17
+ *
18
+ * // Create a checkout session
19
+ * const session = await commerce.createCheckoutSession({
20
+ * items: [{ id: 'plan_pro', quantity: 1 }],
21
+ * couponCode: 'LAUNCH50',
22
+ * referrerId: 'ref_abc123',
23
+ * successUrl: 'https://app.example.com/success',
24
+ * cancelUrl: 'https://app.example.com/cancel',
25
+ * })
26
+ * window.location.href = session.checkoutUrl
27
+ *
28
+ * // Tokenize a card (S2S — no external SDK required)
29
+ * const token = await commerce.tokenizeCard({
30
+ * number: '4242424242424242',
31
+ * expiryMonth: '12',
32
+ * expiryYear: '2027',
33
+ * cvc: '123',
34
+ * name: 'Jane Smith',
35
+ * })
36
+ *
37
+ * // Subscribe
38
+ * const sub = await commerce.subscribe({ planId: 'pro', userId: 'user_xyz' })
39
+ * ```
40
+ */
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // Config
44
+ // ---------------------------------------------------------------------------
45
+
46
+ export type CommerceClientConfig = {
47
+ /**
48
+ * Commerce API base URL.
49
+ * Defaults to https://api.hanzo.ai
50
+ */
51
+ baseUrl?: string
52
+ /** @deprecated use baseUrl */
53
+ commerceUrl?: string
54
+ /** IAM access token for authenticated requests. */
55
+ token?: string
56
+ /** Request timeout in milliseconds. Default 15 000. */
57
+ timeoutMs?: number
58
+ }
59
+
60
+ // ---------------------------------------------------------------------------
61
+ // Core types
62
+ // ---------------------------------------------------------------------------
63
+
64
+ export type Balance = {
65
+ balance: number
66
+ holds: number
67
+ available: number
68
+ }
69
+
70
+ export type Transaction = {
71
+ id?: string
72
+ owner?: string
73
+ type: 'hold' | 'hold-removed' | 'transfer' | 'deposit' | 'withdraw'
74
+ destinationId?: string
75
+ destinationKind?: string
76
+ sourceId?: string
77
+ sourceKind?: string
78
+ currency: string
79
+ amount: number
80
+ tags?: string[]
81
+ expiresAt?: string
82
+ metadata?: Record<string, unknown>
83
+ createdAt?: string
84
+ }
85
+
86
+ export type Subscription = {
87
+ id?: string
88
+ planId?: string
89
+ userId?: string
90
+ customerId?: string
91
+ status?: 'trialing' | 'active' | 'past_due' | 'canceled' | 'unpaid' | string
92
+ billingType?: 'charge_automatically' | 'send_invoice'
93
+ periodStart?: string
94
+ periodEnd?: string
95
+ trialStart?: string
96
+ trialEnd?: string
97
+ quantity?: number
98
+ createdAt?: string
99
+ cancelAtPeriodEnd?: boolean
100
+ currentPeriodEnd?: string
101
+ }
102
+
103
+ export type Plan = {
104
+ id?: string
105
+ slug?: string
106
+ name?: string
107
+ description?: string
108
+ price?: number
109
+ priceMonthly?: number
110
+ priceAnnual?: number
111
+ currency?: string
112
+ interval?: 'monthly' | 'yearly' | string
113
+ intervalCount?: number
114
+ trialPeriodDays?: number
115
+ features?: string[]
116
+ popular?: boolean
117
+ contactSales?: boolean
118
+ metadata?: Record<string, unknown>
119
+ }
120
+
121
+ export type Payment = {
122
+ id?: string
123
+ orderId?: string
124
+ userId?: string
125
+ amount?: number
126
+ amountRefunded?: number
127
+ fee?: number
128
+ currency?: string
129
+ status?: 'cancelled' | 'credit' | 'disputed' | 'failed' | 'fraudulent' | 'paid' | 'refunded' | 'unpaid' | string
130
+ captured?: boolean
131
+ live?: boolean
132
+ createdAt?: string
133
+ }
134
+
135
+ export type UsageRecord = {
136
+ user: string
137
+ currency?: string
138
+ amount: number
139
+ model?: string
140
+ provider?: string
141
+ tokens?: number
142
+ promptTokens?: number
143
+ completionTokens?: number
144
+ }
145
+
146
+ // ---------------------------------------------------------------------------
147
+ // Coupon / Discount types
148
+ // ---------------------------------------------------------------------------
149
+
150
+ export type CouponType = 'Percent' | 'Flat' | 'FreeShipping' | 'FreeItem'
151
+
152
+ export type Coupon = {
153
+ id?: string
154
+ code: string
155
+ type: CouponType
156
+ /** Amount: percentage (0-100) for Percent, cents for Flat */
157
+ amount: number
158
+ description?: string
159
+ limit?: number
160
+ used?: number
161
+ startDate?: string
162
+ endDate?: string
163
+ enabled?: boolean
164
+ /** Calculated discount in cents (returned by validateCoupon) */
165
+ discountCents?: number
166
+ }
167
+
168
+ export type CouponValidateResult = {
169
+ valid: boolean
170
+ coupon?: Coupon
171
+ error?: string
172
+ /** Discount in cents for a given subtotal */
173
+ discountCents?: number
174
+ }
175
+
176
+ export type Discount = {
177
+ id?: string
178
+ type: 'Percent' | 'Flat' | 'FreeShipping' | 'FreeItem' | 'Bulk'
179
+ amount: number
180
+ scope?: 'Product' | 'Variant' | 'Collection' | 'Store'
181
+ enabled?: boolean
182
+ startDate?: string
183
+ endDate?: string
184
+ }
185
+
186
+ // ---------------------------------------------------------------------------
187
+ // Checkout types
188
+ // ---------------------------------------------------------------------------
189
+
190
+ export type CheckoutItem = {
191
+ id: string
192
+ quantity?: number
193
+ /** Price override in cents */
194
+ price?: number
195
+ name?: string
196
+ description?: string
197
+ imageUrl?: string
198
+ metadata?: Record<string, unknown>
199
+ }
200
+
201
+ export type CheckoutSessionRequest = {
202
+ items: CheckoutItem[]
203
+ successUrl: string
204
+ cancelUrl: string
205
+ couponCode?: string
206
+ referrerId?: string
207
+ affiliateId?: string
208
+ currency?: string
209
+ customer?: {
210
+ email?: string
211
+ name?: string
212
+ address?: string
213
+ city?: string
214
+ zip?: string
215
+ }
216
+ metadata?: Record<string, unknown>
217
+ }
218
+
219
+ export type CheckoutSessionResponse = {
220
+ checkoutUrl: string
221
+ sessionId: string
222
+ /** Original total in cents before discount */
223
+ originalTotal?: number
224
+ /** Final total in cents after discount */
225
+ finalTotal?: number
226
+ discount?: {
227
+ code: string
228
+ type: CouponType
229
+ amount: number
230
+ discountCents: number
231
+ }
232
+ }
233
+
234
+ // ---------------------------------------------------------------------------
235
+ // Card tokenization (S2S — no provider SDK needed on the frontend)
236
+ // ---------------------------------------------------------------------------
237
+
238
+ export type CardTokenizeRequest = {
239
+ number: string
240
+ expiryMonth: string // "01"–"12"
241
+ expiryYear: string // "2025"–"2099"
242
+ cvc: string
243
+ name?: string
244
+ zip?: string
245
+ }
246
+
247
+ export type CardTokenizeResult = {
248
+ token: string
249
+ brand: string
250
+ last4: string
251
+ expiryMonth: string
252
+ expiryYear: string
253
+ provider: string
254
+ }
255
+
256
+ // ---------------------------------------------------------------------------
257
+ // Payment method types
258
+ // ---------------------------------------------------------------------------
259
+
260
+ export type PaymentMethodType = 'card' | 'bank_account' | 'balance' | 'crypto' | 'wire'
261
+
262
+ export type PaymentMethod = {
263
+ id: string
264
+ type: PaymentMethodType
265
+ isDefault?: boolean
266
+ customerId?: string
267
+ card?: {
268
+ brand: string
269
+ last4: string
270
+ expMonth: number
271
+ expYear: number
272
+ }
273
+ providerRef?: string
274
+ providerType?: string
275
+ createdAt?: string
276
+ }
277
+
278
+ // ---------------------------------------------------------------------------
279
+ // Referral / Affiliate types
280
+ // ---------------------------------------------------------------------------
281
+
282
+ export type Referral = {
283
+ id?: string
284
+ userId?: string
285
+ referrerId?: string
286
+ affiliateId?: string
287
+ orderId?: string
288
+ fee?: number
289
+ createdAt?: string
290
+ }
291
+
292
+ export type Referrer = {
293
+ id?: string
294
+ userId?: string
295
+ enabled?: boolean
296
+ code?: string
297
+ referrals?: Referral[]
298
+ }
299
+
300
+ export type Affiliate = {
301
+ id?: string
302
+ userId?: string
303
+ enabled?: boolean
304
+ commission?: number
305
+ couponId?: string
306
+ connectUrl?: string
307
+ }
308
+
309
+ // ---------------------------------------------------------------------------
310
+ // Credit grant
311
+ // ---------------------------------------------------------------------------
312
+
313
+ export type CreditGrant = {
314
+ id?: string
315
+ userId?: string
316
+ amount: number
317
+ currency: string
318
+ expiresAt?: string
319
+ tags?: string[]
320
+ }
321
+
322
+ // ---------------------------------------------------------------------------
323
+ // Client
324
+ // ---------------------------------------------------------------------------
325
+
326
+ const DEFAULT_BASE_URL = 'https://api.hanzo.ai'
327
+ const DEFAULT_TIMEOUT_MS = 15_000
328
+
329
+ export class Commerce {
330
+ private readonly baseUrl: string
331
+ private token: string | undefined
332
+ private readonly timeoutMs: number
333
+
334
+ constructor(config: CommerceClientConfig = {}) {
335
+ this.baseUrl = (config.baseUrl ?? config.commerceUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, '')
336
+ this.token = config.token
337
+ this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS
338
+ }
339
+
340
+ /** Update the auth token (e.g. after IAM token refresh). */
341
+ setToken(token: string): void {
342
+ this.token = token
343
+ }
344
+
345
+ private async request<T>(
346
+ path: string,
347
+ opts?: {
348
+ method?: string
349
+ body?: unknown
350
+ token?: string
351
+ params?: Record<string, string>
352
+ },
353
+ ): Promise<T> {
354
+ const url = new URL(path, this.baseUrl)
355
+ if (opts?.params) {
356
+ for (const [k, v] of Object.entries(opts.params)) {
357
+ url.searchParams.set(k, v)
358
+ }
359
+ }
360
+
361
+ const controller = new AbortController()
362
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs)
363
+
364
+ const headers: Record<string, string> = { Accept: 'application/json' }
365
+ const authToken = opts?.token ?? this.token
366
+ if (authToken) headers['Authorization'] = `Bearer ${authToken}`
367
+ if (opts?.body) headers['Content-Type'] = 'application/json'
368
+
369
+ try {
370
+ const res = await fetch(url.toString(), {
371
+ method: opts?.method ?? 'GET',
372
+ headers,
373
+ body: opts?.body ? JSON.stringify(opts.body) : undefined,
374
+ signal: controller.signal,
375
+ })
376
+
377
+ if (!res.ok) {
378
+ const text = await res.text().catch(() => '')
379
+ throw new CommerceApiError(res.status, `${res.statusText}: ${text}`.trim())
380
+ }
381
+
382
+ return (await res.json()) as T
383
+ } finally {
384
+ clearTimeout(timer)
385
+ }
386
+ }
387
+
388
+ // -----------------------------------------------------------------------
389
+ // Balance
390
+ // -----------------------------------------------------------------------
391
+
392
+ async getBalance(user: string, currency = 'usd', token?: string): Promise<Balance> {
393
+ return this.request<Balance>('/api/v1/billing/balance', {
394
+ params: { user, currency }, token,
395
+ })
396
+ }
397
+
398
+ async getAllBalances(user: string, token?: string): Promise<Record<string, Balance>> {
399
+ return this.request<Record<string, Balance>>('/api/v1/billing/balance/all', {
400
+ params: { user }, token,
401
+ })
402
+ }
403
+
404
+ // -----------------------------------------------------------------------
405
+ // Usage
406
+ // -----------------------------------------------------------------------
407
+
408
+ async addUsageRecord(record: UsageRecord, token?: string): Promise<Transaction> {
409
+ return this.request<Transaction>('/api/v1/billing/usage', {
410
+ method: 'POST', body: record, token,
411
+ })
412
+ }
413
+
414
+ async getUsageRecords(user: string, currency = 'usd', token?: string): Promise<Transaction[]> {
415
+ return this.request<Transaction[]>('/api/v1/billing/usage', {
416
+ params: { user, currency }, token,
417
+ })
418
+ }
419
+
420
+ // -----------------------------------------------------------------------
421
+ // Deposits / Credits
422
+ // -----------------------------------------------------------------------
423
+
424
+ async addDeposit(
425
+ params: { user: string; currency?: string; amount: number; notes?: string; tags?: string[]; expiresIn?: string },
426
+ token?: string,
427
+ ): Promise<Transaction> {
428
+ return this.request<Transaction>('/api/v1/billing/deposit', {
429
+ method: 'POST', body: params, token,
430
+ })
431
+ }
432
+
433
+ async grantStarterCredit(user: string, token?: string): Promise<Transaction> {
434
+ return this.request<Transaction>('/api/v1/billing/credit', {
435
+ method: 'POST', body: { user }, token,
436
+ })
437
+ }
438
+
439
+ // -----------------------------------------------------------------------
440
+ // Plans
441
+ // -----------------------------------------------------------------------
442
+
443
+ async getPlans(token?: string): Promise<Plan[]> {
444
+ return this.request<Plan[]>('/api/v1/billing/plans', { token })
445
+ }
446
+
447
+ async getPlan(planId: string, token?: string): Promise<Plan | null> {
448
+ try {
449
+ return await this.request<Plan>(`/api/v1/billing/plans/${planId}`, { token })
450
+ } catch { return null }
451
+ }
452
+
453
+ // -----------------------------------------------------------------------
454
+ // Subscriptions
455
+ // -----------------------------------------------------------------------
456
+
457
+ async subscribe(
458
+ params: { planId: string; userId?: string; customerId?: string; paymentMethodId?: string; couponCode?: string; trialDays?: number },
459
+ token?: string,
460
+ ): Promise<Subscription> {
461
+ return this.request<Subscription>('/api/v1/billing/subscriptions', {
462
+ method: 'POST', body: params, token,
463
+ })
464
+ }
465
+
466
+ async getSubscription(subscriptionId: string, token?: string): Promise<Subscription | null> {
467
+ try {
468
+ return await this.request<Subscription>(`/api/v1/billing/subscriptions/${subscriptionId}`, { token })
469
+ } catch { return null }
470
+ }
471
+
472
+ async listSubscriptions(params?: { customerId?: string }, token?: string): Promise<Subscription[]> {
473
+ return this.request<Subscription[]>('/api/v1/billing/subscriptions', {
474
+ params: params as Record<string, string> | undefined, token,
475
+ })
476
+ }
477
+
478
+ async updateSubscription(subscriptionId: string, update: Partial<Subscription>, token?: string): Promise<Subscription> {
479
+ return this.request<Subscription>(`/api/v1/billing/subscriptions/${subscriptionId}`, {
480
+ method: 'PATCH', body: update, token,
481
+ })
482
+ }
483
+
484
+ async cancelSubscription(subscriptionId: string, immediately = false, token?: string): Promise<Subscription> {
485
+ return this.request<Subscription>(`/api/v1/billing/subscriptions/${subscriptionId}/cancel`, {
486
+ method: 'POST', body: { immediately }, token,
487
+ })
488
+ }
489
+
490
+ async reactivateSubscription(subscriptionId: string, token?: string): Promise<Subscription> {
491
+ return this.request<Subscription>(`/api/v1/billing/subscriptions/${subscriptionId}/reactivate`, {
492
+ method: 'POST', token,
493
+ })
494
+ }
495
+
496
+ // -----------------------------------------------------------------------
497
+ // Checkout sessions
498
+ // -----------------------------------------------------------------------
499
+
500
+ /**
501
+ * Create a hosted checkout session.
502
+ * Returns a URL to redirect the customer to for payment.
503
+ * Supports coupons, referral tracking, and multiple currencies.
504
+ */
505
+ async createCheckoutSession(
506
+ params: CheckoutSessionRequest,
507
+ token?: string,
508
+ ): Promise<CheckoutSessionResponse> {
509
+ return this.request<CheckoutSessionResponse>('/api/v1/checkout/sessions', {
510
+ method: 'POST', body: params, token,
511
+ })
512
+ }
513
+
514
+ // -----------------------------------------------------------------------
515
+ // Card tokenization (S2S — no external SDK required)
516
+ // -----------------------------------------------------------------------
517
+
518
+ /**
519
+ * Tokenize a payment card server-side.
520
+ * No external SDK (Square.js, Stripe.js, etc.) is needed on the frontend.
521
+ * The card data is sent to the Hanzo Commerce API over HTTPS, which
522
+ * tokenizes via the configured payment provider (Stripe).
523
+ */
524
+ async tokenizeCard(card: CardTokenizeRequest, token?: string): Promise<CardTokenizeResult> {
525
+ return this.request<CardTokenizeResult>('/api/v1/billing/card/tokenize', {
526
+ method: 'POST',
527
+ body: {
528
+ number: card.number.replace(/\s/g, ''),
529
+ expiry_month: card.expiryMonth,
530
+ expiry_year: card.expiryYear,
531
+ cvc: card.cvc,
532
+ name: card.name,
533
+ zip: card.zip,
534
+ },
535
+ token,
536
+ })
537
+ }
538
+
539
+ // -----------------------------------------------------------------------
540
+ // Payment methods
541
+ // -----------------------------------------------------------------------
542
+
543
+ async addPaymentMethod(
544
+ params: {
545
+ customerId: string
546
+ type: PaymentMethodType
547
+ token?: string // from tokenizeCard
548
+ providerRef?: string
549
+ providerType?: string
550
+ },
551
+ token?: string,
552
+ ): Promise<PaymentMethod> {
553
+ return this.request<PaymentMethod>('/api/v1/billing/payment-methods', {
554
+ method: 'POST', body: params, token,
555
+ })
556
+ }
557
+
558
+ async listPaymentMethods(customerId: string, token?: string): Promise<PaymentMethod[]> {
559
+ return this.request<PaymentMethod[]>('/api/v1/billing/payment-methods', {
560
+ params: { customerId }, token,
561
+ })
562
+ }
563
+
564
+ async removePaymentMethod(paymentMethodId: string, token?: string): Promise<void> {
565
+ await this.request<void>(`/api/v1/billing/payment-methods/${paymentMethodId}`, {
566
+ method: 'DELETE', token,
567
+ })
568
+ }
569
+
570
+ async setDefaultPaymentMethod(customerId: string, paymentMethodId: string, token?: string): Promise<PaymentMethod> {
571
+ return this.request<PaymentMethod>(`/api/v1/billing/customers/${customerId}/default-payment-method`, {
572
+ method: 'POST', body: { paymentMethodId }, token,
573
+ })
574
+ }
575
+
576
+ // -----------------------------------------------------------------------
577
+ // Coupons / Promo codes
578
+ // -----------------------------------------------------------------------
579
+
580
+ /**
581
+ * Validate a coupon code.
582
+ * Optionally pass a subtotalCents to get the calculated discount amount.
583
+ */
584
+ async validateCoupon(code: string, subtotalCents?: number, token?: string): Promise<CouponValidateResult> {
585
+ try {
586
+ const result = await this.request<Coupon>('/api/v1/coupon/validate', {
587
+ method: 'POST',
588
+ body: { code: code.toUpperCase().trim(), subtotalCents },
589
+ token,
590
+ })
591
+ return { valid: true, coupon: result }
592
+ } catch (err) {
593
+ const msg = err instanceof CommerceApiError ? err.message : 'Invalid coupon'
594
+ return { valid: false, error: msg }
595
+ }
596
+ }
597
+
598
+ /**
599
+ * Redeem a coupon for a user. Creates credit grant records.
600
+ */
601
+ async redeemCoupon(code: string, userId: string, token?: string): Promise<CreditGrant[]> {
602
+ return this.request<CreditGrant[]>('/api/v1/coupon/redeem', {
603
+ method: 'POST',
604
+ body: { code: code.toUpperCase().trim(), userId },
605
+ token,
606
+ })
607
+ }
608
+
609
+ // -----------------------------------------------------------------------
610
+ // Referrals & Affiliates
611
+ // -----------------------------------------------------------------------
612
+
613
+ /**
614
+ * Get or create a referrer record for a user.
615
+ * Returns the referral code/link the user can share.
616
+ */
617
+ async getOrCreateReferrer(userId: string, token?: string): Promise<Referrer> {
618
+ return this.request<Referrer>('/api/v1/referrer', {
619
+ method: 'POST', body: { userId }, token,
620
+ })
621
+ }
622
+
623
+ async getReferrals(userId: string, token?: string): Promise<Referral[]> {
624
+ return this.request<Referral[]>(`/api/v1/user/${userId}/referrals`, { token })
625
+ }
626
+
627
+ async getReferrers(userId: string, token?: string): Promise<Referrer[]> {
628
+ return this.request<Referrer[]>(`/api/v1/user/${userId}/referrers`, { token })
629
+ }
630
+
631
+ /**
632
+ * Get affiliate details for a user.
633
+ */
634
+ async getAffiliate(userId: string, token?: string): Promise<Affiliate | null> {
635
+ try {
636
+ return await this.request<Affiliate>(`/api/v1/user/${userId}/affiliate`, { token })
637
+ } catch { return null }
638
+ }
639
+
640
+ /**
641
+ * Create an affiliate account for a user.
642
+ * After creation, user can connect their bank via the returnedconnectUrl.
643
+ */
644
+ async createAffiliate(userId: string, token?: string): Promise<Affiliate> {
645
+ return this.request<Affiliate>('/api/v1/affiliate', {
646
+ method: 'POST', body: { userId }, token,
647
+ })
648
+ }
649
+
650
+ async getAffiliateReferrals(affiliateId: string, token?: string): Promise<Referral[]> {
651
+ return this.request<Referral[]>(`/api/v1/affiliate/${affiliateId}/referrals`, { token })
652
+ }
653
+
654
+ async getAffiliateOrders(affiliateId: string, token?: string): Promise<unknown[]> {
655
+ return this.request<unknown[]>(`/api/v1/affiliate/${affiliateId}/orders`, { token })
656
+ }
657
+
658
+ async getAffiliateTransactions(affiliateId: string, token?: string): Promise<Transaction[]> {
659
+ return this.request<Transaction[]>(`/api/v1/affiliate/${affiliateId}/transactions`, { token })
660
+ }
661
+
662
+ // -----------------------------------------------------------------------
663
+ // Legacy checkout (order-based)
664
+ // -----------------------------------------------------------------------
665
+
666
+ async authorize(orderId: string, token?: string): Promise<Payment> {
667
+ return this.request<Payment>(`/api/v1/authorize/${orderId}`, { method: 'POST', token })
668
+ }
669
+
670
+ async capture(orderId: string, token?: string): Promise<Payment> {
671
+ return this.request<Payment>(`/api/v1/capture/${orderId}`, { method: 'POST', token })
672
+ }
673
+
674
+ async charge(orderId: string, token?: string): Promise<Payment> {
675
+ return this.request<Payment>(`/api/v1/charge/${orderId}`, { method: 'POST', token })
676
+ }
677
+
678
+ async refund(paymentId: string, token?: string): Promise<Payment> {
679
+ return this.request<Payment>(`/api/v1/refund/${paymentId}`, { method: 'POST', token })
680
+ }
681
+
682
+ async billingRefund(
683
+ params: { user: string; amount: number; originalTransactionId: string; currency?: string; notes?: string },
684
+ token?: string,
685
+ ): Promise<Transaction> {
686
+ return this.request<Transaction>('/api/v1/billing/refund', {
687
+ method: 'POST', body: params, token,
688
+ })
689
+ }
690
+ }
691
+
692
+ // ---------------------------------------------------------------------------
693
+ // Standalone factory helpers — import these for quick setup
694
+ // ---------------------------------------------------------------------------
695
+
696
+ /**
697
+ * Create a commerce client pre-configured for api.hanzo.ai.
698
+ * Pass your IAM access token (read from localStorage or cookie).
699
+ *
700
+ * @example
701
+ * ```ts
702
+ * import { hanzoCommerce } from '@hanzo/commerce/client'
703
+ * const commerce = hanzoCommerce(localStorage.getItem('hanzo-auth-token') ?? undefined)
704
+ * const plans = await commerce.getPlans()
705
+ * ```
706
+ */
707
+ export function hanzoCommerce(token?: string): Commerce {
708
+ return new Commerce({ token })
709
+ }
710
+
711
+ // ---------------------------------------------------------------------------
712
+ // Error
713
+ // ---------------------------------------------------------------------------
714
+
715
+ export class CommerceApiError extends Error {
716
+ readonly status: number
717
+
718
+ constructor(status: number, message: string) {
719
+ super(message)
720
+ this.name = 'CommerceApiError'
721
+ this.status = status
722
+ }
723
+ }
@@ -56,7 +56,7 @@ const PromoCode = observer(() => {
56
56
  }, [cmmc.appliedPromo])
57
57
 
58
58
  const form = useForm<z.infer<typeof formSchema>>({
59
- resolver: zodResolver(formSchema),
59
+ resolver: zodResolver(formSchema as any),
60
60
  defaultValues: {
61
61
  code: '',
62
62
  },
@@ -1,5 +1,5 @@
1
1
  'use client'
2
- import React, { useEffect, useState } from 'react'
2
+ import React, { useState } from 'react'
3
3
  import { observer } from 'mobx-react-lite'
4
4
 
5
5
  import { zodResolver } from '@hookform/resolvers/zod'
@@ -7,7 +7,6 @@ import * as z from 'zod'
7
7
  import { useForm } from 'react-hook-form'
8
8
 
9
9
  import { Tabs, TabsContent, TabsList, TabsTrigger } from '@hanzo/ui/primitives'
10
- import { useAuth } from '@hanzo/auth/service'
11
10
 
12
11
  import { useCommerce } from '../../../service/context'
13
12
  import { sendFBEvent, sendGAEvent } from '../../../util/analytics'
@@ -26,29 +25,17 @@ const PaymentStepForm: React.FC<CheckoutStepComponentProps> = observer(({
26
25
  setOrderId
27
26
  }) => {
28
27
  const cmmc = useCommerce()
29
- const auth = useAuth() // may be null in some cases
30
28
 
31
29
  const [transactionStatus, setTransactionStatus] = useState<TransactionStatus>('unpaid')
32
30
 
33
- if (!auth) {
34
- console.log("PAYMENT STEP FORM: auth service is null! ")
35
- }
36
-
37
31
  const contactForm = useForm<z.infer<typeof contactFormSchema>>({
38
- resolver: zodResolver(contactFormSchema),
32
+ resolver: zodResolver(contactFormSchema as any),
39
33
  defaultValues: {
40
- name: auth?.user?.displayName ?? '',
41
- email: auth?.user?.email ?? '',
34
+ name: '',
35
+ email: '',
42
36
  },
43
37
  })
44
38
 
45
- useEffect(() => {
46
- if (auth?.loggedIn) {
47
- contactForm.setValue('name', auth!.user?.displayName ?? '')
48
- contactForm.setValue('email', auth!.user?.email ?? '')
49
- }
50
- }, [auth?.loggedIn])
51
-
52
39
  const storePaymentInfo = async (paymentInfo: any) => {
53
40
  const {name, email} = contactForm.getValues()
54
41
  let id: string | undefined = undefined
@@ -111,7 +98,7 @@ const PaymentStepForm: React.FC<CheckoutStepComponentProps> = observer(({
111
98
  transactionStatus={transactionStatus}
112
99
  setTransactionStatus={setTransactionStatus}
113
100
  storePaymentInfo={storePaymentInfo}
114
- contactForm={contactForm}
101
+ contactForm={contactForm as any}
115
102
  />
116
103
  </TabsContent>
117
104
  ))}
@@ -45,7 +45,7 @@ const ShippingStepForm: React.FC<CheckoutStepComponentProps> = ({
45
45
  const cmmc = useCommerce()
46
46
 
47
47
  const shippingForm = useForm<z.infer<typeof shippingFormSchema>>({
48
- resolver: zodResolver(shippingFormSchema),
48
+ resolver: zodResolver(shippingFormSchema as any),
49
49
  defaultValues: {
50
50
  addressLine1: '',
51
51
  addressLine2: '',
package/index.ts CHANGED
@@ -2,12 +2,39 @@ export * from './service/context'
2
2
  export * from './components'
3
3
  // Impl-dependent, so leave w impl
4
4
  export type { StandaloneServiceOptions as ServiceOptions } from './service/impls/standalone'
5
- export {
6
- useSyncSkuParamWithCurrentItem,
7
- getFacetValuesMutator,
5
+ export {
6
+ useSyncSkuParamWithCurrentItem,
7
+ getFacetValuesMutator,
8
8
  formatCurrencyValue,
9
9
  ProductMediaAccessor,
10
- LineItemRef
10
+ LineItemRef
11
11
  } from './util'
12
12
 
13
- export * from './util/selection-ui-specifiers'
13
+ export * from './util/selection-ui-specifiers'
14
+
15
+ // Commerce API client
16
+ export { Commerce, CommerceApiError, hanzoCommerce } from './client'
17
+ export type {
18
+ CommerceClientConfig,
19
+ Balance,
20
+ Transaction,
21
+ Subscription,
22
+ Plan,
23
+ Payment,
24
+ UsageRecord,
25
+ Coupon,
26
+ CouponType,
27
+ CouponValidateResult,
28
+ Discount,
29
+ CheckoutItem,
30
+ CheckoutSessionRequest,
31
+ CheckoutSessionResponse,
32
+ CardTokenizeRequest,
33
+ CardTokenizeResult,
34
+ PaymentMethod,
35
+ PaymentMethodType,
36
+ Referral,
37
+ Referrer,
38
+ Affiliate,
39
+ CreditGrant,
40
+ } from './client'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzo/commerce",
3
- "version": "7.5.0",
3
+ "version": "7.6.0",
4
4
  "description": "e-commerce framework.",
5
5
  "publishConfig": {
6
6
  "registry": "https://registry.npmjs.org/",
@@ -11,7 +11,7 @@
11
11
  "license": "BSD-3-Clause",
12
12
  "repository": {
13
13
  "type": "git",
14
- "url": "git+https://github.com/hanzoai/react-sdk.git",
14
+ "url": "git+https://github.com/hanzoai/ui.git",
15
15
  "directory": "pkg/commerce"
16
16
  },
17
17
  "keywords": [
@@ -28,17 +28,16 @@
28
28
  "exports": {
29
29
  ".": "./index.ts",
30
30
  "./types": "./types/index.ts",
31
+ "./client": "./client.ts",
32
+ "./billing": "./billing.ts",
31
33
  "./debug": "./service/debug.ts"
32
34
  },
33
35
  "dependencies": {
34
36
  "ethers": "^6.12.0",
35
- "next-usequerystate": "^1.17.1",
36
- "react-square-web-payments-sdk": "^3.2.1",
37
- "square": "^35.1.0"
37
+ "next-usequerystate": "^1.17.1"
38
38
  },
39
39
  "peerDependencies": {
40
- "@hanzo/auth": "workspace:*",
41
- "@hanzo/ui": "workspace:*",
40
+ "@hanzo/ui": ">=5.0.0",
42
41
  "@hookform/resolvers": "^3.3.4",
43
42
  "@radix-ui/react-radio-group": "^1.1.3",
44
43
  "lucide-react": ">=0.456.0",