@wix/agent-skills 1.20.0 → 1.21.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.
Files changed (183) hide show
  1. package/package.json +1 -1
  2. package/skills/wix-app/SKILL.md +14 -112
  3. package/skills/wix-app/references/APP_TOOLS.md +2 -1
  4. package/skills/wix-app/references/BACKEND_EVENT.md +3 -3
  5. package/skills/wix-app/references/CUSTOM_ELEMENT_WIDGET.md +41 -152
  6. package/skills/wix-app/references/DASHBOARD_MODAL.md +4 -4
  7. package/skills/wix-app/references/SDK_MODULE_MAP.md +44 -0
  8. package/skills/wix-app/references/SITE_PLUGIN.md +36 -190
  9. package/skills/wix-app/references/app-tools/TOOLS.md +3 -1
  10. package/skills/wix-app/references/custom-element-widget/SETTINGS_PANEL.md +21 -0
  11. package/skills/wix-app/references/dashboard-page/ECOM_NAVIGATION.md +17 -0
  12. package/skills/wix-app/references/dashboard-plugin/SLOTS.md +4 -18
  13. package/skills/wix-app/references/site-plugin/EXAMPLES.md +1 -1
  14. package/skills/wix-headless-fast/SKILL.md +302 -51
  15. package/skills/wix-headless-fast/entry/skill.md +11 -2
  16. package/skills/wix-headless-fast/install/AGENTS.md +32 -0
  17. package/skills/wix-headless-fast/install/agents-md.mjs +39 -0
  18. package/skills/wix-headless-fast/install/attach.mjs +360 -0
  19. package/skills/wix-headless-fast/install/deploy.mjs +109 -9
  20. package/skills/wix-headless-fast/install/setup.mjs +239 -0
  21. package/skills/wix-headless-fast/references/blog/INSTRUCTIONS.md +226 -60
  22. package/skills/wix-headless-fast/references/blog/app/components/blog/BlogFeedView.tsx +2 -3
  23. package/skills/wix-headless-fast/references/blog/app/components/blog/PostView.tsx +2 -3
  24. package/skills/wix-headless-fast/references/blog/app/hooks/blog/useBlogFeed.ts +24 -116
  25. package/skills/wix-headless-fast/references/blog/app/hooks/blog/usePost.ts +16 -64
  26. package/skills/wix-headless-fast/references/blog/app/wix/blog/blog-feed-store.ts +137 -0
  27. package/skills/wix-headless-fast/references/blog/app/wix/blog/post-store.ts +78 -0
  28. package/skills/wix-headless-fast/references/blog/app/wix/blog/posts-core.ts +117 -0
  29. package/skills/wix-headless-fast/references/blog/app/wix/blog/posts.ts +13 -60
  30. package/skills/wix-headless-fast/references/blog/app/wix/blog/taxonomy-core.ts +47 -0
  31. package/skills/wix-headless-fast/references/blog/app/wix/blog/taxonomy.ts +10 -28
  32. package/skills/wix-headless-fast/references/blog/app-astro/pages/blog/[...slug].astro +2 -4
  33. package/skills/wix-headless-fast/references/blog/rest/posts.ts +42 -0
  34. package/skills/wix-headless-fast/references/blog/rest/taxonomy.ts +62 -0
  35. package/skills/wix-headless-fast/references/blog/seed/SEED.md +9 -0
  36. package/skills/wix-headless-fast/references/blog/seed/read-site.mjs +22 -0
  37. package/skills/wix-headless-fast/references/bookings/INSTRUCTIONS.md +181 -33
  38. package/skills/wix-headless-fast/references/bookings/app/components/bookings/ServiceBookingView.tsx +2 -1
  39. package/skills/wix-headless-fast/references/bookings/app/hooks/bookings/useBookingFlow.ts +26 -132
  40. package/skills/wix-headless-fast/references/bookings/app/hooks/bookings/useServices.ts +18 -49
  41. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/booking-core.ts +218 -0
  42. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/booking-flow-store.ts +169 -0
  43. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/booking.ts +57 -142
  44. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/services-core.ts +92 -0
  45. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/services-store.ts +90 -0
  46. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/services.ts +22 -61
  47. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/types.ts +3 -1
  48. package/skills/wix-headless-fast/references/bookings/rest/booking.ts +113 -0
  49. package/skills/wix-headless-fast/references/bookings/rest/services.ts +55 -0
  50. package/skills/wix-headless-fast/references/bookings/seed/SEED.md +10 -0
  51. package/skills/wix-headless-fast/references/bookings/seed/read-site.mjs +28 -0
  52. package/skills/wix-headless-fast/references/cms/INSTRUCTIONS.md +238 -62
  53. package/skills/wix-headless-fast/references/cms/app/hooks/cms/useCollection.ts +19 -52
  54. package/skills/wix-headless-fast/references/cms/app/hooks/cms/useItem.ts +16 -36
  55. package/skills/wix-headless-fast/references/cms/app/wix/cms/collection-store.ts +158 -0
  56. package/skills/wix-headless-fast/references/cms/app/wix/cms/item-store.ts +93 -0
  57. package/skills/wix-headless-fast/references/cms/app/wix/cms/items-core.ts +177 -0
  58. package/skills/wix-headless-fast/references/cms/app/wix/cms/items.ts +13 -41
  59. package/skills/wix-headless-fast/references/cms/rest/items.ts +125 -0
  60. package/skills/wix-headless-fast/references/cms/seed/SEED.md +9 -0
  61. package/skills/wix-headless-fast/references/cms/seed/read-site.mjs +20 -0
  62. package/skills/wix-headless-fast/references/events/INSTRUCTIONS.md +241 -83
  63. package/skills/wix-headless-fast/references/events/app/components/events/EventConfirmationView.tsx +1 -1
  64. package/skills/wix-headless-fast/references/events/app/components/events/EventRegistrationView.tsx +7 -5
  65. package/skills/wix-headless-fast/references/events/app/components/events/EventsView.tsx +2 -2
  66. package/skills/wix-headless-fast/references/events/app/hooks/events/useEventRegistration.ts +28 -110
  67. package/skills/wix-headless-fast/references/events/app/hooks/events/useEvents.ts +19 -54
  68. package/skills/wix-headless-fast/references/events/app/wix/events/events-core.ts +130 -0
  69. package/skills/wix-headless-fast/references/events/app/wix/events/events-store.ts +97 -0
  70. package/skills/wix-headless-fast/references/events/app/wix/events/events.ts +13 -94
  71. package/skills/wix-headless-fast/references/events/app/wix/events/registration-core.ts +113 -0
  72. package/skills/wix-headless-fast/references/events/app/wix/events/registration-store.ts +156 -0
  73. package/skills/wix-headless-fast/references/events/app/wix/events/registration.ts +38 -69
  74. package/skills/wix-headless-fast/references/events/rest/events.ts +57 -0
  75. package/skills/wix-headless-fast/references/events/rest/registration.ts +83 -0
  76. package/skills/wix-headless-fast/references/events/seed/SEED.md +9 -0
  77. package/skills/wix-headless-fast/references/events/seed/read-site.mjs +26 -0
  78. package/skills/wix-headless-fast/references/forms/INSTRUCTIONS.md +216 -26
  79. package/skills/wix-headless-fast/references/forms/app/hooks/forms/useWixForm.ts +35 -267
  80. package/skills/wix-headless-fast/references/forms/app/wix/forms/form-store.ts +332 -0
  81. package/skills/wix-headless-fast/references/forms/app/wix/forms/forms-core.ts +238 -0
  82. package/skills/wix-headless-fast/references/forms/app/wix/forms/forms.ts +28 -243
  83. package/skills/wix-headless-fast/references/forms/app/wix/forms/submissions-core.ts +216 -0
  84. package/skills/wix-headless-fast/references/forms/app/wix/forms/submissions.ts +32 -204
  85. package/skills/wix-headless-fast/references/forms/rest/forms.ts +34 -0
  86. package/skills/wix-headless-fast/references/forms/rest/submissions.ts +56 -0
  87. package/skills/wix-headless-fast/references/forms/seed/SEED.md +8 -0
  88. package/skills/wix-headless-fast/references/forms/seed/read-site.mjs +20 -0
  89. package/skills/wix-headless-fast/references/forms/seed/seed-forms.mjs +15 -8
  90. package/skills/wix-headless-fast/references/members/INSTRUCTIONS.md +316 -63
  91. package/skills/wix-headless-fast/references/members/app/components/members/LoginForm.tsx +1 -1
  92. package/skills/wix-headless-fast/references/members/app/wix/members/auth-core.ts +105 -0
  93. package/skills/wix-headless-fast/references/members/app/wix/members/auth.ts +36 -65
  94. package/skills/wix-headless-fast/references/members/app/wix/members/client.ts +1 -1
  95. package/skills/wix-headless-fast/references/members/app/wix/members/members-core.ts +43 -0
  96. package/skills/wix-headless-fast/references/members/app/wix/members/members.ts +11 -32
  97. package/skills/wix-headless-fast/references/members/rest/auth.ts +215 -0
  98. package/skills/wix-headless-fast/references/members/rest/members.ts +26 -0
  99. package/skills/wix-headless-fast/references/members/seed/read-site.mjs +15 -0
  100. package/skills/wix-headless-fast/references/portfolio/INSTRUCTIONS.md +240 -59
  101. package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/CollectionProjectsView.tsx +2 -2
  102. package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/CollectionsView.tsx +2 -2
  103. package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/GalleryMedia.tsx +35 -0
  104. package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/ProjectDetailView.tsx +8 -38
  105. package/skills/wix-headless-fast/references/portfolio/app/hooks/portfolio/useCollectionProjects.ts +23 -50
  106. package/skills/wix-headless-fast/references/portfolio/app/hooks/portfolio/useCollections.ts +16 -34
  107. package/skills/wix-headless-fast/references/portfolio/app/hooks/portfolio/useProjectDetail.ts +22 -49
  108. package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/collection-projects-store.ts +82 -0
  109. package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/collections-store.ts +58 -0
  110. package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/portfolio-core.ts +177 -0
  111. package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/portfolio.ts +31 -118
  112. package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/project-detail-store.ts +81 -0
  113. package/skills/wix-headless-fast/references/portfolio/rest/portfolio.ts +75 -0
  114. package/skills/wix-headless-fast/references/portfolio/seed/SEED.md +8 -0
  115. package/skills/wix-headless-fast/references/portfolio/seed/read-site.mjs +20 -0
  116. package/skills/wix-headless-fast/references/portfolio/seed/seed-portfolio.mjs +19 -21
  117. package/skills/wix-headless-fast/references/pricing-plans/INSTRUCTIONS.md +214 -51
  118. package/skills/wix-headless-fast/references/pricing-plans/app/components/pricing-plans/PlanDetailView.tsx +9 -20
  119. package/skills/wix-headless-fast/references/pricing-plans/app/components/pricing-plans/PlansView.tsx +7 -32
  120. package/skills/wix-headless-fast/references/pricing-plans/app/components/pricing-plans/SubscribeButton.tsx +41 -0
  121. package/skills/wix-headless-fast/references/pricing-plans/app/hooks/pricing-plans/usePlanPurchase.ts +12 -33
  122. package/skills/wix-headless-fast/references/pricing-plans/app/hooks/pricing-plans/usePlans.ts +17 -34
  123. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plan-purchase-store.ts +54 -0
  124. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plans-core.ts +106 -0
  125. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plans-store.ts +64 -0
  126. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plans.ts +7 -55
  127. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/purchase-core.ts +43 -0
  128. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/purchase.ts +11 -34
  129. package/skills/wix-headless-fast/references/pricing-plans/rest/plans.ts +31 -0
  130. package/skills/wix-headless-fast/references/pricing-plans/rest/purchase.ts +20 -0
  131. package/skills/wix-headless-fast/references/pricing-plans/seed/SEED.md +8 -0
  132. package/skills/wix-headless-fast/references/pricing-plans/seed/read-site.mjs +21 -0
  133. package/skills/wix-headless-fast/references/restaurants/INSTRUCTIONS.md +242 -47
  134. package/skills/wix-headless-fast/references/restaurants/app/components/restaurants/MenuView.tsx +2 -2
  135. package/skills/wix-headless-fast/references/restaurants/app/components/restaurants/OrderCartDrawer.tsx +2 -2
  136. package/skills/wix-headless-fast/references/restaurants/app/hooks/restaurants/useMenus.ts +14 -35
  137. package/skills/wix-headless-fast/references/restaurants/app/hooks/restaurants/useReservation.ts +28 -171
  138. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/menu-core.ts +155 -0
  139. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/menu-store.ts +89 -0
  140. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/menu.ts +23 -117
  141. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/ordering-core.ts +124 -0
  142. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/ordering.ts +45 -114
  143. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/reservation-store.ts +193 -0
  144. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/reservations-core.ts +94 -0
  145. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/reservations.ts +15 -87
  146. package/skills/wix-headless-fast/references/restaurants/rest/menu.ts +60 -0
  147. package/skills/wix-headless-fast/references/restaurants/rest/ordering.ts +169 -0
  148. package/skills/wix-headless-fast/references/restaurants/rest/reservations.ts +68 -0
  149. package/skills/wix-headless-fast/references/restaurants/seed/SEED.md +8 -0
  150. package/skills/wix-headless-fast/references/restaurants/seed/read-site.mjs +25 -0
  151. package/skills/wix-headless-fast/references/restaurants/seed/seed-restaurants.mjs +25 -5
  152. package/skills/wix-headless-fast/references/shared/SUPPLIED-CONTENT.md +23 -0
  153. package/skills/wix-headless-fast/references/shared/app/wix/media.ts +45 -4
  154. package/skills/wix-headless-fast/references/shared/app/wix/sdk.ts +13 -0
  155. package/skills/wix-headless-fast/references/shared/rest/client.ts +155 -0
  156. package/skills/wix-headless-fast/references/shared/rest/config.ts +3 -0
  157. package/skills/wix-headless-fast/references/shared/rest/media.ts +65 -0
  158. package/skills/wix-headless-fast/references/shared/seed/read-site.mjs +99 -0
  159. package/skills/wix-headless-fast/references/storefront/INSTRUCTIONS.md +186 -63
  160. package/skills/wix-headless-fast/references/storefront/app/components/storefront/OptionPicker.tsx +95 -0
  161. package/skills/wix-headless-fast/references/storefront/app/components/storefront/QuickAdd.tsx +29 -58
  162. package/skills/wix-headless-fast/references/storefront/app/hooks/storefront/useProductDetail.ts +29 -163
  163. package/skills/wix-headless-fast/references/storefront/app/hooks/storefront/useShop.ts +26 -300
  164. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/cart-core.ts +121 -0
  165. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/cart.ts +49 -162
  166. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/catalog-core.ts +314 -0
  167. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/catalog.ts +47 -370
  168. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/product-detail-store.ts +147 -0
  169. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/shop-store.ts +289 -0
  170. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/types.ts +2 -0
  171. package/skills/wix-headless-fast/references/storefront/rest/cart.ts +132 -0
  172. package/skills/wix-headless-fast/references/storefront/rest/catalog.ts +123 -0
  173. package/skills/wix-headless-fast/references/storefront/seed/SEED.md +31 -9
  174. package/skills/wix-headless-fast/references/storefront/seed/read-site.mjs +38 -0
  175. package/skills/wix-headless-fast/references/storefront/seed/seed-store.mjs +57 -50
  176. package/skills/wix-manage/references/ecommerce/ecom-shipping.md +1 -1
  177. package/skills/wix-manage/references/ecommerce/pricing-promotions/ecom-pricing-create-coupon.md +10 -19
  178. package/skills/wix-manage/references/events/create-wix-event.md +5 -1
  179. package/skills/wix-vibe-headless/references/storefront/INSTRUCTIONS.md +0 -40
  180. package/skills/wix-vibe-headless/references/storefront/seed/SEED.md +3 -0
  181. package/skills/wix-vibe-headless/references/storefront/seed/seed-store.cjs +6 -0
  182. package/skills/wix-headless-fast/install/fast-path.mjs +0 -186
  183. package/skills/wix-headless-fast/install/pin-agents-md.mjs +0 -57
@@ -1,13 +1,10 @@
1
- // REFERENCE detail surface: price/billing block + perks + terms + subscribe CTA, on the
2
- // @theme tokens. Correct and complete; per the skill's model you design and build your own
3
- // on usePlanPurchase (the plan itself arrives as an SSR-fetched DTO prop).
4
- import { usePlanPurchase } from "../../hooks/pricing-plans/usePlanPurchase";
1
+ // REFERENCE detail surface: price/billing block + perks + terms + the shipped SubscribeButton, on
2
+ // the @theme tokens. Correct and complete; per the skill's model you design and build your own
3
+ // (the plan itself arrives as an SSR-fetched DTO prop; the purchase control is SubscribeButton).
4
+ import SubscribeButton from "./SubscribeButton";
5
5
  import type { PlanDetail } from "../../wix/pricing-plans/types";
6
6
 
7
7
  export default function PlanDetailView({ plan }: { plan: PlanDetail }) {
8
- const { purchase, purchasingId, error } = usePlanPurchase();
9
- const purchasing = purchasingId === plan.id;
10
-
11
8
  return (
12
9
  <div>
13
10
  <p>
@@ -31,19 +28,11 @@ export default function PlanDetailView({ plan }: { plan: PlanDetail }) {
31
28
  </ul>
32
29
  )}
33
30
 
34
- {error && <p className="mt-4 text-sm text-red-600">{error}</p>}
35
- {plan.buyable ? (
36
- <button
37
- type="button"
38
- disabled={purchasing}
39
- onClick={() => void purchase(plan.id).catch(() => {})}
40
- className="mt-6 rounded-full bg-primary px-8 py-3 text-sm font-semibold text-primary-foreground transition-opacity hover:opacity-90 disabled:opacity-50"
41
- >
42
- {purchasing ? "Redirecting…" : plan.free ? "Get this plan" : `Subscribe · ${plan.price}`}
43
- </button>
44
- ) : (
45
- <p className="mt-6 text-sm text-muted-foreground">This plan is assigned by the site owner.</p>
46
- )}
31
+ <div className="mt-6">
32
+ <SubscribeButton plan={plan} className="rounded-full bg-primary px-8 py-3 text-sm font-semibold text-primary-foreground transition-opacity hover:opacity-90 disabled:opacity-50">
33
+ {plan.free ? "Get this plan" : `Subscribe · ${plan.price}`}
34
+ </SubscribeButton>
35
+ </div>
47
36
 
48
37
  {plan.termsAndConditions && (
49
38
  <div className="mt-10 border-t border-border pt-5">
@@ -1,9 +1,9 @@
1
1
  // REFERENCE pricing surface: plan cards in a grid on the @theme tokens. Correct and
2
- // complete; per the skill's model you design and build your own on usePlans +
3
- // usePlanPurchase.
2
+ // complete; per the skill's model you design and build your own on usePlans, with the shipped
3
+ // SubscribeButton as each card's purchase control.
4
4
  import type { ComponentType, ReactNode } from "react";
5
5
  import { usePlans } from "../../hooks/pricing-plans/usePlans";
6
- import { usePlanPurchase } from "../../hooks/pricing-plans/usePlanPurchase";
6
+ import SubscribeButton from "./SubscribeButton";
7
7
  import type { PlanSummary } from "../../wix/pricing-plans/types";
8
8
 
9
9
  export interface LinkLikeProps {
@@ -20,19 +20,11 @@ const PlainLink = ({ href, className, children }: LinkLikeProps) => (
20
20
 
21
21
  export interface PlanCardProps {
22
22
  plan: PlanSummary;
23
- onSubscribe: (planId: string) => void;
24
- purchasing: boolean;
25
23
  planHref?: (slug: string) => string;
26
24
  LinkComponent?: ComponentType<LinkLikeProps>;
27
25
  }
28
26
 
29
- export function PlanCard({
30
- plan,
31
- onSubscribe,
32
- purchasing,
33
- planHref = (slug) => `/plans/${slug}`,
34
- LinkComponent = PlainLink,
35
- }: PlanCardProps) {
27
+ export function PlanCard({ plan, planHref = (slug) => `/plans/${slug}`, LinkComponent = PlainLink }: PlanCardProps) {
36
28
  return (
37
29
  <div className="flex h-full flex-col rounded-lg border border-border bg-background p-6">
38
30
  <p className="text-base font-semibold text-foreground">{plan.name}</p>
@@ -57,16 +49,7 @@ export function PlanCard({
57
49
  </ul>
58
50
  )}
59
51
  <div className="mt-auto grid gap-2 pt-6">
60
- {plan.buyable && (
61
- <button
62
- type="button"
63
- disabled={purchasing}
64
- onClick={() => onSubscribe(plan.id)}
65
- className="rounded-full bg-primary px-6 py-2.5 text-sm font-semibold text-primary-foreground transition-opacity hover:opacity-90 disabled:opacity-50"
66
- >
67
- {purchasing ? "Redirecting…" : "Subscribe"}
68
- </button>
69
- )}
52
+ <SubscribeButton plan={plan} assignedText={null} />
70
53
  <LinkComponent
71
54
  href={planHref(plan.slug)}
72
55
  className="rounded-full border border-border px-6 py-2.5 text-center text-sm font-semibold text-foreground no-underline transition-colors hover:bg-secondary"
@@ -94,11 +77,10 @@ export default function PlansView({
94
77
  CardComponent = PlanCard,
95
78
  }: PlansViewProps) {
96
79
  const { plans, error } = usePlans({ initialPlans });
97
- const { purchase, purchasingId, error: purchaseError } = usePlanPurchase();
98
80
 
99
81
  return (
100
82
  <div>
101
- {(error ?? purchaseError) && <p className="mb-4 text-sm text-red-600">{error ?? purchaseError}</p>}
83
+ {error && <p className="mb-4 text-sm text-red-600">{error}</p>}
102
84
  {plans === null ? (
103
85
  <div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3" aria-busy="true">
104
86
  {Array.from({ length: 3 }, (_, i) => (
@@ -110,14 +92,7 @@ export default function PlansView({
110
92
  ) : (
111
93
  <div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
112
94
  {plans.map((p) => (
113
- <CardComponent
114
- key={p.id}
115
- plan={p}
116
- onSubscribe={(id) => void purchase(id).catch(() => {})}
117
- purchasing={purchasingId === p.id}
118
- planHref={planHref}
119
- LinkComponent={LinkComponent}
120
- />
95
+ <CardComponent key={p.id} plan={p} planHref={planHref} LinkComponent={LinkComponent} />
121
96
  ))}
122
97
  </div>
123
98
  )}
@@ -0,0 +1,41 @@
1
+ // The purchase control for one plan — wire as-is on every card, detail page, and home strip.
2
+ // It owns the rules a CTA gets wrong when rewritten: rendered only for a `buyable` plan (a PUBLIC
3
+ // plan can be merchant-assigned — `assignedText` shows instead, or nothing when null), the label
4
+ // from the DTO ("Get this plan" for a free plan, "Subscribe" otherwise, your children override it),
5
+ // "Redirecting…" while the hosted checkout is starting, the failure inline. The redirect itself is
6
+ // usePlanPurchase → purchasePlan; nothing here builds a URL or marks anything paid.
7
+ import type { ReactNode } from "react";
8
+ import { usePlanPurchase } from "../../hooks/pricing-plans/usePlanPurchase";
9
+ import type { PurchaseOptions } from "../../wix/pricing-plans/purchase";
10
+ import type { PlanSummary } from "../../wix/pricing-plans/types";
11
+
12
+ export interface SubscribeButtonProps {
13
+ plan: PlanSummary;
14
+ /** thankYouPageUrl / postFlowUrl for the hosted flow — see purchase.ts; omit for the defaults. */
15
+ options?: PurchaseOptions;
16
+ /** Replaces the default label ("Get this plan" / "Subscribe"). */
17
+ children?: ReactNode;
18
+ className?: string;
19
+ /** Shown for a plan that isn't buyable; null renders nothing (a grid card). */
20
+ assignedText?: string | null;
21
+ }
22
+
23
+ export default function SubscribeButton({
24
+ plan,
25
+ options,
26
+ children,
27
+ className = "rounded-full bg-primary px-6 py-2.5 text-sm font-semibold text-primary-foreground transition-opacity hover:opacity-90 disabled:opacity-50",
28
+ assignedText = "This plan is assigned by the site owner.",
29
+ }: SubscribeButtonProps) {
30
+ const { purchase, purchasingId, error } = usePlanPurchase();
31
+ if (!plan.buyable) return assignedText ? <p className="text-sm text-muted-foreground">{assignedText}</p> : null;
32
+ const purchasing = purchasingId === plan.id;
33
+ return (
34
+ <div className="grid gap-2">
35
+ <button type="button" disabled={purchasing} onClick={() => void purchase(plan.id, options).catch(() => {})} className={className}>
36
+ {purchasing ? "Redirecting…" : children ?? (plan.free ? "Get this plan" : "Subscribe")}
37
+ </button>
38
+ {error && <p className="text-sm text-red-600" role="alert">{error}</p>}
39
+ </div>
40
+ );
41
+ }
@@ -1,37 +1,16 @@
1
- // The purchase action for any surface (grid card, detail CTA, home strip). All correctness
2
- // (the hosted redirect session, member login/signup, payment) lives in the data layer —
3
- // this hook tracks in-flight state; you own how it looks.
4
- import { useState } from "react";
5
- import { purchasePlan } from "../../wix/pricing-plans/purchase";
6
- import type { PurchaseOptions } from "../../wix/pricing-plans/purchase";
1
+ // React binding of the purchase store (wix/pricing-plans/plan-purchase-store.ts) — the purchase
2
+ // action for any surface (grid card, detail CTA, home strip). All correctness (the hosted redirect
3
+ // session, member login/signup, payment) lives in the data layer; the store tracks in-flight state;
4
+ // you own how it looks.
5
+ import { useRef, useSyncExternalStore } from "react";
6
+ import { createPlanPurchaseStore, type PlanPurchaseState, type PlanPurchaseStore } from "../../wix/pricing-plans/plan-purchase-store";
7
7
 
8
- export interface UsePlanPurchase {
9
- /**
10
- * Starts the hosted checkout — when it resolves the browser is already navigating away.
11
- * Rejects with a visitor-facing message otherwise — surface it, don't swallow it.
12
- */
13
- purchase: (planId: string, options?: PurchaseOptions) => Promise<void>;
14
- /** The plan id a purchase is in flight for (null when idle) — key CTA spinners off it. */
15
- purchasingId: string | null;
16
- error: string | null;
17
- }
8
+ export type UsePlanPurchase = PlanPurchaseState & Pick<PlanPurchaseStore, "purchase">;
18
9
 
19
10
  export function usePlanPurchase(): UsePlanPurchase {
20
- const [purchasingId, setPurchasingId] = useState<string | null>(null);
21
- const [error, setError] = useState<string | null>(null);
22
-
23
- async function purchase(planId: string, options?: PurchaseOptions): Promise<void> {
24
- setPurchasingId(planId);
25
- setError(null);
26
- try {
27
- window.location.href = await purchasePlan(planId, options);
28
- // stays "purchasing" — the browser is leaving for the hosted checkout
29
- } catch (e) {
30
- setPurchasingId(null);
31
- setError(e instanceof Error ? e.message : String(e));
32
- throw e;
33
- }
34
- }
35
-
36
- return { purchase, purchasingId, error };
11
+ const ref = useRef<PlanPurchaseStore | null>(null);
12
+ if (!ref.current) ref.current = createPlanPurchaseStore();
13
+ const store = ref.current;
14
+ const state = useSyncExternalStore(store.subscribe, store.getState, store.getState);
15
+ return { ...state, purchase: store.purchase };
37
16
  }
@@ -1,39 +1,22 @@
1
- // Plans listing. SSR-friendly: pass server-fetched data as `initialPlans` (Astro frontmatter
2
- // / server component) and no client fetch happens; a SPA passes nothing.
3
- import { useEffect, useState } from "react";
4
- import { fetchPlans } from "../../wix/pricing-plans/plans";
5
- import type { PlanSummary } from "../../wix/pricing-plans/types";
1
+ // React binding of the plans store (wix/pricing-plans/plans-store.ts) — the listing logic lives
2
+ // there, framework-free; this hook subscribes to one instance per mounted listing. SSR-friendly:
3
+ // pass server-fetched data as `initialPlans` (Astro frontmatter / server component) and no client
4
+ // fetch happens; a SPA passes nothing. Astro islands and React SPAs use this; a static page, Vue,
5
+ // or Svelte uses the store directly.
6
+ import { useEffect, useRef, useSyncExternalStore } from "react";
7
+ import { createPlansStore, type PlansState, type PlansStore, type PlansStoreOptions } from "../../wix/pricing-plans/plans-store";
6
8
 
7
- export interface UsePlansOptions {
8
- initialPlans?: PlanSummary[];
9
- }
10
-
11
- export interface UsePlans {
12
- /** null while the first load is in flight — render skeletons, not an empty state. */
13
- plans: PlanSummary[] | null;
14
- error: string | null;
15
- }
9
+ export type UsePlansOptions = PlansStoreOptions;
16
10
 
17
- export function usePlans({ initialPlans }: UsePlansOptions = {}): UsePlans {
18
- const [plans, setPlans] = useState<PlanSummary[] | null>(initialPlans ?? null);
19
- const [error, setError] = useState<string | null>(null);
11
+ export type UsePlans = PlansState;
20
12
 
13
+ export function usePlans(options: UsePlansOptions = {}): UsePlans {
14
+ const ref = useRef<PlansStore | null>(null);
15
+ if (!ref.current) ref.current = createPlansStore(options);
16
+ const store = ref.current;
21
17
  useEffect(() => {
22
- let alive = true;
23
- if (!initialPlans) {
24
- fetchPlans()
25
- .then((p) => alive && setPlans(p))
26
- .catch((e) => {
27
- if (!alive) return;
28
- setPlans([]);
29
- setError(e instanceof Error ? e.message : String(e));
30
- });
31
- }
32
- return () => {
33
- alive = false;
34
- };
35
- // eslint-disable-next-line react-hooks/exhaustive-deps
36
- }, []);
37
-
38
- return { plans, error };
18
+ store.start();
19
+ return () => store.stop();
20
+ }, [store]);
21
+ return useSyncExternalStore(store.subscribe, store.getState, store.getState);
39
22
  }
@@ -0,0 +1,54 @@
1
+ // The purchase action as a framework-free store — the logic behind usePlanPurchase, usable from
2
+ // React (usePlanPurchase wraps it), from a static page's card or detail CTA, from Vue/Svelte, or
3
+ // as the specification for a port. All correctness (the hosted redirect session, member
4
+ // login/signup, payment) lives in the data layer; this store tracks the in-flight plan and the
5
+ // last failure, and navigates the FULL document to the hosted checkout.
6
+ //
7
+ // One store per purchase surface (a grid and a detail CTA each key their own spinner):
8
+ // createPlanPurchaseStore(), not a singleton. No async work on mount, so no start/stop.
9
+ import { purchasePlan, type PurchaseOptions } from "./purchase";
10
+
11
+ export interface PlanPurchaseState {
12
+ /** The plan id a purchase is in flight for (null when idle) — key CTA spinners off it. */
13
+ purchasingId: string | null;
14
+ /** The last failed purchase's visitor-facing message; a new purchase clears it. */
15
+ error: string | null;
16
+ }
17
+
18
+ export interface PlanPurchaseStore {
19
+ getState(): PlanPurchaseState;
20
+ subscribe(listener: () => void): () => void;
21
+ /**
22
+ * Starts the hosted checkout — when it resolves the browser is already navigating away, and
23
+ * `purchasingId` stays set. Rejects (and records `error`) otherwise — surface it, don't swallow it.
24
+ */
25
+ purchase(planId: string, options?: PurchaseOptions): Promise<void>;
26
+ }
27
+
28
+ export function createPlanPurchaseStore(): PlanPurchaseStore {
29
+ let state: PlanPurchaseState = { purchasingId: null, error: null };
30
+ const listeners = new Set<() => void>();
31
+
32
+ function setState(patch: Partial<PlanPurchaseState>): void {
33
+ state = { ...state, ...patch };
34
+ for (const fn of listeners) fn();
35
+ }
36
+
37
+ return {
38
+ getState: () => state,
39
+ subscribe(fn) {
40
+ listeners.add(fn);
41
+ return () => listeners.delete(fn);
42
+ },
43
+ async purchase(planId, options) {
44
+ setState({ purchasingId: planId, error: null });
45
+ try {
46
+ window.location.href = await purchasePlan(planId, options);
47
+ // stays "purchasing" — the browser is leaving for the hosted checkout
48
+ } catch (e) {
49
+ setState({ purchasingId: null, error: e instanceof Error ? e.message : String(e) });
50
+ throw e;
51
+ }
52
+ },
53
+ };
54
+ }
@@ -0,0 +1,106 @@
1
+ // Plan rules and DTO mapping — transport-agnostic, imported by BOTH transports: ./plans.ts (the
2
+ // SDK, managed Astro and React) and the REST twin in references/pricing-plans/rest/plans.ts
3
+ // (fetch, a static site or a port to another language). Every rule about prices, billing labels,
4
+ // perks, buyability, images lives HERE, once. A raw plan may come from the SDK (`_id`, image as a
5
+ // `wix:image://` string) or from REST (`id`, image as { id, url }); the mappers accept both.
6
+ // Imports are type-only so a strip to JS emits no imports.
7
+ import type { PlanDetail, PlanSummary } from "./types";
8
+
9
+ /** A raw Plans V3 entity as either transport returns it. */
10
+ export type Raw = Record<string, any>;
11
+ /** Media value + size → https URL. Injected: the SDK transport scales through @wix/sdk, REST through the URL form. */
12
+ export type ImgSrc = (value: any, width: number, height: number) => string;
13
+
14
+ const id = (raw: Raw | undefined): string => raw?._id ?? raw?.id ?? "";
15
+
16
+ // ---- query ----------------------------------------------------------------------------------------
17
+
18
+ export interface PlansQueryOptions {
19
+ /** 1–100, default 100 (the grid shows every public plan). */
20
+ limit?: number;
21
+ /** One plan by URL slug (the detail page). */
22
+ slug?: string;
23
+ }
24
+
25
+ /**
26
+ * The Query Plans body both transports send: only PUBLIC plans (a HIDDEN plan is merchant-assigned
27
+ * and never listed), cursor paging, and the slug when one plan is wanted. The SDK builder spells the
28
+ * same filter as `.eq("visibility", "PUBLIC")`.
29
+ */
30
+ export function plansQuery({ limit = 100, slug }: PlansQueryOptions = {}): Raw {
31
+ if (!Number.isInteger(limit) || limit < 1 || limit > 100) throw new Error("limit must be between 1 and 100.");
32
+ const filter: Raw = { visibility: "PUBLIC" };
33
+ if (slug) filter.slug = slug;
34
+ return { filter, cursorPaging: { limit } };
35
+ }
36
+
37
+ // ---- price rules ----------------------------------------------------------------------------------
38
+
39
+ /** "€29.00" in the visitor's locale for the plan's currency; "Free" for no or zero amount. Never assume USD. */
40
+ export function formatPrice(value: string | undefined, currency: string | undefined): string {
41
+ if (value == null || value === "" || Number(value) === 0) return "Free";
42
+ try {
43
+ return new Intl.NumberFormat(undefined, { style: "currency", currency: currency || "USD" }).format(Number(value));
44
+ } catch {
45
+ return `${value} ${currency ?? ""}`.trim();
46
+ }
47
+ }
48
+
49
+ /**
50
+ * billingTerms → a display label. billingCycle.count is a STRING on both transports ("1") — Number()
51
+ * it. No billingCycle → a one-time payment; endType CYCLES_COMPLETED with billingCycleCount 1 also
52
+ * bills once; more cycles read "per month × 6".
53
+ */
54
+ export function billingLabel(terms: Raw | undefined): string {
55
+ const cycle = terms?.billingCycle;
56
+ if (!cycle) return "one-time";
57
+ const cyclesTotal = Number(terms?.cyclesCompletedDetails?.billingCycleCount ?? 0);
58
+ if (terms?.endType === "CYCLES_COMPLETED" && cyclesTotal === 1) return "one-time";
59
+ const count = Number(cycle.count ?? 1) || 1;
60
+ const period = String(cycle.period ?? "MONTH").toLowerCase();
61
+ const base = count === 1 ? `per ${period}` : `every ${count} ${period}s`;
62
+ return terms?.endType === "CYCLES_COMPLETED" && cyclesTotal > 1 ? `${base} × ${cyclesTotal}` : base;
63
+ }
64
+
65
+ /**
66
+ * The plan's image as a value imgSrc resolves: the SDK hands a `wix:image://…` string; REST hands
67
+ * an Image object { id, url } — its url when present, else the media id in the wix:image form the
68
+ * URL builder scales. "" when the plan has no image.
69
+ */
70
+ export function planImage(raw: Raw): string {
71
+ const img = raw.image;
72
+ if (!img) return "";
73
+ if (typeof img === "string") return img;
74
+ if (img.url) return String(img.url);
75
+ return img.id ? `wix:image://v1/${img.id}/${img.id}` : "";
76
+ }
77
+
78
+ // ---- DTO mappers -----------------------------------------------------------------------------------
79
+
80
+ /**
81
+ * The price is DISPLAY-ONLY: a decimal string at pricingVariants[0].pricingStrategies[0]
82
+ * .flatRate.amount ("0" = free), paired with plan.currency (site-derived). Wix settles the actual
83
+ * charge, tax, and schedule at the hosted checkout.
84
+ */
85
+ export function toSummary(raw: Raw, imgSrc: ImgSrc): PlanSummary {
86
+ const variant: Raw = raw.pricingVariants?.[0] ?? {};
87
+ const amount: string | undefined = variant.pricingStrategies?.[0]?.flatRate?.amount;
88
+ const free = amount == null || Number(amount) === 0;
89
+ return {
90
+ id: id(raw),
91
+ slug: raw.slug ?? "",
92
+ name: raw.name ?? "",
93
+ description: raw.description ?? "",
94
+ price: free ? "Free" : formatPrice(amount, raw.currency),
95
+ free,
96
+ billing: free ? "" : billingLabel(variant.billingTerms),
97
+ freeTrialDays: variant.freeTrialDays || null,
98
+ perks: ((raw.perks ?? []) as Raw[]).map((p) => p.description ?? "").filter((d: string) => d),
99
+ buyable: raw.buyable === true, // a PUBLIC plan can still be merchant-assigned — no CTA then
100
+ imageUrl: imgSrc(planImage(raw), 800, 800),
101
+ };
102
+ }
103
+
104
+ export function toDetail(raw: Raw, imgSrc: ImgSrc): PlanDetail {
105
+ return { ...toSummary(raw, imgSrc), termsAndConditions: raw.termsAndConditions ?? "" };
106
+ }
@@ -0,0 +1,64 @@
1
+ // The plans listing as a framework-free store — the logic behind usePlans, usable from React
2
+ // (usePlans wraps it with useSyncExternalStore), from a static page or Vue/Svelte (subscribe and
3
+ // render), or as the specification for a port. State, subscribe/getState, start/stop, emit after
4
+ // every change.
5
+ //
6
+ // SSR-friendly: seed with `initialPlans` and they render at once with no client fetch; without a
7
+ // seed `start()` fetches the public plans. A failed load leaves `plans: []` with the message in
8
+ // `error`, so the surface renders its empty state and the message, never a skeleton forever.
9
+ //
10
+ // One store per mounted listing (a pricing page and a home-page strip can coexist): createPlansStore(),
11
+ // not a singleton. It imports the data layer by names the REST twin exports identically, so the
12
+ // same file runs over the SDK in Astro and over REST on a static page.
13
+ import { fetchPlans } from "./plans";
14
+ import type { PlanSummary } from "./types";
15
+
16
+ export interface PlansStoreOptions {
17
+ initialPlans?: PlanSummary[];
18
+ }
19
+
20
+ /** Everything a pricing grid renders from. Read it with getState() or through a subscription. */
21
+ export interface PlansState {
22
+ /** null while the first load is in flight — render skeletons, not an empty state. */
23
+ plans: PlanSummary[] | null;
24
+ error: string | null;
25
+ }
26
+
27
+ export interface PlansStore {
28
+ getState(): PlansState;
29
+ subscribe(listener: () => void): () => void;
30
+ /** Fetch the plans when no `initialPlans` were given. Call once when mounted (a browser). */
31
+ start(): void;
32
+ /** Stop reacting; drop a late response. */
33
+ stop(): void;
34
+ }
35
+
36
+ export function createPlansStore({ initialPlans }: PlansStoreOptions = {}): PlansStore {
37
+ let state: PlansState = { plans: initialPlans ?? null, error: null };
38
+ let started = false;
39
+ const listeners = new Set<() => void>();
40
+
41
+ function setState(patch: Partial<PlansState>): void {
42
+ state = { ...state, ...patch };
43
+ for (const fn of listeners) fn();
44
+ }
45
+
46
+ return {
47
+ getState: () => state,
48
+ subscribe(fn) {
49
+ listeners.add(fn);
50
+ return () => listeners.delete(fn);
51
+ },
52
+ start() {
53
+ if (started) return;
54
+ started = true;
55
+ if (initialPlans) return;
56
+ fetchPlans()
57
+ .then((plans) => { if (started) setState({ plans }); })
58
+ .catch((e) => { if (started) setState({ plans: [], error: e instanceof Error ? e.message : String(e) }); });
59
+ },
60
+ stop() {
61
+ started = false;
62
+ },
63
+ };
64
+ }
@@ -1,75 +1,27 @@
1
- // Plan reads (Wix Pricing Plans, Plans V3) — the only file that touches raw plan entities.
2
- // Everything it returns is a plain DTO from ./types. Copy as-is; extend by adding functions,
3
- // not by editing these.
1
+ // Plan reads (Wix Pricing Plans, Plans V3) over the SDK — the only file that touches raw plan
2
+ // entities on this transport. Everything it returns is a plain DTO from ./types. The rules and
3
+ // mappers live in ./plans-core (shared with the REST twin in references/pricing-plans/rest/);
4
+ // this file is the transport only. Copy as-is; extend by adding functions, not by editing these.
4
5
  //
5
6
  // The read module is plansV3 — NOT `plans` (that's the V2 namespace; it has no queryPlans).
6
7
  // docs: https://dev.wix.com/docs/api-reference/business-solutions/pricing-plans/plans-v3/query-plans.md
7
8
  import { plansV3 } from "@wix/pricing-plans";
8
9
  import { wixModule } from "../sdk";
9
10
  import { imgSrc } from "../media";
11
+ import { toDetail, toSummary, type Raw } from "./plans-core";
10
12
  import type { PlanDetail, PlanSummary } from "./types";
11
13
 
12
14
  const plans = wixModule(plansV3);
13
15
 
14
- type Raw = Record<string, any>;
15
-
16
- function formatPrice(value: string | undefined, currency: string | undefined): string {
17
- if (value == null || value === "" || Number(value) === 0) return "Free";
18
- try {
19
- return new Intl.NumberFormat(undefined, { style: "currency", currency: currency || "USD" }).format(Number(value));
20
- } catch {
21
- return `${value} ${currency ?? ""}`.trim();
22
- }
23
- }
24
-
25
- // billingTerms → a display label. billingCycle.count comes back as a STRING in SDK reads
26
- // ("1") — Number() it. endType CYCLES_COMPLETED with billingCycleCount 1 bills once.
27
- function billingLabel(terms: Raw | undefined): string {
28
- const cycle = terms?.billingCycle;
29
- if (!cycle) return "one-time";
30
- const cyclesTotal = Number(terms?.cyclesCompletedDetails?.billingCycleCount ?? 0);
31
- if (terms?.endType === "CYCLES_COMPLETED" && cyclesTotal === 1) return "one-time";
32
- const count = Number(cycle.count ?? 1) || 1;
33
- const period = String(cycle.period ?? "MONTH").toLowerCase();
34
- const base = count === 1 ? `per ${period}` : `every ${count} ${period}s`;
35
- return terms?.endType === "CYCLES_COMPLETED" && cyclesTotal > 1 ? `${base} × ${cyclesTotal}` : base;
36
- }
37
-
38
- // The price is DISPLAY-ONLY: a decimal string at pricingVariants[0].pricingStrategies[0]
39
- // .flatRate.amount ("0" = free), paired with plan.currency (site-derived — never assume
40
- // USD). Wix settles the actual charge, tax, and schedule at the hosted checkout.
41
- function toSummary(raw: Raw): PlanSummary {
42
- const variant: Raw = raw.pricingVariants?.[0] ?? {};
43
- const amount: string | undefined = variant.pricingStrategies?.[0]?.flatRate?.amount;
44
- const free = amount == null || Number(amount) === 0;
45
- return {
46
- id: raw._id ?? "", // SDK entity id is _id, never id (that's the REST view)
47
- slug: raw.slug ?? "",
48
- name: raw.name ?? "",
49
- description: raw.description ?? "",
50
- price: free ? "Free" : formatPrice(amount, raw.currency),
51
- free,
52
- billing: free ? "" : billingLabel(variant.billingTerms),
53
- freeTrialDays: variant.freeTrialDays || null,
54
- perks: (raw.perks ?? []).map((p: Raw) => p.description ?? "").filter((d: string) => d),
55
- buyable: raw.buyable === true,
56
- imageUrl: imgSrc(raw.image, 800, 800),
57
- };
58
- }
59
-
60
- function toDetail(raw: Raw): PlanDetail {
61
- return { ...toSummary(raw), termsAndConditions: raw.termsAndConditions ?? "" };
62
- }
63
-
64
16
  /** List the public plans for the pricing grid, as card-ready DTOs. */
65
17
  export async function fetchPlans({ limit = 100 } = {}): Promise<PlanSummary[]> {
66
18
  const res = await plans.queryPlans().eq("visibility", "PUBLIC").limit(limit).find();
67
- return (res.items ?? []).map((p: Raw) => toSummary(p));
19
+ return (res.items ?? []).map((p) => toSummary(p as Raw, imgSrc));
68
20
  }
69
21
 
70
22
  /** Fetch one public plan by its URL slug. Null when not found. */
71
23
  export async function fetchPlanBySlug(slug: string): Promise<PlanDetail | null> {
72
24
  const res = await plans.queryPlans().eq("visibility", "PUBLIC").eq("slug", slug).limit(1).find();
73
25
  const raw = res.items?.[0];
74
- return raw ? toDetail(raw as Raw) : null;
26
+ return raw ? toDetail(raw as Raw, imgSrc) : null;
75
27
  }
@@ -0,0 +1,43 @@
1
+ // Purchase rules — transport-agnostic, imported by both ./purchase.ts (SDK) and the REST twin in
2
+ // references/pricing-plans/rest/purchase.ts (fetch). The redirect-session body for a plan and the
3
+ // reading of its result live HERE, once. Type-only imports: a strip to JS emits none.
4
+ import type { Raw } from "./plans-core";
5
+
6
+ export interface PurchaseOptions {
7
+ /**
8
+ * Success-only return URL — Wix appends `?planOrderId=<GUID>` to it. Omit → Wix shows its
9
+ * hosted thank-you page, then returns to postFlowUrl. Point it at a page of yours only if
10
+ * that page reads the param and renders a real confirmation.
11
+ */
12
+ thankYouPageUrl?: string;
13
+ /**
14
+ * Where a completed, abandoned, or interrupted flow returns (default: the current page).
15
+ * Landing here is NOT a success signal — only thankYouPageUrl carries one.
16
+ */
17
+ postFlowUrl?: string;
18
+ }
19
+
20
+ /**
21
+ * The Create Redirect Session body for a plan: `paidPlansCheckout: { planId }` plus the callbacks.
22
+ * postFlowUrl defaults to the current page (browser); a server caller passes it explicitly.
23
+ * The return origin must be the site's real https origin as registered on the OAuth app's allowed
24
+ * domains — an unlisted or http origin 403s on return.
25
+ */
26
+ export function purchaseBody(planId: string, { thankYouPageUrl, postFlowUrl }: PurchaseOptions = {}): Raw {
27
+ if (!planId) throw new Error("A plan id is required to start checkout.");
28
+ const href = typeof window !== "undefined" ? window.location.href : "";
29
+ return {
30
+ paidPlansCheckout: { planId },
31
+ callbacks: {
32
+ postFlowUrl: postFlowUrl ?? (href || undefined),
33
+ ...(thankYouPageUrl ? { thankYouPageUrl } : {}),
34
+ },
35
+ };
36
+ }
37
+
38
+ /** The hosted checkout URL out of a redirect-session response; throws when there is none. */
39
+ export function redirectUrl(session: Raw | null | undefined): string {
40
+ const url = session?.redirectSession?.fullUrl;
41
+ if (!url) throw new Error("Checkout couldn't start — please try again.");
42
+ return url;
43
+ }