@mohasinac/appkit 4.69.1 → 4.71.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/PROVIDERS.md +190 -0
- package/dist/.build-stamp.json +3 -3
- package/dist/_internal/client/features/lottery/LotteryAdminSlotView.js +2 -1
- package/dist/_internal/server/features/checkout/actions.js +43 -8
- package/dist/_internal/server/features/orders/actions.js +2 -1
- package/dist/_internal/server/features/orders/adapters.js +18 -0
- package/dist/_internal/server/features/orders/balance.js +2 -1
- package/dist/_internal/server/features/orders/cancellation-refund.d.ts +0 -89
- package/dist/_internal/server/features/orders/cancellation-refund.js +3 -2
- package/dist/_internal/server/features/payouts/actions.js +2 -1
- package/dist/_internal/server/features/points/award.js +3 -2
- package/dist/_internal/server/features/protection-claims/actions.js +2 -1
- package/dist/_internal/server/features/refunds/actions.js +3 -2
- package/dist/_internal/server/features/shipping/courier-block.js +5 -2
- package/dist/_internal/server/features/shipping/reconcile.js +3 -2
- package/dist/_internal/server/features/shipping/reimbursement.js +3 -2
- package/dist/_internal/server/features/stores/adapters.d.ts +1 -1
- package/dist/_internal/server/features/stores/adapters.js +12 -2
- package/dist/_internal/server/jobs/core/dailyStatusDigest.js +7 -3
- package/dist/_internal/server/jobs/core/emiInstallmentReminder.js +2 -1
- package/dist/_internal/server/jobs/core/obligationSweep.js +5 -4
- package/dist/_internal/server/jobs/core/onScamReportCreate.js +2 -1
- package/dist/_internal/server/jobs/core/paymentReconcile.js +5 -4
- package/dist/_internal/shared/checkout/rules/auction.rule.js +25 -7
- package/dist/_internal/shared/checkout/rules/prize-draw.rule.js +26 -8
- package/dist/_internal/shared/config/schema.d.ts +15 -0
- package/dist/_internal/shared/content/published-values.d.ts +2 -2
- package/dist/_internal/shared/content/published-values.js +31 -12
- package/dist/_internal/shared/features/auctions/errors.js +2 -1
- package/dist/_internal/shared/features/orders/refund-copy.js +3 -2
- package/dist/_internal/shared/features/promotions/errors.js +2 -1
- package/dist/constants/field-names.d.ts +23 -0
- package/dist/constants/field-names.js +23 -0
- package/dist/constants/storage-keys.d.ts +90 -0
- package/dist/constants/storage-keys.js +119 -0
- package/dist/contracts/index.d.ts +1 -1
- package/dist/contracts/payment.d.ts +42 -0
- package/dist/contracts/registry.d.ts +8 -0
- package/dist/contracts/tax.d.ts +118 -0
- package/dist/contracts/tax.js +3 -0
- package/dist/core/brand-resolver.d.ts +92 -0
- package/dist/core/brand-resolver.js +120 -0
- package/dist/core/brand-text.d.ts +15 -0
- package/dist/core/brand-text.js +73 -0
- package/dist/core/hooks/useNewsletterSubscribe.d.ts +30 -0
- package/dist/core/hooks/useNewsletterSubscribe.js +121 -0
- package/dist/features/about/components/DeveloperView.js +2 -1
- package/dist/features/about/index.d.ts +0 -2
- package/dist/features/about/index.js +0 -1
- package/dist/features/account/components/BuyerAccountGuideView.js +2 -1
- package/dist/features/account/components/BuyerAuctionsGuideView.js +4 -3
- package/dist/features/account/components/BuyerOrdersGuideView.js +6 -5
- package/dist/features/account/components/BuyerShoppingGuideView.js +3 -2
- package/dist/features/account/components/UserOffersPanel.js +2 -1
- package/dist/features/account/hooks/useCollapsedSections.js +5 -3
- package/dist/features/account/schemas/index.js +1 -1
- package/dist/features/admin/actions/notification-actions.js +9 -2
- package/dist/features/admin/components/AdminAnalyticsGuideView.js +2 -1
- package/dist/features/admin/components/AdminBundlesView.js +2 -1
- package/dist/features/admin/components/AdminCatalogGuideView.js +2 -1
- package/dist/features/admin/components/AdminContentGuideView.js +2 -1
- package/dist/features/admin/components/AdminFaqEditorView.js +2 -1
- package/dist/features/admin/components/AdminGatewayEventsView.js +2 -1
- package/dist/features/admin/components/AdminGuideHubView.js +2 -1
- package/dist/features/admin/components/AdminMoneyCasesView.js +2 -1
- package/dist/features/admin/components/AdminOrdersGuideView.js +2 -1
- package/dist/features/admin/components/AdminProductsView.js +3 -2
- package/dist/features/admin/components/AdminSectionsView.js +3 -2
- package/dist/features/admin/components/AdminSiteSettingsView.js +8 -7
- package/dist/features/admin/components/AdminStoresGuideView.js +2 -1
- package/dist/features/admin/components/AdminSupportTicketDetailView.js +2 -1
- package/dist/features/admin/components/AdminTeamGuideView.js +2 -1
- package/dist/features/admin/components/AdminTrustGuideView.js +3 -2
- package/dist/features/admin/components/AdminUsersGuideView.js +2 -1
- package/dist/features/admin/components/sections/adminSectionsTypes.js +1 -1
- package/dist/features/admin/schemas/firestore.js +0 -1
- package/dist/features/auth/actions/user-slug.js +21 -2
- package/dist/features/auth/hooks/useAuth.js +11 -4
- package/dist/features/cart/utils/pending-ops.js +16 -3
- package/dist/features/catalogue/utils/product-from-item.d.ts +18 -2
- package/dist/features/catalogue/utils/product-from-item.js +23 -4
- package/dist/features/categories/components/BundleItemsPicker.js +2 -1
- package/dist/features/categories/components/CategoryDetailPageView.js +2 -1
- package/dist/features/categories/components/ContextualTrail.d.ts +9 -2
- package/dist/features/categories/components/ContextualTrail.js +11 -3
- package/dist/features/categories/components/LastCategoryBeacon.js +2 -2
- package/dist/features/checkout/actions/checkout-value-otp-actions.js +9 -1
- package/dist/features/contact/email.js +11 -12
- package/dist/features/email/notification-templates.js +4 -5
- package/dist/features/email/render-static-markup.d.ts +34 -0
- package/dist/features/email/render-static-markup.js +4 -0
- package/dist/features/events/utils/event-charts.js +2 -1
- package/dist/features/homepage/components/AnnouncementBar.js +5 -3
- package/dist/features/homepage/components/FAQSection.js +1 -1
- package/dist/features/homepage/components/MarketplaceHomepageView.js +34 -0
- package/dist/features/homepage/components/WelcomeSection.d.ts +3 -1
- package/dist/features/homepage/components/WelcomeSection.js +2 -2
- package/dist/features/homepage/lib/section-renderer.d.ts +8 -0
- package/dist/features/homepage/lib/section-renderer.js +1 -1
- package/dist/features/layout/AppLayoutShell.d.ts +3 -1
- package/dist/features/layout/AppLayoutShell.js +2 -2
- package/dist/features/layout/TitleBarLayout.d.ts +8 -1
- package/dist/features/layout/TitleBarLayout.js +2 -2
- package/dist/features/media/MediaVideo.js +2 -1
- package/dist/features/orders/actions/refund-actions.js +3 -2
- package/dist/features/orders/components/RefundRequestView.js +15 -2
- package/dist/features/orders/repository/orders.repository.js +2 -1
- package/dist/features/orders/types/index.d.ts +14 -0
- package/dist/features/orders/utils/payment-proof-message.js +5 -2
- package/dist/features/price-index/components/PriceIndexToolView.js +6 -5
- package/dist/features/pricing/components/AdminPricingView.js +2 -1
- package/dist/features/print/components/labels.js +2 -1
- package/dist/features/products/components/CityCollectiblesView.js +2 -1
- package/dist/features/products/components/MakeOfferButton.js +2 -1
- package/dist/features/products/components/PrizeDrawCollage.js +3 -2
- package/dist/features/products/components/PrizeRevealModal.js +2 -1
- package/dist/features/products/repository/products.repository.js +2 -1
- package/dist/features/promotions/components/CouponCard.js +3 -2
- package/dist/features/promotions/components/CouponHelpDetails.js +2 -1
- package/dist/features/promotions/constants/coupon-help.d.ts +1 -1
- package/dist/features/promotions/constants/coupon-help.js +8 -1
- package/dist/features/scams/components/ScamAwarenessModal.js +2 -1
- package/dist/features/scams/components/ScamProfileView.js +5 -3
- package/dist/features/scams/components/ScamRegistryView.js +2 -1
- package/dist/features/scams/constants/scam-types.d.ts +13 -1
- package/dist/features/scams/constants/scam-types.js +67 -23
- package/dist/features/scams/schemas/firestore.js +1 -1
- package/dist/features/search/components/Search.js +5 -1
- package/dist/features/seller/actions/offer-actions.js +47 -9
- package/dist/features/seller/components/DispatchForm.js +3 -2
- package/dist/features/seller/components/PrintCenterView.js +4 -2
- package/dist/features/seller/components/SellerBundlesView.js +2 -1
- package/dist/features/seller/components/SellerPayoutSettingsView.js +3 -2
- package/dist/features/seller/components/SellerProductShell.js +3 -2
- package/dist/features/seller/components/SellerProductsView.js +4 -3
- package/dist/features/store-extensions/repository/store-extensions.repositories.d.ts +30 -4
- package/dist/features/store-extensions/repository/store-extensions.repositories.js +46 -7
- package/dist/features/store-extensions/schemas/firestore.d.ts +22 -3
- package/dist/features/store-extensions/schemas/firestore.js +20 -0
- package/dist/features/stores/components/StoreCapabilitiesGuideView.js +7 -6
- package/dist/features/stores/components/StoreFinanceGuideView.js +6 -5
- package/dist/features/stores/components/StoreGuideHubView.js +2 -1
- package/dist/features/stores/components/StoreListingsGuideView.js +2 -1
- package/dist/features/stores/components/StoreOrdersGuideView.js +5 -4
- package/dist/features/stores/components/StoreSettingsGuideView.js +2 -1
- package/dist/features/support/schemas/index.d.ts +2 -2
- package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.d.ts +1 -1
- package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.js +3 -2
- package/dist/features/whatsapp-bot/helpers/whatsapp.js +5 -2
- package/dist/index.d.ts +0 -3
- package/dist/index.js +0 -6
- package/dist/providers/payment-manual/index.d.ts +11 -0
- package/dist/providers/payment-manual/index.js +13 -0
- package/dist/providers/payment-phonepe/index.d.ts +23 -7
- package/dist/providers/payment-phonepe/index.js +15 -0
- package/dist/providers/payment-razorpay/index.d.ts +13 -0
- package/dist/providers/payment-razorpay/index.js +21 -1
- package/dist/providers/tax-india-gst/index.d.ts +45 -0
- package/dist/providers/tax-india-gst/index.js +78 -0
- package/dist/react/hooks/useScrollToTop.d.ts +5 -0
- package/dist/react/hooks/useScrollToTop.js +70 -0
- package/dist/schemas/registry.d.ts +2 -2
- package/dist/seo/json-ld.js +25 -4
- package/dist/styles.css +20 -0
- package/dist/ui/components/SiteLogo.d.ts +20 -2
- package/dist/ui/components/SiteLogo.js +28 -6
- package/dist/ui/components/SiteMark.d.ts +1 -1
- package/dist/ui/components/SiteMark.js +21 -6
- package/dist/ui/components/Tabs.d.ts +9 -1
- package/dist/ui/components/Tabs.js +20 -3
- package/dist/ui/components/Tabs.style.css +20 -0
- package/dist/ui/rich-text/RichText.d.ts +0 -12
- package/dist/ui/rich-text/RichText.js +52 -1
- package/dist/utils/date.formatter.d.ts +20 -0
- package/dist/utils/date.formatter.js +25 -0
- package/dist/utils/number.formatter.d.ts +47 -0
- package/dist/utils/number.formatter.js +79 -1
- package/dist/validation/phone.validator.d.ts +0 -1
- package/dist/validation/phone.validator.js +17 -3
- package/package.json +17 -3
- package/dist/features/about/components/AboutView.d.ts +0 -30
- package/dist/features/about/components/AboutView.js +0 -14
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-storage and postMessage keys — one namespace, configurable.
|
|
3
|
+
*
|
|
4
|
+
* ## Why this exists
|
|
5
|
+
*
|
|
6
|
+
* Seven call sites hardcoded a `letitrip`-prefixed key:
|
|
7
|
+
*
|
|
8
|
+
* localStorage "letitrip:section-state" · "letitrip:last-category"
|
|
9
|
+
* "letitrip:announcement-dismissed" · "letitrip_oauth_redirect"
|
|
10
|
+
* "letitrip_search_type"
|
|
11
|
+
* postMessage "letitrip_auth_close"
|
|
12
|
+
*
|
|
13
|
+
* For the app that shipped them that is harmless. For a LIBRARY it is not:
|
|
14
|
+
* every consumer inherits one marketplace's namespace, two appkit apps on the
|
|
15
|
+
* same origin collide, and the brand is baked into data the user's browser
|
|
16
|
+
* keeps. Tier AK's whole premise is that appkit should not know whose app it
|
|
17
|
+
* is in.
|
|
18
|
+
*
|
|
19
|
+
* ## 🛑 The prefix is read at CALL TIME, not at module load
|
|
20
|
+
*
|
|
21
|
+
* `storageKey()` is a function and the keys below are getters. A consumer sets
|
|
22
|
+
* the namespace from its own bootstrap, which runs after this module is
|
|
23
|
+
* evaluated — so a `const KEY = PREFIX + ":x"` captured at import time would
|
|
24
|
+
* freeze the default and silently ignore the configuration. That is the same
|
|
25
|
+
* trap as a mirror field (Root Cause #42): two things that must agree, where
|
|
26
|
+
* only one is ever written.
|
|
27
|
+
*
|
|
28
|
+
* ## 🛑 Changing the namespace ORPHANS existing values
|
|
29
|
+
*
|
|
30
|
+
* These keys are how a returning visitor's dismissed banner and collapsed
|
|
31
|
+
* sections are remembered. Renaming the prefix does not migrate them — the old
|
|
32
|
+
* entries simply stop being read. That is acceptable for UI state and would NOT
|
|
33
|
+
* be for anything load-bearing, which is why nothing load-bearing belongs here:
|
|
34
|
+
* the auth session is an HTTP-only cookie, not a storage key.
|
|
35
|
+
*
|
|
36
|
+
* ## The separator is deliberately NOT normalised
|
|
37
|
+
*
|
|
38
|
+
* Five keys use `:` and three use `_`. They are kept exactly as they were,
|
|
39
|
+
* because normalising them is a silent data migration for every existing
|
|
40
|
+
* visitor in exchange for tidiness. New keys should use `:`.
|
|
41
|
+
*/
|
|
42
|
+
/**
|
|
43
|
+
* Default namespace.
|
|
44
|
+
*
|
|
45
|
+
* Deliberately NOT "letitrip" — a library's default must not name one
|
|
46
|
+
* consumer. `"appkit"` is what an unconfigured install gets, which is both
|
|
47
|
+
* neutral and obviously-a-default when seen in devtools.
|
|
48
|
+
*/
|
|
49
|
+
const DEFAULT_STORAGE_NAMESPACE = "appkit";
|
|
50
|
+
let namespace = DEFAULT_STORAGE_NAMESPACE;
|
|
51
|
+
/**
|
|
52
|
+
* Set the prefix every appkit storage key carries. Call once, from the
|
|
53
|
+
* consumer's client bootstrap, before any UI mounts.
|
|
54
|
+
*
|
|
55
|
+
* Idempotent and last-call-wins; there is no unset, because a namespace that
|
|
56
|
+
* changes mid-session would strand whatever was written under the previous one.
|
|
57
|
+
*/
|
|
58
|
+
export function setStorageNamespace(value) {
|
|
59
|
+
const trimmed = value.trim();
|
|
60
|
+
if (trimmed)
|
|
61
|
+
namespace = trimmed;
|
|
62
|
+
}
|
|
63
|
+
/** The active namespace. Exported for diagnostics, not for building keys. */
|
|
64
|
+
export function getStorageNamespace() {
|
|
65
|
+
return namespace;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Build a namespaced key.
|
|
69
|
+
*
|
|
70
|
+
* @param suffix the part after the separator, e.g. `"last-category"`
|
|
71
|
+
* @param separator `":"` for new keys; `"_"` only where an existing key used it
|
|
72
|
+
*/
|
|
73
|
+
export function storageKey(suffix, separator = ":") {
|
|
74
|
+
return `${namespace}${separator}${suffix}`;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* The keys appkit itself owns.
|
|
78
|
+
*
|
|
79
|
+
* Getters, not constants — see the call-time note above. Each records the
|
|
80
|
+
* literal it replaced so the migration is auditable and a reader can tell at a
|
|
81
|
+
* glance which separator is historical rather than chosen.
|
|
82
|
+
*/
|
|
83
|
+
export const STORAGE_KEYS = {
|
|
84
|
+
/** Collapsed account-page sections. Was `"letitrip:section-state"`. */
|
|
85
|
+
get sectionState() {
|
|
86
|
+
return storageKey("section-state");
|
|
87
|
+
},
|
|
88
|
+
/** Last category visited, for the contextual trail. Was `"letitrip:last-category"`. */
|
|
89
|
+
get lastCategory() {
|
|
90
|
+
return storageKey("last-category");
|
|
91
|
+
},
|
|
92
|
+
/** Announcement bar dismissal. Was `"letitrip:announcement-dismissed"`. */
|
|
93
|
+
get announcementDismissed() {
|
|
94
|
+
return storageKey("announcement-dismissed");
|
|
95
|
+
},
|
|
96
|
+
/** OAuth return URL across the popup round-trip. Was `"letitrip_oauth_redirect"`. */
|
|
97
|
+
get oauthRedirect() {
|
|
98
|
+
return storageKey("oauth_redirect", "_");
|
|
99
|
+
},
|
|
100
|
+
/** Remembered search-type tab. Was `"letitrip_search_type"`. */
|
|
101
|
+
get searchType() {
|
|
102
|
+
return storageKey("search_type", "_");
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* postMessage `type` values.
|
|
107
|
+
*
|
|
108
|
+
* 🛑 Namespaced for the same reason, and with a sharper edge: a `message`
|
|
109
|
+
* listener receives events from ANY window that can reach it, so an
|
|
110
|
+
* unnamespaced type is both a collision and an input-validation question. The
|
|
111
|
+
* listeners that read this must keep checking `event.origin` regardless — a
|
|
112
|
+
* namespace is not an authentication mechanism.
|
|
113
|
+
*/
|
|
114
|
+
export const MESSAGE_TYPES = {
|
|
115
|
+
/** Auth popup signalling it is done. Was `"letitrip_auth_close"`. */
|
|
116
|
+
get authClose() {
|
|
117
|
+
return storageKey("auth_close", "_");
|
|
118
|
+
},
|
|
119
|
+
};
|
|
@@ -2,7 +2,7 @@ export type { WhereOp, SieveQuery, PagedResult, IReadRepository, IWriteRepositor
|
|
|
2
2
|
export type { AuthPayload, AuthUser, CreateUserInput, IAuthProvider, ISessionProvider, } from "./auth";
|
|
3
3
|
export type { EmailAttachment, EmailOptions, EmailResult, IEmailProvider, } from "./email";
|
|
4
4
|
export type { UploadOptions, StorageFile, IStorageProvider } from "./storage";
|
|
5
|
-
export type { PaymentOrder, PaymentCapture, Refund, Dispute, GatewayPaymentRecord, } from "./payment";
|
|
5
|
+
export type { CheckoutIntentMetadata, PaymentOrder, PaymentCapture, Refund, Dispute, GatewayPaymentRecord, } from "./payment";
|
|
6
6
|
export { IPaymentProvider } from "./payment";
|
|
7
7
|
export type { ShippingAddress, CreateShipmentInput, Shipment, TrackingEvent, TrackingInfo, ServiceabilityResult, WalletBalance, } from "./shipping";
|
|
8
8
|
export { IShippingProvider } from "./shipping";
|
|
@@ -17,6 +17,23 @@ export interface PaymentOrder {
|
|
|
17
17
|
/** When this order/attempt expires, if the gateway reports one (PhonePe's `expireAt`). ISO-8601. */
|
|
18
18
|
expiresAt?: string;
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* What the buyer was checking out, round-tripped through the gateway's own
|
|
22
|
+
* metadata so the confirmation path can rebuild the order.
|
|
23
|
+
*
|
|
24
|
+
* 🛑 This lived in `providers/payment-phonepe/index.ts` as
|
|
25
|
+
* `PhonePeCheckoutIntent`, and the name was the only gateway-shaped thing about
|
|
26
|
+
* it — every field is OURS. That naming forced generic checkout code to import
|
|
27
|
+
* a concrete provider for a type, which is the coupling Tier AK A4 exists to
|
|
28
|
+
* remove. PhonePe happens to carry it in `udf1..udf4`; another gateway would
|
|
29
|
+
* carry it some other way, and neither fact belongs in the type's name.
|
|
30
|
+
*/
|
|
31
|
+
export interface CheckoutIntentMetadata {
|
|
32
|
+
uid: string;
|
|
33
|
+
addressId?: string;
|
|
34
|
+
outOfStockPolicy?: string;
|
|
35
|
+
notes?: string;
|
|
36
|
+
}
|
|
20
37
|
export interface PaymentCapture {
|
|
21
38
|
id: string;
|
|
22
39
|
orderId: string;
|
|
@@ -146,6 +163,31 @@ export declare abstract class IPaymentProvider {
|
|
|
146
163
|
abstract createOrder(amount: number, currency: string, metadata?: Record<string, JsonValue>): Promise<PaymentOrder>;
|
|
147
164
|
/** Returns true if the webhook signature is valid. */
|
|
148
165
|
abstract verifyWebhook(payload: string, signature: string): boolean;
|
|
166
|
+
/**
|
|
167
|
+
* Verify the BUYER-PRESENT checkout callback — a different exchange from a
|
|
168
|
+
* webhook, which is why `verifyWebhook` cannot serve it.
|
|
169
|
+
*
|
|
170
|
+
* A webhook is server-to-server and signed over a raw body. This is the
|
|
171
|
+
* redirect/callback the buyer's own browser hands back after paying
|
|
172
|
+
* (Razorpay's `razorpay_order_id` + `razorpay_payment_id` +
|
|
173
|
+
* `razorpay_signature`). Until this existed, generic checkout code imported
|
|
174
|
+
* Razorpay's verifier directly — a concrete gateway named in the one place
|
|
175
|
+
* that must work for every gateway.
|
|
176
|
+
*
|
|
177
|
+
* 🛑 MUST FAIL CLOSED. A gateway with no such handshake returns `false`, not
|
|
178
|
+
* `true`: this call is what decides whether a payment is genuine, so the
|
|
179
|
+
* default for "I cannot verify this" is rejection. Returning `true` would
|
|
180
|
+
* accept a forged callback.
|
|
181
|
+
*
|
|
182
|
+
* 🛑 The SECRET stays the caller's to resolve, deliberately. The call site
|
|
183
|
+
* already reads it from settings and fails closed when absent; moving that
|
|
184
|
+
* inside would change which code holds the key and what happens when it is
|
|
185
|
+
* missing, which is a bigger change than the dispatch this method is for.
|
|
186
|
+
*
|
|
187
|
+
* @param payload the gateway's callback fields, shape-checked by the provider
|
|
188
|
+
* @param secret the key secret, resolved by the caller
|
|
189
|
+
*/
|
|
190
|
+
abstract verifyCheckoutSignature(payload: Record<string, string>, secret: string): boolean;
|
|
149
191
|
abstract capturePayment(orderId: string): Promise<PaymentCapture>;
|
|
150
192
|
abstract refund(paymentId: string, amount?: number): Promise<Refund>;
|
|
151
193
|
abstract getOrder(orderId: string): Promise<PaymentOrder>;
|
|
@@ -3,6 +3,7 @@ import type { IEmailProvider } from "./email";
|
|
|
3
3
|
import type { IStorageProvider } from "./storage";
|
|
4
4
|
import type { IPaymentProvider } from "./payment";
|
|
5
5
|
import type { IShippingProvider } from "./shipping";
|
|
6
|
+
import type { ITaxProvider } from "./tax";
|
|
6
7
|
import type { ISearchProvider } from "./search";
|
|
7
8
|
import type { ICacheProvider, IQueueProvider, IEventBus } from "./infra";
|
|
8
9
|
import type { IStyleAdapter } from "./style";
|
|
@@ -22,6 +23,13 @@ export interface ProviderRegistry {
|
|
|
22
23
|
db?: IDbProvider;
|
|
23
24
|
payment?: IPaymentProvider;
|
|
24
25
|
shipping?: IShippingProvider;
|
|
26
|
+
/**
|
|
27
|
+
* Jurisdiction tax engine. Optional, and defaults to India GST at the call
|
|
28
|
+
* site rather than here — an install that never registers one keeps today's
|
|
29
|
+
* behaviour exactly, which is what makes adopting the seam safe one caller at
|
|
30
|
+
* a time instead of in a single 42-file sweep.
|
|
31
|
+
*/
|
|
32
|
+
tax?: ITaxProvider;
|
|
25
33
|
search?: ISearchProvider;
|
|
26
34
|
cache?: ICacheProvider;
|
|
27
35
|
queue?: IQueueProvider;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The tax seam. Tier AK A3.
|
|
3
|
+
*
|
|
4
|
+
* ## Why this exists
|
|
5
|
+
*
|
|
6
|
+
* `gstRate`, `hsnCode` and `placeOfSupply` are read across 42 files, and
|
|
7
|
+
* CGST/SGST/IGST is an Indian GST concept — so appkit shipped an *Indian*
|
|
8
|
+
* ecommerce tax model rather than a tax model. A buyer of this package selling
|
|
9
|
+
* in the EU needs VAT with a reverse charge; in the US, destination sales tax
|
|
10
|
+
* with nexus. Neither is expressible as "split the rate in half when the states
|
|
11
|
+
* match".
|
|
12
|
+
*
|
|
13
|
+
* ## 🛑 The FIELDS DO NOT MOVE, and this is the whole reason the phase is cheap
|
|
14
|
+
*
|
|
15
|
+
* `gstRate` / `hsnCode` / `placeOfSupply` stay exactly where they are on
|
|
16
|
+
* `ProductDocument` and `OrderDocument`. They become the India provider's
|
|
17
|
+
* PAYLOAD rather than appkit's universal schema. That buys the seam with:
|
|
18
|
+
*
|
|
19
|
+
* · no Firestore migration, so no stored document becomes wrong (Rule #3)
|
|
20
|
+
* · no change to any index or query
|
|
21
|
+
* · nothing to roll back if a buyer never needs a second jurisdiction
|
|
22
|
+
*
|
|
23
|
+
* A schema migration was the first plan and it was the wrong one: the goal is
|
|
24
|
+
* that appkit does not *compute* one country's tax, not that it forgets the
|
|
25
|
+
* country exists.
|
|
26
|
+
*
|
|
27
|
+
* ## 🛑 Implementations DELEGATE. They do not reimplement.
|
|
28
|
+
*
|
|
29
|
+
* `IndiaGstTaxProvider` calls the existing `calculateGst` /
|
|
30
|
+
* `calculateGstInclusive` / `allocateAcrossMembers`. It is a seam, not a
|
|
31
|
+
* rewrite — and that is deliberate, because the two existing functions split
|
|
32
|
+
* the half DIFFERENTLY and the difference is load-bearing:
|
|
33
|
+
*
|
|
34
|
+
* calculateGst cgst = sgst = round(gst / 2), gst RE-DERIVED as half*2
|
|
35
|
+
* calculateGstInclusive cgst = round(gst / 2), sgst = round(gst - cgst)
|
|
36
|
+
*
|
|
37
|
+
* On ₹66.67 at 18% inclusive that is cgst 5.09 / sgst 5.08, preserving the
|
|
38
|
+
* 10.17 total. A reimplementation that "tidied" the two into agreement would
|
|
39
|
+
* move a paisa on real invoices, and GST is filed on those figures.
|
|
40
|
+
*
|
|
41
|
+
* The gate for this phase is `.build-memory/tax-roundtrip.mjs`: 397 breakdowns
|
|
42
|
+
* across every slab, both supply directions and the rounding edges, required to
|
|
43
|
+
* be byte-identical before and after. One paisa of drift is a defect.
|
|
44
|
+
*/
|
|
45
|
+
/** A single tax component on a line, named by the jurisdiction's own vocabulary. */
|
|
46
|
+
export interface TaxComponent {
|
|
47
|
+
/** `"cgst"`, `"sgst"`, `"igst"`, `"vat"`, `"salesTax"` — the provider's term. */
|
|
48
|
+
code: string;
|
|
49
|
+
/** Human label for an invoice line. */
|
|
50
|
+
label: string;
|
|
51
|
+
/** Percentage applied, e.g. `9` for half of an 18% intra-state GST. */
|
|
52
|
+
rate: number;
|
|
53
|
+
/** Money, in MAJOR currency units — `1.50`, never a minor-unit `150`. */
|
|
54
|
+
amount: number;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The result of taxing one amount.
|
|
58
|
+
*
|
|
59
|
+
* `components` sums to `taxAmount` exactly — providers are responsible for
|
|
60
|
+
* assigning any rounding remainder to one component rather than letting the
|
|
61
|
+
* parts disagree with the total.
|
|
62
|
+
*/
|
|
63
|
+
export interface TaxBreakdown {
|
|
64
|
+
/** The amount tax was computed ON (ex-tax base). */
|
|
65
|
+
taxableAmount: number;
|
|
66
|
+
/** Total tax. Equals the sum of `components`. */
|
|
67
|
+
taxAmount: number;
|
|
68
|
+
components: TaxComponent[];
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Where a sale happens, in the only terms a tax engine actually needs.
|
|
72
|
+
*
|
|
73
|
+
* Deliberately NOT `placeOfSupply: string`. That is India's word for the
|
|
74
|
+
* destination state code, and naming the contract after it would re-import the
|
|
75
|
+
* jurisdiction this interface exists to abstract.
|
|
76
|
+
*/
|
|
77
|
+
export interface TaxJurisdiction {
|
|
78
|
+
/** Seller's region code — an Indian state code, a US state, an EU country. */
|
|
79
|
+
origin?: string;
|
|
80
|
+
/** Buyer's region code. */
|
|
81
|
+
destination?: string;
|
|
82
|
+
/** ISO 3166-1 alpha-2, when a provider needs to distinguish cross-border. */
|
|
83
|
+
country?: string;
|
|
84
|
+
}
|
|
85
|
+
/** What is being taxed. `taxCode` is the jurisdiction's classification (HSN, SIC, …). */
|
|
86
|
+
export interface TaxableItem {
|
|
87
|
+
/** Rate as a percentage, e.g. `18`. Resolved by the caller from its own data. */
|
|
88
|
+
rate: number;
|
|
89
|
+
/** Jurisdiction classification code — India's HSN, and whatever elsewhere. */
|
|
90
|
+
taxCode?: string;
|
|
91
|
+
}
|
|
92
|
+
export declare abstract class ITaxProvider {
|
|
93
|
+
/** Stable id, e.g. `"india-gst"`. */
|
|
94
|
+
abstract readonly id: string;
|
|
95
|
+
/**
|
|
96
|
+
* Tax ADDED to an ex-tax amount.
|
|
97
|
+
*
|
|
98
|
+
* @param taxableAmount the ex-tax base, in major units
|
|
99
|
+
*/
|
|
100
|
+
abstract onExclusive(item: TaxableItem, jurisdiction: TaxJurisdiction, taxableAmount: number): TaxBreakdown;
|
|
101
|
+
/**
|
|
102
|
+
* Tax EXTRACTED from a tax-inclusive amount.
|
|
103
|
+
*
|
|
104
|
+
* 🛑 Not derivable from `onExclusive` by the caller. Backing out of a gross
|
|
105
|
+
* figure and adding onto a net one round differently, and this codebase
|
|
106
|
+
* already relies on the difference — see the class docstring.
|
|
107
|
+
*
|
|
108
|
+
* @param gross the tax-inclusive total, in major units
|
|
109
|
+
*/
|
|
110
|
+
abstract onInclusive(item: TaxableItem, jurisdiction: TaxJurisdiction, gross: number): TaxBreakdown;
|
|
111
|
+
/**
|
|
112
|
+
* Is this sale taxed as same-region? India splits CGST/SGST when it is and
|
|
113
|
+
* charges IGST when it is not; other jurisdictions may ignore the question
|
|
114
|
+
* entirely, which is why it is the provider's to answer and not a caller's
|
|
115
|
+
* boolean to pass around.
|
|
116
|
+
*/
|
|
117
|
+
abstract isSameRegion(jurisdiction: TaxJurisdiction): boolean;
|
|
118
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The consumer's brand identity, injected once at boot.
|
|
3
|
+
*
|
|
4
|
+
* ── Why this seam exists ──────────────────────────────────────────────────
|
|
5
|
+
*
|
|
6
|
+
* `appkit.config.js → brand` is the consumer-owned owner of the site's name,
|
|
7
|
+
* short name and TLD. appkit has no runtime reader for a consumer config file,
|
|
8
|
+
* so — exactly as `supplier-of-record.ts` does for the statutory identity, and
|
|
9
|
+
* `baseline-resolver.ts` for currency and locale — the consumer injects it.
|
|
10
|
+
*
|
|
11
|
+
* Measured 2026-10-29, this was the FIFTH owner problem that
|
|
12
|
+
* `supplier-of-record.ts` was written to end, one field over. `brand.name` is
|
|
13
|
+
* `"LetItRip"`, but five appkit modules read `NEXT_PUBLIC_SITE_NAME` directly
|
|
14
|
+
* with five DIFFERENT fallbacks (`"LetItRip"`, `"App"`, `"App"`, `""`, and a
|
|
15
|
+
* comment), and that env var holds `"Let It Rip India"` — which is
|
|
16
|
+
* `brand.heroCaption`, not the brand name. So the checkout value-OTP message
|
|
17
|
+
* and the JSON-LD `publisher` both named the site something the brand owner
|
|
18
|
+
* does not call it, while `SiteLogo` drew a third spelling.
|
|
19
|
+
*
|
|
20
|
+
* `seo/json-ld.ts:408` already records the previous round of this:
|
|
21
|
+
* "Production had NEXT_PUBLIC_SITE_NAME='Letitrip' — the wrong casing". The
|
|
22
|
+
* env var is not the owner; it never was.
|
|
23
|
+
*
|
|
24
|
+
* ── 🛑 The defaults here are NEUTRAL, and that asymmetry is deliberate ─────
|
|
25
|
+
*
|
|
26
|
+
* `baseline-resolver.ts`'s BASELINE is documented as matching letitrip.in
|
|
27
|
+
* ("INR / en-IN / IN / +91 / Asia/Kolkata") and that is defensible: a wrong
|
|
28
|
+
* currency is visible in the first rendered price, so an unconfigured install
|
|
29
|
+
* discovers it immediately.
|
|
30
|
+
*
|
|
31
|
+
* A wrong brand name is not. It renders plausibly, reads as intentional, and
|
|
32
|
+
* is someone else's trademark. So an unconfigured install gets `"Site"` —
|
|
33
|
+
* obviously unconfigured — never this marketplace's name.
|
|
34
|
+
*
|
|
35
|
+
* ── 🛑 TWO injection points, because this one is isomorphic ────────────────
|
|
36
|
+
*
|
|
37
|
+
* `supplier-of-record` is server-only (invoices). Brand is rendered by client
|
|
38
|
+
* components — the guide views are `"use client"` prose — so module state set
|
|
39
|
+
* in `providers.config.ts` (server, via the instrumentation hook) is not
|
|
40
|
+
* visible in the browser bundle. The consumer therefore calls
|
|
41
|
+
* `configureBrand()` in BOTH `providers.config.ts` and
|
|
42
|
+
* `client-providers-init.ts`, which is the same symmetry `setStorageNamespace`
|
|
43
|
+
* already uses, reading from the one owner in each case.
|
|
44
|
+
*
|
|
45
|
+
* @tag domain:core
|
|
46
|
+
* @tag layer:shared
|
|
47
|
+
* @tag access:isomorphic
|
|
48
|
+
* @tag sideEffects:module-state
|
|
49
|
+
*/
|
|
50
|
+
export interface BrandIdentity {
|
|
51
|
+
/** Display name, e.g. "LetItRip". Appears in prose, email and structured data. */
|
|
52
|
+
name: string;
|
|
53
|
+
/** Compact form for tight chrome, e.g. "LT". Defaults to `name`. */
|
|
54
|
+
shortName: string;
|
|
55
|
+
/**
|
|
56
|
+
* Bare host with no scheme, e.g. "letitrip.in". Used for the media watermark
|
|
57
|
+
* default and for addresses quoted in prose.
|
|
58
|
+
*/
|
|
59
|
+
domain: string;
|
|
60
|
+
/** Where buyers and sellers are told to write. Derived from `domain` when unset. */
|
|
61
|
+
supportEmail: string;
|
|
62
|
+
/** Envelope sender for transactional mail. Derived from `domain` when unset. */
|
|
63
|
+
noreplyEmail: string;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Register the consumer's brand. Call once, at boot, on BOTH sides.
|
|
67
|
+
*
|
|
68
|
+
* Merges rather than replaces, so a partial call cannot blank a field that was
|
|
69
|
+
* already supplied — the same shape `configureMarketDefaults` and
|
|
70
|
+
* `configurePlatformSupplier` use.
|
|
71
|
+
*/
|
|
72
|
+
export declare function configureBrand(brand: Partial<BrandIdentity>): void;
|
|
73
|
+
/** Reset to the neutral defaults. For tests and the seed CLI's isolated runs. */
|
|
74
|
+
export declare function resetBrand(): void;
|
|
75
|
+
/** Display name. `"Site"` when nothing was injected. */
|
|
76
|
+
export declare function getBrandName(): string;
|
|
77
|
+
/** Compact name, falling back to the display name rather than to `"Site"`. */
|
|
78
|
+
export declare function getBrandShortName(): string;
|
|
79
|
+
/** Bare host, no scheme. */
|
|
80
|
+
export declare function getBrandDomain(): string;
|
|
81
|
+
/**
|
|
82
|
+
* Support address, derived from `domain` when not injected explicitly.
|
|
83
|
+
*
|
|
84
|
+
* Derived rather than required because a consumer who has given us a domain
|
|
85
|
+
* has already answered this; making it mandatory would mean two config fields
|
|
86
|
+
* that can disagree (Root Cause #42) for no gain.
|
|
87
|
+
*/
|
|
88
|
+
export declare function getBrandSupportEmail(): string;
|
|
89
|
+
/** Transactional envelope sender, derived from `domain` when not injected. */
|
|
90
|
+
export declare function getBrandNoreplyEmail(): string;
|
|
91
|
+
/** Full resolved identity (neutral defaults + injected overrides + derivations). */
|
|
92
|
+
export declare function getBrand(): Readonly<BrandIdentity>;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The consumer's brand identity, injected once at boot.
|
|
3
|
+
*
|
|
4
|
+
* ── Why this seam exists ──────────────────────────────────────────────────
|
|
5
|
+
*
|
|
6
|
+
* `appkit.config.js → brand` is the consumer-owned owner of the site's name,
|
|
7
|
+
* short name and TLD. appkit has no runtime reader for a consumer config file,
|
|
8
|
+
* so — exactly as `supplier-of-record.ts` does for the statutory identity, and
|
|
9
|
+
* `baseline-resolver.ts` for currency and locale — the consumer injects it.
|
|
10
|
+
*
|
|
11
|
+
* Measured 2026-10-29, this was the FIFTH owner problem that
|
|
12
|
+
* `supplier-of-record.ts` was written to end, one field over. `brand.name` is
|
|
13
|
+
* `"LetItRip"`, but five appkit modules read `NEXT_PUBLIC_SITE_NAME` directly
|
|
14
|
+
* with five DIFFERENT fallbacks (`"LetItRip"`, `"App"`, `"App"`, `""`, and a
|
|
15
|
+
* comment), and that env var holds `"Let It Rip India"` — which is
|
|
16
|
+
* `brand.heroCaption`, not the brand name. So the checkout value-OTP message
|
|
17
|
+
* and the JSON-LD `publisher` both named the site something the brand owner
|
|
18
|
+
* does not call it, while `SiteLogo` drew a third spelling.
|
|
19
|
+
*
|
|
20
|
+
* `seo/json-ld.ts:408` already records the previous round of this:
|
|
21
|
+
* "Production had NEXT_PUBLIC_SITE_NAME='Letitrip' — the wrong casing". The
|
|
22
|
+
* env var is not the owner; it never was.
|
|
23
|
+
*
|
|
24
|
+
* ── 🛑 The defaults here are NEUTRAL, and that asymmetry is deliberate ─────
|
|
25
|
+
*
|
|
26
|
+
* `baseline-resolver.ts`'s BASELINE is documented as matching letitrip.in
|
|
27
|
+
* ("INR / en-IN / IN / +91 / Asia/Kolkata") and that is defensible: a wrong
|
|
28
|
+
* currency is visible in the first rendered price, so an unconfigured install
|
|
29
|
+
* discovers it immediately.
|
|
30
|
+
*
|
|
31
|
+
* A wrong brand name is not. It renders plausibly, reads as intentional, and
|
|
32
|
+
* is someone else's trademark. So an unconfigured install gets `"Site"` —
|
|
33
|
+
* obviously unconfigured — never this marketplace's name.
|
|
34
|
+
*
|
|
35
|
+
* ── 🛑 TWO injection points, because this one is isomorphic ────────────────
|
|
36
|
+
*
|
|
37
|
+
* `supplier-of-record` is server-only (invoices). Brand is rendered by client
|
|
38
|
+
* components — the guide views are `"use client"` prose — so module state set
|
|
39
|
+
* in `providers.config.ts` (server, via the instrumentation hook) is not
|
|
40
|
+
* visible in the browser bundle. The consumer therefore calls
|
|
41
|
+
* `configureBrand()` in BOTH `providers.config.ts` and
|
|
42
|
+
* `client-providers-init.ts`, which is the same symmetry `setStorageNamespace`
|
|
43
|
+
* already uses, reading from the one owner in each case.
|
|
44
|
+
*
|
|
45
|
+
* @tag domain:core
|
|
46
|
+
* @tag layer:shared
|
|
47
|
+
* @tag access:isomorphic
|
|
48
|
+
* @tag sideEffects:module-state
|
|
49
|
+
*/
|
|
50
|
+
/**
|
|
51
|
+
* 🛑 "Site" and "example.invalid", not this marketplace's values.
|
|
52
|
+
*
|
|
53
|
+
* `.invalid` is reserved by RFC 2606 precisely so it can never resolve, which
|
|
54
|
+
* makes an unconfigured install fail visibly at the first send rather than
|
|
55
|
+
* quietly mailing a real domain someone else owns.
|
|
56
|
+
*/
|
|
57
|
+
const NEUTRAL = Object.freeze({
|
|
58
|
+
name: "Site",
|
|
59
|
+
shortName: "Site",
|
|
60
|
+
domain: "example.invalid",
|
|
61
|
+
supportEmail: "support@example.invalid",
|
|
62
|
+
noreplyEmail: "noreply@example.invalid",
|
|
63
|
+
});
|
|
64
|
+
let _brand = {};
|
|
65
|
+
/**
|
|
66
|
+
* Register the consumer's brand. Call once, at boot, on BOTH sides.
|
|
67
|
+
*
|
|
68
|
+
* Merges rather than replaces, so a partial call cannot blank a field that was
|
|
69
|
+
* already supplied — the same shape `configureMarketDefaults` and
|
|
70
|
+
* `configurePlatformSupplier` use.
|
|
71
|
+
*/
|
|
72
|
+
export function configureBrand(brand) {
|
|
73
|
+
_brand = { ..._brand, ...brand };
|
|
74
|
+
}
|
|
75
|
+
/** Reset to the neutral defaults. For tests and the seed CLI's isolated runs. */
|
|
76
|
+
export function resetBrand() {
|
|
77
|
+
_brand = {};
|
|
78
|
+
}
|
|
79
|
+
/** Display name. `"Site"` when nothing was injected. */
|
|
80
|
+
export function getBrandName() {
|
|
81
|
+
return _brand.name?.trim() || NEUTRAL.name;
|
|
82
|
+
}
|
|
83
|
+
/** Compact name, falling back to the display name rather than to `"Site"`. */
|
|
84
|
+
export function getBrandShortName() {
|
|
85
|
+
return _brand.shortName?.trim() || getBrandName();
|
|
86
|
+
}
|
|
87
|
+
/** Bare host, no scheme. */
|
|
88
|
+
export function getBrandDomain() {
|
|
89
|
+
return _brand.domain?.trim() || NEUTRAL.domain;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Support address, derived from `domain` when not injected explicitly.
|
|
93
|
+
*
|
|
94
|
+
* Derived rather than required because a consumer who has given us a domain
|
|
95
|
+
* has already answered this; making it mandatory would mean two config fields
|
|
96
|
+
* that can disagree (Root Cause #42) for no gain.
|
|
97
|
+
*/
|
|
98
|
+
export function getBrandSupportEmail() {
|
|
99
|
+
if (_brand.supportEmail?.trim())
|
|
100
|
+
return _brand.supportEmail.trim();
|
|
101
|
+
const domain = _brand.domain?.trim();
|
|
102
|
+
return domain ? `support@${domain}` : NEUTRAL.supportEmail;
|
|
103
|
+
}
|
|
104
|
+
/** Transactional envelope sender, derived from `domain` when not injected. */
|
|
105
|
+
export function getBrandNoreplyEmail() {
|
|
106
|
+
if (_brand.noreplyEmail?.trim())
|
|
107
|
+
return _brand.noreplyEmail.trim();
|
|
108
|
+
const domain = _brand.domain?.trim();
|
|
109
|
+
return domain ? `noreply@${domain}` : NEUTRAL.noreplyEmail;
|
|
110
|
+
}
|
|
111
|
+
/** Full resolved identity (neutral defaults + injected overrides + derivations). */
|
|
112
|
+
export function getBrand() {
|
|
113
|
+
return Object.freeze({
|
|
114
|
+
name: getBrandName(),
|
|
115
|
+
shortName: getBrandShortName(),
|
|
116
|
+
domain: getBrandDomain(),
|
|
117
|
+
supportEmail: getBrandSupportEmail(),
|
|
118
|
+
noreplyEmail: getBrandNoreplyEmail(),
|
|
119
|
+
});
|
|
120
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** The site-name token, in the vocabulary this codebase already uses. */
|
|
2
|
+
export declare const BRAND_TOKEN = "{{siteName}}";
|
|
3
|
+
/** The bare-host token, e.g. `support@{{siteDomain}}`. */
|
|
4
|
+
export declare const BRAND_DOMAIN_TOKEN = "{{siteDomain}}";
|
|
5
|
+
/**
|
|
6
|
+
* Substitute every published token in one string, brand included.
|
|
7
|
+
*
|
|
8
|
+
* Resolved per call rather than from a module-level snapshot: the values are
|
|
9
|
+
* read from the injected brand, and a hoisted `const` would capture appkit's
|
|
10
|
+
* neutral default before `configureBrand()` ran. The resolver is a string
|
|
11
|
+
* replace over a small map — cheap enough per render.
|
|
12
|
+
*/
|
|
13
|
+
export declare function withBrand(text: string): string;
|
|
14
|
+
/** `withBrand` over an array — the common shape for advice and rule lists. */
|
|
15
|
+
export declare function withBrandAll(texts: readonly string[]): string[];
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brand tokens for prose that cannot interpolate where it is written.
|
|
3
|
+
*
|
|
4
|
+
* ── Why a token at all ────────────────────────────────────────────────────
|
|
5
|
+
*
|
|
6
|
+
* Most brand mentions sit inside a component and can just call
|
|
7
|
+
* `getBrandName()` at render time. A few cannot: they live in module-level
|
|
8
|
+
* `const` data (`SCAM_TYPES` alone is 19 mentions across ~600 lines of advice),
|
|
9
|
+
* and a template literal there is evaluated at IMPORT time — before the
|
|
10
|
+
* consumer's `configureBrand()` has run. It would capture appkit's neutral
|
|
11
|
+
* `"Site"` permanently and look like it worked.
|
|
12
|
+
*
|
|
13
|
+
* Turning those arrays into functions was the alternative, and it is a
|
|
14
|
+
* BREAKING change for a consumer pinning appkit from the registry: six modules
|
|
15
|
+
* import `SCAM_TYPES` directly, one of them a consumer API route. So the data
|
|
16
|
+
* keeps its shape and carries `{{siteName}}`.
|
|
17
|
+
*
|
|
18
|
+
* ── 🛑 THIS IS A WRAPPER, NOT A SECOND TOKEN SYSTEM ───────────────────────
|
|
19
|
+
*
|
|
20
|
+
* The substitution is `resolvePublishedCopy` — the engine that already renders
|
|
21
|
+
* every disclaimer, FAQ answer and refund-policy sentence, with five live
|
|
22
|
+
* consumers and `audit-disclaimer-coverage` behind it. `siteName` was ALREADY
|
|
23
|
+
* one of its keys.
|
|
24
|
+
*
|
|
25
|
+
* The first draft of this file shipped its own `{brand}` token and its own
|
|
26
|
+
* `split/join` substitution, which was two mistakes at once:
|
|
27
|
+
*
|
|
28
|
+
* - a second resolver for the same job — precisely the duplication Tier AK
|
|
29
|
+
* exists to remove, and it was caught only because the guarding audit
|
|
30
|
+
* flagged `disclaimers.ts`, which had been doing this correctly all along;
|
|
31
|
+
* and
|
|
32
|
+
* - a COLLIDING spelling. `{brand}` is an ordinary JSX expression in seven
|
|
33
|
+
* detail views, and `{{brand}}` was already taken — it means the PRODUCT's
|
|
34
|
+
* manufacturer (6 live uses) against 21 of `{{siteName}}` for the site. So
|
|
35
|
+
* a token reading `{brand}` in platform prose would have said "Takara-Tomy"
|
|
36
|
+
* to anyone who knew the other system.
|
|
37
|
+
*
|
|
38
|
+
* What survives is the ergonomics: `withBrandAll` over an array of advice
|
|
39
|
+
* strings, and one import instead of two at every call site.
|
|
40
|
+
*
|
|
41
|
+
* 🛑 An UNKNOWN token is left VERBATIM by the engine, deliberately — see its
|
|
42
|
+
* own note: a blanked token reads as clumsy copy nobody reports, while a
|
|
43
|
+
* visible `{{siteNam}}` on a live page is reported within minutes. Do not add
|
|
44
|
+
* a "safe" fallback here that defeats that.
|
|
45
|
+
*
|
|
46
|
+
* @tag domain:core
|
|
47
|
+
* @tag layer:shared
|
|
48
|
+
* @tag access:isomorphic
|
|
49
|
+
* @tag sideEffects:none
|
|
50
|
+
*/
|
|
51
|
+
import { resolvePublishedCopy, resolvePublishedValues, } from "../_internal/shared/content/published-values.js";
|
|
52
|
+
/** The site-name token, in the vocabulary this codebase already uses. */
|
|
53
|
+
export const BRAND_TOKEN = "{{siteName}}";
|
|
54
|
+
/** The bare-host token, e.g. `support@{{siteDomain}}`. */
|
|
55
|
+
export const BRAND_DOMAIN_TOKEN = "{{siteDomain}}";
|
|
56
|
+
/**
|
|
57
|
+
* Substitute every published token in one string, brand included.
|
|
58
|
+
*
|
|
59
|
+
* Resolved per call rather than from a module-level snapshot: the values are
|
|
60
|
+
* read from the injected brand, and a hoisted `const` would capture appkit's
|
|
61
|
+
* neutral default before `configureBrand()` ran. The resolver is a string
|
|
62
|
+
* replace over a small map — cheap enough per render.
|
|
63
|
+
*/
|
|
64
|
+
export function withBrand(text) {
|
|
65
|
+
if (!text.includes("{{"))
|
|
66
|
+
return text;
|
|
67
|
+
return resolvePublishedCopy(text, resolvePublishedValues());
|
|
68
|
+
}
|
|
69
|
+
/** `withBrand` over an array — the common shape for advice and rule lists. */
|
|
70
|
+
export function withBrandAll(texts) {
|
|
71
|
+
const values = resolvePublishedValues();
|
|
72
|
+
return texts.map((t) => t.includes("{{") ? resolvePublishedCopy(t, values) : t);
|
|
73
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { SupportedNewsletterSource } from "../newsletter-actions";
|
|
2
|
+
export interface UseNewsletterSubscribeOptions {
|
|
3
|
+
/**
|
|
4
|
+
* The subscribe endpoint. Supplied by the consumer, which owns its route
|
|
5
|
+
* map — appkit must not name a consumer's URL.
|
|
6
|
+
*/
|
|
7
|
+
endpoint: string;
|
|
8
|
+
/**
|
|
9
|
+
* Which box this is.
|
|
10
|
+
*
|
|
11
|
+
* 🛑 Typed against `SupportedNewsletterSource` rather than `string`, so a
|
|
12
|
+
* typo is a compile error instead of a row in `newsletterSubscribers` whose
|
|
13
|
+
* `source` matches no report.
|
|
14
|
+
*/
|
|
15
|
+
source: SupportedNewsletterSource;
|
|
16
|
+
/** Toast shown on success. */
|
|
17
|
+
successToast: string;
|
|
18
|
+
/** Toast shown when the request fails. */
|
|
19
|
+
errorMessage?: string;
|
|
20
|
+
/** Runs after a successful subscribe — clear local UI, flip to a thank-you. */
|
|
21
|
+
onSuccess?: () => void;
|
|
22
|
+
}
|
|
23
|
+
export declare function useNewsletterSubscribe({ endpoint, source, successToast, errorMessage, onSuccess, }: UseNewsletterSubscribeOptions): {
|
|
24
|
+
email: string;
|
|
25
|
+
setEmail: import("react").Dispatch<import("react").SetStateAction<string>>;
|
|
26
|
+
submit: () => Promise<void>;
|
|
27
|
+
isPending: boolean;
|
|
28
|
+
/** Pass to `<Form shellCtx={…}>` — see the note on `form` above. */
|
|
29
|
+
shellCtx: import("../..").FormShellContextValue;
|
|
30
|
+
};
|