@cimplify/sdk 0.9.10 → 0.10.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/dist/{ads-MkGm5l1T.d.mts → ads-BxbWrwqp.d.mts} +0 -8
- package/dist/{ads-MkGm5l1T.d.ts → ads-BxbWrwqp.d.ts} +0 -8
- package/dist/advanced.d.mts +2 -2
- package/dist/advanced.d.ts +2 -2
- package/dist/advanced.js +93 -80
- package/dist/advanced.mjs +93 -80
- package/dist/cli.js +184 -0
- package/dist/{client-BQ1gIg8t.d.mts → client-BSrq89H1.d.mts} +42 -374
- package/dist/{client-C3TQtGuy.d.ts → client-xBhdHLq4.d.ts} +42 -374
- package/dist/index.d.mts +6 -10
- package/dist/index.d.ts +6 -10
- package/dist/index.js +98 -126
- package/dist/index.mjs +98 -126
- package/dist/{payment-CTalZM5l.d.mts → payment-CrNyrc-D.d.mts} +145 -95
- package/dist/{payment-CTalZM5l.d.ts → payment-CrNyrc-D.d.ts} +145 -95
- package/dist/price-C9Z-hr49.d.mts +21 -0
- package/dist/price-RKKoTz-9.d.ts +21 -0
- package/dist/react.d.mts +1285 -35
- package/dist/react.d.ts +1285 -35
- package/dist/react.js +6596 -2598
- package/dist/react.mjs +6550 -2600
- package/dist/utils.d.mts +55 -2
- package/dist/utils.d.ts +55 -2
- package/dist/utils.js +23 -20
- package/dist/utils.mjs +23 -20
- package/package.json +13 -3
- package/registry/add-on-selector.json +15 -0
- package/registry/availability-badge.json +15 -0
- package/registry/booking-card.json +16 -0
- package/registry/booking-list.json +16 -0
- package/registry/booking-page.json +18 -0
- package/registry/bookings-page.json +17 -0
- package/registry/bundle-selector.json +15 -0
- package/registry/cart-page.json +17 -0
- package/registry/cart-summary.json +16 -0
- package/registry/catalogue-page.json +18 -0
- package/registry/category-filter.json +15 -0
- package/registry/category-grid.json +15 -0
- package/registry/checkout-page.json +15 -0
- package/registry/cn.json +13 -0
- package/registry/collection-page.json +16 -0
- package/registry/composite-selector.json +15 -0
- package/registry/date-slot-picker.json +16 -0
- package/registry/deal-banner.json +16 -0
- package/registry/deals-page.json +19 -0
- package/registry/discount-input.json +16 -0
- package/registry/index.json +411 -0
- package/registry/order-detail-page.json +16 -0
- package/registry/order-history-page.json +17 -0
- package/registry/order-history.json +16 -0
- package/registry/order-summary.json +16 -0
- package/registry/price.json +13 -0
- package/registry/product-card.json +17 -0
- package/registry/product-customizer.json +20 -0
- package/registry/product-grid.json +16 -0
- package/registry/product-image-gallery.json +13 -0
- package/registry/product-page.json +19 -0
- package/registry/product-sheet.json +18 -0
- package/registry/quantity-selector.json +13 -0
- package/registry/sale-badge.json +16 -0
- package/registry/search-input.json +15 -0
- package/registry/search-page.json +16 -0
- package/registry/service-card.json +16 -0
- package/registry/service-grid.json +16 -0
- package/registry/slot-picker.json +16 -0
- package/registry/staff-picker.json +15 -0
- package/registry/store-nav.json +15 -0
- package/registry/variant-selector.json +15 -0
- package/dist/index-B_25cFc1.d.ts +0 -320
- package/dist/index-Cd0shhZU.d.mts +0 -320
package/dist/utils.d.mts
CHANGED
|
@@ -1,2 +1,55 @@
|
|
|
1
|
-
|
|
2
|
-
import './
|
|
1
|
+
import { M as Money, C as CurrencyCode, aB as PricePathTaxInfo, bm as PaymentErrorDetails, bk as PaymentResponse, bl as PaymentStatusResponse } from './payment-CrNyrc-D.mjs';
|
|
2
|
+
import { P as ProductWithPrice } from './price-C9Z-hr49.mjs';
|
|
3
|
+
|
|
4
|
+
declare const CURRENCY_SYMBOLS: Record<string, string>;
|
|
5
|
+
declare function getCurrencySymbol(currencyCode: CurrencyCode): string;
|
|
6
|
+
declare function formatNumberCompact(value: number, decimals?: number): string;
|
|
7
|
+
declare function formatPrice(amount: number | Money, currency?: CurrencyCode, locale?: string): string;
|
|
8
|
+
declare function formatPriceAdjustment(amount: number, currency?: CurrencyCode, locale?: string): string;
|
|
9
|
+
declare function formatPriceCompact(amount: number | Money, currency?: CurrencyCode, decimals?: number): string;
|
|
10
|
+
declare function formatMoney(amount: Money | number, currency?: CurrencyCode): string;
|
|
11
|
+
declare function parsePrice(value: Money | string | number | undefined | null): number;
|
|
12
|
+
declare function hasTaxInfo(priceInfo: {
|
|
13
|
+
tax_info?: PricePathTaxInfo;
|
|
14
|
+
}): boolean;
|
|
15
|
+
declare function getTaxAmount(priceInfo: {
|
|
16
|
+
tax_info?: PricePathTaxInfo;
|
|
17
|
+
}): number;
|
|
18
|
+
declare function isTaxInclusive(priceInfo: {
|
|
19
|
+
tax_info?: PricePathTaxInfo;
|
|
20
|
+
}): boolean;
|
|
21
|
+
declare function formatPriceWithTax(priceInfo: {
|
|
22
|
+
final_price: Money;
|
|
23
|
+
tax_info?: PricePathTaxInfo;
|
|
24
|
+
}, currency?: CurrencyCode): string;
|
|
25
|
+
declare function getDisplayPrice(product: ProductWithPrice): number;
|
|
26
|
+
declare function getBasePrice(product: ProductWithPrice): number;
|
|
27
|
+
declare function isOnSale(product: ProductWithPrice): boolean;
|
|
28
|
+
declare function getDiscountPercentage(product: ProductWithPrice): number;
|
|
29
|
+
declare function getMarkupPercentage(product: ProductWithPrice): number;
|
|
30
|
+
declare function getProductCurrency(product: ProductWithPrice): CurrencyCode;
|
|
31
|
+
declare function formatProductPrice(product: ProductWithPrice, locale?: string): string;
|
|
32
|
+
|
|
33
|
+
declare function categorizePaymentError(error: Error, errorCode?: string): PaymentErrorDetails;
|
|
34
|
+
declare function normalizePaymentResponse(response: unknown): PaymentResponse;
|
|
35
|
+
declare function isPaymentStatusSuccess(status: string | undefined): boolean;
|
|
36
|
+
declare function isPaymentStatusFailure(status: string | undefined): boolean;
|
|
37
|
+
declare function isPaymentStatusRequiresAction(status: string | undefined): boolean;
|
|
38
|
+
declare function normalizeStatusResponse(response: unknown): PaymentStatusResponse;
|
|
39
|
+
declare const MOBILE_MONEY_PROVIDERS: {
|
|
40
|
+
readonly mtn: {
|
|
41
|
+
readonly name: "MTN Mobile Money";
|
|
42
|
+
readonly prefix: readonly ["024", "054", "055", "059"];
|
|
43
|
+
};
|
|
44
|
+
readonly vodafone: {
|
|
45
|
+
readonly name: "Vodafone Cash";
|
|
46
|
+
readonly prefix: readonly ["020", "050"];
|
|
47
|
+
};
|
|
48
|
+
readonly airtel: {
|
|
49
|
+
readonly name: "AirtelTigo Money";
|
|
50
|
+
readonly prefix: readonly ["027", "057", "026", "056"];
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
declare function detectMobileMoneyProvider(phoneNumber: string): "mtn" | "vodafone" | "airtel" | null;
|
|
54
|
+
|
|
55
|
+
export { CURRENCY_SYMBOLS, MOBILE_MONEY_PROVIDERS, categorizePaymentError, detectMobileMoneyProvider, formatMoney, formatNumberCompact, formatPrice, formatPriceAdjustment, formatPriceCompact, formatPriceWithTax, formatProductPrice, getBasePrice, getCurrencySymbol, getDiscountPercentage, getDisplayPrice, getMarkupPercentage, getProductCurrency, getTaxAmount, hasTaxInfo, isOnSale, isPaymentStatusFailure, isPaymentStatusRequiresAction, isPaymentStatusSuccess, isTaxInclusive, normalizePaymentResponse, normalizeStatusResponse, parsePrice };
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,2 +1,55 @@
|
|
|
1
|
-
|
|
2
|
-
import './
|
|
1
|
+
import { M as Money, C as CurrencyCode, aB as PricePathTaxInfo, bm as PaymentErrorDetails, bk as PaymentResponse, bl as PaymentStatusResponse } from './payment-CrNyrc-D.js';
|
|
2
|
+
import { P as ProductWithPrice } from './price-RKKoTz-9.js';
|
|
3
|
+
|
|
4
|
+
declare const CURRENCY_SYMBOLS: Record<string, string>;
|
|
5
|
+
declare function getCurrencySymbol(currencyCode: CurrencyCode): string;
|
|
6
|
+
declare function formatNumberCompact(value: number, decimals?: number): string;
|
|
7
|
+
declare function formatPrice(amount: number | Money, currency?: CurrencyCode, locale?: string): string;
|
|
8
|
+
declare function formatPriceAdjustment(amount: number, currency?: CurrencyCode, locale?: string): string;
|
|
9
|
+
declare function formatPriceCompact(amount: number | Money, currency?: CurrencyCode, decimals?: number): string;
|
|
10
|
+
declare function formatMoney(amount: Money | number, currency?: CurrencyCode): string;
|
|
11
|
+
declare function parsePrice(value: Money | string | number | undefined | null): number;
|
|
12
|
+
declare function hasTaxInfo(priceInfo: {
|
|
13
|
+
tax_info?: PricePathTaxInfo;
|
|
14
|
+
}): boolean;
|
|
15
|
+
declare function getTaxAmount(priceInfo: {
|
|
16
|
+
tax_info?: PricePathTaxInfo;
|
|
17
|
+
}): number;
|
|
18
|
+
declare function isTaxInclusive(priceInfo: {
|
|
19
|
+
tax_info?: PricePathTaxInfo;
|
|
20
|
+
}): boolean;
|
|
21
|
+
declare function formatPriceWithTax(priceInfo: {
|
|
22
|
+
final_price: Money;
|
|
23
|
+
tax_info?: PricePathTaxInfo;
|
|
24
|
+
}, currency?: CurrencyCode): string;
|
|
25
|
+
declare function getDisplayPrice(product: ProductWithPrice): number;
|
|
26
|
+
declare function getBasePrice(product: ProductWithPrice): number;
|
|
27
|
+
declare function isOnSale(product: ProductWithPrice): boolean;
|
|
28
|
+
declare function getDiscountPercentage(product: ProductWithPrice): number;
|
|
29
|
+
declare function getMarkupPercentage(product: ProductWithPrice): number;
|
|
30
|
+
declare function getProductCurrency(product: ProductWithPrice): CurrencyCode;
|
|
31
|
+
declare function formatProductPrice(product: ProductWithPrice, locale?: string): string;
|
|
32
|
+
|
|
33
|
+
declare function categorizePaymentError(error: Error, errorCode?: string): PaymentErrorDetails;
|
|
34
|
+
declare function normalizePaymentResponse(response: unknown): PaymentResponse;
|
|
35
|
+
declare function isPaymentStatusSuccess(status: string | undefined): boolean;
|
|
36
|
+
declare function isPaymentStatusFailure(status: string | undefined): boolean;
|
|
37
|
+
declare function isPaymentStatusRequiresAction(status: string | undefined): boolean;
|
|
38
|
+
declare function normalizeStatusResponse(response: unknown): PaymentStatusResponse;
|
|
39
|
+
declare const MOBILE_MONEY_PROVIDERS: {
|
|
40
|
+
readonly mtn: {
|
|
41
|
+
readonly name: "MTN Mobile Money";
|
|
42
|
+
readonly prefix: readonly ["024", "054", "055", "059"];
|
|
43
|
+
};
|
|
44
|
+
readonly vodafone: {
|
|
45
|
+
readonly name: "Vodafone Cash";
|
|
46
|
+
readonly prefix: readonly ["020", "050"];
|
|
47
|
+
};
|
|
48
|
+
readonly airtel: {
|
|
49
|
+
readonly name: "AirtelTigo Money";
|
|
50
|
+
readonly prefix: readonly ["027", "057", "026", "056"];
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
declare function detectMobileMoneyProvider(phoneNumber: string): "mtn" | "vodafone" | "airtel" | null;
|
|
54
|
+
|
|
55
|
+
export { CURRENCY_SYMBOLS, MOBILE_MONEY_PROVIDERS, categorizePaymentError, detectMobileMoneyProvider, formatMoney, formatNumberCompact, formatPrice, formatPriceAdjustment, formatPriceCompact, formatPriceWithTax, formatProductPrice, getBasePrice, getCurrencySymbol, getDiscountPercentage, getDisplayPrice, getMarkupPercentage, getProductCurrency, getTaxAmount, hasTaxInfo, isOnSale, isPaymentStatusFailure, isPaymentStatusRequiresAction, isPaymentStatusSuccess, isTaxInclusive, normalizePaymentResponse, normalizeStatusResponse, parsePrice };
|
package/dist/utils.js
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
// src/utils/price.ts
|
|
4
4
|
var CURRENCY_SYMBOLS = {
|
|
5
|
-
// Major world currencies
|
|
6
5
|
USD: "$",
|
|
7
6
|
EUR: "\u20AC",
|
|
8
7
|
GBP: "\xA3",
|
|
@@ -35,7 +34,6 @@ var CURRENCY_SYMBOLS = {
|
|
|
35
34
|
IDR: "Rp",
|
|
36
35
|
VND: "\u20AB",
|
|
37
36
|
TWD: "NT$",
|
|
38
|
-
// African currencies
|
|
39
37
|
GHS: "GH\u20B5",
|
|
40
38
|
NGN: "\u20A6",
|
|
41
39
|
KES: "KSh",
|
|
@@ -226,12 +224,9 @@ function formatProductPrice(product, locale = "en-US") {
|
|
|
226
224
|
|
|
227
225
|
// src/types/common.ts
|
|
228
226
|
function money(value) {
|
|
229
|
-
return value;
|
|
227
|
+
return typeof value === "string" ? parseFloat(value) || 0 : value;
|
|
230
228
|
}
|
|
231
229
|
function moneyFromNumber(value) {
|
|
232
|
-
return value.toFixed(2);
|
|
233
|
-
}
|
|
234
|
-
function currencyCode(value) {
|
|
235
230
|
return value;
|
|
236
231
|
}
|
|
237
232
|
var DOCS_ERROR_BASE_URL = "https://docs.cimplify.io/reference/error-codes";
|
|
@@ -365,8 +360,8 @@ function normalizePaymentResponse(response) {
|
|
|
365
360
|
}
|
|
366
361
|
if (isWebPaymentResponse(response)) {
|
|
367
362
|
const authType = response.authorization_type?.toLowerCase();
|
|
368
|
-
const
|
|
369
|
-
const safeAuthType = authType &&
|
|
363
|
+
const validStringAuthTypes = ["otp", "pin", "phone", "birthday"];
|
|
364
|
+
const safeAuthType = authType && validStringAuthTypes.includes(authType) ? authType : void 0;
|
|
370
365
|
return {
|
|
371
366
|
provider: response.transaction.provider_type?.toLowerCase() || "unknown",
|
|
372
367
|
requires_action: response.requires_action || false,
|
|
@@ -405,7 +400,8 @@ var PAYMENT_FAILURE_STATUSES = /* @__PURE__ */ new Set([
|
|
|
405
400
|
"declined",
|
|
406
401
|
"cancelled",
|
|
407
402
|
"voided",
|
|
408
|
-
"error"
|
|
403
|
+
"error",
|
|
404
|
+
"abandoned"
|
|
409
405
|
]);
|
|
410
406
|
var PAYMENT_REQUIRES_ACTION_STATUSES = /* @__PURE__ */ new Set([
|
|
411
407
|
"requires_action",
|
|
@@ -435,29 +431,34 @@ var PAYMENT_STATUS_ALIAS_MAP = {
|
|
|
435
431
|
unresolved: "pending"
|
|
436
432
|
};
|
|
437
433
|
var KNOWN_PAYMENT_STATUSES = /* @__PURE__ */ new Set([
|
|
434
|
+
"initialized",
|
|
438
435
|
"pending",
|
|
439
436
|
"processing",
|
|
437
|
+
"authorized",
|
|
438
|
+
"captured",
|
|
439
|
+
"failed",
|
|
440
|
+
"cancelled",
|
|
441
|
+
"refunded",
|
|
442
|
+
"unknown",
|
|
443
|
+
"voided",
|
|
444
|
+
"requires_action",
|
|
445
|
+
"abandoned",
|
|
446
|
+
"reversed",
|
|
447
|
+
"disputed",
|
|
448
|
+
"refund_pending",
|
|
440
449
|
"created",
|
|
441
450
|
"pending_confirmation",
|
|
442
451
|
"success",
|
|
443
452
|
"succeeded",
|
|
444
|
-
"failed",
|
|
445
453
|
"declined",
|
|
446
|
-
"authorized",
|
|
447
|
-
"refunded",
|
|
448
454
|
"partially_refunded",
|
|
449
455
|
"partially_paid",
|
|
450
456
|
"paid",
|
|
451
457
|
"unpaid",
|
|
452
|
-
"requires_action",
|
|
453
458
|
"requires_payment_method",
|
|
454
459
|
"requires_capture",
|
|
455
|
-
"captured",
|
|
456
|
-
"cancelled",
|
|
457
460
|
"completed",
|
|
458
|
-
"
|
|
459
|
-
"error",
|
|
460
|
-
"unknown"
|
|
461
|
+
"error"
|
|
461
462
|
]);
|
|
462
463
|
function normalizeStatusToken(status) {
|
|
463
464
|
return status?.trim().toLowerCase().replace(/[\s-]+/g, "_") ?? "";
|
|
@@ -486,13 +487,15 @@ function normalizeStatusResponse(response) {
|
|
|
486
487
|
return {
|
|
487
488
|
status: "pending",
|
|
488
489
|
paid: false,
|
|
490
|
+
amount: moneyFromNumber(0),
|
|
491
|
+
currency: "",
|
|
489
492
|
message: "No status available"
|
|
490
493
|
};
|
|
491
494
|
}
|
|
492
495
|
const res = response;
|
|
493
496
|
const normalizedStatus = normalizePaymentStatusValue(res.status ?? void 0);
|
|
494
|
-
const normalizedAmount = typeof res.amount === "string" ? money(res.amount) : typeof res.amount === "number" && Number.isFinite(res.amount) ? moneyFromNumber(res.amount) :
|
|
495
|
-
const normalizedCurrency = typeof res.currency === "string" && res.currency.trim().length > 0 ?
|
|
497
|
+
const normalizedAmount = typeof res.amount === "string" ? money(res.amount) : typeof res.amount === "number" && Number.isFinite(res.amount) ? moneyFromNumber(res.amount) : moneyFromNumber(0);
|
|
498
|
+
const normalizedCurrency = typeof res.currency === "string" && res.currency.trim().length > 0 ? res.currency : "";
|
|
496
499
|
const paidValue = res.paid === true;
|
|
497
500
|
const derivedPaid = paidValue || [
|
|
498
501
|
"success",
|
package/dist/utils.mjs
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
// src/utils/price.ts
|
|
2
2
|
var CURRENCY_SYMBOLS = {
|
|
3
|
-
// Major world currencies
|
|
4
3
|
USD: "$",
|
|
5
4
|
EUR: "\u20AC",
|
|
6
5
|
GBP: "\xA3",
|
|
@@ -33,7 +32,6 @@ var CURRENCY_SYMBOLS = {
|
|
|
33
32
|
IDR: "Rp",
|
|
34
33
|
VND: "\u20AB",
|
|
35
34
|
TWD: "NT$",
|
|
36
|
-
// African currencies
|
|
37
35
|
GHS: "GH\u20B5",
|
|
38
36
|
NGN: "\u20A6",
|
|
39
37
|
KES: "KSh",
|
|
@@ -224,12 +222,9 @@ function formatProductPrice(product, locale = "en-US") {
|
|
|
224
222
|
|
|
225
223
|
// src/types/common.ts
|
|
226
224
|
function money(value) {
|
|
227
|
-
return value;
|
|
225
|
+
return typeof value === "string" ? parseFloat(value) || 0 : value;
|
|
228
226
|
}
|
|
229
227
|
function moneyFromNumber(value) {
|
|
230
|
-
return value.toFixed(2);
|
|
231
|
-
}
|
|
232
|
-
function currencyCode(value) {
|
|
233
228
|
return value;
|
|
234
229
|
}
|
|
235
230
|
var DOCS_ERROR_BASE_URL = "https://docs.cimplify.io/reference/error-codes";
|
|
@@ -363,8 +358,8 @@ function normalizePaymentResponse(response) {
|
|
|
363
358
|
}
|
|
364
359
|
if (isWebPaymentResponse(response)) {
|
|
365
360
|
const authType = response.authorization_type?.toLowerCase();
|
|
366
|
-
const
|
|
367
|
-
const safeAuthType = authType &&
|
|
361
|
+
const validStringAuthTypes = ["otp", "pin", "phone", "birthday"];
|
|
362
|
+
const safeAuthType = authType && validStringAuthTypes.includes(authType) ? authType : void 0;
|
|
368
363
|
return {
|
|
369
364
|
provider: response.transaction.provider_type?.toLowerCase() || "unknown",
|
|
370
365
|
requires_action: response.requires_action || false,
|
|
@@ -403,7 +398,8 @@ var PAYMENT_FAILURE_STATUSES = /* @__PURE__ */ new Set([
|
|
|
403
398
|
"declined",
|
|
404
399
|
"cancelled",
|
|
405
400
|
"voided",
|
|
406
|
-
"error"
|
|
401
|
+
"error",
|
|
402
|
+
"abandoned"
|
|
407
403
|
]);
|
|
408
404
|
var PAYMENT_REQUIRES_ACTION_STATUSES = /* @__PURE__ */ new Set([
|
|
409
405
|
"requires_action",
|
|
@@ -433,29 +429,34 @@ var PAYMENT_STATUS_ALIAS_MAP = {
|
|
|
433
429
|
unresolved: "pending"
|
|
434
430
|
};
|
|
435
431
|
var KNOWN_PAYMENT_STATUSES = /* @__PURE__ */ new Set([
|
|
432
|
+
"initialized",
|
|
436
433
|
"pending",
|
|
437
434
|
"processing",
|
|
435
|
+
"authorized",
|
|
436
|
+
"captured",
|
|
437
|
+
"failed",
|
|
438
|
+
"cancelled",
|
|
439
|
+
"refunded",
|
|
440
|
+
"unknown",
|
|
441
|
+
"voided",
|
|
442
|
+
"requires_action",
|
|
443
|
+
"abandoned",
|
|
444
|
+
"reversed",
|
|
445
|
+
"disputed",
|
|
446
|
+
"refund_pending",
|
|
438
447
|
"created",
|
|
439
448
|
"pending_confirmation",
|
|
440
449
|
"success",
|
|
441
450
|
"succeeded",
|
|
442
|
-
"failed",
|
|
443
451
|
"declined",
|
|
444
|
-
"authorized",
|
|
445
|
-
"refunded",
|
|
446
452
|
"partially_refunded",
|
|
447
453
|
"partially_paid",
|
|
448
454
|
"paid",
|
|
449
455
|
"unpaid",
|
|
450
|
-
"requires_action",
|
|
451
456
|
"requires_payment_method",
|
|
452
457
|
"requires_capture",
|
|
453
|
-
"captured",
|
|
454
|
-
"cancelled",
|
|
455
458
|
"completed",
|
|
456
|
-
"
|
|
457
|
-
"error",
|
|
458
|
-
"unknown"
|
|
459
|
+
"error"
|
|
459
460
|
]);
|
|
460
461
|
function normalizeStatusToken(status) {
|
|
461
462
|
return status?.trim().toLowerCase().replace(/[\s-]+/g, "_") ?? "";
|
|
@@ -484,13 +485,15 @@ function normalizeStatusResponse(response) {
|
|
|
484
485
|
return {
|
|
485
486
|
status: "pending",
|
|
486
487
|
paid: false,
|
|
488
|
+
amount: moneyFromNumber(0),
|
|
489
|
+
currency: "",
|
|
487
490
|
message: "No status available"
|
|
488
491
|
};
|
|
489
492
|
}
|
|
490
493
|
const res = response;
|
|
491
494
|
const normalizedStatus = normalizePaymentStatusValue(res.status ?? void 0);
|
|
492
|
-
const normalizedAmount = typeof res.amount === "string" ? money(res.amount) : typeof res.amount === "number" && Number.isFinite(res.amount) ? moneyFromNumber(res.amount) :
|
|
493
|
-
const normalizedCurrency = typeof res.currency === "string" && res.currency.trim().length > 0 ?
|
|
495
|
+
const normalizedAmount = typeof res.amount === "string" ? money(res.amount) : typeof res.amount === "number" && Number.isFinite(res.amount) ? moneyFromNumber(res.amount) : moneyFromNumber(0);
|
|
496
|
+
const normalizedCurrency = typeof res.currency === "string" && res.currency.trim().length > 0 ? res.currency : "";
|
|
494
497
|
const paidValue = res.paid === true;
|
|
495
498
|
const derivedPaid = paidValue || [
|
|
496
499
|
"success",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cimplify/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Cimplify Commerce SDK for storefronts",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cimplify",
|
|
@@ -9,8 +9,12 @@
|
|
|
9
9
|
"storefront"
|
|
10
10
|
],
|
|
11
11
|
"license": "MIT",
|
|
12
|
+
"bin": {
|
|
13
|
+
"cimplify-sdk": "./dist/cli.js"
|
|
14
|
+
},
|
|
12
15
|
"files": [
|
|
13
|
-
"dist"
|
|
16
|
+
"dist",
|
|
17
|
+
"registry"
|
|
14
18
|
],
|
|
15
19
|
"main": "./dist/index.js",
|
|
16
20
|
"module": "./dist/index.mjs",
|
|
@@ -38,7 +42,8 @@
|
|
|
38
42
|
}
|
|
39
43
|
},
|
|
40
44
|
"scripts": {
|
|
41
|
-
"build": "tsup",
|
|
45
|
+
"build": "tsup && bun run build:registry",
|
|
46
|
+
"build:registry": "bun scripts/build-registry.ts",
|
|
42
47
|
"dev": "tsup --watch",
|
|
43
48
|
"typecheck": "tsgo --noEmit",
|
|
44
49
|
"clean": "rm -rf dist",
|
|
@@ -69,8 +74,13 @@
|
|
|
69
74
|
"@typescript/native-preview": "^7.0.0-dev.20260228.1",
|
|
70
75
|
"jsdom": "^28.1.0",
|
|
71
76
|
"react": "^19.2.4",
|
|
77
|
+
"react-dom": "^19.2.4",
|
|
72
78
|
"tsup": "^8.5.1",
|
|
73
79
|
"typescript": "5.9.3",
|
|
74
80
|
"vitest": "^4.0.18"
|
|
81
|
+
},
|
|
82
|
+
"dependencies": {
|
|
83
|
+
"clsx": "^2.1.0",
|
|
84
|
+
"tailwind-merge": "^2.6.0"
|
|
75
85
|
}
|
|
76
86
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "add-on-selector",
|
|
3
|
+
"title": "AddOnSelector",
|
|
4
|
+
"description": "Modifier groups with single-select or multi-select options.",
|
|
5
|
+
"type": "component",
|
|
6
|
+
"registryDependencies": [
|
|
7
|
+
"price"
|
|
8
|
+
],
|
|
9
|
+
"files": [
|
|
10
|
+
{
|
|
11
|
+
"path": "add-on-selector.tsx",
|
|
12
|
+
"content": "\"use client\";\n\nimport React, { useCallback } from \"react\";\nimport type { AddOnWithOptions } from \"@cimplify/sdk\";\nimport { Price } from \"@cimplify/sdk/react\";\n\nexport interface AddOnSelectorProps {\n addOns: AddOnWithOptions[];\n selectedOptions: string[];\n onOptionsChange: (optionIds: string[]) => void;\n className?: string;\n}\n\n/**\n * AddOnSelector — modifier groups with single-select (radio) or multi-select (checkbox).\n *\n * Respects min/max constraints, required flags, and mutual exclusivity.\n */\nexport function AddOnSelector({\n addOns,\n selectedOptions,\n onOptionsChange,\n className,\n}: AddOnSelectorProps): React.ReactElement | null {\n const isOptionSelected = useCallback(\n (optionId: string) => selectedOptions.includes(optionId),\n [selectedOptions],\n );\n\n const toggleOption = useCallback(\n (addOn: AddOnWithOptions, optionId: string) => {\n const isSelected = selectedOptions.includes(optionId);\n\n if (addOn.is_mutually_exclusive || !addOn.is_multiple_allowed) {\n const groupOptionIds = new Set(addOn.options.map((o) => o.id));\n const withoutGroup = selectedOptions.filter((id) => !groupOptionIds.has(id));\n\n if (isSelected) {\n if (!addOn.is_required) {\n onOptionsChange(withoutGroup);\n }\n } else {\n onOptionsChange([...withoutGroup, optionId]);\n }\n } else {\n if (isSelected) {\n onOptionsChange(selectedOptions.filter((id) => id !== optionId));\n } else {\n const currentCount = selectedOptions.filter((id) =>\n addOn.options.some((o) => o.id === id),\n ).length;\n\n if (addOn.max_selections && currentCount >= addOn.max_selections) {\n return;\n }\n\n onOptionsChange([...selectedOptions, optionId]);\n }\n }\n },\n [selectedOptions, onOptionsChange],\n );\n\n if (!addOns || addOns.length === 0) {\n return null;\n }\n\n return (\n <div data-cimplify-addon-selector className={className}>\n {addOns.map((addOn) => {\n const currentSelections = selectedOptions.filter((id) =>\n addOn.options.some((o) => o.id === id),\n ).length;\n const minMet = !addOn.min_selections || currentSelections >= addOn.min_selections;\n const isSingleSelect = addOn.is_mutually_exclusive || !addOn.is_multiple_allowed;\n\n return (\n <div key={addOn.id} data-cimplify-addon-group>\n {/* Group header */}\n <div data-cimplify-addon-header>\n <div>\n <span data-cimplify-addon-name>\n {addOn.name}\n {addOn.is_required && <span data-cimplify-addon-required> *</span>}\n </span>\n {(addOn.min_selections || addOn.max_selections) && (\n <span data-cimplify-addon-constraint>\n {addOn.min_selections && addOn.max_selections\n ? `Choose ${addOn.min_selections}\\u2013${addOn.max_selections}`\n : addOn.min_selections\n ? `Choose at least ${addOn.min_selections}`\n : `Choose up to ${addOn.max_selections}`}\n </span>\n )}\n </div>\n {!minMet && <span data-cimplify-addon-validation>Required</span>}\n </div>\n\n {/* Options */}\n <div data-cimplify-addon-options>\n {addOn.options.map((option) => {\n const isSelected = isOptionSelected(option.id);\n\n return (\n <button\n key={option.id}\n type=\"button\"\n role={isSingleSelect ? \"radio\" : \"checkbox\"}\n aria-checked={isSelected}\n onClick={() => toggleOption(addOn, option.id)}\n data-cimplify-addon-option\n data-selected={isSelected || undefined}\n >\n <span data-cimplify-addon-option-name>{option.name}</span>\n\n {option.default_price != null && option.default_price !== 0 && (\n <Price\n amount={option.default_price}\n prefix=\"+\"\n />\n )}\n </button>\n );\n })}\n </div>\n </div>\n );\n })}\n </div>\n );\n}\n"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "availability-badge",
|
|
3
|
+
"title": "AvailabilityBadge",
|
|
4
|
+
"description": "Displays in-stock / out-of-stock status for tracked products.",
|
|
5
|
+
"type": "component",
|
|
6
|
+
"registryDependencies": [
|
|
7
|
+
"cn"
|
|
8
|
+
],
|
|
9
|
+
"files": [
|
|
10
|
+
{
|
|
11
|
+
"path": "availability-badge.tsx",
|
|
12
|
+
"content": "\"use client\";\n\nimport React from \"react\";\nimport type { Product } from \"@cimplify/sdk\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface AvailabilityBadgeClassNames {\n root?: string;\n dot?: string;\n label?: string;\n}\n\nexport interface AvailabilityBadgeProps {\n /** The product to check availability for. */\n product: Product;\n /** Override availability (e.g. from location_availability). */\n isAvailable?: boolean;\n /** Override stock status (e.g. from location_availability). */\n isInStock?: boolean;\n className?: string;\n classNames?: AvailabilityBadgeClassNames;\n}\n\n/**\n * AvailabilityBadge — displays in-stock / out-of-stock status for tracked products.\n *\n * Returns `null` for products that don't have inventory tracking enabled,\n * since there's no meaningful stock state to show.\n */\nexport function AvailabilityBadge({\n product,\n isAvailable,\n isInStock,\n className,\n classNames,\n}: AvailabilityBadgeProps): React.ReactElement | null {\n if (product.is_tracked !== true) {\n return null;\n }\n\n const outOfStock = isInStock === false || isAvailable === false;\n const stockState = outOfStock ? \"out_of_stock\" : \"in_stock\";\n const label = outOfStock ? \"Out of Stock\" : \"In Stock\";\n\n return (\n <span\n data-cimplify-availability-badge\n data-stock-state={stockState}\n className={cn(className, classNames?.root)}\n style={{ display: \"inline-flex\", alignItems: \"center\", gap: \"0.375rem\" }}\n >\n <span\n data-cimplify-availability-dot\n className={classNames?.dot}\n style={{\n display: \"inline-block\",\n width: \"0.5rem\",\n height: \"0.5rem\",\n borderRadius: \"9999px\",\n }}\n />\n <span data-cimplify-availability-label className={classNames?.label}>\n {label}\n </span>\n </span>\n );\n}\n"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "booking-card",
|
|
3
|
+
"title": "BookingCard",
|
|
4
|
+
"description": "Single booking display with status, time, and action buttons.",
|
|
5
|
+
"type": "component",
|
|
6
|
+
"registryDependencies": [
|
|
7
|
+
"price",
|
|
8
|
+
"cn"
|
|
9
|
+
],
|
|
10
|
+
"files": [
|
|
11
|
+
{
|
|
12
|
+
"path": "booking-card.tsx",
|
|
13
|
+
"content": "\"use client\";\n\nimport React from \"react\";\nimport type { CustomerBooking, BookingStatus } from \"@cimplify/sdk\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface BookingCardClassNames {\n root?: string;\n service?: string;\n status?: string;\n time?: string;\n confirmationCode?: string;\n total?: string;\n actions?: string;\n cancelButton?: string;\n rescheduleButton?: string;\n}\n\nexport interface BookingCardProps {\n /** The booking to display. */\n booking: CustomerBooking;\n /** Called when cancel is clicked. */\n onCancel?: (booking: CustomerBooking) => void;\n /** Called when reschedule is clicked. */\n onReschedule?: (booking: CustomerBooking) => void;\n /** Custom renderer for the booking. */\n renderBooking?: (booking: CustomerBooking) => React.ReactNode;\n className?: string;\n classNames?: BookingCardClassNames;\n}\n\nconst STATUS_LABELS: Record<string, string> = {\n pending: \"Pending\",\n confirmed: \"Confirmed\",\n in_progress: \"In Progress\",\n completed: \"Completed\",\n cancelled: \"Cancelled\",\n no_show: \"No Show\",\n};\n\nfunction isActiveStatus(status: BookingStatus): boolean {\n const s = String(status).toLowerCase();\n return s !== \"completed\" && s !== \"cancelled\" && s !== \"no_show\";\n}\n\nfunction getFirstServiceItem(booking: CustomerBooking) {\n return booking.service_items[0] ?? null;\n}\n\nfunction formatScheduledTime(start?: string | null, end?: string | null): string | null {\n if (!start) return null;\n try {\n const startDate = new Date(start);\n const options: Intl.DateTimeFormatOptions = {\n month: \"short\",\n day: \"numeric\",\n hour: \"numeric\",\n minute: \"2-digit\",\n };\n let formatted = startDate.toLocaleString(undefined, options);\n if (end) {\n const endDate = new Date(end);\n formatted += ` – ${endDate.toLocaleTimeString(undefined, { hour: \"numeric\", minute: \"2-digit\" })}`;\n }\n return formatted;\n } catch {\n return start;\n }\n}\n\nexport function BookingCard({\n booking,\n onCancel,\n onReschedule,\n renderBooking,\n className,\n classNames,\n}: BookingCardProps): React.ReactElement {\n if (renderBooking) {\n return (\n <div\n data-cimplify-booking-card\n data-status={booking.status}\n className={cn(className, classNames?.root)}\n >\n {renderBooking(booking)}\n </div>\n );\n }\n\n const firstItem = getFirstServiceItem(booking);\n const active = isActiveStatus(booking.status);\n const scheduledTime = firstItem\n ? formatScheduledTime(firstItem.scheduled_start, firstItem.scheduled_end)\n : null;\n\n return (\n <div\n data-cimplify-booking-card\n data-status={booking.status}\n className={cn(className, classNames?.root)}\n >\n <div data-cimplify-booking-main>\n <span data-cimplify-booking-service className={classNames?.service}>\n Booking #{booking.order_id.slice(0, 8)}\n </span>\n <span\n data-cimplify-booking-status\n data-status={booking.status}\n className={classNames?.status}\n >\n {STATUS_LABELS[booking.status] ?? booking.status}\n </span>\n </div>\n\n {firstItem?.confirmation_code && (\n <span data-cimplify-booking-code className={classNames?.confirmationCode}>\n {firstItem.confirmation_code}\n </span>\n )}\n\n {scheduledTime && (\n <time data-cimplify-booking-time className={classNames?.time}>\n {scheduledTime}\n </time>\n )}\n\n <span data-cimplify-booking-total className={classNames?.total}>\n <Price amount={booking.total_price} />\n </span>\n\n {active && (onCancel || onReschedule) && (\n <div data-cimplify-booking-actions className={classNames?.actions}>\n {onReschedule && (\n <button\n type=\"button\"\n onClick={() => onReschedule(booking)}\n data-cimplify-booking-reschedule\n className={classNames?.rescheduleButton}\n >\n Reschedule\n </button>\n )}\n {onCancel && (\n <button\n type=\"button\"\n onClick={() => onCancel(booking)}\n data-cimplify-booking-cancel\n className={classNames?.cancelButton}\n >\n Cancel\n </button>\n )}\n </div>\n )}\n </div>\n );\n}\n"
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "booking-list",
|
|
3
|
+
"title": "BookingList",
|
|
4
|
+
"description": "List of booking cards with optional self-fetching.",
|
|
5
|
+
"type": "component",
|
|
6
|
+
"registryDependencies": [
|
|
7
|
+
"booking-card",
|
|
8
|
+
"cn"
|
|
9
|
+
],
|
|
10
|
+
"files": [
|
|
11
|
+
{
|
|
12
|
+
"path": "booking-list.tsx",
|
|
13
|
+
"content": "\"use client\";\n\nimport React from \"react\";\nimport type { CustomerBooking } from \"@cimplify/sdk\";\nimport { useBookings } from \"@cimplify/sdk/react\";\nimport { BookingCard } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface BookingListClassNames {\n root?: string;\n item?: string;\n loading?: string;\n empty?: string;\n}\n\nexport interface BookingListProps {\n /** Pre-fetched bookings (skips fetch). */\n bookings?: CustomerBooking[];\n /** Filter: \"all\", \"upcoming\", or \"past\". */\n filter?: \"all\" | \"upcoming\" | \"past\";\n /** Called when cancel is clicked on a booking. */\n onCancel?: (booking: CustomerBooking) => void;\n /** Called when reschedule is clicked on a booking. */\n onReschedule?: (booking: CustomerBooking) => void;\n /** Called when a booking is clicked. */\n onBookingClick?: (booking: CustomerBooking) => void;\n /** Custom booking renderer. */\n renderBooking?: (booking: CustomerBooking) => React.ReactNode;\n /** Text shown when empty. */\n emptyMessage?: string;\n className?: string;\n classNames?: BookingListClassNames;\n}\n\nexport function BookingList({\n bookings: bookingsProp,\n filter,\n onCancel,\n onReschedule,\n onBookingClick,\n renderBooking,\n emptyMessage = \"No bookings yet\",\n className,\n classNames,\n}: BookingListProps): React.ReactElement {\n const { bookings: fetched, isLoading } = useBookings({\n filter,\n enabled: bookingsProp === undefined,\n });\n\n const bookings = bookingsProp ?? fetched;\n\n if (isLoading && bookings.length === 0) {\n return (\n <div\n data-cimplify-booking-list\n aria-busy=\"true\"\n className={cn(className, classNames?.root, classNames?.loading)}\n />\n );\n }\n\n if (bookings.length === 0) {\n return (\n <div\n data-cimplify-booking-list\n data-empty\n className={cn(className, classNames?.root, classNames?.empty)}\n >\n <p>{emptyMessage}</p>\n </div>\n );\n }\n\n return (\n <div data-cimplify-booking-list className={cn(className, classNames?.root)}>\n {bookings.map((booking) => (\n <div\n key={booking.order_id}\n data-cimplify-booking-list-item\n className={classNames?.item}\n onClick={() => onBookingClick?.(booking)}\n role={onBookingClick ? \"button\" : undefined}\n tabIndex={onBookingClick ? 0 : undefined}\n onKeyDown={\n onBookingClick\n ? (e) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n onBookingClick(booking);\n }\n }\n : undefined\n }\n >\n <BookingCard\n booking={booking}\n onCancel={onCancel}\n onReschedule={onReschedule}\n renderBooking={renderBooking}\n />\n </div>\n ))}\n </div>\n );\n}\n"
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "booking-page",
|
|
3
|
+
"title": "BookingPage",
|
|
4
|
+
"description": "Full booking flow with date/slot picker, staff selection, and cart integration.",
|
|
5
|
+
"type": "component",
|
|
6
|
+
"registryDependencies": [
|
|
7
|
+
"date-slot-picker",
|
|
8
|
+
"staff-picker",
|
|
9
|
+
"price",
|
|
10
|
+
"cn"
|
|
11
|
+
],
|
|
12
|
+
"files": [
|
|
13
|
+
{
|
|
14
|
+
"path": "booking-page.tsx",
|
|
15
|
+
"content": "\"use client\";\n\nimport React, { useState, useCallback } from \"react\";\nimport type { Product } from \"@cimplify/sdk\";\nimport type { Service, Staff, AvailableSlot } from \"@cimplify/sdk\";\nimport { useCart } from \"@cimplify/sdk/react\";\nimport { DateSlotPicker } from \"@cimplify/sdk/react\";\nimport { StaffPicker } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface BookingPageClassNames {\n root?: string;\n header?: string;\n title?: string;\n serviceInfo?: string;\n step?: string;\n stepTitle?: string;\n summary?: string;\n summaryRow?: string;\n confirmButton?: string;\n backButton?: string;\n error?: string;\n}\n\nexport interface BookingPageProps {\n /** The service being booked. */\n service: Service;\n /** Optional staff list for staff selection step. */\n staff?: Staff[];\n /** Number of participants. */\n participantCount?: number;\n /** Page title. */\n title?: string;\n /** Called after successfully adding to cart. */\n onBooked?: (slot: AvailableSlot, staffId: string | null) => void;\n /** Called when user wants to go back. */\n onBack?: () => void;\n /** Show price on slots. Default: true. */\n showPrice?: boolean;\n className?: string;\n classNames?: BookingPageClassNames;\n}\n\ntype BookingStep = \"select-slot\" | \"select-staff\" | \"confirm\";\n\nexport function BookingPage({\n service,\n staff,\n participantCount,\n title,\n onBooked,\n onBack,\n showPrice = true,\n className,\n classNames,\n}: BookingPageProps): React.ReactElement {\n const { addItem } = useCart();\n\n const [step, setStep] = useState<BookingStep>(\"select-slot\");\n const [selectedSlot, setSelectedSlot] = useState<AvailableSlot | null>(null);\n const [selectedDate, setSelectedDate] = useState<string | null>(null);\n const [selectedStaffId, setSelectedStaffId] = useState<string | null>(null);\n const [isSubmitting, setIsSubmitting] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n const hasStaffStep = staff && staff.length > 0;\n\n const handleSlotSelect = useCallback(\n (slot: AvailableSlot, date: string) => {\n setSelectedSlot(slot);\n setSelectedDate(date);\n setError(null);\n if (hasStaffStep) {\n setStep(\"select-staff\");\n } else {\n setStep(\"confirm\");\n }\n },\n [hasStaffStep],\n );\n\n const handleStaffSelect = useCallback((staffId: string | null) => {\n setSelectedStaffId(staffId);\n setStep(\"confirm\");\n }, []);\n\n const handleBack = useCallback(() => {\n setError(null);\n if (step === \"confirm\" && hasStaffStep) {\n setStep(\"select-staff\");\n } else if (step === \"confirm\" || step === \"select-staff\") {\n setStep(\"select-slot\");\n } else {\n onBack?.();\n }\n }, [step, hasStaffStep, onBack]);\n\n const handleConfirm = useCallback(async () => {\n if (!selectedSlot || !selectedDate) return;\n\n setIsSubmitting(true);\n setError(null);\n\n try {\n const serviceProduct = {\n id: service.product_id || service.id,\n business_id: service.business_id || \"\",\n category_id: service.category_id || undefined,\n name: service.name,\n slug: service.id,\n description: service.description || undefined,\n image_url: service.image_url || undefined,\n default_price: (service.price || \"0\") as Product[\"default_price\"],\n type: \"service\" as const,\n inventory_type: \"none\" as const,\n variant_strategy: \"fetch_all\" as const,\n is_active: service.is_available,\n created_at: new Date().toISOString(),\n updated_at: new Date().toISOString(),\n };\n\n await addItem(serviceProduct, 1, {\n scheduledStart: selectedSlot.start_time,\n scheduledEnd: selectedSlot.end_time,\n staffId: selectedStaffId || undefined,\n });\n\n onBooked?.(selectedSlot, selectedStaffId);\n } catch (err) {\n setError(err instanceof Error ? err.message : \"Failed to add booking to cart\");\n } finally {\n setIsSubmitting(false);\n }\n }, [selectedSlot, selectedDate, selectedStaffId, service, addItem, onBooked]);\n\n return (\n <div data-cimplify-booking-page className={cn(className, classNames?.root)}>\n <div data-cimplify-booking-page-header className={classNames?.header}>\n {onBack && step === \"select-slot\" && (\n <button\n type=\"button\"\n onClick={onBack}\n data-cimplify-booking-page-back\n className={classNames?.backButton}\n >\n Back\n </button>\n )}\n {step !== \"select-slot\" && (\n <button\n type=\"button\"\n onClick={handleBack}\n data-cimplify-booking-page-back\n className={classNames?.backButton}\n >\n Back\n </button>\n )}\n <h1 data-cimplify-booking-page-title className={classNames?.title}>\n {title || `Book ${service.name}`}\n </h1>\n </div>\n\n <div data-cimplify-booking-service-info className={classNames?.serviceInfo}>\n <span data-cimplify-booking-service-name>{service.name}</span>\n <span data-cimplify-booking-service-duration>{service.duration_minutes} min</span>\n {service.price && (\n <span data-cimplify-booking-service-price>\n <Price amount={service.price} />\n </span>\n )}\n </div>\n\n {error && (\n <div data-cimplify-booking-error className={classNames?.error}>\n {error}\n </div>\n )}\n\n {step === \"select-slot\" && (\n <div data-cimplify-booking-step=\"select-slot\" className={classNames?.step}>\n <h2 data-cimplify-booking-step-title className={classNames?.stepTitle}>\n Select a date & time\n </h2>\n <DateSlotPicker\n serviceId={service.id}\n participantCount={participantCount}\n selectedSlot={selectedSlot}\n onSlotSelect={handleSlotSelect}\n showPrice={showPrice}\n />\n </div>\n )}\n\n {step === \"select-staff\" && staff && (\n <div data-cimplify-booking-step=\"select-staff\" className={classNames?.step}>\n <h2 data-cimplify-booking-step-title className={classNames?.stepTitle}>\n Choose a provider\n </h2>\n <StaffPicker\n staff={staff}\n selectedStaffId={selectedStaffId}\n onStaffSelect={handleStaffSelect}\n />\n </div>\n )}\n\n {step === \"confirm\" && selectedSlot && (\n <div data-cimplify-booking-step=\"confirm\" className={classNames?.step}>\n <h2 data-cimplify-booking-step-title className={classNames?.stepTitle}>\n Confirm your booking\n </h2>\n <div data-cimplify-booking-summary className={classNames?.summary}>\n <div data-cimplify-booking-summary-row className={classNames?.summaryRow}>\n <span>Service</span>\n <span>{service.name}</span>\n </div>\n <div data-cimplify-booking-summary-row className={classNames?.summaryRow}>\n <span>Date</span>\n <span>{selectedDate}</span>\n </div>\n <div data-cimplify-booking-summary-row className={classNames?.summaryRow}>\n <span>Time</span>\n <span>\n {new Date(selectedSlot.start_time).toLocaleTimeString(undefined, {\n hour: \"numeric\",\n minute: \"2-digit\",\n })}\n </span>\n </div>\n <div data-cimplify-booking-summary-row className={classNames?.summaryRow}>\n <span>Duration</span>\n <span>{service.duration_minutes} min</span>\n </div>\n {selectedStaffId && staff && (\n <div data-cimplify-booking-summary-row className={classNames?.summaryRow}>\n <span>Provider</span>\n <span>{staff.find((s) => s.id === selectedStaffId)?.name ?? \"Selected\"}</span>\n </div>\n )}\n {(selectedSlot.price || service.price) && (\n <div data-cimplify-booking-summary-row className={classNames?.summaryRow}>\n <span>Price</span>\n <span>\n <Price amount={selectedSlot.price || service.price!} />\n </span>\n </div>\n )}\n </div>\n <button\n type=\"button\"\n onClick={handleConfirm}\n disabled={isSubmitting}\n data-cimplify-booking-confirm\n className={classNames?.confirmButton}\n >\n {isSubmitting ? \"Adding to cart…\" : \"Add to cart\"}\n </button>\n </div>\n )}\n </div>\n );\n}\n"
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bookings-page",
|
|
3
|
+
"title": "BookingsPage",
|
|
4
|
+
"description": "Booking history with filter tabs and inline detail view.",
|
|
5
|
+
"type": "component",
|
|
6
|
+
"registryDependencies": [
|
|
7
|
+
"booking-list",
|
|
8
|
+
"booking-card",
|
|
9
|
+
"cn"
|
|
10
|
+
],
|
|
11
|
+
"files": [
|
|
12
|
+
{
|
|
13
|
+
"path": "bookings-page.tsx",
|
|
14
|
+
"content": "\"use client\";\n\nimport React, { useState, useCallback } from \"react\";\nimport type { CustomerBooking } from \"@cimplify/sdk\";\nimport { BookingList } from \"@cimplify/sdk/react\";\nimport { BookingCard } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface BookingsPageClassNames {\n root?: string;\n header?: string;\n title?: string;\n filters?: string;\n filterButton?: string;\n list?: string;\n detail?: string;\n backButton?: string;\n}\n\nexport interface BookingsPageProps {\n /** Page title. */\n title?: string;\n /** Pre-fetched bookings for SSR. */\n bookings?: CustomerBooking[];\n /** Called when navigating to a booking detail (e.g. for routing). */\n onBookingNavigate?: (booking: CustomerBooking) => void;\n /** Called when cancel is clicked. */\n onCancel?: (booking: CustomerBooking) => void;\n /** Called when reschedule is clicked. */\n onReschedule?: (booking: CustomerBooking) => void;\n /** Show filter tabs. Default: true. */\n showFilters?: boolean;\n /** Custom booking renderer. */\n renderBooking?: (booking: CustomerBooking) => React.ReactNode;\n className?: string;\n classNames?: BookingsPageClassNames;\n}\n\nconst BOOKING_FILTERS: { label: string; value: \"all\" | \"upcoming\" | \"past\" }[] = [\n { label: \"All\", value: \"all\" },\n { label: \"Upcoming\", value: \"upcoming\" },\n { label: \"Past\", value: \"past\" },\n];\n\nexport function BookingsPage({\n title = \"My Bookings\",\n bookings: bookingsProp,\n onBookingNavigate,\n onCancel,\n onReschedule,\n showFilters = true,\n renderBooking,\n className,\n classNames,\n}: BookingsPageProps): React.ReactElement {\n const [filter, setFilter] = useState<\"all\" | \"upcoming\" | \"past\">(\"all\");\n const [selectedBooking, setSelectedBooking] = useState<CustomerBooking | null>(null);\n\n const handleBookingClick = useCallback(\n (booking: CustomerBooking) => {\n if (onBookingNavigate) {\n onBookingNavigate(booking);\n } else {\n setSelectedBooking(booking);\n }\n },\n [onBookingNavigate],\n );\n\n const handleBack = useCallback(() => {\n setSelectedBooking(null);\n }, []);\n\n if (selectedBooking && !onBookingNavigate) {\n return (\n <div data-cimplify-bookings-page className={cn(className, classNames?.root)}>\n <div data-cimplify-bookings-detail className={classNames?.detail}>\n <button\n type=\"button\"\n onClick={handleBack}\n data-cimplify-bookings-back\n className={classNames?.backButton}\n >\n Back to bookings\n </button>\n <BookingCard\n booking={selectedBooking}\n onCancel={onCancel}\n onReschedule={onReschedule}\n />\n </div>\n </div>\n );\n }\n\n return (\n <div data-cimplify-bookings-page className={cn(className, classNames?.root)}>\n <div data-cimplify-bookings-header className={classNames?.header}>\n <h1 data-cimplify-bookings-title className={classNames?.title}>\n {title}\n </h1>\n </div>\n\n {showFilters && (\n <div data-cimplify-bookings-filters className={classNames?.filters} role=\"tablist\">\n {BOOKING_FILTERS.map((f) => (\n <button\n key={f.value}\n type=\"button\"\n role=\"tab\"\n aria-selected={filter === f.value}\n onClick={() => setFilter(f.value)}\n data-cimplify-booking-filter\n data-selected={filter === f.value || undefined}\n className={classNames?.filterButton}\n >\n {f.label}\n </button>\n ))}\n </div>\n )}\n\n <div data-cimplify-bookings-list className={classNames?.list}>\n <BookingList\n bookings={bookingsProp}\n filter={filter}\n onCancel={onCancel}\n onReschedule={onReschedule}\n onBookingClick={handleBookingClick}\n renderBooking={renderBooking}\n />\n </div>\n </div>\n );\n}\n"
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bundle-selector",
|
|
3
|
+
"title": "BundleSelector",
|
|
4
|
+
"description": "Bundle component picker with variant choices and price summary.",
|
|
5
|
+
"type": "component",
|
|
6
|
+
"registryDependencies": [
|
|
7
|
+
"price"
|
|
8
|
+
],
|
|
9
|
+
"files": [
|
|
10
|
+
{
|
|
11
|
+
"path": "bundle-selector.tsx",
|
|
12
|
+
"content": "\"use client\";\n\nimport React, { useState, useCallback, useMemo, useEffect, useRef } from \"react\";\nimport type { BundleComponentView, BundleComponentVariantView, BundlePriceType } from \"@cimplify/sdk\";\nimport type { Money } from \"@cimplify/sdk\";\nimport type { BundleSelectionInput } from \"@cimplify/sdk\";\nimport { parsePrice } from \"@cimplify/sdk\";\nimport { Price } from \"@cimplify/sdk/react\";\n\nexport interface BundleSelectorProps {\n components: BundleComponentView[];\n bundlePrice?: Money;\n discountValue?: Money;\n pricingType?: BundlePriceType;\n onSelectionsChange: (selections: BundleSelectionInput[]) => void;\n onPriceChange?: (price: number) => void;\n onReady?: (ready: boolean) => void;\n className?: string;\n}\n\nexport function BundleSelector({\n components,\n bundlePrice,\n discountValue,\n pricingType,\n onSelectionsChange,\n onPriceChange,\n onReady,\n className,\n}: BundleSelectorProps): React.ReactElement | null {\n const [variantChoices, setVariantChoices] = useState<Record<string, string>>({});\n const lastComponentIds = useRef(\"\");\n\n useEffect(() => {\n const ids = components.map((c) => c.id).sort().join();\n if (ids === lastComponentIds.current) return;\n lastComponentIds.current = ids;\n\n const defaults: Record<string, string> = {};\n for (const comp of components) {\n if (comp.variant_id) {\n defaults[comp.id] = comp.variant_id;\n } else if (comp.available_variants.length > 0) {\n const defaultVariant =\n comp.available_variants.find((v) => v.is_default) || comp.available_variants[0];\n if (defaultVariant) {\n defaults[comp.id] = defaultVariant.id;\n }\n }\n }\n setVariantChoices(defaults);\n }, [components]);\n\n const selections = useMemo((): BundleSelectionInput[] => {\n return components.map((comp) => ({\n component_id: comp.id,\n variant_id: variantChoices[comp.id],\n quantity: comp.quantity,\n }));\n }, [components, variantChoices]);\n\n useEffect(() => {\n onSelectionsChange(selections);\n }, [selections, onSelectionsChange]);\n\n useEffect(() => {\n onReady?.(components.length > 0 && selections.length > 0);\n }, [components, selections, onReady]);\n\n const totalPrice = useMemo(() => {\n if (pricingType === \"fixed\" && bundlePrice) {\n return parsePrice(bundlePrice);\n }\n const componentsTotal = components.reduce((sum, comp) => {\n return sum + getComponentPrice(comp, variantChoices[comp.id]) * comp.quantity;\n }, 0);\n if (pricingType === \"percentage_discount\" && discountValue) {\n return componentsTotal * (1 - parsePrice(discountValue) / 100);\n }\n if (pricingType === \"fixed_discount\" && discountValue) {\n return componentsTotal - parsePrice(discountValue);\n }\n return componentsTotal;\n }, [components, variantChoices, pricingType, bundlePrice, discountValue]);\n\n useEffect(() => {\n onPriceChange?.(totalPrice);\n }, [totalPrice, onPriceChange]);\n\n const handleVariantChange = useCallback(\n (componentId: string, variantId: string) => {\n setVariantChoices((prev) => ({ ...prev, [componentId]: variantId }));\n },\n [],\n );\n\n if (components.length === 0) {\n return null;\n }\n\n return (\n <div data-cimplify-bundle-selector className={className}>\n <span data-cimplify-bundle-heading>Included in this bundle</span>\n\n <div data-cimplify-bundle-components>\n {components.map((comp) => (\n <BundleComponentCard\n key={comp.id}\n component={comp}\n selectedVariantId={variantChoices[comp.id]}\n onVariantChange={(variantId) =>\n handleVariantChange(comp.id, variantId)\n }\n />\n ))}\n </div>\n\n {bundlePrice && (\n <div data-cimplify-bundle-summary>\n <span>Bundle price</span>\n <Price amount={bundlePrice} />\n </div>\n )}\n {discountValue && (\n <div data-cimplify-bundle-savings>\n <span>You save</span>\n <Price amount={discountValue} />\n </div>\n )}\n </div>\n );\n}\n\nfunction getComponentPrice(\n component: BundleComponentView,\n selectedVariantId: string | undefined,\n): number {\n if (!selectedVariantId || component.available_variants.length === 0) {\n return parsePrice(component.effective_price);\n }\n if (selectedVariantId === component.variant_id) {\n return parsePrice(component.effective_price);\n }\n const bakedAdj = component.variant_id\n ? component.available_variants.find((v) => v.id === component.variant_id)\n : undefined;\n const selectedAdj = component.available_variants.find((v) => v.id === selectedVariantId);\n if (!selectedAdj) return parsePrice(component.effective_price);\n return parsePrice(component.effective_price)\n - parsePrice(bakedAdj?.price_adjustment ?? \"0\")\n + parsePrice(selectedAdj.price_adjustment);\n}\n\ninterface BundleComponentCardProps {\n component: BundleComponentView;\n selectedVariantId?: string;\n onVariantChange: (variantId: string) => void;\n}\n\nfunction BundleComponentCard({\n component,\n selectedVariantId,\n onVariantChange,\n}: BundleComponentCardProps): React.ReactElement {\n const showVariantPicker =\n component.allow_variant_choice && component.available_variants.length > 1;\n\n const displayPrice = useMemo(\n () => getComponentPrice(component, selectedVariantId),\n [component, selectedVariantId],\n );\n\n return (\n <div data-cimplify-bundle-component>\n <div data-cimplify-bundle-component-header>\n <div>\n {component.quantity > 1 && (\n <span data-cimplify-bundle-component-qty>×{component.quantity}</span>\n )}\n <span data-cimplify-bundle-component-name>{component.product_name}</span>\n </div>\n <Price amount={displayPrice} />\n </div>\n\n {showVariantPicker && (\n <div data-cimplify-bundle-variant-picker>\n {component.available_variants.map((variant: BundleComponentVariantView) => {\n const isSelected = selectedVariantId === variant.id;\n const adjustment = parsePrice(variant.price_adjustment);\n\n return (\n <button\n key={variant.id}\n type=\"button\"\n aria-pressed={isSelected}\n onClick={() => onVariantChange(variant.id)}\n data-cimplify-bundle-variant-option\n data-selected={isSelected || undefined}\n >\n {variant.display_name}\n {adjustment !== 0 && (\n <span data-cimplify-bundle-variant-adjustment>\n {adjustment > 0 ? \"+\" : \"\"}\n <Price amount={variant.price_adjustment} />\n </span>\n )}\n </button>\n );\n })}\n </div>\n )}\n </div>\n );\n}\n"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cart-page",
|
|
3
|
+
"title": "CartPage",
|
|
4
|
+
"description": "Full-page cart with summary, discount input, and checkout.",
|
|
5
|
+
"type": "component",
|
|
6
|
+
"registryDependencies": [
|
|
7
|
+
"cart-summary",
|
|
8
|
+
"discount-input",
|
|
9
|
+
"cn"
|
|
10
|
+
],
|
|
11
|
+
"files": [
|
|
12
|
+
{
|
|
13
|
+
"path": "cart-page.tsx",
|
|
14
|
+
"content": "\"use client\";\n\nimport React from \"react\";\nimport { useCart } from \"@cimplify/sdk/react\";\nimport { CartSummary } from \"./cart-summary\";\nimport { DiscountInput } from \"@cimplify/sdk/react\";\nimport type { DiscountValidation } from \"@cimplify/sdk\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface CartPageClassNames {\n root?: string;\n header?: string;\n title?: string;\n content?: string;\n discount?: string;\n footer?: string;\n checkoutButton?: string;\n continueButton?: string;\n empty?: string;\n}\n\nexport interface CartPageProps {\n /** Page title. */\n title?: string;\n /** Called when checkout is initiated. */\n onCheckout?: () => void;\n /** Called when \"continue shopping\" is clicked. */\n onContinueShopping?: () => void;\n /** Called when a valid discount is applied. */\n onDiscountApply?: (validation: DiscountValidation) => void;\n /** Called when discount is removed. */\n onDiscountClear?: () => void;\n /** Show discount code input. Default: true. */\n showDiscount?: boolean;\n /** Show \"continue shopping\" link. Default: true. */\n showContinueShopping?: boolean;\n /** Checkout button text. */\n checkoutLabel?: string;\n className?: string;\n classNames?: CartPageClassNames;\n}\n\n/**\n * CartPage — full-page cart view with summary, discount input, and checkout.\n *\n * Composes CartSummary and DiscountInput into a complete cart experience.\n */\nexport function CartPage({\n title = \"Your Cart\",\n onCheckout,\n onContinueShopping,\n onDiscountApply,\n onDiscountClear,\n showDiscount = true,\n showContinueShopping = true,\n checkoutLabel = \"Proceed to Checkout\",\n className,\n classNames,\n}: CartPageProps): React.ReactElement {\n const { isEmpty, subtotal } = useCart();\n\n return (\n <div data-cimplify-cart-page className={cn(className, classNames?.root)}>\n {/* Header */}\n <div data-cimplify-cart-page-header className={classNames?.header}>\n <h1 data-cimplify-cart-page-title className={classNames?.title}>\n {title}\n </h1>\n {showContinueShopping && (\n <button\n type=\"button\"\n onClick={onContinueShopping}\n data-cimplify-cart-continue\n className={classNames?.continueButton}\n >\n Continue Shopping\n </button>\n )}\n </div>\n\n {/* Cart content */}\n <div data-cimplify-cart-page-content className={classNames?.content}>\n <CartSummary />\n\n {/* Discount input */}\n {showDiscount && !isEmpty && (\n <div data-cimplify-cart-page-discount className={classNames?.discount}>\n <DiscountInput\n orderSubtotal={subtotal.toString()}\n onApply={onDiscountApply}\n onClear={onDiscountClear}\n />\n </div>\n )}\n </div>\n\n {/* Footer with checkout */}\n {!isEmpty && (\n <div data-cimplify-cart-page-footer className={classNames?.footer}>\n <button\n type=\"button\"\n onClick={onCheckout}\n data-cimplify-cart-checkout-button\n className={classNames?.checkoutButton}\n >\n {checkoutLabel}\n </button>\n </div>\n )}\n </div>\n );\n}\n"
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cart-summary",
|
|
3
|
+
"title": "CartSummary",
|
|
4
|
+
"description": "Cart line items with quantity controls and totals.",
|
|
5
|
+
"type": "component",
|
|
6
|
+
"registryDependencies": [
|
|
7
|
+
"price",
|
|
8
|
+
"quantity-selector"
|
|
9
|
+
],
|
|
10
|
+
"files": [
|
|
11
|
+
{
|
|
12
|
+
"path": "cart-summary.tsx",
|
|
13
|
+
"content": "\"use client\";\n\nimport React from \"react\";\nimport type { UseCartItem } from \"@cimplify/sdk/react\";\nimport { useCart } from \"@cimplify/sdk/react\";\nimport { getVariantDisplayName } from \"@cimplify/sdk\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { QuantitySelector } from \"@cimplify/sdk/react\";\nimport { useProductPrice } from \"@cimplify/sdk/react\";\n\nexport interface CartSummaryProps {\n onCheckout?: () => void;\n onItemRemove?: (itemId: string) => void;\n onQuantityChange?: (itemId: string, quantity: number) => void;\n emptyMessage?: string;\n className?: string;\n}\n\nfunction CartLineItemRow({\n item,\n onRemove,\n onQuantityChange,\n}: {\n item: UseCartItem;\n onRemove: (itemId: string) => void;\n onQuantityChange: (itemId: string, qty: number) => void;\n}): React.ReactElement {\n const { unitPrice } = useProductPrice({\n product: item.product,\n variant: item.variant,\n addOnOptions: item.addOnOptions,\n });\n const hasComposite = item.compositeSelections && item.compositeSelections.length > 0;\n const hasBundle = item.bundleSelections && item.bundleSelections.length > 0;\n\n return (\n <div data-cimplify-cart-item>\n <div data-cimplify-cart-item-info>\n <span data-cimplify-cart-item-name>{item.product.name}</span>\n\n {item.variant && (\n <span data-cimplify-cart-item-variant>\n {getVariantDisplayName(item.variant, item.product.name)}\n </span>\n )}\n\n {item.addOnOptions && item.addOnOptions.length > 0 && (\n <span data-cimplify-cart-item-addons>\n + {item.addOnOptions.map((opt) => opt.name).join(\", \")}\n </span>\n )}\n\n {(hasComposite || hasBundle) && (\n <span data-cimplify-cart-item-badge>\n {hasComposite ? \"Custom\" : \"Bundle\"}\n </span>\n )}\n\n <Price amount={unitPrice} />\n </div>\n\n <div data-cimplify-cart-item-controls>\n <QuantitySelector\n value={item.quantity}\n onChange={(qty) => onQuantityChange(item.id, qty)}\n min={0}\n />\n <button\n type=\"button\"\n onClick={() => onRemove(item.id)}\n data-cimplify-cart-item-remove\n aria-label={`Remove ${item.product.name}`}\n >\n Remove\n </button>\n </div>\n </div>\n );\n}\n\n/**\n * CartSummary — renders cart line items + totals.\n *\n * NOT a drawer or modal — just the cart content. Templates wrap this in\n * their own drawer/modal shell with animations.\n */\nexport function CartSummary({\n onCheckout,\n onItemRemove,\n onQuantityChange,\n emptyMessage = \"Your cart is empty\",\n className,\n}: CartSummaryProps): React.ReactElement {\n const { items, itemCount, subtotal, tax, total, isEmpty, removeItem, updateQuantity } =\n useCart();\n\n const handleRemove = (itemId: string) => {\n if (onItemRemove) {\n onItemRemove(itemId);\n } else {\n void removeItem(itemId);\n }\n };\n\n const handleQuantityChange = (itemId: string, qty: number) => {\n if (onQuantityChange) {\n onQuantityChange(itemId, qty);\n } else {\n void updateQuantity(itemId, qty);\n }\n };\n\n return (\n <div data-cimplify-cart-summary className={className}>\n {isEmpty ? (\n <div data-cimplify-cart-empty>\n <p>{emptyMessage}</p>\n </div>\n ) : (\n <>\n {/* Line items */}\n <div data-cimplify-cart-items>\n {items.map((item) => (\n <CartLineItemRow\n key={item.id}\n item={item}\n onRemove={handleRemove}\n onQuantityChange={handleQuantityChange}\n />\n ))}\n </div>\n\n {/* Totals */}\n <div data-cimplify-cart-totals>\n <div data-cimplify-cart-subtotal>\n <span>Subtotal ({itemCount} {itemCount === 1 ? \"item\" : \"items\"})</span>\n <Price amount={subtotal} />\n </div>\n <div data-cimplify-cart-tax>\n <span>Tax</span>\n <Price amount={tax} />\n </div>\n <div data-cimplify-cart-total>\n <span>Total</span>\n <Price amount={total} />\n </div>\n </div>\n\n {onCheckout && (\n <button\n type=\"button\"\n onClick={onCheckout}\n data-cimplify-cart-checkout\n >\n Proceed to Checkout\n </button>\n )}\n </>\n )}\n </div>\n );\n}\n"
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|