@cimplify/cli 0.7.9 → 0.7.11

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 (123) hide show
  1. package/dist/{add-VVHQBW36.mjs → add-3CEDUXNO.mjs} +1 -1
  2. package/dist/chunk-DAE3YSKU.mjs +6151 -0
  3. package/dist/{chunk-YSUPIC6B.mjs → chunk-XB4MMPXC.mjs} +2 -2
  4. package/dist/{chunk-STSY4IG3.mjs → chunk-ZFCWAYK2.mjs} +1 -1
  5. package/dist/dispatcher.mjs +9 -9
  6. package/dist/{doctor-TCI6VXGG.mjs → doctor-A3YU5YOX.mjs} +2 -2
  7. package/dist/{explain-VCSTQ54F.mjs → explain-WZQTCB3C.mjs} +1 -1
  8. package/dist/{introspect-GQVDSF7A.mjs → introspect-2GJIQ6CP.mjs} +2 -2
  9. package/dist/{list-IYNO5OV6.mjs → list-F3LBI7HF.mjs} +1 -1
  10. package/dist/{update-ORQGRL6E.mjs → update-PINBW3NG.mjs} +1 -1
  11. package/package.json +2 -2
  12. package/templates/storefront-auto/app/account/addresses/actions.ts +56 -0
  13. package/templates/storefront-auto/app/account/addresses/page.tsx +178 -15
  14. package/templates/storefront-auto/app/account/orders/page.tsx +84 -15
  15. package/templates/storefront-auto/app/account/page.tsx +39 -16
  16. package/templates/storefront-auto/app/account/wallets/actions.ts +52 -0
  17. package/templates/storefront-auto/app/account/wallets/page.tsx +185 -0
  18. package/templates/storefront-auto/app/sitemap-page/page.tsx +1 -1
  19. package/templates/storefront-auto/bun.lock +8 -2
  20. package/templates/storefront-auto/lib/brand.ts +1 -1
  21. package/templates/storefront-auto/lib/cimplify-server.ts +54 -0
  22. package/templates/storefront-auto/package.json +1 -1
  23. package/templates/storefront-bakery/AGENTS.md +4 -4
  24. package/templates/storefront-bakery/app/account/addresses/actions.ts +56 -0
  25. package/templates/storefront-bakery/app/account/addresses/page.tsx +178 -15
  26. package/templates/storefront-bakery/app/account/orders/page.tsx +84 -15
  27. package/templates/storefront-bakery/app/account/page.tsx +39 -16
  28. package/templates/storefront-bakery/app/account/wallets/actions.ts +52 -0
  29. package/templates/storefront-bakery/app/account/wallets/page.tsx +185 -0
  30. package/templates/storefront-bakery/app/sitemap-page/page.tsx +1 -1
  31. package/templates/storefront-bakery/bun.lock +8 -2
  32. package/templates/storefront-bakery/lib/brand.ts +1 -1
  33. package/templates/storefront-bakery/lib/cimplify-server.ts +54 -0
  34. package/templates/storefront-bakery/package.json +1 -1
  35. package/templates/storefront-fashion/AGENTS.md +4 -4
  36. package/templates/storefront-fashion/app/account/addresses/actions.ts +56 -0
  37. package/templates/storefront-fashion/app/account/addresses/page.tsx +178 -15
  38. package/templates/storefront-fashion/app/account/orders/page.tsx +84 -15
  39. package/templates/storefront-fashion/app/account/page.tsx +39 -16
  40. package/templates/storefront-fashion/app/account/wallets/actions.ts +52 -0
  41. package/templates/storefront-fashion/app/account/wallets/page.tsx +185 -0
  42. package/templates/storefront-fashion/app/sitemap-page/page.tsx +1 -1
  43. package/templates/storefront-fashion/bun.lock +8 -2
  44. package/templates/storefront-fashion/lib/brand.ts +1 -1
  45. package/templates/storefront-fashion/lib/cimplify-server.ts +54 -0
  46. package/templates/storefront-fashion/package.json +1 -1
  47. package/templates/storefront-grocery/AGENTS.md +4 -4
  48. package/templates/storefront-grocery/app/account/addresses/actions.ts +56 -0
  49. package/templates/storefront-grocery/app/account/addresses/page.tsx +178 -15
  50. package/templates/storefront-grocery/app/account/orders/page.tsx +84 -15
  51. package/templates/storefront-grocery/app/account/page.tsx +39 -16
  52. package/templates/storefront-grocery/app/account/wallets/actions.ts +52 -0
  53. package/templates/storefront-grocery/app/account/wallets/page.tsx +185 -0
  54. package/templates/storefront-grocery/app/sitemap-page/page.tsx +1 -1
  55. package/templates/storefront-grocery/bun.lock +8 -2
  56. package/templates/storefront-grocery/lib/brand.ts +1 -1
  57. package/templates/storefront-grocery/lib/cimplify-server.ts +54 -0
  58. package/templates/storefront-grocery/package.json +1 -1
  59. package/templates/storefront-pharmacy/AGENTS.md +4 -4
  60. package/templates/storefront-pharmacy/app/account/addresses/actions.ts +56 -0
  61. package/templates/storefront-pharmacy/app/account/addresses/page.tsx +178 -15
  62. package/templates/storefront-pharmacy/app/account/orders/page.tsx +84 -15
  63. package/templates/storefront-pharmacy/app/account/page.tsx +39 -16
  64. package/templates/storefront-pharmacy/app/account/wallets/actions.ts +52 -0
  65. package/templates/storefront-pharmacy/app/account/wallets/page.tsx +185 -0
  66. package/templates/storefront-pharmacy/app/sitemap-page/page.tsx +1 -1
  67. package/templates/storefront-pharmacy/bun.lock +8 -2
  68. package/templates/storefront-pharmacy/lib/brand.ts +1 -1
  69. package/templates/storefront-pharmacy/lib/cimplify-server.ts +54 -0
  70. package/templates/storefront-pharmacy/package.json +1 -1
  71. package/templates/storefront-restaurant/AGENTS.md +4 -4
  72. package/templates/storefront-restaurant/app/account/addresses/actions.ts +56 -0
  73. package/templates/storefront-restaurant/app/account/addresses/page.tsx +178 -15
  74. package/templates/storefront-restaurant/app/account/orders/page.tsx +84 -15
  75. package/templates/storefront-restaurant/app/account/page.tsx +39 -16
  76. package/templates/storefront-restaurant/app/account/wallets/actions.ts +52 -0
  77. package/templates/storefront-restaurant/app/account/wallets/page.tsx +185 -0
  78. package/templates/storefront-restaurant/app/sitemap-page/page.tsx +1 -1
  79. package/templates/storefront-restaurant/bun.lock +8 -2
  80. package/templates/storefront-restaurant/lib/brand.ts +1 -1
  81. package/templates/storefront-restaurant/lib/cimplify-server.ts +54 -0
  82. package/templates/storefront-restaurant/package.json +1 -1
  83. package/templates/storefront-retail/AGENTS.md +4 -4
  84. package/templates/storefront-retail/app/account/addresses/actions.ts +56 -0
  85. package/templates/storefront-retail/app/account/addresses/page.tsx +178 -15
  86. package/templates/storefront-retail/app/account/orders/page.tsx +84 -15
  87. package/templates/storefront-retail/app/account/page.tsx +39 -16
  88. package/templates/storefront-retail/app/account/wallets/actions.ts +52 -0
  89. package/templates/storefront-retail/app/account/wallets/page.tsx +185 -0
  90. package/templates/storefront-retail/app/sitemap-page/page.tsx +1 -1
  91. package/templates/storefront-retail/bun.lock +8 -2
  92. package/templates/storefront-retail/lib/brand.ts +1 -1
  93. package/templates/storefront-retail/lib/cimplify-server.ts +54 -0
  94. package/templates/storefront-retail/package.json +1 -1
  95. package/templates/storefront-services/AGENTS.md +4 -4
  96. package/templates/storefront-services/app/account/addresses/actions.ts +56 -0
  97. package/templates/storefront-services/app/account/addresses/page.tsx +178 -15
  98. package/templates/storefront-services/app/account/orders/page.tsx +84 -15
  99. package/templates/storefront-services/app/account/page.tsx +39 -16
  100. package/templates/storefront-services/app/account/wallets/actions.ts +52 -0
  101. package/templates/storefront-services/app/account/wallets/page.tsx +185 -0
  102. package/templates/storefront-services/app/sitemap-page/page.tsx +1 -1
  103. package/templates/storefront-services/bun.lock +8 -2
  104. package/templates/storefront-services/lib/brand.ts +1 -1
  105. package/templates/storefront-services/lib/cimplify-server.ts +54 -0
  106. package/templates/storefront-services/package.json +1 -1
  107. package/dist/chunk-UE4YMNDU.mjs +0 -6131
  108. package/templates/storefront-auto/app/account/settings/page.tsx +0 -21
  109. package/templates/storefront-auto/components/account-iframe.tsx +0 -19
  110. package/templates/storefront-bakery/app/account/settings/page.tsx +0 -21
  111. package/templates/storefront-bakery/components/account-iframe.tsx +0 -19
  112. package/templates/storefront-fashion/app/account/settings/page.tsx +0 -21
  113. package/templates/storefront-fashion/components/account-iframe.tsx +0 -19
  114. package/templates/storefront-grocery/app/account/settings/page.tsx +0 -21
  115. package/templates/storefront-grocery/components/account-iframe.tsx +0 -19
  116. package/templates/storefront-pharmacy/app/account/settings/page.tsx +0 -21
  117. package/templates/storefront-pharmacy/components/account-iframe.tsx +0 -19
  118. package/templates/storefront-restaurant/app/account/settings/page.tsx +0 -21
  119. package/templates/storefront-restaurant/components/account-iframe.tsx +0 -19
  120. package/templates/storefront-retail/app/account/settings/page.tsx +0 -21
  121. package/templates/storefront-retail/components/account-iframe.tsx +0 -19
  122. package/templates/storefront-services/app/account/settings/page.tsx +0 -21
  123. package/templates/storefront-services/components/account-iframe.tsx +0 -19
@@ -1,21 +1,184 @@
1
- import type { Metadata } from "next";
2
- import { AccountIframe } from "@/components/account-iframe";
1
+ import {
2
+ AccountContent,
3
+ AccountHero,
4
+ AccountNav,
5
+ AccountShell,
6
+ AccountSidebar,
7
+ AccountSignedOutPrompt,
8
+ Button,
9
+ EmptyState,
10
+ Section,
11
+ } from "@cimplify/sdk/react";
12
+ import type { CustomerAddress } from "@cimplify/sdk";
3
13
  import { brand } from "@/lib/brand";
14
+ import { ACCOUNT_NAV, apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";
15
+ import {
16
+ createAddressAction,
17
+ deleteAddressAction,
18
+ setDefaultAddressAction,
19
+ } from "./actions";
4
20
 
5
- export const metadata: Metadata = {
6
- title: `Addresses ${brand.name}`,
7
- };
21
+ const CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";
22
+ const REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";
23
+
24
+ function singleLine(address: CustomerAddress): string {
25
+ return [
26
+ address.street_address,
27
+ address.apartment,
28
+ address.city,
29
+ address.region,
30
+ address.postal_code,
31
+ address.country,
32
+ ]
33
+ .filter(Boolean)
34
+ .join(", ");
35
+ }
36
+
37
+ function AddressCard({ address }: { address: CustomerAddress }) {
38
+ const isDefault = address.is_default === true;
39
+ return (
40
+ <div className="flex items-start justify-between gap-4 rounded-[14px] border border-border bg-card p-5">
41
+ <div className="min-w-0">
42
+ <div className="flex items-center gap-2 mb-1.5">
43
+ <span className="text-[14px] font-medium text-foreground">{address.label || "Address"}</span>
44
+ {isDefault && (
45
+ <span className="text-[10px] font-medium tracking-[0.08em] uppercase text-foreground/60 bg-foreground/5 px-2 py-0.5 rounded-full">
46
+ Default
47
+ </span>
48
+ )}
49
+ </div>
50
+ <p className="text-[13px] text-foreground/70 leading-relaxed truncate">
51
+ {singleLine(address)}
52
+ </p>
53
+ </div>
54
+ <div className="flex items-center gap-2 shrink-0">
55
+ {!isDefault && (
56
+ <form action={setDefaultAddressAction}>
57
+ <input type="hidden" name="address_id" value={address.id} />
58
+ <button
59
+ type="submit"
60
+ className="text-[12px] font-medium text-foreground/70 hover:text-foreground px-2 py-1"
61
+ >
62
+ Make default
63
+ </button>
64
+ </form>
65
+ )}
66
+ <form action={deleteAddressAction}>
67
+ <input type="hidden" name="address_id" value={address.id} />
68
+ <button
69
+ type="submit"
70
+ className="text-[12px] font-medium text-destructive/80 hover:text-destructive px-2 py-1"
71
+ >
72
+ Remove
73
+ </button>
74
+ </form>
75
+ </div>
76
+ </div>
77
+ );
78
+ }
79
+
80
+ function AddAddressForm() {
81
+ return (
82
+ <form
83
+ action={createAddressAction}
84
+ className="rounded-[14px] border border-dashed border-border bg-card p-5 space-y-3"
85
+ >
86
+ <h3 className="text-[13px] font-semibold tracking-[0.02em] text-foreground">Add an address</h3>
87
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
88
+ <input
89
+ name="label"
90
+ placeholder="Label (Home, Work)"
91
+ className="rounded-[10px] border border-border bg-background px-3 h-10 text-[13px]"
92
+ />
93
+ <input
94
+ name="street_address"
95
+ required
96
+ placeholder="Street address"
97
+ className="rounded-[10px] border border-border bg-background px-3 h-10 text-[13px]"
98
+ />
99
+ <input
100
+ name="apartment"
101
+ placeholder="Apartment / unit (optional)"
102
+ className="rounded-[10px] border border-border bg-background px-3 h-10 text-[13px]"
103
+ />
104
+ <input
105
+ name="city"
106
+ required
107
+ placeholder="City"
108
+ className="rounded-[10px] border border-border bg-background px-3 h-10 text-[13px]"
109
+ />
110
+ <input
111
+ name="region"
112
+ placeholder="Region / state"
113
+ className="rounded-[10px] border border-border bg-background px-3 h-10 text-[13px]"
114
+ />
115
+ <input
116
+ name="postal_code"
117
+ placeholder="Postal code"
118
+ className="rounded-[10px] border border-border bg-background px-3 h-10 text-[13px]"
119
+ />
120
+ </div>
121
+ <input
122
+ name="country"
123
+ placeholder="Country"
124
+ className="rounded-[10px] border border-border bg-background px-3 h-10 text-[13px] w-full"
125
+ />
126
+ <div className="flex justify-end">
127
+ <Button type="submit" variant="primary">
128
+ Save address
129
+ </Button>
130
+ </div>
131
+ </form>
132
+ );
133
+ }
134
+
135
+ export default async function AddressesPage() {
136
+ const session = await serverSession();
137
+ if (!session) {
138
+ return (
139
+ <AccountSignedOutPrompt
140
+ clientId={CLIENT_ID}
141
+ redirectUri={REDIRECT_URI}
142
+ eyebrow={brand.account.loginEyebrow}
143
+ title={brand.account.loginTitle}
144
+ description={brand.account.loginSubtitle}
145
+ />
146
+ );
147
+ }
148
+
149
+ const bearer = await serverAccessToken();
150
+ const addresses = (await apiFetch<CustomerAddress[]>("/v1/link/addresses", { bearer })) ?? [];
8
151
 
9
- export default function AddressesPage() {
10
152
  return (
11
- <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">
12
- <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">
13
- Account
14
- </p>
15
- <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">
16
- Your addresses
17
- </h1>
18
- <AccountIframe section="addresses" />
19
- </article>
153
+ <AccountShell
154
+ sidebar={
155
+ <AccountSidebar>
156
+ <AccountNav items={ACCOUNT_NAV} activeHref="/account/addresses" />
157
+ </AccountSidebar>
158
+ }
159
+ >
160
+ <AccountHero
161
+ eyebrow="Saved addresses"
162
+ title="Where should we deliver?"
163
+ subtitle="Saved addresses sync across every Cimplify shop you use."
164
+ />
165
+ <AccountContent>
166
+ <Section title="Your addresses" meta={`${addresses.length} saved`}>
167
+ <div className="space-y-3">
168
+ {addresses.length === 0 ? (
169
+ <EmptyState
170
+ title="No addresses yet"
171
+ description="Add a delivery address to check out faster next time."
172
+ />
173
+ ) : (
174
+ addresses.map((address) => <AddressCard key={address.id} address={address} />)
175
+ )}
176
+ </div>
177
+ </Section>
178
+ <Section title="Add another">
179
+ <AddAddressForm />
180
+ </Section>
181
+ </AccountContent>
182
+ </AccountShell>
20
183
  );
21
184
  }
@@ -1,21 +1,90 @@
1
- import type { Metadata } from "next";
2
- import { AccountIframe } from "@/components/account-iframe";
1
+ import {
2
+ AccountOrdersPage,
3
+ AccountSignedOutPrompt,
4
+ } from "@cimplify/sdk/react";
5
+ import type { OrderRowData, OrderRowStatus } from "@cimplify/sdk/react";
6
+ import type { Order, OrderStatus } from "@cimplify/sdk";
7
+ import { formatMoney } from "@cimplify/sdk";
3
8
  import { brand } from "@/lib/brand";
9
+ import { ACCOUNT_NAV, apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";
4
10
 
5
- export const metadata: Metadata = {
6
- title: `Orders ${brand.name}`,
7
- };
11
+ const CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";
12
+ const REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";
13
+
14
+ function mapStatus(status: OrderStatus): OrderRowStatus {
15
+ switch (status) {
16
+ case "in_preparation":
17
+ case "ready_to_serve":
18
+ case "partially_served":
19
+ return { kind: "preparing", label: "Preparing" };
20
+ case "delivered":
21
+ return { kind: "delivered" };
22
+ case "picked_up":
23
+ return { kind: "delivered", label: "Picked up" };
24
+ case "served":
25
+ case "completed":
26
+ return { kind: "completed" };
27
+ case "cancelled":
28
+ case "refunded":
29
+ return { kind: "cancelled", label: status === "refunded" ? "Refunded" : undefined };
30
+ case "pending":
31
+ case "created":
32
+ case "confirmed":
33
+ default:
34
+ return { kind: "active", label: "Confirmed" };
35
+ }
36
+ }
37
+
38
+ function relativeMeta(iso: string, orderNumber: string): string {
39
+ const when = new Date(iso);
40
+ const formatted = when.toLocaleDateString(undefined, {
41
+ month: "short",
42
+ day: "numeric",
43
+ year: when.getFullYear() === new Date().getFullYear() ? undefined : "numeric",
44
+ });
45
+ return `Order ${orderNumber} · ${formatted}`;
46
+ }
47
+
48
+ function toRowData(order: Order): OrderRowData {
49
+ const count = order.items?.length ?? 0;
50
+ return {
51
+ id: order.id,
52
+ href: `/account/orders/${order.id}`,
53
+ summary: count > 0 ? `${count} ${count === 1 ? "item" : "items"}` : "Order",
54
+ meta: relativeMeta(order.created_at, order.user_friendly_id ?? order.id),
55
+ status: mapStatus(order.status),
56
+ total: formatMoney(order.total_price, order.currency),
57
+ };
58
+ }
59
+
60
+ export default async function OrdersPage() {
61
+ const session = await serverSession();
62
+ if (!session) {
63
+ return (
64
+ <AccountSignedOutPrompt
65
+ clientId={CLIENT_ID}
66
+ redirectUri={REDIRECT_URI}
67
+ eyebrow={brand.account.loginEyebrow}
68
+ title={brand.account.loginTitle}
69
+ description={brand.account.loginSubtitle}
70
+ />
71
+ );
72
+ }
73
+
74
+ const bearer = await serverAccessToken();
75
+ const orders = await apiFetch<Order[]>(
76
+ `/v1/link/orders?business_id=${encodeURIComponent(brand.mock.businessId)}`,
77
+ { bearer },
78
+ );
8
79
 
9
- export default function OrdersPage() {
10
80
  return (
11
- <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">
12
- <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">
13
- Account
14
- </p>
15
- <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">
16
- Your orders
17
- </h1>
18
- <AccountIframe section="orders" />
19
- </article>
81
+ <AccountOrdersPage
82
+ customerName={session.name ?? ""}
83
+ customerEmail={session.email}
84
+ customerId={session.sub}
85
+ merchantName={brand.name}
86
+ nav={ACCOUNT_NAV}
87
+ orders={(orders ?? []).map(toRowData)}
88
+ />
20
89
  );
21
90
  }
@@ -1,22 +1,45 @@
1
- import type { Metadata } from "next";
2
- import { AccountIframe } from "@/components/account-iframe";
1
+ import {
2
+ AccountDashboardPage,
3
+ AccountSignedOutPrompt,
4
+ } from "@cimplify/sdk/react";
5
+ import type { CustomerAddress, CustomerMobileMoney } from "@cimplify/sdk";
3
6
  import { brand } from "@/lib/brand";
7
+ import { ACCOUNT_NAV, apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";
4
8
 
5
- export const metadata: Metadata = {
6
- title: `Account ${brand.name}`,
7
- description: brand.account.signupSubtitle,
8
- };
9
+ const CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";
10
+ const REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";
11
+
12
+ export default async function AccountPage() {
13
+ const session = await serverSession();
14
+ if (!session) {
15
+ return (
16
+ <AccountSignedOutPrompt
17
+ clientId={CLIENT_ID}
18
+ redirectUri={REDIRECT_URI}
19
+ eyebrow={brand.account.loginEyebrow}
20
+ title={brand.account.loginTitle}
21
+ description={brand.account.loginSubtitle}
22
+ />
23
+ );
24
+ }
25
+
26
+ const bearer = await serverAccessToken();
27
+ const [addresses, wallets] = await Promise.all([
28
+ apiFetch<CustomerAddress[]>("/v1/link/addresses", { bearer }),
29
+ apiFetch<CustomerMobileMoney[]>("/v1/link/mobile-money", { bearer }),
30
+ ]);
9
31
 
10
- export default function AccountPage() {
11
32
  return (
12
- <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">
13
- <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">
14
- {brand.account.accountEyebrow}
15
- </p>
16
- <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">
17
- {brand.account.accountTitle}
18
- </h1>
19
- <AccountIframe />
20
- </article>
33
+ <AccountDashboardPage
34
+ customerName={session.name ?? ""}
35
+ customerEmail={session.email}
36
+ customerId={session.sub}
37
+ merchantName={brand.name}
38
+ nav={ACCOUNT_NAV}
39
+ identitySummary={{
40
+ addresses: addresses?.length ?? 0,
41
+ paymentMethods: wallets?.length ?? 0,
42
+ }}
43
+ />
21
44
  );
22
45
  }
@@ -0,0 +1,52 @@
1
+ "use server";
2
+
3
+ import { revalidatePath } from "next/cache";
4
+ import { apiFetch, serverAccessToken } from "@/lib/cimplify-server";
5
+ import type { CustomerMobileMoney, MobileMoneyProvider } from "@cimplify/sdk";
6
+
7
+ const ACCOUNT_WALLETS = "/account/wallets";
8
+
9
+ function required(form: FormData, key: string): string {
10
+ const value = String(form.get(key) ?? "").trim();
11
+ if (!value) throw new Error(`Missing required field: ${key}`);
12
+ return value;
13
+ }
14
+
15
+ function optional(form: FormData, key: string): string | undefined {
16
+ const value = String(form.get(key) ?? "").trim();
17
+ return value || undefined;
18
+ }
19
+
20
+ export async function createWalletAction(form: FormData): Promise<void> {
21
+ const bearer = await serverAccessToken();
22
+ await apiFetch<CustomerMobileMoney>("/v1/link/mobile-money", {
23
+ bearer,
24
+ method: "POST",
25
+ body: JSON.stringify({
26
+ phone_number: required(form, "phone_number"),
27
+ provider: required(form, "provider") as MobileMoneyProvider,
28
+ label: optional(form, "label"),
29
+ }),
30
+ });
31
+ revalidatePath(ACCOUNT_WALLETS);
32
+ }
33
+
34
+ export async function deleteWalletAction(form: FormData): Promise<void> {
35
+ const bearer = await serverAccessToken();
36
+ const id = required(form, "mobile_money_id");
37
+ await apiFetch<unknown>(`/v1/link/mobile-money/${encodeURIComponent(id)}`, {
38
+ bearer,
39
+ method: "DELETE",
40
+ });
41
+ revalidatePath(ACCOUNT_WALLETS);
42
+ }
43
+
44
+ export async function setDefaultWalletAction(form: FormData): Promise<void> {
45
+ const bearer = await serverAccessToken();
46
+ const id = required(form, "mobile_money_id");
47
+ await apiFetch<unknown>(`/v1/link/mobile-money/${encodeURIComponent(id)}/default`, {
48
+ bearer,
49
+ method: "POST",
50
+ });
51
+ revalidatePath(ACCOUNT_WALLETS);
52
+ }
@@ -0,0 +1,185 @@
1
+ import {
2
+ AccountContent,
3
+ AccountHero,
4
+ AccountNav,
5
+ AccountShell,
6
+ AccountSidebar,
7
+ AccountSignedOutPrompt,
8
+ Button,
9
+ EmptyState,
10
+ Section,
11
+ } from "@cimplify/sdk/react";
12
+ import type { CustomerMobileMoney } from "@cimplify/sdk";
13
+ import { MOBILE_MONEY_PROVIDER } from "@cimplify/sdk";
14
+ import { brand } from "@/lib/brand";
15
+ import { ACCOUNT_NAV, apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";
16
+ import {
17
+ createWalletAction,
18
+ deleteWalletAction,
19
+ setDefaultWalletAction,
20
+ } from "./actions";
21
+
22
+ const CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";
23
+ const REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";
24
+
25
+ const PROVIDER_LABEL: Record<string, string> = {
26
+ [MOBILE_MONEY_PROVIDER.MTN]: "MTN",
27
+ [MOBILE_MONEY_PROVIDER.VODAFONE]: "Vodafone",
28
+ [MOBILE_MONEY_PROVIDER.TELECEL]: "Telecel",
29
+ [MOBILE_MONEY_PROVIDER.AIRTEL]: "Airtel",
30
+ [MOBILE_MONEY_PROVIDER.AIRTELTIGO]: "AirtelTigo",
31
+ [MOBILE_MONEY_PROVIDER.MPESA]: "M-Pesa",
32
+ };
33
+
34
+ function maskedPhone(phone: string): string {
35
+ const digits = phone.replace(/\D/g, "");
36
+ if (digits.length < 4) return phone;
37
+ return `${phone.slice(0, 4)} ••• ${digits.slice(-3)}`;
38
+ }
39
+
40
+ function WalletCard({ wallet }: { wallet: CustomerMobileMoney }) {
41
+ const isDefault = wallet.is_default === true;
42
+ const providerLabel = PROVIDER_LABEL[wallet.provider] ?? wallet.provider;
43
+ return (
44
+ <div className="flex items-start justify-between gap-4 rounded-[14px] border border-border bg-card p-5">
45
+ <div className="min-w-0">
46
+ <div className="flex items-center gap-2 mb-1.5">
47
+ <span className="text-[14px] font-medium text-foreground">
48
+ {wallet.label || providerLabel}
49
+ </span>
50
+ {isDefault && (
51
+ <span className="text-[10px] font-medium tracking-[0.08em] uppercase text-foreground/60 bg-foreground/5 px-2 py-0.5 rounded-full">
52
+ Default
53
+ </span>
54
+ )}
55
+ </div>
56
+ <p className="text-[13px] text-foreground/70 leading-relaxed">
57
+ {providerLabel} · {maskedPhone(wallet.phone_number)}
58
+ </p>
59
+ </div>
60
+ <div className="flex items-center gap-2 shrink-0">
61
+ {!isDefault && (
62
+ <form action={setDefaultWalletAction}>
63
+ <input type="hidden" name="mobile_money_id" value={wallet.id} />
64
+ <button
65
+ type="submit"
66
+ className="text-[12px] font-medium text-foreground/70 hover:text-foreground px-2 py-1"
67
+ >
68
+ Make default
69
+ </button>
70
+ </form>
71
+ )}
72
+ <form action={deleteWalletAction}>
73
+ <input type="hidden" name="mobile_money_id" value={wallet.id} />
74
+ <button
75
+ type="submit"
76
+ className="text-[12px] font-medium text-destructive/80 hover:text-destructive px-2 py-1"
77
+ >
78
+ Remove
79
+ </button>
80
+ </form>
81
+ </div>
82
+ </div>
83
+ );
84
+ }
85
+
86
+ function AddWalletForm() {
87
+ return (
88
+ <form
89
+ action={createWalletAction}
90
+ className="rounded-[14px] border border-dashed border-border bg-card p-5 space-y-3"
91
+ >
92
+ <h3 className="text-[13px] font-semibold tracking-[0.02em] text-foreground">
93
+ Add a mobile money wallet
94
+ </h3>
95
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
96
+ <select
97
+ name="provider"
98
+ required
99
+ defaultValue=""
100
+ className="rounded-[10px] border border-border bg-background px-3 h-10 text-[13px]"
101
+ >
102
+ <option value="" disabled>
103
+ Provider
104
+ </option>
105
+ {Object.entries(PROVIDER_LABEL).map(([value, label]) => (
106
+ <option key={value} value={value}>
107
+ {label}
108
+ </option>
109
+ ))}
110
+ </select>
111
+ <input
112
+ name="phone_number"
113
+ required
114
+ type="tel"
115
+ inputMode="tel"
116
+ autoComplete="tel"
117
+ placeholder="+233 24 400 0000"
118
+ className="rounded-[10px] border border-border bg-background px-3 h-10 text-[13px]"
119
+ />
120
+ </div>
121
+ <input
122
+ name="label"
123
+ placeholder="Label (Personal, Business — optional)"
124
+ className="rounded-[10px] border border-border bg-background px-3 h-10 text-[13px] w-full"
125
+ />
126
+ <div className="flex justify-end">
127
+ <Button type="submit" variant="primary">
128
+ Save wallet
129
+ </Button>
130
+ </div>
131
+ </form>
132
+ );
133
+ }
134
+
135
+ export default async function WalletsPage() {
136
+ const session = await serverSession();
137
+ if (!session) {
138
+ return (
139
+ <AccountSignedOutPrompt
140
+ clientId={CLIENT_ID}
141
+ redirectUri={REDIRECT_URI}
142
+ eyebrow={brand.account.loginEyebrow}
143
+ title={brand.account.loginTitle}
144
+ description={brand.account.loginSubtitle}
145
+ />
146
+ );
147
+ }
148
+
149
+ const bearer = await serverAccessToken();
150
+ const wallets =
151
+ (await apiFetch<CustomerMobileMoney[]>("/v1/link/mobile-money", { bearer })) ?? [];
152
+
153
+ return (
154
+ <AccountShell
155
+ sidebar={
156
+ <AccountSidebar>
157
+ <AccountNav items={ACCOUNT_NAV} activeHref="/account/wallets" />
158
+ </AccountSidebar>
159
+ }
160
+ >
161
+ <AccountHero
162
+ eyebrow="Wallets"
163
+ title="How would you like to pay?"
164
+ subtitle="Saved wallets work across every Cimplify shop you use."
165
+ />
166
+ <AccountContent>
167
+ <Section title="Your wallets" meta={`${wallets.length} saved`}>
168
+ <div className="space-y-3">
169
+ {wallets.length === 0 ? (
170
+ <EmptyState
171
+ title="No wallets yet"
172
+ description="Add a mobile money wallet to skip the number entry next checkout."
173
+ />
174
+ ) : (
175
+ wallets.map((wallet) => <WalletCard key={wallet.id} wallet={wallet} />)
176
+ )}
177
+ </div>
178
+ </Section>
179
+ <Section title="Add another">
180
+ <AddWalletForm />
181
+ </Section>
182
+ </AccountContent>
183
+ </AccountShell>
184
+ );
185
+ }
@@ -55,7 +55,7 @@ const STATIC_LINKS: { title: string; links: { href: string; label: string }[] }[
55
55
  { href: "/account", label: "Account" },
56
56
  { href: "/account/orders", label: "Orders" },
57
57
  { href: "/account/addresses", label: "Addresses" },
58
- { href: "/account/settings", label: "Settings" },
58
+ { href: "/account/wallets", label: "Wallets" },
59
59
  { href: "/login", label: "Sign in" },
60
60
  { href: "/signup", label: "Create account" },
61
61
  { href: "/track-order", label: "Track an order" },
@@ -5,7 +5,7 @@
5
5
  "": {
6
6
  "name": "__STOREFRONT_NAME__",
7
7
  "dependencies": {
8
- "@cimplify/sdk": "^0.54.0",
8
+ "@cimplify/sdk": "^0.64.1",
9
9
  "next": "^16.2.6",
10
10
  "react": "^19.0.0",
11
11
  "react-dom": "^19.0.0",
@@ -32,7 +32,7 @@
32
32
 
33
33
  "@base-ui/utils": ["@base-ui/utils@0.2.9", "", { "dependencies": { "@babel/runtime": "^7.29.2", "@floating-ui/utils": "^0.2.11", "reselect": "^5.1.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-x/PDDCYzoqPpjrdyb3VcyylTI2IjUXEtYDGi5foh7KsnmNJIIaVwA2GLgDH1dps1GgXiJbA60hM+AyuTfQzIvw=="],
34
34
 
35
- "@cimplify/sdk": ["@cimplify/sdk@0.54.0", "", { "dependencies": { "@base-ui/react": "^1.4.1", "clsx": "^2.1.1", "date-fns": "^4.1.0", "libphonenumber-js": "1.12.41", "react-day-picker": "^9.14.0", "tailwind-merge": "^3.5.0", "zod": "^4.4.3" }, "peerDependencies": { "@paystack/inline-js": "^2.22.8", "msw": ">=2.0.0", "react": ">=17.0.0", "vitest": ">=2.0.0" }, "optionalPeers": ["@paystack/inline-js", "msw", "react", "vitest"], "bin": { "cimplify-mock": "dist/mock/cli.mjs" } }, "sha512-YUN/lOqViHci1v9USWWwAWty29pEXBdLTuKarSl8EpafWh5jkdGZ1Q+Ne9/bfgTZmR8iTUltA8U7glV2FoRSgg=="],
35
+ "@cimplify/sdk": ["@cimplify/sdk@0.64.1", "", { "dependencies": { "@base-ui/react": "^1.4.1", "clsx": "^2.1.1", "date-fns": "^4.1.0", "jose": "^6.2.3", "libphonenumber-js": "1.12.41", "react-day-picker": "^9.14.0", "tailwind-merge": "^3.5.0", "xss": "^1.0.15", "zod": "^4.4.3" }, "peerDependencies": { "@paystack/inline-js": "^2.22.8", "msw": ">=2.0.0", "react": ">=17.0.0", "vitest": ">=2.0.0" }, "optionalPeers": ["@paystack/inline-js", "msw", "react", "vitest"], "bin": { "cimplify-mock": "dist/mock/cli.mjs" } }, "sha512-qY3ngAxAyeRwHR5jJZ7eeeNKgvayZlVUuNglp779bJiwtwL3cZ8Ux4L+k3SgRzgAxtKz9YU2mmVBZxEu14frUw=="],
36
36
 
37
37
  "@date-fns/tz": ["@date-fns/tz@1.4.1", "", {}, "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA=="],
38
38
 
@@ -316,6 +316,8 @@
316
316
 
317
317
  "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
318
318
 
319
+ "cssfilter": ["cssfilter@0.0.10", "", {}, "sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw=="],
320
+
319
321
  "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
320
322
 
321
323
  "date-fns": ["date-fns@4.2.1", "", {}, "sha512-37RhSdxaG1suen6VDCza6rNrQfooyQh57HFVPwQGEq2QWliVLzPQZ8Oa017weOu+HZCnzI7N3Pf/wyoBKfEqrA=="],
@@ -352,6 +354,8 @@
352
354
 
353
355
  "jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="],
354
356
 
357
+ "jose": ["jose@6.2.3", "", {}, "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw=="],
358
+
355
359
  "libphonenumber-js": ["libphonenumber-js@1.12.41", "", {}, "sha512-lsmMmGXBxXIK/VMLEj0kL6MtUs1kBGj1nTCzi6zgQoG1DEwqwt2DQyHxcLykceIxAnfE3hya7NuIh6PpC6S3fA=="],
356
360
 
357
361
  "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
@@ -474,6 +478,8 @@
474
478
 
475
479
  "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
476
480
 
481
+ "xss": ["xss@1.0.15", "", { "dependencies": { "commander": "^2.20.3", "cssfilter": "0.0.10" }, "bin": { "xss": "bin/xss" } }, "sha512-FVdlVVC67WOIPvfOwhoMETV72f6GbW7aOabBC3WxN/oUdoEMDyLz4OgRv5/gck2ZeNqEQu+Tb0kloovXOfpYVg=="],
482
+
477
483
  "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
478
484
 
479
485
  "yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="],
@@ -490,7 +490,7 @@ export const brand: Brand = {
490
490
  { label: "Sign in", href: "/login" },
491
491
  { label: "Create account", href: "/signup" },
492
492
  { label: "Your orders", href: "/account/orders" },
493
- { label: "Settings", href: "/account/settings" },
493
+ { label: "Wallets", href: "/account/wallets" },
494
494
  ],
495
495
  },
496
496
  {