@wix/himalaya-cli 0.1378.0 → 0.1380.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/app-info/src/index.d.ts +4 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/ecom/src/extensions.d.ts +113 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/app-info.mjs +2 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/iap-checkout.mjs +2 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/screens.mjs +2 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-ecom.mjs +77 -0
- package/dist/cli.mjs +23 -24
- package/dist/preview-assets.generated.bin +0 -0
- package/guides/importable-sdks.md +1 -1
- package/guides/sdk-reference.json +128 -2
- package/guides/wix-api-usage.md +2 -0
- package/package.json +2 -2
- package/templates/beauty-salon/config.ts +23 -0
- package/templates/beauty-salon/dev/index.ts +2611 -1
- package/templates/beauty-salon/himalaya.content.json +9 -0
- package/templates/beauty-salon/tier5-src/_shared/branded-lite-hosted.ts +3 -0
- package/templates/beauty-salon/tier5-src/_shared/member-access.ts +14 -0
- package/templates/beauty-salon/tier5-src/_shared/member-account-routes.ts +43 -0
- package/templates/beauty-salon/tier5-src/_shared/member-address.ts +129 -0
- package/templates/beauty-salon/tier5-src/_shared/saved-card.ts +20 -0
- package/templates/beauty-salon/tier5-src/himi-branded-lite-address/index.ts +223 -0
- package/templates/beauty-salon/tier5-src/himi-branded-lite-addresses/index.ts +202 -0
- package/templates/beauty-salon/tier5-src/himi-branded-lite-wallet/index.ts +217 -0
- package/templates/beauty-salon/tier5-src/himi-store-account/index.ts +17 -0
- package/templates/beauty-salon-owner/dev/index.ts +5 -27
- package/templates/beauty-salon-owner/tier5-src/_shared/aria-chat.ts +99 -40
- package/templates/beauty-salon-owner/tier5-src/_shared/aria-widget-oauth.ts +42 -0
- package/templates/beauty-salon-owner/tier5-src/_shared/aria-widget-view.ts +6 -0
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-aria/index.ts +12 -1
- package/templates/branded-lite/config.ts +23 -0
- package/templates/branded-lite/dev/index.ts +2614 -2
- package/templates/branded-lite/himalaya.content.json +9 -0
- package/templates/branded-lite/tier5-src/_shared/branded-lite-hosted.ts +3 -0
- package/templates/branded-lite/tier5-src/_shared/member-access.ts +14 -0
- package/templates/branded-lite/tier5-src/_shared/member-account-routes.ts +43 -0
- package/templates/branded-lite/tier5-src/_shared/member-address.ts +129 -0
- package/templates/branded-lite/tier5-src/_shared/saved-card.ts +20 -0
- package/templates/branded-lite/tier5-src/himi-branded-lite-address/index.ts +223 -0
- package/templates/branded-lite/tier5-src/himi-branded-lite-addresses/index.ts +202 -0
- package/templates/branded-lite/tier5-src/himi-branded-lite-wallet/index.ts +217 -0
- package/templates/branded-lite/tier5-src/himi-store-account/index.ts +17 -0
- package/templates/community-club/dev/index.ts +3 -1
- package/templates/community-club/tier5-src/_shared/branded-lite-hosted.ts +3 -0
- package/templates/community-club/tier5-src/_shared/member-access.ts +14 -0
- package/templates/community-club/tier5-src/_shared/member-account-routes.ts +43 -0
- package/templates/community-club/tier5-src/himi-store-account/index.ts +17 -0
- package/templates/community-club-owner/dev/index.ts +5 -27
- package/templates/community-club-owner/tier5-src/_shared/aria-chat.ts +99 -40
- package/templates/community-club-owner/tier5-src/_shared/aria-widget-oauth.ts +42 -0
- package/templates/community-club-owner/tier5-src/_shared/aria-widget-view.ts +6 -0
- package/templates/community-club-owner/tier5-src/himi-owner-lite-aria/index.ts +12 -1
- package/templates/events-meetups/dev/index.ts +3 -1
- package/templates/events-meetups/tier5-src/_shared/branded-lite-hosted.ts +3 -0
- package/templates/events-meetups/tier5-src/_shared/member-access.ts +14 -0
- package/templates/events-meetups/tier5-src/_shared/member-account-routes.ts +43 -0
- package/templates/events-meetups/tier5-src/himi-store-account/index.ts +17 -0
- package/templates/events-meetups-owner/dev/index.ts +5 -27
- package/templates/events-meetups-owner/tier5-src/_shared/aria-chat.ts +99 -40
- package/templates/events-meetups-owner/tier5-src/_shared/aria-widget-oauth.ts +42 -0
- package/templates/events-meetups-owner/tier5-src/_shared/aria-widget-view.ts +6 -0
- package/templates/events-meetups-owner/tier5-src/himi-owner-lite-aria/index.ts +12 -1
- package/templates/gym-fitness/config.ts +23 -0
- package/templates/gym-fitness/dev/index.ts +2611 -1
- package/templates/gym-fitness/himalaya.content.json +9 -0
- package/templates/gym-fitness/tier5-src/_shared/branded-lite-hosted.ts +3 -0
- package/templates/gym-fitness/tier5-src/_shared/member-access.ts +14 -0
- package/templates/gym-fitness/tier5-src/_shared/member-account-routes.ts +43 -0
- package/templates/gym-fitness/tier5-src/_shared/member-address.ts +129 -0
- package/templates/gym-fitness/tier5-src/_shared/saved-card.ts +20 -0
- package/templates/gym-fitness/tier5-src/himi-branded-lite-address/index.ts +223 -0
- package/templates/gym-fitness/tier5-src/himi-branded-lite-addresses/index.ts +202 -0
- package/templates/gym-fitness/tier5-src/himi-branded-lite-wallet/index.ts +217 -0
- package/templates/gym-fitness/tier5-src/himi-store-account/index.ts +17 -0
- package/templates/gym-fitness-owner/dev/index.ts +5 -27
- package/templates/gym-fitness-owner/tier5-src/_shared/aria-chat.ts +99 -40
- package/templates/gym-fitness-owner/tier5-src/_shared/aria-widget-oauth.ts +42 -0
- package/templates/gym-fitness-owner/tier5-src/_shared/aria-widget-view.ts +6 -0
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-aria/index.ts +12 -1
- package/templates/nonprofit-cause/config.ts +7 -0
- package/templates/nonprofit-cause/dev/index.ts +762 -1
- package/templates/nonprofit-cause/himalaya.content.json +3 -0
- package/templates/nonprofit-cause/tier5-src/_shared/branded-lite-hosted.ts +3 -0
- package/templates/nonprofit-cause/tier5-src/_shared/member-access.ts +14 -0
- package/templates/nonprofit-cause/tier5-src/_shared/member-account-routes.ts +43 -0
- package/templates/nonprofit-cause/tier5-src/_shared/saved-card.ts +20 -0
- package/templates/nonprofit-cause/tier5-src/himi-branded-lite-wallet/index.ts +217 -0
- package/templates/nonprofit-cause/tier5-src/himi-store-account/index.ts +17 -0
- package/templates/nonprofit-cause-owner/dev/index.ts +5 -27
- package/templates/nonprofit-cause-owner/tier5-src/_shared/aria-chat.ts +99 -40
- package/templates/nonprofit-cause-owner/tier5-src/_shared/aria-widget-oauth.ts +42 -0
- package/templates/nonprofit-cause-owner/tier5-src/_shared/aria-widget-view.ts +6 -0
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-aria/index.ts +12 -1
- package/templates/owner-lite/dev/index.ts +5 -27
- package/templates/owner-lite/tier5-src/_shared/aria-chat.ts +99 -40
- package/templates/owner-lite/tier5-src/_shared/aria-widget-oauth.ts +42 -0
- package/templates/owner-lite/tier5-src/_shared/aria-widget-view.ts +6 -0
- package/templates/owner-lite/tier5-src/himi-owner-lite-aria/index.ts +12 -1
- package/templates/podcast-creator/config.ts +7 -0
- package/templates/podcast-creator/dev/index.ts +762 -1
- package/templates/podcast-creator/himalaya.content.json +3 -0
- package/templates/podcast-creator/tier5-src/_shared/branded-lite-hosted.ts +3 -0
- package/templates/podcast-creator/tier5-src/_shared/member-access.ts +14 -0
- package/templates/podcast-creator/tier5-src/_shared/member-account-routes.ts +43 -0
- package/templates/podcast-creator/tier5-src/_shared/saved-card.ts +20 -0
- package/templates/podcast-creator/tier5-src/himi-branded-lite-wallet/index.ts +217 -0
- package/templates/podcast-creator/tier5-src/himi-store-account/index.ts +17 -0
- package/templates/podcast-creator-owner/dev/index.ts +5 -27
- package/templates/podcast-creator-owner/tier5-src/_shared/aria-chat.ts +99 -40
- package/templates/podcast-creator-owner/tier5-src/_shared/aria-widget-oauth.ts +42 -0
- package/templates/podcast-creator-owner/tier5-src/_shared/aria-widget-view.ts +6 -0
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-aria/index.ts +12 -1
- package/templates/portfolio-creative/config.ts +23 -0
- package/templates/portfolio-creative/dev/index.ts +2611 -1
- package/templates/portfolio-creative/himalaya.content.json +9 -0
- package/templates/portfolio-creative/tier5-src/_shared/branded-lite-hosted.ts +3 -0
- package/templates/portfolio-creative/tier5-src/_shared/member-access.ts +14 -0
- package/templates/portfolio-creative/tier5-src/_shared/member-account-routes.ts +43 -0
- package/templates/portfolio-creative/tier5-src/_shared/member-address.ts +129 -0
- package/templates/portfolio-creative/tier5-src/_shared/saved-card.ts +20 -0
- package/templates/portfolio-creative/tier5-src/himi-branded-lite-address/index.ts +223 -0
- package/templates/portfolio-creative/tier5-src/himi-branded-lite-addresses/index.ts +202 -0
- package/templates/portfolio-creative/tier5-src/himi-branded-lite-wallet/index.ts +217 -0
- package/templates/portfolio-creative/tier5-src/himi-store-account/index.ts +17 -0
- package/templates/portfolio-creative-owner/dev/index.ts +5 -27
- package/templates/portfolio-creative-owner/tier5-src/_shared/aria-chat.ts +99 -40
- package/templates/portfolio-creative-owner/tier5-src/_shared/aria-widget-oauth.ts +42 -0
- package/templates/portfolio-creative-owner/tier5-src/_shared/aria-widget-view.ts +6 -0
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-aria/index.ts +12 -1
- package/templates/restaurant/config.ts +23 -0
- package/templates/restaurant/dev/index.ts +2611 -1
- package/templates/restaurant/himalaya.content.json +9 -0
- package/templates/restaurant/tier5-src/_shared/branded-lite-hosted.ts +3 -0
- package/templates/restaurant/tier5-src/_shared/member-access.ts +14 -0
- package/templates/restaurant/tier5-src/_shared/member-account-routes.ts +43 -0
- package/templates/restaurant/tier5-src/_shared/member-address.ts +129 -0
- package/templates/restaurant/tier5-src/_shared/saved-card.ts +20 -0
- package/templates/restaurant/tier5-src/himi-branded-lite-address/index.ts +223 -0
- package/templates/restaurant/tier5-src/himi-branded-lite-addresses/index.ts +202 -0
- package/templates/restaurant/tier5-src/himi-branded-lite-wallet/index.ts +217 -0
- package/templates/restaurant/tier5-src/himi-store-account/index.ts +17 -0
- package/templates/restaurant-owner/dev/index.ts +5 -27
- package/templates/restaurant-owner/tier5-src/_shared/aria-chat.ts +99 -40
- package/templates/restaurant-owner/tier5-src/_shared/aria-widget-oauth.ts +42 -0
- package/templates/restaurant-owner/tier5-src/_shared/aria-widget-view.ts +6 -0
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-aria/index.ts +12 -1
- package/templates/retail-store/config.ts +23 -0
- package/templates/retail-store/dev/index.ts +2611 -1
- package/templates/retail-store/himalaya.content.json +9 -0
- package/templates/retail-store/tier5-src/_shared/branded-lite-hosted.ts +3 -0
- package/templates/retail-store/tier5-src/_shared/member-access.ts +14 -0
- package/templates/retail-store/tier5-src/_shared/member-account-routes.ts +43 -0
- package/templates/retail-store/tier5-src/_shared/member-address.ts +129 -0
- package/templates/retail-store/tier5-src/_shared/saved-card.ts +20 -0
- package/templates/retail-store/tier5-src/himi-branded-lite-address/index.ts +223 -0
- package/templates/retail-store/tier5-src/himi-branded-lite-addresses/index.ts +202 -0
- package/templates/retail-store/tier5-src/himi-branded-lite-wallet/index.ts +217 -0
- package/templates/retail-store/tier5-src/himi-store-account/index.ts +17 -0
- package/templates/retail-store-owner/dev/index.ts +5 -27
- package/templates/retail-store-owner/tier5-src/_shared/aria-chat.ts +99 -40
- package/templates/retail-store-owner/tier5-src/_shared/aria-widget-oauth.ts +42 -0
- package/templates/retail-store-owner/tier5-src/_shared/aria-widget-view.ts +6 -0
- package/templates/retail-store-owner/tier5-src/himi-owner-lite-aria/index.ts +12 -1
- package/templates/yoga-studio/config.ts +7 -0
- package/templates/yoga-studio/dev/index.ts +762 -1
- package/templates/yoga-studio/himalaya.content.json +3 -0
- package/templates/yoga-studio/tier5-src/_shared/branded-lite-hosted.ts +3 -0
- package/templates/yoga-studio/tier5-src/_shared/member-access.ts +14 -0
- package/templates/yoga-studio/tier5-src/_shared/member-account-routes.ts +43 -0
- package/templates/yoga-studio/tier5-src/_shared/saved-card.ts +20 -0
- package/templates/yoga-studio/tier5-src/himi-branded-lite-wallet/index.ts +217 -0
- package/templates/yoga-studio/tier5-src/himi-store-account/index.ts +17 -0
- package/templates/yoga-studio-owner/dev/index.ts +5 -27
- package/templates/yoga-studio-owner/tier5-src/_shared/aria-chat.ts +99 -40
- package/templates/yoga-studio-owner/tier5-src/_shared/aria-widget-oauth.ts +42 -0
- package/templates/yoga-studio-owner/tier5-src/_shared/aria-widget-view.ts +6 -0
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-aria/index.ts +12 -1
|
@@ -4,6 +4,12 @@
|
|
|
4
4
|
"himi_account": {
|
|
5
5
|
"displayName": "Profile"
|
|
6
6
|
},
|
|
7
|
+
"himi_address": {
|
|
8
|
+
"displayName": "Address"
|
|
9
|
+
},
|
|
10
|
+
"himi_addresses": {
|
|
11
|
+
"displayName": "Addresses"
|
|
12
|
+
},
|
|
7
13
|
"himi_app_info": {
|
|
8
14
|
"displayName": "App Info"
|
|
9
15
|
},
|
|
@@ -247,6 +253,9 @@
|
|
|
247
253
|
"himi_view_profile": {
|
|
248
254
|
"displayName": "View Profile"
|
|
249
255
|
},
|
|
256
|
+
"himi_wallet": {
|
|
257
|
+
"displayName": "Wallet"
|
|
258
|
+
},
|
|
250
259
|
"himi_whats_new": {
|
|
251
260
|
"displayName": "Whats New"
|
|
252
261
|
}
|
|
@@ -65,6 +65,8 @@ export const VERTICAL_ROOT_SCREENS: Readonly<Record<string, readonly string[]>>
|
|
|
65
65
|
* (`brandedLiteScreenOwners`, checked by the app's hosted-mode tests), so a screen added
|
|
66
66
|
* to a vertical without being listed here fails the build. */
|
|
67
67
|
export const HOSTED_SCREEN_VERTICALS: Readonly<Record<string, readonly BrandedLiteVertical[]>> = {
|
|
68
|
+
himi_address: ["restaurants", "store"],
|
|
69
|
+
himi_addresses: ["restaurants", "store"],
|
|
68
70
|
himi_badge: ["badges"],
|
|
69
71
|
himi_badges: ["badges"],
|
|
70
72
|
himi_blog: ["blog"],
|
|
@@ -127,6 +129,7 @@ export const HOSTED_SCREEN_VERTICALS: Readonly<Record<string, readonly BrandedLi
|
|
|
127
129
|
himi_video: ["videos"],
|
|
128
130
|
himi_video_channel: ["videos"],
|
|
129
131
|
himi_videos: ["videos"],
|
|
132
|
+
himi_wallet: ["bookings", "plans", "restaurants", "store"],
|
|
130
133
|
};
|
|
131
134
|
|
|
132
135
|
/** The templated (`<prefix>[_/]<id>`) detail routes each vertical owns, pinned the same way. */
|
|
@@ -36,3 +36,17 @@ export async function assertMemberAccess(member: MemberAccess): Promise<void> {
|
|
|
36
36
|
export function memberAccessDenied(error: unknown): boolean {
|
|
37
37
|
return (error as { name?: string } | null)?.name === "MemberAccessChanged" || /(?:HTTP\s*(?:401|403)|READ_ORDER_FORBIDDEN|permission denied|forbidden)/i.test(String(error));
|
|
38
38
|
}
|
|
39
|
+
|
|
40
|
+
/** Did this write fail because the member changed (signed out, or another account)? Then
|
|
41
|
+
* the screen reloads for whoever is signed in now. A 401/403 for the SAME member is only a
|
|
42
|
+
* failed write: the caller keeps what the member typed and says so. */
|
|
43
|
+
export async function memberChanged(member: MemberAccess | undefined, error: unknown): Promise<boolean> {
|
|
44
|
+
if ((error as { name?: string } | null)?.name === "MemberAccessChanged") return true;
|
|
45
|
+
if (!member || !memberAccessDenied(error)) return false;
|
|
46
|
+
try {
|
|
47
|
+
await assertMemberAccess(member);
|
|
48
|
+
return false;
|
|
49
|
+
} catch {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Can this host reach the member account routes My Addresses and My Wallet need?
|
|
2
|
+
//
|
|
3
|
+
// Both are served OFF www.wixapis.com (the address book on ecom.wix.com, saved cards on
|
|
4
|
+
// the member's own site), so only a native runtime whose WixApiCapability pins them can
|
|
5
|
+
// call them: HimalayaCore 1.4.0 and later. The web bridges refuse absolute URLs, and a
|
|
6
|
+
// hosted app (Spaces) signs its calls with a capability pinned to www.wixapis.com. Saved
|
|
7
|
+
// cards also need the shell to have bound its own site (app_info `memberSiteBound`): a
|
|
8
|
+
// multi-app host such as Himi Player binds none, so its wallet route has no host. The
|
|
9
|
+
// Account screen hides a link whose route is unreachable, and the screen says so instead
|
|
10
|
+
// of failing a load.
|
|
11
|
+
import { get as getAppInfo } from "@himalaya/app-info";
|
|
12
|
+
import { isHostedWorker } from "./hosted-checkout.js";
|
|
13
|
+
|
|
14
|
+
/** The HimalayaCore version whose WixApiCapability pins these routes. Checked here, at run
|
|
15
|
+
* time: the screens declare no `minBinaryVersion`, because a release takes its highest
|
|
16
|
+
* screen floor as its own and would then refuse every older binary. */
|
|
17
|
+
export const MEMBER_ACCOUNT_ROUTES_MIN_BINARY = "1.4.0";
|
|
18
|
+
|
|
19
|
+
function atLeast(version: string, floor: string): boolean {
|
|
20
|
+
const parts = (v: string) => v.split("-")[0].split(".").map((p) => Number.parseInt(p, 10) || 0);
|
|
21
|
+
const a = parts(version), b = parts(floor);
|
|
22
|
+
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
|
23
|
+
const x = a[i] ?? 0, y = b[i] ?? 0;
|
|
24
|
+
if (x !== y) return x > y;
|
|
25
|
+
}
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface MemberAccountRoutes {
|
|
30
|
+
/** The address book on ecom.wix.com (My Addresses). */
|
|
31
|
+
addresses: boolean;
|
|
32
|
+
/** Saved cards on the member's own site (My Wallet). */
|
|
33
|
+
wallet: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function memberAccountRoutes(): Promise<MemberAccountRoutes> {
|
|
37
|
+
if (isHostedWorker()) return { addresses: false, wallet: false };
|
|
38
|
+
const info = await getAppInfo().catch(() => undefined);
|
|
39
|
+
if (!info) return { addresses: false, wallet: false };
|
|
40
|
+
const native = info.platform === "ios" || info.platform === "android" || info.platform === "macos";
|
|
41
|
+
const addresses = native && atLeast(info.binaryVersion ?? "", MEMBER_ACCOUNT_ROUTES_MIN_BINARY);
|
|
42
|
+
return { addresses, wallet: addresses && info.memberSiteBound === true };
|
|
43
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// A saved member address (@himalaya/wix-ecom WixMemberAddress) as display lines, and the
|
|
2
|
+
// editor's field values → the address the service accepts. Shared by the My Addresses
|
|
3
|
+
// list (himi-branded-lite-addresses) and its editor (himi-branded-lite-address).
|
|
4
|
+
//
|
|
5
|
+
// Pure + Hermes-safe: no host globals, no Node APIs.
|
|
6
|
+
import type { WixMemberAddress } from "@himalaya/wix-ecom";
|
|
7
|
+
|
|
8
|
+
/** ISO-3166-2 `US-NY` → `NY` for display; anything else unchanged. */
|
|
9
|
+
function shortSubdivision(subdivision: string, country: string): string {
|
|
10
|
+
return country && subdivision.toUpperCase().startsWith(`${country.toUpperCase()}-`)
|
|
11
|
+
? subdivision.slice(country.length + 1)
|
|
12
|
+
: subdivision;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function join(parts: Array<string | undefined>, separator: string): string {
|
|
16
|
+
return parts.map((part) => (part ?? "").trim()).filter(Boolean).join(separator);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** The recipient line and the postal lines, one per line, as the site's page shows them. */
|
|
20
|
+
export function memberAddressLines(address: WixMemberAddress): { name: string; lines: string } {
|
|
21
|
+
const country = address.country?.trim() ?? "";
|
|
22
|
+
const street = address.addressLine1?.trim()
|
|
23
|
+
|| join([address.street?.number, address.street?.name, address.street?.apt], " ");
|
|
24
|
+
const name = join([address.fullName?.firstName, address.fullName?.lastName], " ");
|
|
25
|
+
const lines = join([
|
|
26
|
+
address.company,
|
|
27
|
+
street,
|
|
28
|
+
address.addressLine2,
|
|
29
|
+
join([address.city, shortSubdivision(address.subdivision?.trim() ?? "", country), address.zipCode], ", "),
|
|
30
|
+
country.toUpperCase(),
|
|
31
|
+
], "\n");
|
|
32
|
+
return { name, lines };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The editor's text fields, as typed. */
|
|
36
|
+
export interface MemberAddressText {
|
|
37
|
+
firstName: string;
|
|
38
|
+
lastName: string;
|
|
39
|
+
company: string;
|
|
40
|
+
addressLine1: string;
|
|
41
|
+
addressLine2: string;
|
|
42
|
+
city: string;
|
|
43
|
+
subdivision: string;
|
|
44
|
+
zipCode: string;
|
|
45
|
+
country: string;
|
|
46
|
+
phoneNumber: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The text fields, plus where the saved address keeps its street line. Checkout can save
|
|
50
|
+
* it as a structured `street` instead of `addressLine1`; the service will not clear a
|
|
51
|
+
* street once set, so a save writes the line back to every form the address already has. */
|
|
52
|
+
export interface MemberAddressFields extends MemberAddressText {
|
|
53
|
+
savedStreet: boolean;
|
|
54
|
+
savedAddressLine1: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const EMPTY_ADDRESS_FIELDS: MemberAddressFields = {
|
|
58
|
+
firstName: "", lastName: "", company: "", addressLine1: "", addressLine2: "",
|
|
59
|
+
city: "", subdivision: "", zipCode: "", country: "", phoneNumber: "",
|
|
60
|
+
savedStreet: false, savedAddressLine1: false,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/** A saved address → the editor's fields. A street-only address (no addressLine1)
|
|
64
|
+
* is shown as one line, so saving it keeps what the member saw. */
|
|
65
|
+
export function addressFields(address: WixMemberAddress): MemberAddressFields {
|
|
66
|
+
const country = address.country?.trim().toUpperCase() ?? "";
|
|
67
|
+
const street = join([address.street?.number, address.street?.name, address.street?.apt], " ");
|
|
68
|
+
return {
|
|
69
|
+
firstName: address.fullName?.firstName ?? "",
|
|
70
|
+
lastName: address.fullName?.lastName ?? "",
|
|
71
|
+
company: address.company ?? "",
|
|
72
|
+
addressLine1: address.addressLine1?.trim() || street,
|
|
73
|
+
addressLine2: address.addressLine2 ?? "",
|
|
74
|
+
city: address.city ?? "",
|
|
75
|
+
subdivision: shortSubdivision(address.subdivision?.trim() ?? "", country),
|
|
76
|
+
zipCode: address.zipCode ?? "",
|
|
77
|
+
country,
|
|
78
|
+
phoneNumber: address.phoneNumber ?? "",
|
|
79
|
+
savedStreet: !!street,
|
|
80
|
+
savedAddressLine1: !!address.addressLine1?.trim(),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The required fields the member left empty, in form order. */
|
|
85
|
+
export function missingAddressFields(fields: MemberAddressText): Array<keyof MemberAddressText> {
|
|
86
|
+
const required: Array<keyof MemberAddressText> = ["firstName", "lastName", "addressLine1", "city", "country"];
|
|
87
|
+
return required.filter((key) => !fields[key].trim());
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** A 2-letter ISO-3166 country code, upper-cased; "" when the input is not one. */
|
|
91
|
+
export function normalizeCountry(value: string): string {
|
|
92
|
+
const code = value.trim().toUpperCase();
|
|
93
|
+
return /^[A-Z]{2}$/.test(code) ? code : "";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** A region CODE becomes ISO-3166-2 (`ny` + `US` → `US-NY`, `us-ca` → `US-CA`), the form
|
|
97
|
+
* checkout reads; a region NAME stays as typed. The service validates neither: it stores
|
|
98
|
+
* whatever it is sent (verified live), so this is the only place the shape is decided.
|
|
99
|
+
* Only a 1-2 character entry is taken as a bare code: three letters are as often a name
|
|
100
|
+
* ("Goa", "Mie", "Ain") as a code, so a 3-character code is kept only when typed in full
|
|
101
|
+
* (`AU-NSW`). */
|
|
102
|
+
function normalizeSubdivision(value: string, country: string): string {
|
|
103
|
+
const typed = value.trim();
|
|
104
|
+
const code = typed.toUpperCase();
|
|
105
|
+
if (country && /^[A-Z0-9]{1,2}$/.test(code)) return `${country}-${code}`;
|
|
106
|
+
if (/^[A-Z]{2}-[A-Z0-9]{1,3}$/.test(code)) return code;
|
|
107
|
+
return typed;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** The fields → the address body. Every editable field is sent (an empty one clears
|
|
111
|
+
* it on update). The street line goes to each form the saved address has — the whole
|
|
112
|
+
* line in `street.name` for a structured street (verified live: the service keeps an
|
|
113
|
+
* empty number and apt) — and to `addressLine1` for a new address. */
|
|
114
|
+
export function addressFromFields(fields: MemberAddressFields): WixMemberAddress {
|
|
115
|
+
const country = normalizeCountry(fields.country);
|
|
116
|
+
const line = fields.addressLine1.trim();
|
|
117
|
+
return {
|
|
118
|
+
fullName: { firstName: fields.firstName.trim(), lastName: fields.lastName.trim() },
|
|
119
|
+
company: fields.company.trim(),
|
|
120
|
+
...(fields.savedStreet ? { street: { name: line, number: "", apt: "" } } : {}),
|
|
121
|
+
...(fields.savedAddressLine1 || !fields.savedStreet ? { addressLine1: line } : {}),
|
|
122
|
+
addressLine2: fields.addressLine2.trim(),
|
|
123
|
+
city: fields.city.trim(),
|
|
124
|
+
subdivision: normalizeSubdivision(fields.subdivision, country),
|
|
125
|
+
zipCode: fields.zipCode.trim(),
|
|
126
|
+
country,
|
|
127
|
+
phoneNumber: fields.phoneNumber.trim(),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// A saved card's network from its BIN (the first 4-6 digits Wix returns with a saved
|
|
2
|
+
// payment method), for My Wallet's row titles. Wix returns the network only as a brand
|
|
3
|
+
// GUID, so this is the readable name. Brand names are not copy.
|
|
4
|
+
//
|
|
5
|
+
// Pure + Hermes-safe: no host globals, no Node APIs.
|
|
6
|
+
|
|
7
|
+
/** The card network from its BIN, for the row title. Brand names are not copy. */
|
|
8
|
+
export function cardBrand(bin: string): string {
|
|
9
|
+
const d = bin.replace(/\D/g, "");
|
|
10
|
+
const n = (len: number) => Number(d.slice(0, len));
|
|
11
|
+
if (!d) return "";
|
|
12
|
+
if (d.startsWith("4")) return "Visa";
|
|
13
|
+
if ((n(2) >= 51 && n(2) <= 55) || (n(4) >= 2221 && n(4) <= 2720)) return "Mastercard";
|
|
14
|
+
if (d.startsWith("34") || d.startsWith("37")) return "American Express";
|
|
15
|
+
if (d.startsWith("6011") || d.startsWith("65") || (n(3) >= 644 && n(3) <= 649)) return "Discover";
|
|
16
|
+
if (d.startsWith("35")) return "JCB";
|
|
17
|
+
if (d.startsWith("36") || d.startsWith("38") || (n(3) >= 300 && n(3) <= 305)) return "Diners Club";
|
|
18
|
+
if (d.startsWith("62")) return "UnionPay";
|
|
19
|
+
return "";
|
|
20
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// Tier-5 bundle: Branded Lite — add / edit one saved address (himi_address).
|
|
2
|
+
//
|
|
3
|
+
// No `id` nav param → a new address; `?id=` → that address, prefilled. Save writes it
|
|
4
|
+
// to the member's address book (@himalaya/wix-ecom) and goes back to My Addresses,
|
|
5
|
+
// which reloads on appear. Edit also offers Remove, behind an in-screen confirm.
|
|
6
|
+
import { defineService, navigate } from "@himalaya/state";
|
|
7
|
+
import {
|
|
8
|
+
createMemberAddress,
|
|
9
|
+
deleteMemberAddress,
|
|
10
|
+
getMemberAddress,
|
|
11
|
+
updateMemberAddress,
|
|
12
|
+
} from "@himalaya/wix-ecom";
|
|
13
|
+
import { copyParam, stashCopy } from "../_shared/himi-copy.js";
|
|
14
|
+
import { strOf } from "../_shared/himi-account.js";
|
|
15
|
+
import { assertMemberAccess, memberAccess, memberAccessDenied, memberChanged, type MemberAccess } from "../_shared/member-access.js";
|
|
16
|
+
import {
|
|
17
|
+
addressFields,
|
|
18
|
+
addressFromFields,
|
|
19
|
+
EMPTY_ADDRESS_FIELDS,
|
|
20
|
+
missingAddressFields,
|
|
21
|
+
normalizeCountry,
|
|
22
|
+
type MemberAddressFields,
|
|
23
|
+
type MemberAddressText,
|
|
24
|
+
} from "../_shared/member-address.js";
|
|
25
|
+
import { memberAccountRoutes } from "../_shared/member-account-routes.js";
|
|
26
|
+
|
|
27
|
+
interface State extends MemberAddressFields {
|
|
28
|
+
status: "loading" | "ready" | "error" | "notFound";
|
|
29
|
+
id: string;
|
|
30
|
+
title: string;
|
|
31
|
+
needsSignIn: boolean;
|
|
32
|
+
/** This host cannot reach the address book (web, a hosted app): say so, load nothing. */
|
|
33
|
+
unavailable: boolean;
|
|
34
|
+
noUnavailable: boolean;
|
|
35
|
+
makeDefault: boolean;
|
|
36
|
+
/** Already the default: no toggle, it cannot be un-set from here. */
|
|
37
|
+
isDefault: boolean;
|
|
38
|
+
message: string;
|
|
39
|
+
confirmingRemove: boolean;
|
|
40
|
+
isLoading: boolean;
|
|
41
|
+
notLoading: boolean;
|
|
42
|
+
notErrored: boolean;
|
|
43
|
+
noSignIn: boolean;
|
|
44
|
+
hideForm: boolean;
|
|
45
|
+
hideDefaultToggle: boolean;
|
|
46
|
+
hideRemove: boolean;
|
|
47
|
+
hideRemoveConfirm: boolean;
|
|
48
|
+
hideNotFound: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const FIELD_LABELS: Record<keyof MemberAddressText, [string, string]> = {
|
|
52
|
+
firstName: ["address.firstNameLabel", "First name"],
|
|
53
|
+
lastName: ["address.lastNameLabel", "Last name"],
|
|
54
|
+
company: ["address.companyLabel", "Company"],
|
|
55
|
+
addressLine1: ["address.line1Label", "Address"],
|
|
56
|
+
addressLine2: ["address.line2Label", "Apartment, suite, etc."],
|
|
57
|
+
city: ["address.cityLabel", "City"],
|
|
58
|
+
subdivision: ["address.subdivisionLabel", "State / region"],
|
|
59
|
+
zipCode: ["address.zipLabel", "ZIP / postal code"],
|
|
60
|
+
country: ["address.countryLabel", "Country code"],
|
|
61
|
+
phoneNumber: ["address.phoneLabel", "Phone"],
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
let member: MemberAccess | undefined;
|
|
65
|
+
let busy = false;
|
|
66
|
+
|
|
67
|
+
function inputId(): string {
|
|
68
|
+
const input = (globalThis as { himalaya?: { input?: { id?: unknown } } }).himalaya?.input;
|
|
69
|
+
return typeof input?.id === "string" ? input.id.trim() : "";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function derive(state: State): State {
|
|
73
|
+
const ready = state.status === "ready" && !state.unavailable;
|
|
74
|
+
return {
|
|
75
|
+
...state,
|
|
76
|
+
noUnavailable: !state.unavailable,
|
|
77
|
+
title: state.id ? copyParam("address.editTitle", "Edit address") : copyParam("address.addTitle", "Add address"),
|
|
78
|
+
isLoading: state.status === "loading",
|
|
79
|
+
notLoading: state.status !== "loading",
|
|
80
|
+
notErrored: state.status !== "error",
|
|
81
|
+
noSignIn: !ready || !state.needsSignIn,
|
|
82
|
+
hideForm: !ready || state.needsSignIn,
|
|
83
|
+
hideDefaultToggle: !ready || state.needsSignIn || state.isDefault,
|
|
84
|
+
hideRemove: !ready || state.needsSignIn || !state.id || state.confirmingRemove,
|
|
85
|
+
hideRemoveConfirm: !ready || !state.confirmingRemove,
|
|
86
|
+
hideNotFound: state.status !== "notFound",
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const INITIAL: State = derive({
|
|
91
|
+
...EMPTY_ADDRESS_FIELDS,
|
|
92
|
+
status: "loading", id: "", title: "", needsSignIn: false, unavailable: false, noUnavailable: true, makeDefault: false, isDefault: false,
|
|
93
|
+
message: "", confirmingRemove: false, isLoading: true, notLoading: false, notErrored: true,
|
|
94
|
+
noSignIn: true, hideForm: true, hideDefaultToggle: true, hideRemove: true, hideRemoveConfirm: true, hideNotFound: true,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
function isNotFound(error: unknown): boolean {
|
|
98
|
+
return /cant_find_address_by_id|HTTP 404/.test(String((error as { message?: unknown } | null)?.message ?? error));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function load(_state: State, arg?: unknown): Promise<State> {
|
|
102
|
+
stashCopy(arg);
|
|
103
|
+
const id = inputId();
|
|
104
|
+
const base: State = { ...INITIAL, id };
|
|
105
|
+
if (!(await memberAccountRoutes()).addresses) return derive({ ...base, status: "ready", unavailable: true });
|
|
106
|
+
try {
|
|
107
|
+
member = await memberAccess();
|
|
108
|
+
if (!member.email) return derive({ ...base, status: "ready", needsSignIn: true });
|
|
109
|
+
if (!id) return derive({ ...base, status: "ready" });
|
|
110
|
+
const response = await getMemberAddress(id);
|
|
111
|
+
await assertMemberAccess(member);
|
|
112
|
+
if (!response.address) return derive({ ...base, status: "notFound" });
|
|
113
|
+
return derive({ ...base, ...addressFields(response.address), status: "ready", isDefault: response.isDefaultAddress === true });
|
|
114
|
+
} catch (error) {
|
|
115
|
+
if (isNotFound(error)) return derive({ ...base, status: "notFound" });
|
|
116
|
+
return derive({
|
|
117
|
+
...base,
|
|
118
|
+
status: "error",
|
|
119
|
+
message: memberAccessDenied(error)
|
|
120
|
+
? copyParam("addresses.accessErrorText", "Your addresses are unavailable for this account. Try signing in again.")
|
|
121
|
+
: copyParam("address.loadErrorText", "This address could not be loaded."),
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function setter(field: keyof MemberAddressFields) {
|
|
127
|
+
return (state: State, arg?: unknown): State => derive({ ...state, [field]: strOf(arg), message: "" });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function validation(state: State): string {
|
|
131
|
+
const missing = missingAddressFields(state);
|
|
132
|
+
if (missing.length) {
|
|
133
|
+
const names = missing.map((key) => copyParam(FIELD_LABELS[key][0], FIELD_LABELS[key][1])).join(", ");
|
|
134
|
+
return `${copyParam("address.missingPrefix", "Fill in:")} ${names}`;
|
|
135
|
+
}
|
|
136
|
+
if (!normalizeCountry(state.country)) {
|
|
137
|
+
return copyParam("address.countryInvalid", "Enter the country as a 2-letter code, like US or GB.");
|
|
138
|
+
}
|
|
139
|
+
return "";
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function save(state: State): Promise<State> {
|
|
143
|
+
if (busy || state.status !== "ready" || state.needsSignIn || !member?.email) return state;
|
|
144
|
+
const invalid = validation(state);
|
|
145
|
+
if (invalid) return derive({ ...state, message: invalid });
|
|
146
|
+
busy = true;
|
|
147
|
+
try {
|
|
148
|
+
await assertMemberAccess(member);
|
|
149
|
+
const address = addressFromFields(state);
|
|
150
|
+
const setAsDefault = state.makeDefault && !state.isDefault;
|
|
151
|
+
if (state.id) await updateMemberAddress(state.id, address, { setAsDefault });
|
|
152
|
+
else await createMemberAddress(address, { setAsDefault });
|
|
153
|
+
return navigate(derive({ ...state, message: "" }), { presentation: "back" });
|
|
154
|
+
} catch (error) {
|
|
155
|
+
if (await memberChanged(member, error)) return load(state);
|
|
156
|
+
if (state.id && isNotFound(error)) return derive({ ...state, status: "notFound" });
|
|
157
|
+
return derive({ ...state, message: copyParam("address.saveFailed", "The address could not be saved. Try again.") });
|
|
158
|
+
} finally {
|
|
159
|
+
busy = false;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function remove(state: State): Promise<State> {
|
|
164
|
+
if (busy || !state.id || !state.confirmingRemove || !member?.email) return state;
|
|
165
|
+
busy = true;
|
|
166
|
+
try {
|
|
167
|
+
await assertMemberAccess(member);
|
|
168
|
+
await deleteMemberAddress(state.id);
|
|
169
|
+
return navigate(derive({ ...state, confirmingRemove: false }), { presentation: "back" });
|
|
170
|
+
} catch (error) {
|
|
171
|
+
if (await memberChanged(member, error)) return load(state);
|
|
172
|
+
if (isNotFound(error)) return navigate(derive(state), { presentation: "back" });
|
|
173
|
+
return derive({ ...state, message: copyParam("addresses.removeFailed", "The address could not be removed. Try again.") });
|
|
174
|
+
} finally {
|
|
175
|
+
busy = false;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const svc = defineService<State, {
|
|
180
|
+
load: (state: State, arg?: unknown) => Promise<State>;
|
|
181
|
+
retry: (state: State) => Promise<State>;
|
|
182
|
+
setFirstName: (state: State, arg?: unknown) => State;
|
|
183
|
+
setLastName: (state: State, arg?: unknown) => State;
|
|
184
|
+
setCompany: (state: State, arg?: unknown) => State;
|
|
185
|
+
setAddressLine1: (state: State, arg?: unknown) => State;
|
|
186
|
+
setAddressLine2: (state: State, arg?: unknown) => State;
|
|
187
|
+
setCity: (state: State, arg?: unknown) => State;
|
|
188
|
+
setSubdivision: (state: State, arg?: unknown) => State;
|
|
189
|
+
setZipCode: (state: State, arg?: unknown) => State;
|
|
190
|
+
setCountry: (state: State, arg?: unknown) => State;
|
|
191
|
+
setPhoneNumber: (state: State, arg?: unknown) => State;
|
|
192
|
+
setMakeDefault: (state: State, arg?: unknown) => State;
|
|
193
|
+
save: (state: State) => Promise<State>;
|
|
194
|
+
askRemove: (state: State) => State;
|
|
195
|
+
keep: (state: State) => State;
|
|
196
|
+
remove: (state: State) => Promise<State>;
|
|
197
|
+
}>("himiBrandedAddress", {
|
|
198
|
+
state: INITIAL,
|
|
199
|
+
actions: {
|
|
200
|
+
load,
|
|
201
|
+
retry: (state) => load(state),
|
|
202
|
+
setFirstName: setter("firstName"),
|
|
203
|
+
setLastName: setter("lastName"),
|
|
204
|
+
setCompany: setter("company"),
|
|
205
|
+
setAddressLine1: setter("addressLine1"),
|
|
206
|
+
setAddressLine2: setter("addressLine2"),
|
|
207
|
+
setCity: setter("city"),
|
|
208
|
+
setSubdivision: setter("subdivision"),
|
|
209
|
+
setZipCode: setter("zipCode"),
|
|
210
|
+
setCountry: setter("country"),
|
|
211
|
+
setPhoneNumber: setter("phoneNumber"),
|
|
212
|
+
setMakeDefault: (state, arg) => {
|
|
213
|
+
const value = (arg as { value?: unknown } | null)?.value ?? arg;
|
|
214
|
+
return derive({ ...state, makeDefault: value === true || value === "true" });
|
|
215
|
+
},
|
|
216
|
+
save,
|
|
217
|
+
askRemove: (state) => (state.id ? derive({ ...state, confirmingRemove: true, message: "" }) : state),
|
|
218
|
+
keep: (state) => derive({ ...state, confirmingRemove: false }),
|
|
219
|
+
remove,
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
export { svc };
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// Tier-5 bundle: Branded Lite — My Addresses (the member's saved address book).
|
|
2
|
+
//
|
|
3
|
+
// The same book the site's Members Area "My Addresses" page and checkout use
|
|
4
|
+
// (addresses-web, see @himalaya/wix-ecom listMemberAddresses). A member sees every
|
|
5
|
+
// saved address, picks the default, and removes one after an in-screen confirm;
|
|
6
|
+
// add/edit open himi_address. A guest gets the shared member gate.
|
|
7
|
+
import { defineService, navigate } from "@himalaya/state";
|
|
8
|
+
import {
|
|
9
|
+
deleteMemberAddress,
|
|
10
|
+
listMemberAddresses,
|
|
11
|
+
setDefaultMemberAddress,
|
|
12
|
+
type WixMemberAddress,
|
|
13
|
+
} from "@himalaya/wix-ecom";
|
|
14
|
+
import { copyParam, stashCopy } from "../_shared/himi-copy.js";
|
|
15
|
+
import { assertMemberAccess, memberAccess, memberAccessDenied, memberChanged, type MemberAccess } from "../_shared/member-access.js";
|
|
16
|
+
import { memberAddressLines } from "../_shared/member-address.js";
|
|
17
|
+
import { memberAccountRoutes } from "../_shared/member-account-routes.js";
|
|
18
|
+
|
|
19
|
+
interface AddressRow {
|
|
20
|
+
id: string;
|
|
21
|
+
name: string;
|
|
22
|
+
lines: string;
|
|
23
|
+
phone: string;
|
|
24
|
+
isDefault: boolean;
|
|
25
|
+
notDefault: boolean;
|
|
26
|
+
confirming: boolean;
|
|
27
|
+
notConfirming: boolean;
|
|
28
|
+
/** The row's actions (edit / set default / remove) while no remove is being confirmed. */
|
|
29
|
+
hideActions: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface State {
|
|
33
|
+
status: "loading" | "ready" | "error";
|
|
34
|
+
rows: AddressRow[];
|
|
35
|
+
message: string;
|
|
36
|
+
errorText: string;
|
|
37
|
+
needsSignIn: boolean;
|
|
38
|
+
/** This host cannot reach the address book (web, a hosted app): say so, load nothing. */
|
|
39
|
+
unavailable: boolean;
|
|
40
|
+
noUnavailable: boolean;
|
|
41
|
+
isLoading: boolean;
|
|
42
|
+
notLoading: boolean;
|
|
43
|
+
notErrored: boolean;
|
|
44
|
+
noSignIn: boolean;
|
|
45
|
+
hideList: boolean;
|
|
46
|
+
noEmpty: boolean;
|
|
47
|
+
/** The Add button: hidden for a guest and while loading or errored. */
|
|
48
|
+
hideAdd: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let member: MemberAccess | undefined;
|
|
52
|
+
let addresses: WixMemberAddress[] = [];
|
|
53
|
+
let defaultId = "";
|
|
54
|
+
let confirmId = "";
|
|
55
|
+
let busy = false;
|
|
56
|
+
|
|
57
|
+
function rowOf(address: WixMemberAddress): AddressRow {
|
|
58
|
+
const id = address.id ?? "";
|
|
59
|
+
const { name, lines } = memberAddressLines(address);
|
|
60
|
+
const isDefault = id === defaultId;
|
|
61
|
+
const confirming = id === confirmId;
|
|
62
|
+
return {
|
|
63
|
+
id,
|
|
64
|
+
name: name || copyParam("addresses.unnamedLabel", "Saved address"),
|
|
65
|
+
lines,
|
|
66
|
+
phone: address.phoneNumber?.trim() ?? "",
|
|
67
|
+
isDefault,
|
|
68
|
+
notDefault: !isDefault,
|
|
69
|
+
confirming,
|
|
70
|
+
notConfirming: !confirming,
|
|
71
|
+
hideActions: confirming,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function derive(state: State): State {
|
|
76
|
+
// The default address leads, like the site's own page.
|
|
77
|
+
const ordered = [...addresses].sort((a, b) => Number(b.id === defaultId) - Number(a.id === defaultId));
|
|
78
|
+
const rows = state.status === "ready" ? ordered.filter((a) => !!a.id).map(rowOf) : [];
|
|
79
|
+
const ready = state.status === "ready" && !state.unavailable;
|
|
80
|
+
return {
|
|
81
|
+
...state,
|
|
82
|
+
rows,
|
|
83
|
+
noUnavailable: !state.unavailable,
|
|
84
|
+
isLoading: state.status === "loading",
|
|
85
|
+
notLoading: state.status !== "loading",
|
|
86
|
+
notErrored: state.status !== "error",
|
|
87
|
+
noSignIn: !ready || !state.needsSignIn,
|
|
88
|
+
hideList: !ready || state.needsSignIn || rows.length === 0,
|
|
89
|
+
noEmpty: !ready || state.needsSignIn || rows.length > 0,
|
|
90
|
+
hideAdd: !ready || state.needsSignIn,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const INITIAL: State = derive({
|
|
95
|
+
status: "loading", rows: [], message: "", errorText: "", needsSignIn: false, unavailable: false, noUnavailable: true,
|
|
96
|
+
isLoading: true, notLoading: false, notErrored: true, noSignIn: true, hideList: true, noEmpty: true, hideAdd: true,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
async function load(state: State, arg?: unknown): Promise<State> {
|
|
100
|
+
stashCopy(arg);
|
|
101
|
+
confirmId = "";
|
|
102
|
+
if (!(await memberAccountRoutes()).addresses) {
|
|
103
|
+
addresses = [];
|
|
104
|
+
return derive({ ...state, status: "ready", unavailable: true, needsSignIn: false, message: "", errorText: "" });
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
member = await memberAccess();
|
|
108
|
+
if (!member.email) {
|
|
109
|
+
addresses = [];
|
|
110
|
+
return derive({ ...state, status: "ready", unavailable: false, needsSignIn: true, message: "", errorText: "" });
|
|
111
|
+
}
|
|
112
|
+
const response = await listMemberAddresses();
|
|
113
|
+
await assertMemberAccess(member);
|
|
114
|
+
addresses = response.addresses ?? [];
|
|
115
|
+
defaultId = response.defaultAddressId ?? "";
|
|
116
|
+
// A fresh load drops the last write's confirmation; write() sets it again after this.
|
|
117
|
+
return derive({ ...state, status: "ready", unavailable: false, needsSignIn: false, message: "", errorText: "" });
|
|
118
|
+
} catch (error) {
|
|
119
|
+
addresses = [];
|
|
120
|
+
return derive({
|
|
121
|
+
...state,
|
|
122
|
+
status: "error",
|
|
123
|
+
unavailable: false,
|
|
124
|
+
message: "",
|
|
125
|
+
errorText: memberAccessDenied(error)
|
|
126
|
+
? copyParam("addresses.accessErrorText", "Your addresses are unavailable for this account. Try signing in again.")
|
|
127
|
+
: copyParam("addresses.loadErrorText", "Your addresses could not be loaded."),
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function argId(arg: unknown): string {
|
|
133
|
+
const id = (arg as { id?: unknown } | null)?.id;
|
|
134
|
+
return typeof id === "string" ? id : "";
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Run one write for the signed-in member, then re-read the book. A changed or lost
|
|
138
|
+
* identity reloads instead of writing for whoever signed in since. */
|
|
139
|
+
async function write(state: State, run: () => Promise<unknown>, done: string, failed: string): Promise<State> {
|
|
140
|
+
if (busy || !member?.email) return state;
|
|
141
|
+
busy = true;
|
|
142
|
+
try {
|
|
143
|
+
await assertMemberAccess(member);
|
|
144
|
+
await run();
|
|
145
|
+
const next = await load(state);
|
|
146
|
+
return next.status === "ready" ? { ...next, message: done } : next;
|
|
147
|
+
} catch (error) {
|
|
148
|
+
if (await memberChanged(member, error)) return load(state);
|
|
149
|
+
return derive({ ...state, message: failed });
|
|
150
|
+
} finally {
|
|
151
|
+
busy = false;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const svc = defineService<State, {
|
|
156
|
+
load: (state: State, arg?: unknown) => Promise<State>;
|
|
157
|
+
retry: (state: State) => Promise<State>;
|
|
158
|
+
add: (state: State) => State;
|
|
159
|
+
edit: (state: State, arg?: unknown) => State;
|
|
160
|
+
setDefault: (state: State, arg?: unknown) => Promise<State>;
|
|
161
|
+
askRemove: (state: State, arg?: unknown) => State;
|
|
162
|
+
keep: (state: State) => State;
|
|
163
|
+
remove: (state: State, arg?: unknown) => Promise<State>;
|
|
164
|
+
}>("himiBrandedAddresses", {
|
|
165
|
+
state: INITIAL,
|
|
166
|
+
actions: {
|
|
167
|
+
load,
|
|
168
|
+
retry: (state) => load(derive({ ...state, status: "loading", message: "" })),
|
|
169
|
+
add: (state) => navigate({ ...state, message: "" }, { screenId: "himi_address" }),
|
|
170
|
+
edit: (state, arg) => {
|
|
171
|
+
const id = argId(arg);
|
|
172
|
+
return addresses.some((a) => a.id === id) ? navigate({ ...state, message: "" }, { screenId: "himi_address", args: { id } }) : state;
|
|
173
|
+
},
|
|
174
|
+
setDefault: (state, arg) => {
|
|
175
|
+
const id = argId(arg);
|
|
176
|
+
if (!id || id === defaultId || !addresses.some((a) => a.id === id)) return Promise.resolve(state);
|
|
177
|
+
return write(state, () => setDefaultMemberAddress(id),
|
|
178
|
+
copyParam("addresses.defaultDone", "Default address updated."),
|
|
179
|
+
copyParam("addresses.defaultFailed", "The default address could not be changed. Try again."));
|
|
180
|
+
},
|
|
181
|
+
askRemove: (state, arg) => {
|
|
182
|
+
const id = argId(arg);
|
|
183
|
+
if (!addresses.some((a) => a.id === id)) return state;
|
|
184
|
+
confirmId = id;
|
|
185
|
+
return derive({ ...state, message: "" });
|
|
186
|
+
},
|
|
187
|
+
keep: (state) => {
|
|
188
|
+
confirmId = "";
|
|
189
|
+
return derive(state);
|
|
190
|
+
},
|
|
191
|
+
remove: (state, arg) => {
|
|
192
|
+
// Only the address the member is confirming — a stale row tap removes nothing.
|
|
193
|
+
const id = argId(arg);
|
|
194
|
+
if (!id || id !== confirmId) return Promise.resolve(state);
|
|
195
|
+
return write(state, () => deleteMemberAddress(id),
|
|
196
|
+
copyParam("addresses.removeDone", "Address removed."),
|
|
197
|
+
copyParam("addresses.removeFailed", "The address could not be removed. Try again."));
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
export { svc };
|