@wix/himalaya-cli 0.1404.0 → 0.1405.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 (102) hide show
  1. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/dev-server/src/builder.d.ts +3 -0
  2. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/schema.d.ts +6 -0
  3. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/ai-site-chat/src/extensions.d.ts +10 -0
  4. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/ai-site-chat/src/index.d.ts +102 -0
  5. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-ai-site-chat.mjs +70 -0
  6. package/dist/authoring-runtime/node_modules/@wix/himalaya/package.json +4 -0
  7. package/dist/cli.mjs +95 -37
  8. package/dist/preview-assets.generated.bin +0 -0
  9. package/fixtures/wix-canned.json +3 -0
  10. package/guides/component-reference.json +3 -2
  11. package/guides/component-reference.md +7 -1
  12. package/guides/importable-sdks.md +1 -0
  13. package/guides/sdk-reference.json +107 -0
  14. package/package.json +2 -2
  15. package/templates/beauty-salon/config.ts +7 -0
  16. package/templates/beauty-salon/dev/index.ts +1750 -0
  17. package/templates/beauty-salon/himalaya.content.json +3 -0
  18. package/templates/beauty-salon/site-functions/README.md +37 -0
  19. package/templates/beauty-salon/site-functions/himiMemberPhotoUploadUrl.js +90 -0
  20. package/templates/beauty-salon/tier5-src/_shared/member-account-routes.ts +11 -3
  21. package/templates/beauty-salon/tier5-src/himi-branded-lite-account-settings/blocked-members.ts +242 -0
  22. package/templates/beauty-salon/tier5-src/himi-branded-lite-account-settings/index.ts +1091 -0
  23. package/templates/branded-lite/config.ts +7 -0
  24. package/templates/branded-lite/dev/index.ts +1750 -0
  25. package/templates/branded-lite/himalaya.content.json +3 -0
  26. package/templates/branded-lite/site-functions/README.md +37 -0
  27. package/templates/branded-lite/site-functions/himiMemberPhotoUploadUrl.js +90 -0
  28. package/templates/branded-lite/tier5-src/_shared/member-account-routes.ts +11 -3
  29. package/templates/branded-lite/tier5-src/himi-branded-lite-account-settings/blocked-members.ts +242 -0
  30. package/templates/branded-lite/tier5-src/himi-branded-lite-account-settings/index.ts +1091 -0
  31. package/templates/community-club/config.ts +7 -0
  32. package/templates/community-club/dev/index.ts +1750 -0
  33. package/templates/community-club/himalaya.content.json +3 -0
  34. package/templates/community-club/site-functions/README.md +37 -0
  35. package/templates/community-club/site-functions/himiMemberPhotoUploadUrl.js +90 -0
  36. package/templates/community-club/tier5-src/_shared/member-account-routes.ts +11 -3
  37. package/templates/community-club/tier5-src/himi-branded-lite-account-settings/blocked-members.ts +242 -0
  38. package/templates/community-club/tier5-src/himi-branded-lite-account-settings/index.ts +1091 -0
  39. package/templates/events-meetups/config.ts +7 -0
  40. package/templates/events-meetups/dev/index.ts +1750 -0
  41. package/templates/events-meetups/himalaya.content.json +3 -0
  42. package/templates/events-meetups/site-functions/README.md +37 -0
  43. package/templates/events-meetups/site-functions/himiMemberPhotoUploadUrl.js +90 -0
  44. package/templates/events-meetups/tier5-src/_shared/member-account-routes.ts +11 -3
  45. package/templates/events-meetups/tier5-src/himi-branded-lite-account-settings/blocked-members.ts +242 -0
  46. package/templates/events-meetups/tier5-src/himi-branded-lite-account-settings/index.ts +1091 -0
  47. package/templates/gym-fitness/config.ts +7 -0
  48. package/templates/gym-fitness/dev/index.ts +1750 -0
  49. package/templates/gym-fitness/himalaya.content.json +3 -0
  50. package/templates/gym-fitness/site-functions/README.md +37 -0
  51. package/templates/gym-fitness/site-functions/himiMemberPhotoUploadUrl.js +90 -0
  52. package/templates/gym-fitness/tier5-src/_shared/member-account-routes.ts +11 -3
  53. package/templates/gym-fitness/tier5-src/himi-branded-lite-account-settings/blocked-members.ts +242 -0
  54. package/templates/gym-fitness/tier5-src/himi-branded-lite-account-settings/index.ts +1091 -0
  55. package/templates/nonprofit-cause/config.ts +7 -0
  56. package/templates/nonprofit-cause/dev/index.ts +1750 -0
  57. package/templates/nonprofit-cause/himalaya.content.json +3 -0
  58. package/templates/nonprofit-cause/site-functions/README.md +37 -0
  59. package/templates/nonprofit-cause/site-functions/himiMemberPhotoUploadUrl.js +90 -0
  60. package/templates/nonprofit-cause/tier5-src/_shared/member-account-routes.ts +11 -3
  61. package/templates/nonprofit-cause/tier5-src/himi-branded-lite-account-settings/blocked-members.ts +242 -0
  62. package/templates/nonprofit-cause/tier5-src/himi-branded-lite-account-settings/index.ts +1091 -0
  63. package/templates/podcast-creator/config.ts +7 -0
  64. package/templates/podcast-creator/dev/index.ts +1750 -0
  65. package/templates/podcast-creator/himalaya.content.json +3 -0
  66. package/templates/podcast-creator/site-functions/README.md +37 -0
  67. package/templates/podcast-creator/site-functions/himiMemberPhotoUploadUrl.js +90 -0
  68. package/templates/podcast-creator/tier5-src/_shared/member-account-routes.ts +11 -3
  69. package/templates/podcast-creator/tier5-src/himi-branded-lite-account-settings/blocked-members.ts +242 -0
  70. package/templates/podcast-creator/tier5-src/himi-branded-lite-account-settings/index.ts +1091 -0
  71. package/templates/portfolio-creative/config.ts +7 -0
  72. package/templates/portfolio-creative/dev/index.ts +1750 -0
  73. package/templates/portfolio-creative/himalaya.content.json +3 -0
  74. package/templates/portfolio-creative/site-functions/README.md +37 -0
  75. package/templates/portfolio-creative/site-functions/himiMemberPhotoUploadUrl.js +90 -0
  76. package/templates/portfolio-creative/tier5-src/_shared/member-account-routes.ts +11 -3
  77. package/templates/portfolio-creative/tier5-src/himi-branded-lite-account-settings/blocked-members.ts +242 -0
  78. package/templates/portfolio-creative/tier5-src/himi-branded-lite-account-settings/index.ts +1091 -0
  79. package/templates/restaurant/config.ts +7 -0
  80. package/templates/restaurant/dev/index.ts +1750 -0
  81. package/templates/restaurant/himalaya.content.json +3 -0
  82. package/templates/restaurant/site-functions/README.md +37 -0
  83. package/templates/restaurant/site-functions/himiMemberPhotoUploadUrl.js +90 -0
  84. package/templates/restaurant/tier5-src/_shared/member-account-routes.ts +11 -3
  85. package/templates/restaurant/tier5-src/himi-branded-lite-account-settings/blocked-members.ts +242 -0
  86. package/templates/restaurant/tier5-src/himi-branded-lite-account-settings/index.ts +1091 -0
  87. package/templates/retail-store/config.ts +7 -0
  88. package/templates/retail-store/dev/index.ts +1750 -0
  89. package/templates/retail-store/himalaya.content.json +3 -0
  90. package/templates/retail-store/site-functions/README.md +37 -0
  91. package/templates/retail-store/site-functions/himiMemberPhotoUploadUrl.js +90 -0
  92. package/templates/retail-store/tier5-src/_shared/member-account-routes.ts +11 -3
  93. package/templates/retail-store/tier5-src/himi-branded-lite-account-settings/blocked-members.ts +242 -0
  94. package/templates/retail-store/tier5-src/himi-branded-lite-account-settings/index.ts +1091 -0
  95. package/templates/yoga-studio/config.ts +7 -0
  96. package/templates/yoga-studio/dev/index.ts +1750 -0
  97. package/templates/yoga-studio/himalaya.content.json +3 -0
  98. package/templates/yoga-studio/site-functions/README.md +37 -0
  99. package/templates/yoga-studio/site-functions/himiMemberPhotoUploadUrl.js +90 -0
  100. package/templates/yoga-studio/tier5-src/_shared/member-account-routes.ts +11 -3
  101. package/templates/yoga-studio/tier5-src/himi-branded-lite-account-settings/blocked-members.ts +242 -0
  102. package/templates/yoga-studio/tier5-src/himi-branded-lite-account-settings/index.ts +1091 -0
@@ -0,0 +1,1091 @@
1
+ // Branded-lite member Account Settings. Member profile data is read and updated
2
+ // through the signed-in member's own Members Area record; account credentials
3
+ // and unsupported account controls stay explicitly unavailable here.
4
+ import { defineService } from "@himalaya/state";
5
+ import { dispatch } from "@himalaya/dispatch";
6
+ import { open as openUrl } from "@himalaya/open-url";
7
+ import { getMyMember, updateMember, updateMySlug, deleteMemberPhones, joinCommunity, leaveCommunity } from "@himalaya/wix-members";
8
+ import { generateMemberPhotoUploadUrl } from "@himalaya/wix-site-media";
9
+ import { memberAccess } from "../_shared/member-access.js";
10
+ import { memberAccountRoutes } from "../_shared/member-account-routes.js";
11
+ import { copyParam, stashCopy } from "../_shared/himi-copy.js";
12
+ import "./blocked-members.js";
13
+
14
+ interface ProfileValues {
15
+ displayName: string;
16
+ title: string;
17
+ firstName: string;
18
+ lastName: string;
19
+ phone: string;
20
+ slug: string;
21
+ memberImage: string;
22
+ memberImageId: string;
23
+ memberInitials: string;
24
+ }
25
+
26
+ interface S extends ProfileValues {
27
+ status: "loading" | "loaded" | "error";
28
+ signedOut: boolean;
29
+ errorText: string;
30
+ error: string;
31
+ signInRequired: boolean;
32
+ message: string;
33
+ memberId: string;
34
+ memberEmail: string;
35
+ loginEmail: string;
36
+ original: ProfileValues;
37
+ dirty: boolean;
38
+ saving: boolean;
39
+ displayNameError: string;
40
+ titleError: string;
41
+ firstNameError: string;
42
+ lastNameError: string;
43
+ phoneError: string;
44
+ noError: boolean;
45
+ noMessage: boolean;
46
+ noMember: boolean;
47
+ notLoading: boolean;
48
+ notErrored: boolean;
49
+ loginControlsUnavailable: boolean;
50
+ privacyControlsUnavailable: boolean;
51
+ privacyStatus: string;
52
+ privacySupported: boolean;
53
+ privacyActionLabel: string;
54
+ isPrivate: boolean;
55
+ notPrivate: boolean;
56
+ hasMemberImage: boolean;
57
+ noMemberImage: boolean;
58
+ photoUploadAvailable: boolean;
59
+ noPhotoUploadAvailable: boolean;
60
+ photoUploadFunctionName: string;
61
+ photoUploadSupported: boolean;
62
+ photoDisplayUrl: string;
63
+ uploadUrl: string;
64
+ photoDraftUrl: string;
65
+ photoMediaId: string;
66
+ photoPendingMemberId: string;
67
+ photoPendingMemberEmail: string;
68
+ photoPendingMemberScope: string;
69
+ photoUploadError: string;
70
+ photoUploading: boolean;
71
+ notPhotoUploading: boolean;
72
+ noSignIn: boolean;
73
+ profileUrl: string;
74
+ siteUrl: string;
75
+ memberScope: string;
76
+ originalPhones: string[];
77
+ noSiteUrl: boolean;
78
+ siteHandoff: boolean;
79
+ noSiteHandoff: boolean;
80
+ slugEditing: boolean;
81
+ slugMessage: string;
82
+ noSlugMessage: boolean;
83
+ privacyConfirmVisible: boolean;
84
+ pendingPrivacyStatus: string;
85
+ noPrivacyConfirm: boolean;
86
+ privacyPrompt: string;
87
+ hasProfileUrl: boolean;
88
+ passwordPanelOpen: boolean;
89
+ passwordCredentialSupported: boolean;
90
+ noPasswordCredentialSupport: boolean;
91
+ passwordCurrent: string;
92
+ passwordNew: string;
93
+ passwordConfirm: string;
94
+ passwordSaving: boolean;
95
+ notPasswordSaving: boolean;
96
+ notPasswordChanging: boolean;
97
+ passwordResetSending: boolean;
98
+ notPasswordResetSending: boolean;
99
+ passwordRequestVersion: number;
100
+ passwordError: string;
101
+ passwordNoError: boolean;
102
+ passwordMessage: string;
103
+ passwordNoMessage: boolean;
104
+ emailPanelOpen: boolean;
105
+ emailUnavailable: string;
106
+ emailManageAvailable: boolean;
107
+ noEmailManage: boolean;
108
+ hasAddressesRoute: boolean;
109
+ noAddressesRoute: boolean;
110
+ hasWalletRoute: boolean;
111
+ noWalletRoute: boolean;
112
+ }
113
+
114
+ const BLANK: ProfileValues = { displayName: "", title: "", firstName: "", lastName: "", phone: "", slug: "", memberImage: "", memberImageId: "", memberInitials: "M" };
115
+ const EMPTY: S = {
116
+ ...BLANK,
117
+ status: "loading",
118
+ signedOut: false,
119
+ errorText: "",
120
+ error: "",
121
+ signInRequired: false,
122
+ message: "",
123
+ memberId: "",
124
+ memberEmail: "",
125
+ loginEmail: "",
126
+ original: { ...BLANK },
127
+ dirty: false,
128
+ saving: false,
129
+ displayNameError: "",
130
+ titleError: "",
131
+ firstNameError: "",
132
+ lastNameError: "",
133
+ phoneError: "",
134
+ noError: true,
135
+ noMessage: true,
136
+ noMember: true,
137
+ notLoading: false,
138
+ notErrored: true,
139
+ loginControlsUnavailable: true,
140
+ privacyControlsUnavailable: true,
141
+ privacyStatus: "",
142
+ privacySupported: false,
143
+ privacyActionLabel: "",
144
+ isPrivate: false,
145
+ notPrivate: true,
146
+ hasMemberImage: false,
147
+ noMemberImage: true,
148
+ photoUploadAvailable: false,
149
+ noPhotoUploadAvailable: true,
150
+ photoUploadFunctionName: "",
151
+ photoUploadSupported: false,
152
+ photoDisplayUrl: "",
153
+ uploadUrl: "",
154
+ photoDraftUrl: "",
155
+ photoMediaId: "",
156
+ photoPendingMemberId: "",
157
+ photoPendingMemberEmail: "",
158
+ photoPendingMemberScope: "",
159
+ photoUploadError: "",
160
+ photoUploading: false,
161
+ notPhotoUploading: true,
162
+ noSignIn: true,
163
+ profileUrl: "",
164
+ siteUrl: "",
165
+ memberScope: "",
166
+ originalPhones: [],
167
+ noSiteUrl: true,
168
+ siteHandoff: false,
169
+ noSiteHandoff: true,
170
+ slugEditing: false,
171
+ slugMessage: "",
172
+ noSlugMessage: true,
173
+ privacyConfirmVisible: false,
174
+ pendingPrivacyStatus: "",
175
+ noPrivacyConfirm: true,
176
+ privacyPrompt: "",
177
+ hasProfileUrl: false,
178
+ passwordPanelOpen: false,
179
+ passwordCredentialSupported: false,
180
+ noPasswordCredentialSupport: true,
181
+ passwordCurrent: "",
182
+ passwordNew: "",
183
+ passwordConfirm: "",
184
+ passwordSaving: false,
185
+ notPasswordSaving: true,
186
+ notPasswordChanging: true,
187
+ passwordResetSending: false,
188
+ notPasswordResetSending: true,
189
+ passwordRequestVersion: 0,
190
+ passwordError: "",
191
+ passwordNoError: true,
192
+ passwordMessage: "",
193
+ passwordNoMessage: true,
194
+ emailPanelOpen: false,
195
+ emailUnavailable: "",
196
+ emailManageAvailable: false,
197
+ noEmailManage: true,
198
+ hasAddressesRoute: false,
199
+ noAddressesRoute: true,
200
+ hasWalletRoute: false,
201
+ noWalletRoute: true,
202
+ };
203
+
204
+ type WixMemberResponse = {
205
+ member?: {
206
+ id?: string;
207
+ loginEmail?: string;
208
+ contact?: { firstName?: string; lastName?: string; phones?: string[] };
209
+ profile?: { nickname?: string; title?: string; slug?: string; photo?: { id?: string; url?: string } };
210
+ privacyStatus?: string;
211
+ };
212
+ };
213
+
214
+ function rawText(value: unknown): string { return typeof value === "string" ? value : ""; }
215
+ function clean(value: unknown): string { return rawText(value).trim(); }
216
+ type ProfileField = "displayName" | "title" | "firstName" | "lastName" | "phone" | "photo";
217
+ function fieldErrorKey(field: ProfileField): string { return field === "photo" ? "photoUploadError" : `${field}Error`; }
218
+ function clearFieldErrors(): Pick<S, "displayNameError" | "titleError" | "firstNameError" | "lastNameError" | "phoneError"> {
219
+ return { displayNameError: "", titleError: "", firstNameError: "", lastNameError: "", phoneError: "" };
220
+ }
221
+ function validationBody(error: unknown): Record<string, unknown> | undefined {
222
+ const object = (value: unknown): Record<string, unknown> | undefined => value && typeof value === "object" ? value as Record<string, unknown> : undefined;
223
+ const err = object(error);
224
+ const message = typeof err?.message === "string" ? err.message : "";
225
+ const rawBody = /^HTTP 400:\s*([\s\S]+)$/.exec(message)?.[1];
226
+ let body: Record<string, unknown> | undefined;
227
+ if (rawBody) {
228
+ try { body = object(JSON.parse(rawBody)); } catch { /* Non-JSON HTTP errors are not field validation. */ }
229
+ }
230
+ return body;
231
+ }
232
+ function isDefiniteValidationFailure(error: unknown): boolean {
233
+ const message = typeof (error as { message?: unknown } | null)?.message === "string" ? (error as { message: string }).message : "";
234
+ if (/^HTTP 400 \[INVALID_ARGUMENT\]:/.test(message)) return true;
235
+ const body = validationBody(error);
236
+ if (!body) return false;
237
+ const object = (value: unknown): Record<string, unknown> | undefined => value && typeof value === "object" ? value as Record<string, unknown> : undefined;
238
+ const details = object(body.details);
239
+ const validation = object(details?.validationError) ?? object(body.validationError);
240
+ return !!validation || Array.isArray(details?.fieldViolations) || Array.isArray(body.fieldViolations);
241
+ }
242
+ function fieldViolations(error: unknown): Set<ProfileField> {
243
+ const result = new Set<ProfileField>();
244
+ const body = validationBody(error);
245
+ if (!body) return result;
246
+ const object = (value: unknown): Record<string, unknown> | undefined => value && typeof value === "object" ? value as Record<string, unknown> : undefined;
247
+ const details = object(body.details);
248
+ const validation = object(details?.validationError) ?? object(body.validationError);
249
+ const violations = validation?.fieldViolations ?? details?.fieldViolations ?? body.fieldViolations;
250
+ if (!Array.isArray(violations)) return result;
251
+ const paths: Record<string, ProfileField> = {
252
+ nickname: "displayName", "profile.nickname": "displayName", "member.profile.nickname": "displayName",
253
+ title: "title", "profile.title": "title", "member.profile.title": "title",
254
+ "contact.firstname": "firstName", firstname: "firstName", "member.contact.firstname": "firstName",
255
+ "contact.lastname": "lastName", lastname: "lastName", "member.contact.lastname": "lastName",
256
+ phone: "phone", phones: "phone", "contact.phone": "phone", "contact.phones": "phone", "member.contact.phones": "phone",
257
+ photo: "photo", "profile.photo": "photo", "member.profile.photo": "photo",
258
+ };
259
+ for (const violation of violations) {
260
+ const item = object(violation);
261
+ const path = [item?.field, item?.fieldName, item?.path, item?.property].find((value) => typeof value === "string");
262
+ if (typeof path !== "string") continue;
263
+ const normalized = path.toLowerCase().replace(/\[\d+\]/g, "").replace(/_/g, "");
264
+ const field = paths[normalized];
265
+ if (field) result.add(field);
266
+ }
267
+ return result;
268
+ }
269
+ function changedProfileFields(s: S): Set<ProfileField> {
270
+ const fields = new Set<ProfileField>();
271
+ if (s.displayName.trim() !== s.original.displayName.trim()) fields.add("displayName");
272
+ if (s.title.trim() !== s.original.title.trim()) fields.add("title");
273
+ if (s.firstName.trim() !== s.original.firstName.trim()) fields.add("firstName");
274
+ if (s.lastName.trim() !== s.original.lastName.trim()) fields.add("lastName");
275
+ if (s.phone.trim() !== (s.originalPhones[0] ?? "").trim()) fields.add("phone");
276
+ if (s.photoMediaId && s.photoDraftUrl) fields.add("photo");
277
+ return fields;
278
+ }
279
+ function memberPatch(s: S, fields: Set<ProfileField>): { profile?: Record<string, unknown>; contact?: Record<string, unknown> } {
280
+ const profile: Record<string, unknown> = {};
281
+ const contact: Record<string, unknown> = {};
282
+ if (fields.has("displayName")) profile.nickname = s.displayName.trim();
283
+ if (fields.has("title")) profile.title = s.title.trim();
284
+ if (fields.has("photo") && s.photoMediaId && s.photoDraftUrl) profile.photo = { id: s.photoMediaId, url: s.photoDraftUrl };
285
+ if (fields.has("firstName")) contact.firstName = s.firstName.trim();
286
+ if (fields.has("lastName")) contact.lastName = s.lastName.trim();
287
+ if (fields.has("phone")) {
288
+ const nextPhones = s.phone.trim() ? [s.phone.trim(), ...s.originalPhones.slice(1)] : s.originalPhones.slice(1);
289
+ if (nextPhones.length) contact.phones = nextPhones;
290
+ }
291
+ return { ...(Object.keys(profile).length ? { profile } : {}), ...(Object.keys(contact).length ? { contact } : {}) };
292
+ }
293
+ function photoUploadFunctionName(value: unknown): string {
294
+ const name = clean(value);
295
+ return /^[A-Za-z][A-Za-z0-9_]{0,63}$/.test(name) ? name : "";
296
+ }
297
+ function validUploadUrl(value: unknown): string {
298
+ if (typeof value !== "string") return "";
299
+ // Hand-parsed: JSC (iOS) has no `URL` global. Host is plain letters/digits/dots, so userinfo and ports never match.
300
+ const m = /^https:\/\/([A-Za-z0-9.-]+)(?:[/?][^#\s\\]*)?$/.exec(value);
301
+ const host = m ? m[1].toLowerCase() : "";
302
+ return host === "wixmp.com" || host.endsWith(".wixmp.com") ? value : "";
303
+ }
304
+ function validPhotoUrl(value: unknown): string {
305
+ if (typeof value !== "string") return "";
306
+ // Members v1 documents profile photo URLs as protocol-relative even though
307
+ // upload responses use absolute HTTPS URLs.
308
+ const normalized = value.startsWith("//") ? `https:${value}` : value;
309
+ const m = /^https:\/\/([A-Za-z0-9.-]+)(\/media\/[^?#\s\\]*)(?:\?[^#\s\\]*)?$/.exec(normalized);
310
+ return m && m[1].toLowerCase() === "static.wixstatic.com" && !/\/\.\.?(\/|$)/.test(m[2]) ? normalized : "";
311
+ }
312
+ function memberPhotoDisplayUrl(value: unknown): string {
313
+ const photoUrl = clean(value);
314
+ return photoUrl.startsWith("//") ? `https:${photoUrl}` : photoUrl;
315
+ }
316
+ function canonicalPhotoMediaId(value: unknown): string {
317
+ const mediaId = clean(value);
318
+ try { return decodeURIComponent(mediaId); }
319
+ catch { return mediaId; }
320
+ }
321
+ const SITE_HANDOFF_URL = /^https?:\/\/([^\s/?#\\@:]+)(?::\d+)?[^\s\\]*$/i;
322
+ /** A website handoff needs a real http(s) site: the documented placeholder (example.com) never counts. */
323
+ function siteHandoffAvailable(siteUrl: string): boolean {
324
+ const match = SITE_HANDOFF_URL.exec(siteUrl);
325
+ if (!match) return false;
326
+ const host = match[1].toLowerCase();
327
+ return host !== "example.com" && !host.endsWith(".example.com");
328
+ }
329
+ function profileUrl(siteUrl: string, slug: string): string {
330
+ if (!siteUrl || !slug) return "";
331
+ const m = /^(https?):\/\/([^\s/?#\\@]+)((?:\/[^\s?#\\]*)?)(?:[?#].*)?$/i.exec(siteUrl);
332
+ if (!m) return "";
333
+ return `${m[1].toLowerCase()}://${m[2].toLowerCase()}${m[3].replace(/\/$/, "")}/profile/${encodeURIComponent(slug)}/profile`;
334
+ }
335
+
336
+ function memberValues(member: NonNullable<WixMemberResponse["member"]>): ProfileValues {
337
+ const firstName = clean(member.contact?.firstName);
338
+ const lastName = clean(member.contact?.lastName);
339
+ return {
340
+ displayName: clean(member.profile?.nickname) || [firstName, lastName].filter(Boolean).join(" ") || clean(member.loginEmail),
341
+ title: clean(member.profile?.title),
342
+ firstName,
343
+ lastName,
344
+ phone: clean(member.contact?.phones?.[0]),
345
+ slug: clean(member.profile?.slug),
346
+ memberImage: memberPhotoDisplayUrl(member.profile?.photo?.url),
347
+ memberImageId: clean(member.profile?.photo?.id),
348
+ memberInitials: ([firstName[0], lastName[0]].filter(Boolean).join("") || clean(member.profile?.nickname).slice(0, 1) || "M").toUpperCase(),
349
+ };
350
+ }
351
+
352
+ async function fetchMember(siteUrlInput: unknown): Promise<{ email: string; scope: string; siteUrl: string; member: NonNullable<WixMemberResponse["member"]> }> {
353
+ const access = await memberAccess();
354
+ if (!access.email) throw Object.assign(new Error("signed_out"), { code: "signed_out" });
355
+ const response = await getMyMember({ fieldsets: ["FULL"] }) as WixMemberResponse;
356
+ if (!response.member?.id) throw new Error("The signed-in member profile is unavailable.");
357
+ const siteUrl = clean(siteUrlInput).replace(/\/$/, "");
358
+ return { email: access.email, scope: access.scope, siteUrl, member: response.member };
359
+ }
360
+
361
+ function fromMember(member: NonNullable<WixMemberResponse["member"]>, email: string, scope: string, siteUrl: string, prior?: S, routes?: { addresses: boolean; wallet: boolean; passwordCredentials: boolean }): S {
362
+ const values = memberValues(member);
363
+ const privacyStatus = clean(member.privacyStatus).toUpperCase();
364
+ const privacySupported = privacyStatus === "PUBLIC" || privacyStatus === "PRIVATE";
365
+ const currentProfileUrl = profileUrl(siteUrl, values.slug);
366
+ return {
367
+ ...(prior ?? EMPTY),
368
+ ...values,
369
+ status: "loaded",
370
+ signedOut: false,
371
+ errorText: "",
372
+ error: "",
373
+ signInRequired: false,
374
+ message: "",
375
+ memberId: clean(member.id),
376
+ memberEmail: email,
377
+ loginEmail: clean(member.loginEmail) || email,
378
+ privacyStatus,
379
+ privacySupported,
380
+ privacyActionLabel: privacySupported
381
+ ? copyParam(privacyStatus === "PRIVATE" ? "accountSettings.makePublicCta" : "accountSettings.makePrivateCta", privacyStatus === "PRIVATE" ? "Make profile public" : "Make profile private")
382
+ : "",
383
+ isPrivate: privacyStatus === "PRIVATE",
384
+ notPrivate: privacyStatus !== "PRIVATE",
385
+ hasMemberImage: !!values.memberImage,
386
+ noMemberImage: !values.memberImage,
387
+ photoDisplayUrl: values.memberImage,
388
+ uploadUrl: "",
389
+ photoDraftUrl: "",
390
+ photoMediaId: "",
391
+ photoPendingMemberId: "",
392
+ photoPendingMemberEmail: "",
393
+ photoPendingMemberScope: "",
394
+ photoUploadError: "",
395
+ photoUploading: false,
396
+ notPhotoUploading: true,
397
+ privacyControlsUnavailable: !privacySupported,
398
+ siteUrl,
399
+ siteHandoff: siteHandoffAvailable(siteUrl),
400
+ noSiteHandoff: !siteHandoffAvailable(siteUrl),
401
+ profileUrl: currentProfileUrl,
402
+ noSiteUrl: !currentProfileUrl,
403
+ hasProfileUrl: !!currentProfileUrl,
404
+ memberScope: scope,
405
+ originalPhones: [...(member.contact?.phones ?? [])],
406
+ original: { ...values },
407
+ dirty: false,
408
+ saving: false,
409
+ displayNameError: "",
410
+ titleError: "",
411
+ firstNameError: "",
412
+ lastNameError: "",
413
+ phoneError: "",
414
+ noError: true,
415
+ noMessage: true,
416
+ noMember: false,
417
+ notLoading: true,
418
+ notErrored: true,
419
+ loginControlsUnavailable: true,
420
+ noSignIn: false,
421
+ hasAddressesRoute: routes?.addresses ?? prior?.hasAddressesRoute ?? false,
422
+ noAddressesRoute: !(routes?.addresses ?? prior?.hasAddressesRoute ?? false),
423
+ hasWalletRoute: routes?.wallet ?? prior?.hasWalletRoute ?? false,
424
+ noWalletRoute: !(routes?.wallet ?? prior?.hasWalletRoute ?? false),
425
+ passwordCredentialSupported: routes?.passwordCredentials ?? prior?.passwordCredentialSupported ?? false,
426
+ noPasswordCredentialSupport: !(routes?.passwordCredentials ?? prior?.passwordCredentialSupported ?? false),
427
+ slugEditing: false,
428
+ slugMessage: "",
429
+ noSlugMessage: true,
430
+ privacyConfirmVisible: false,
431
+ pendingPrivacyStatus: "",
432
+ noPrivacyConfirm: true,
433
+ privacyPrompt: "",
434
+ };
435
+ }
436
+
437
+ function failed(s: S, error: unknown): S {
438
+ const signedOut = (error as { code?: string } | null)?.code === "signed_out";
439
+ return {
440
+ ...s,
441
+ ...(signedOut ? {
442
+ ...BLANK,
443
+ memberId: "",
444
+ memberEmail: "",
445
+ loginEmail: "",
446
+ original: { ...BLANK },
447
+ originalPhones: [],
448
+ memberScope: "",
449
+ siteUrl: "",
450
+ siteHandoff: false,
451
+ noSiteHandoff: true,
452
+ profileUrl: "",
453
+ noSiteUrl: true,
454
+ hasProfileUrl: false,
455
+ hasMemberImage: false,
456
+ noMemberImage: true,
457
+ privacySupported: false,
458
+ privacyStatus: "",
459
+ isPrivate: false,
460
+ photoUploadAvailable: false,
461
+ noPhotoUploadAvailable: true,
462
+ photoUploadFunctionName: "",
463
+ photoUploadSupported: false,
464
+ photoDisplayUrl: "",
465
+ uploadUrl: "",
466
+ photoDraftUrl: "",
467
+ photoMediaId: "",
468
+ photoPendingMemberId: "",
469
+ photoPendingMemberEmail: "",
470
+ photoPendingMemberScope: "",
471
+ photoUploadError: "",
472
+ photoUploading: false,
473
+ notPhotoUploading: true,
474
+ dirty: false,
475
+ } : {}),
476
+ status: "error",
477
+ signedOut,
478
+ errorText: signedOut
479
+ ? copyParam("accountSettings.signedOut", "Sign in to view your account details.")
480
+ : copyParam("accountSettings.loadError", "Your account details could not be loaded. Try again."),
481
+ error: signedOut
482
+ ? copyParam("accountSettings.signedOut", "Sign in to view your account details.")
483
+ : copyParam("accountSettings.loadError", "Your account details could not be loaded. Try again."),
484
+ signInRequired: signedOut,
485
+ noError: false,
486
+ noMember: true,
487
+ notLoading: true,
488
+ notErrored: false,
489
+ saving: false,
490
+ noSignIn: !signedOut,
491
+ };
492
+ }
493
+
494
+ let profileMutationInFlight = false;
495
+ let slugMutationInFlight = false;
496
+ let privacyMutationInFlight = false;
497
+ let passwordMutationInFlight = false;
498
+ let passwordResetInFlight = false;
499
+ let passwordMutationVersion = 0;
500
+ let photoUploadMintInFlight = false;
501
+ let photoUploadVersion = 0;
502
+ let accountLoadVersion = 0;
503
+ // The native picker uploads the file itself and never reports a failed PUT back to the worker, so a minted
504
+ // upload URL counts as "upload in progress" only for a bounded window. Save waits inside it, then recovers.
505
+ const PHOTO_UPLOAD_WAIT_MS = 60_000;
506
+ let photoMintedAt = 0;
507
+ function photoUploadPending(s: S): boolean {
508
+ return !!s.uploadUrl && !!s.photoPendingMemberId && Date.now() - photoMintedAt < PHOTO_UPLOAD_WAIT_MS;
509
+ }
510
+
511
+ function clearPassword(s: S): S {
512
+ return { ...s, passwordCurrent: "", passwordNew: "", passwordConfirm: "" };
513
+ }
514
+
515
+ function clearPhotoDraft(s: S): S {
516
+ photoUploadVersion += 1;
517
+ return {
518
+ ...s,
519
+ uploadUrl: "",
520
+ photoDraftUrl: "",
521
+ photoDisplayUrl: s.memberImage,
522
+ photoMediaId: "",
523
+ photoPendingMemberId: "",
524
+ photoPendingMemberEmail: "",
525
+ photoPendingMemberScope: "",
526
+ photoUploadError: "",
527
+ photoUploading: false,
528
+ notPhotoUploading: true,
529
+ };
530
+ }
531
+
532
+ function clearPhotoUploadAttempt(s: S, message = ""): S {
533
+ photoUploadVersion += 1;
534
+ return {
535
+ ...s,
536
+ uploadUrl: "",
537
+ photoPendingMemberId: "",
538
+ photoPendingMemberEmail: "",
539
+ photoPendingMemberScope: "",
540
+ photoUploadError: message,
541
+ photoUploading: false,
542
+ notPhotoUploading: true,
543
+ };
544
+ }
545
+
546
+ async function currentMemberMatches(s: S): Promise<boolean> {
547
+ return currentMemberRecordMatches(s.memberEmail, s.memberScope, s.memberId);
548
+ }
549
+
550
+ async function currentMemberRecordMatches(email: string, scope: string, memberId: string): Promise<boolean> {
551
+ const before = await memberAccess();
552
+ if (!before.email || before.email !== email || before.scope !== scope) return false;
553
+ const response = await getMyMember({ fieldsets: ["FULL"] }) as WixMemberResponse;
554
+ const after = await memberAccess();
555
+ return response.member?.id === memberId && after.email === email && after.scope === scope;
556
+ }
557
+
558
+ type PasswordResult = { status?: string; code?: string; payload?: { status?: string; code?: string; payload?: { code?: string } } } | null;
559
+
560
+ async function performPasswordChange(version: number, memberId: string, memberEmail: string, memberScope: string, currentPassword: string, newPassword: string): Promise<void> {
561
+ try {
562
+ const before = await memberAccess();
563
+ if (!before.email || before.email !== memberEmail || before.scope !== memberScope) {
564
+ await svc.resolvePasswordChange({ version, status: "error", code: "account_changed" });
565
+ return;
566
+ }
567
+ const result = await dispatch("himi.wix.changePassword", { currentPassword, newPassword, expectedMemberId: memberId }) as PasswordResult;
568
+ const current = await memberAccess();
569
+ if (!current.email || current.email !== memberEmail || current.scope !== memberScope) {
570
+ await svc.resolvePasswordChange({ version, status: "error", code: "account_changed" });
571
+ return;
572
+ }
573
+ await svc.resolvePasswordChange({ version, status: result?.status === "ok" || result?.payload?.status === "ok" ? "ok" : "error", code: result?.code ?? result?.payload?.code ?? result?.payload?.payload?.code });
574
+ } catch {
575
+ // A binary without the native handler fails closed here; never report success.
576
+ await svc.resolvePasswordChange({ version, status: "error", code: "unavailable" });
577
+ } finally {
578
+ passwordMutationInFlight = false;
579
+ }
580
+ }
581
+
582
+ async function performPasswordReset(version: number, memberId: string, memberEmail: string, memberScope: string): Promise<void> {
583
+ let result: PasswordResult;
584
+ let code = "";
585
+ try {
586
+ const before = await memberAccess();
587
+ const beforeMember = await getMyMember({ fieldsets: ["FULL"] }) as WixMemberResponse;
588
+ const afterRead = await memberAccess();
589
+ const resetEmail = clean(beforeMember.member?.loginEmail);
590
+ if (!before.email || before.email !== memberEmail || before.scope !== memberScope || afterRead.email !== memberEmail || afterRead.scope !== memberScope || beforeMember.member?.id !== memberId || !resetEmail) {
591
+ code = "account_changed";
592
+ } else {
593
+ result = await dispatch("himi.wix.resetPassword", { email: resetEmail }) as PasswordResult;
594
+ const current = await memberAccess();
595
+ const afterMember = await getMyMember({ fieldsets: ["FULL"] }) as WixMemberResponse;
596
+ const finalAccess = await memberAccess();
597
+ if (current.email !== memberEmail || current.scope !== memberScope || finalAccess.email !== memberEmail || finalAccess.scope !== memberScope || afterMember.member?.id !== memberId) code = "account_changed";
598
+ else if (result?.status !== "ok" && result?.payload?.status !== "ok") code = "reset_failed";
599
+ }
600
+ } catch {
601
+ code = "reset_failed";
602
+ } finally {
603
+ passwordResetInFlight = false;
604
+ }
605
+ await svc.resolvePasswordReset({ version, code });
606
+ }
607
+
608
+ const svc = defineService<S, {
609
+ load: (s: S, arg?: unknown) => Promise<S>;
610
+ setDisplayName: (s: S, arg?: unknown) => S;
611
+ setTitle: (s: S, arg?: unknown) => S;
612
+ setFirstName: (s: S, arg?: unknown) => S;
613
+ setLastName: (s: S, arg?: unknown) => S;
614
+ setPhone: (s: S, arg?: unknown) => S;
615
+ setSlug: (s: S, arg?: unknown) => S;
616
+ discard: (s: S) => S;
617
+ genUpload: (s: S, arg?: { fileName?: unknown; mimeType?: unknown }) => Promise<S>;
618
+ setMedia: (s: S, arg?: { mediaId?: unknown; imageUrl?: unknown }) => Promise<S>;
619
+ save: (s: S) => Promise<S | undefined>;
620
+ saveSlug: (s: S) => Promise<S | undefined>;
621
+ setPrivate: (s: S, arg?: unknown) => Promise<S | undefined>;
622
+ requestPrivacyChange: (s: S, arg?: unknown) => S;
623
+ cancelPrivacyChange: (s: S) => S;
624
+ setPasswordCurrent: (s: S, arg?: unknown) => S;
625
+ setPasswordNew: (s: S, arg?: unknown) => S;
626
+ setPasswordConfirm: (s: S, arg?: unknown) => S;
627
+ openPasswordPanel: (s: S) => S;
628
+ closePasswordPanel: (s: S) => S;
629
+ openEmailPanel: (s: S) => S;
630
+ closeEmailPanel: (s: S) => S;
631
+ changePassword: (s: S) => S;
632
+ resolvePasswordChange: (s: S, arg?: unknown) => S;
633
+ resolvePasswordReset: (s: S, arg?: unknown) => S;
634
+ resetPassword: (s: S) => S;
635
+ openSite: (s: S) => Promise<S | undefined>;
636
+ }> ("himiBrandedAccountSettings", {
637
+ state: { ...EMPTY },
638
+ notHydrated: ["status", "signedOut", "errorText", "error", "message", "memberId", "memberEmail", "loginEmail", "original", "originalPhones", "memberScope", "siteHandoff", "noSiteHandoff", "dirty", "saving", "displayNameError", "titleError", "firstNameError", "lastNameError", "phoneError", "noError", "noMessage", "noMember", "notLoading", "notErrored", "noSignIn", "slugEditing", "slugMessage", "noSlugMessage", "siteUrl", "profileUrl", "noSiteUrl", "privacyConfirmVisible", "noPrivacyConfirm", "pendingPrivacyStatus", "privacyPrompt", "passwordPanelOpen", "passwordCredentialSupported", "noPasswordCredentialSupport", "passwordCurrent", "passwordNew", "passwordConfirm", "passwordSaving", "notPasswordSaving", "notPasswordChanging", "passwordResetSending", "notPasswordResetSending", "passwordRequestVersion", "passwordError", "passwordNoError", "passwordMessage", "passwordNoMessage", "emailPanelOpen", "hasAddressesRoute", "noAddressesRoute", "hasWalletRoute", "noWalletRoute", "photoUploadAvailable", "noPhotoUploadAvailable", "photoUploadFunctionName", "photoUploadSupported", "photoDisplayUrl", "uploadUrl", "photoDraftUrl", "photoMediaId", "photoPendingMemberId", "photoPendingMemberEmail", "photoPendingMemberScope", "photoUploadError", "photoUploading", "notPhotoUploading"],
639
+ actions: {
640
+ load: async (_s, arg): Promise<S> => {
641
+ const version = ++accountLoadVersion;
642
+ photoUploadVersion += 1;
643
+ stashCopy(arg);
644
+ try {
645
+ const { email, scope, siteUrl, member } = await fetchMember((arg as { siteUrl?: unknown } | undefined)?.siteUrl);
646
+ const current = await memberAccess();
647
+ if (!current.email || current.email !== email || current.scope !== scope) {
648
+ if (version !== accountLoadVersion) return svc.getState() as S;
649
+ return failed({ ...EMPTY, notLoading: true }, Object.assign(new Error("signed_out"), { code: "signed_out" }));
650
+ }
651
+ const routes = await memberAccountRoutes();
652
+ if (version !== accountLoadVersion) return svc.getState() as S;
653
+ if (!(await currentMemberRecordMatches(email, scope, clean(member.id)))) {
654
+ if (version !== accountLoadVersion) return svc.getState() as S;
655
+ return failed({ ...EMPTY, notLoading: true }, Object.assign(new Error("signed_out"), { code: "signed_out" }));
656
+ }
657
+ if (version !== accountLoadVersion) return svc.getState() as S;
658
+ const loaded = fromMember(member, email, scope, siteUrl, undefined, routes);
659
+ const configuredFunctionName = photoUploadFunctionName((arg as { photoUploadFunctionName?: unknown } | undefined)?.photoUploadFunctionName);
660
+ const available = !!configuredFunctionName && routes.mediaField;
661
+ return { ...loaded, photoUploadFunctionName: configuredFunctionName, photoUploadSupported: routes.mediaField, photoUploadAvailable: available, noPhotoUploadAvailable: !available };
662
+ } catch (error) {
663
+ if (version !== accountLoadVersion) return svc.getState() as S;
664
+ return failed({ ...EMPTY, notLoading: true }, error);
665
+ }
666
+ },
667
+ setDisplayName: (s, arg) => ({ ...s, displayName: rawText((arg as { value?: unknown } | undefined)?.value), dirty: true, displayNameError: "", message: "", noMessage: true, noError: true }),
668
+ setTitle: (s, arg) => ({ ...s, title: rawText((arg as { value?: unknown } | undefined)?.value), dirty: true, titleError: "", message: "", noMessage: true }),
669
+ setFirstName: (s, arg) => ({ ...s, firstName: rawText((arg as { value?: unknown } | undefined)?.value), dirty: true, firstNameError: "", message: "", noMessage: true }),
670
+ setLastName: (s, arg) => ({ ...s, lastName: rawText((arg as { value?: unknown } | undefined)?.value), dirty: true, lastNameError: "", message: "", noMessage: true }),
671
+ setPhone: (s, arg) => ({ ...s, phone: rawText((arg as { value?: unknown } | undefined)?.value), dirty: true, phoneError: "", message: "", noMessage: true }),
672
+ setSlug: (s, arg) => ({ ...s, slug: rawText((arg as { value?: unknown } | undefined)?.value), dirty: true, slugMessage: "", noSlugMessage: true }),
673
+ genUpload: async (s, arg): Promise<S> => {
674
+ if (photoUploadMintInFlight || s.status !== "loaded" || !s.memberId || !s.photoUploadAvailable || !s.photoUploadFunctionName) {
675
+ return { ...s, uploadUrl: "", photoUploadError: copyParam("accountSettings.profilePhotoUnavailable", "Profile photo changes are not available in this session."), photoUploading: false, notPhotoUploading: true };
676
+ }
677
+ photoUploadMintInFlight = true;
678
+ const version = ++photoUploadVersion;
679
+ try {
680
+ if (!(await currentMemberMatches(s))) {
681
+ if (version !== photoUploadVersion) return svc.getState() as S;
682
+ const latest = svc.getState() as S;
683
+ if (latest.memberId !== s.memberId || latest.memberEmail !== s.memberEmail || latest.memberScope !== s.memberScope) return latest;
684
+ return clearPhotoUploadAttempt(latest, copyParam("accountSettings.profilePhotoAccountChanged", "Your account changed. Reload before choosing a profile photo."));
685
+ }
686
+ const metadata = arg ?? {};
687
+ const mimeType = clean(metadata.mimeType);
688
+ const fileName = clean(metadata.fileName);
689
+ if (!/^image\/(jpeg|png|webp)$/i.test(mimeType) || !/^[A-Za-z0-9._-]{1,128}$/.test(fileName)) {
690
+ const latest = svc.getState() as S;
691
+ if (version !== photoUploadVersion) return latest;
692
+ return clearPhotoUploadAttempt(latest, copyParam("accountSettings.profilePhotoUploadError", "The photo could not be prepared. Choose another image."));
693
+ }
694
+ const minted = await generateMemberPhotoUploadUrl(s.photoUploadFunctionName, mimeType, fileName);
695
+ if (version !== photoUploadVersion) return svc.getState() as S;
696
+ if (!(await currentMemberMatches(s))) {
697
+ if (version !== photoUploadVersion) return svc.getState() as S;
698
+ const latest = svc.getState() as S;
699
+ if (latest.memberId !== s.memberId || latest.memberEmail !== s.memberEmail || latest.memberScope !== s.memberScope) return latest;
700
+ return clearPhotoUploadAttempt(latest, copyParam("accountSettings.profilePhotoAccountChanged", "Your account changed. Reload before choosing a profile photo."));
701
+ }
702
+ if (version !== photoUploadVersion) return svc.getState() as S;
703
+ const uploadUrl = validUploadUrl(minted.uploadUrl);
704
+ if (!uploadUrl) return clearPhotoUploadAttempt(svc.getState() as S, copyParam("accountSettings.profilePhotoUploadError", "The photo could not be prepared. Choose another image."));
705
+ const latest = svc.getState() as S;
706
+ if (latest.memberId !== s.memberId || latest.memberEmail !== s.memberEmail || latest.memberScope !== s.memberScope) return latest;
707
+ photoMintedAt = Date.now();
708
+ return {
709
+ ...latest,
710
+ uploadUrl: uploadUrl,
711
+ photoPendingMemberId: latest.memberId,
712
+ photoPendingMemberEmail: latest.memberEmail,
713
+ photoPendingMemberScope: latest.memberScope,
714
+ photoUploadError: "",
715
+ photoUploading: false,
716
+ notPhotoUploading: true,
717
+ };
718
+ } catch {
719
+ const latest = svc.getState() as S;
720
+ if (version !== photoUploadVersion || latest.memberId !== s.memberId || latest.memberEmail !== s.memberEmail || latest.memberScope !== s.memberScope) return latest;
721
+ return clearPhotoUploadAttempt(latest, copyParam("accountSettings.profilePhotoUploadError", "The photo could not be uploaded. Try again."));
722
+ } finally { photoUploadMintInFlight = false; }
723
+ },
724
+ setMedia: async (s, arg): Promise<S> => {
725
+ const mediaId = clean(arg?.mediaId);
726
+ const imageUrl = validPhotoUrl(arg?.imageUrl);
727
+ if (!s.uploadUrl || !s.photoPendingMemberId || s.photoPendingMemberId !== s.memberId || !/^[A-Za-z0-9._~:/+-]{1,512}$/.test(mediaId) || !imageUrl) {
728
+ return { ...s, uploadUrl: "", photoPendingMemberId: "", photoPendingMemberEmail: "", photoPendingMemberScope: "", photoUploadError: copyParam("accountSettings.profilePhotoUploadError", "The photo could not be uploaded. Try again.") };
729
+ }
730
+ const version = photoUploadVersion;
731
+ try {
732
+ const matches = await currentMemberMatches(s);
733
+ // The identity check awaits the network: merge onto the CURRENT state, never the entry snapshot,
734
+ // and drop the result if the upload attempt was superseded (new pick, discard, member change).
735
+ const latest = svc.getState() as S;
736
+ if (version !== photoUploadVersion || latest.memberId !== s.memberId || latest.memberEmail !== s.memberEmail || latest.memberScope !== s.memberScope) return latest;
737
+ if (!matches || s.photoPendingMemberEmail !== latest.memberEmail || s.photoPendingMemberScope !== latest.memberScope) {
738
+ return clearPhotoUploadAttempt(latest, copyParam("accountSettings.profilePhotoAccountChanged", "Your account changed. Reload before choosing a profile photo."));
739
+ }
740
+ photoMintedAt = 0;
741
+ return {
742
+ ...latest,
743
+ uploadUrl: "",
744
+ photoDraftUrl: imageUrl,
745
+ photoDisplayUrl: imageUrl,
746
+ photoMediaId: mediaId,
747
+ photoPendingMemberId: "",
748
+ photoPendingMemberEmail: "",
749
+ photoPendingMemberScope: "",
750
+ photoUploadError: "",
751
+ dirty: true,
752
+ };
753
+ } catch {
754
+ const latest = svc.getState() as S;
755
+ if (version !== photoUploadVersion || latest.memberId !== s.memberId || latest.memberEmail !== s.memberEmail || latest.memberScope !== s.memberScope) return latest;
756
+ return clearPhotoUploadAttempt(latest, copyParam("accountSettings.profilePhotoAccountChanged", "Your account changed. Reload before choosing a profile photo."));
757
+ }
758
+ },
759
+ discard: (s) => ({ ...clearPhotoDraft(s), ...s.original, ...clearFieldErrors(), dirty: false, message: "", noMessage: true, noError: true }),
760
+ save: async (s): Promise<S | undefined> => {
761
+ if (profileMutationInFlight || photoUploadMintInFlight || s.photoUploading || photoUploadPending(s) || !s.memberId || s.status !== "loaded") return undefined;
762
+ profileMutationInFlight = true;
763
+ const acceptedFields = new Set<ProfileField>();
764
+ try {
765
+ if (!(await currentMemberMatches(s))) {
766
+ return failed(s, Object.assign(new Error("signed_out"), { code: "signed_out" }));
767
+ }
768
+ const attemptedFields = changedProfileFields(s);
769
+ const fields = new Set(attemptedFields);
770
+ const rejected = new Set<ProfileField>();
771
+ if (!s.displayName.trim()) {
772
+ fields.delete("displayName");
773
+ rejected.add("displayName");
774
+ }
775
+ const savingPhotoId = s.photoMediaId;
776
+ const savingPhotoUrl = s.photoDraftUrl;
777
+ const submitFields = async (submitted: Set<ProfileField>): Promise<WixMemberResponse | undefined> => {
778
+ const patch = memberPatch(s, submitted);
779
+ if (!patch.profile && !patch.contact) return undefined;
780
+ if (!(await currentMemberMatches(s))) throw Object.assign(new Error("signed_out"), { code: "signed_out" });
781
+ const response = await updateMember(s.memberId, { member: patch }) as WixMemberResponse;
782
+ if (response.member?.id !== s.memberId) throw new Error("The profile update was not confirmed for this member.");
783
+ for (const field of submitted) acceptedFields.add(field);
784
+ return response;
785
+ };
786
+ let response: WixMemberResponse | undefined;
787
+ let pendingFields = new Set(fields);
788
+ while (pendingFields.size) {
789
+ try {
790
+ response = await submitFields(pendingFields);
791
+ break;
792
+ } catch (error) {
793
+ if (!isDefiniteValidationFailure(error)) throw error;
794
+ const violations = fieldViolations(error);
795
+ const rejectedByResponse = new Set([...violations].filter((field) => pendingFields.has(field)));
796
+ if (rejectedByResponse.size) {
797
+ for (const field of rejectedByResponse) rejected.add(field);
798
+ pendingFields = new Set([...pendingFields].filter((field) => !rejectedByResponse.has(field)));
799
+ // A later validation response may identify another field. Keep
800
+ // filtering the known failures from this changed-field subset.
801
+ continue;
802
+ }
803
+ // Typed INVALID_ARGUMENT responses contain no field details in the
804
+ // native bridge. Isolate only this failed batch's changed fields.
805
+ for (const field of pendingFields) {
806
+ try {
807
+ const one = await submitFields(new Set([field]));
808
+ if (one) response = one;
809
+ } catch (fieldError) {
810
+ if (!isDefiniteValidationFailure(fieldError)) throw fieldError;
811
+ const details = fieldViolations(fieldError);
812
+ if (details.size && !details.has(field)) throw fieldError;
813
+ rejected.add(field);
814
+ }
815
+ }
816
+ pendingFields = new Set();
817
+ }
818
+ }
819
+ // Wix ignores an empty phones array, so clear the primary number only
820
+ // after the other accepted fields have been patched. Secondary numbers
821
+ // remain in place. Validation failure here belongs only to the phone.
822
+ const nextPhones = s.phone.trim() ? [s.phone.trim(), ...s.originalPhones.slice(1)] : s.originalPhones.slice(1);
823
+ if (fields.has("phone") && !rejected.has("phone") && !nextPhones.length && s.originalPhones.length) {
824
+ try {
825
+ if (!(await currentMemberMatches(s))) throw Object.assign(new Error("signed_out"), { code: "signed_out" });
826
+ await deleteMemberPhones(s.memberId);
827
+ acceptedFields.add("phone");
828
+ }
829
+ catch (error) {
830
+ if (!isDefiniteValidationFailure(error)) throw error;
831
+ rejected.add("phone");
832
+ }
833
+ }
834
+ if (response) {
835
+ const afterUpdate = await memberAccess();
836
+ if (!afterUpdate.email || afterUpdate.email !== s.memberEmail || afterUpdate.scope !== s.memberScope) {
837
+ return failed(s, Object.assign(new Error("signed_out"), { code: "signed_out" }));
838
+ }
839
+ }
840
+ const after = await memberAccess();
841
+ if (!after.email || after.email !== s.memberEmail || after.scope !== s.memberScope) {
842
+ return failed(s, Object.assign(new Error("signed_out"), { code: "signed_out" }));
843
+ }
844
+ const refreshed = await getMyMember({ fieldsets: ["FULL"] }) as WixMemberResponse;
845
+ const afterRefresh = await memberAccess();
846
+ if (refreshed.member?.id !== s.memberId || !afterRefresh.email || afterRefresh.email !== s.memberEmail || afterRefresh.scope !== s.memberScope) {
847
+ return failed(s, Object.assign(new Error("signed_out"), { code: "signed_out" }));
848
+ }
849
+ const latestState = svc.getState() as S;
850
+ const readbackValues = memberValues(refreshed.member);
851
+ const expectedPhones = s.phone.trim() ? [s.phone.trim(), ...s.originalPhones.slice(1)] : s.originalPhones.slice(1);
852
+ const actualPhones = refreshed.member.contact?.phones ?? [];
853
+ const phonesMatch = expectedPhones.length === actualPhones.length && expectedPhones.every((phone, index) =>
854
+ phone.trim().replace(/[\s().-]/g, "") === clean(actualPhones[index]).replace(/[\s().-]/g, ""));
855
+ const fieldReadbackMatches: Record<Exclude<ProfileField, "photo">, boolean> = {
856
+ displayName: readbackValues.displayName === s.displayName.trim(),
857
+ title: readbackValues.title === s.title.trim(),
858
+ firstName: readbackValues.firstName === s.firstName.trim(),
859
+ lastName: readbackValues.lastName === s.lastName.trim(),
860
+ phone: phonesMatch,
861
+ };
862
+ for (const field of attemptedFields) {
863
+ if (field === "photo") continue;
864
+ if (rejected.has(field) || latestState[field] !== s[field]) continue;
865
+ if (!fieldReadbackMatches[field]) rejected.add(field);
866
+ }
867
+ const editsSinceSubmit: Partial<ProfileValues> = {};
868
+ for (const key of ["displayName", "title", "firstName", "lastName", "phone"] as const) {
869
+ if (latestState[key] !== s[key]) editsSinceSubmit[key] = latestState[key];
870
+ else if (rejected.has(key)) editsSinceSubmit[key] = s[key];
871
+ }
872
+ const photoChangedDuringSave = latestState.photoMediaId !== s.photoMediaId || latestState.photoDraftUrl !== s.photoDraftUrl;
873
+ const fieldErrors = clearFieldErrors();
874
+ for (const field of rejected) {
875
+ if (field === "photo") continue;
876
+ if (latestState[field] !== s[field]) continue;
877
+ const value = field === "displayName" && !s.displayName.trim()
878
+ ? copyParam("accountSettings.displayNameRequired", "Enter a display name.")
879
+ : copyParam("accountSettings.fieldSaveError", "This field could not be saved. Check it and try again.");
880
+ (fieldErrors as Record<string, string>)[fieldErrorKey(field)] = value;
881
+ }
882
+ const makeDirtyState = (next: S): S => ({ ...next, dirty: changedProfileFields(next).size > 0 });
883
+ const refreshedPhotoId = clean(refreshed.member.profile?.photo?.id);
884
+ const refreshedPhotoUrl = memberPhotoDisplayUrl(refreshed.member.profile?.photo?.url);
885
+ if (savingPhotoId && (rejected.has("photo") || canonicalPhotoMediaId(refreshedPhotoId) !== canonicalPhotoMediaId(savingPhotoId) || !validPhotoUrl(refreshedPhotoUrl))) {
886
+ const keepPhotoDraft = photoChangedDuringSave ? latestState.photoDraftUrl : savingPhotoUrl;
887
+ const keepPhotoId = photoChangedDuringSave ? latestState.photoMediaId : savingPhotoId;
888
+ return makeDirtyState({
889
+ ...fromMember(refreshed.member, s.memberEmail, s.memberScope, s.siteUrl, latestState),
890
+ ...editsSinceSubmit,
891
+ ...fieldErrors,
892
+ photoDraftUrl: keepPhotoDraft,
893
+ photoDisplayUrl: keepPhotoDraft || refreshedPhotoUrl,
894
+ photoMediaId: keepPhotoId,
895
+ uploadUrl: latestState.uploadUrl,
896
+ photoPendingMemberId: latestState.photoPendingMemberId,
897
+ photoPendingMemberEmail: latestState.photoPendingMemberEmail,
898
+ photoPendingMemberScope: latestState.photoPendingMemberScope,
899
+ photoUploadError: rejected.has("photo")
900
+ ? copyParam("accountSettings.fieldSaveError", "This field could not be saved. Check it and try again.")
901
+ : copyParam("accountSettings.profilePhotoSaveError", "Your new profile photo was not confirmed as saved. Try saving it again."),
902
+ message: rejected.size && [...attemptedFields].some((field) => !rejected.has(field)) ? copyParam("accountSettings.partialSave", "Other changes were saved. Fix the highlighted field and try again.") : "",
903
+ noMessage: !(rejected.size && [...attemptedFields].some((field) => !rejected.has(field))),
904
+ });
905
+ }
906
+ const next = makeDirtyState({
907
+ ...fromMember(refreshed.member, s.memberEmail, s.memberScope, s.siteUrl, latestState),
908
+ ...editsSinceSubmit,
909
+ ...fieldErrors,
910
+ ...(photoChangedDuringSave ? { photoDraftUrl: latestState.photoDraftUrl, photoMediaId: latestState.photoMediaId, uploadUrl: latestState.uploadUrl, photoPendingMemberId: latestState.photoPendingMemberId, photoPendingMemberEmail: latestState.photoPendingMemberEmail, photoPendingMemberScope: latestState.photoPendingMemberScope } : {}),
911
+ photoDisplayUrl: photoChangedDuringSave ? latestState.photoDraftUrl || refreshedPhotoUrl : refreshedPhotoUrl,
912
+ photoUploadError: rejected.has("photo")
913
+ ? copyParam("accountSettings.fieldSaveError", "This field could not be saved. Check it and try again.")
914
+ : savingPhotoId && !photoChangedDuringSave && latestState.photoUploadError === s.photoUploadError ? "" : latestState.photoUploadError,
915
+ message: rejected.size
916
+ ? [...attemptedFields].some((field) => !rejected.has(field))
917
+ ? copyParam("accountSettings.partialSave", "Other changes were saved. Fix the highlighted field and try again.")
918
+ : ""
919
+ : copyParam("accountSettings.saved", "Your account details were updated."),
920
+ noMessage: rejected.size > 0 && ![...attemptedFields].some((field) => !rejected.has(field)),
921
+ });
922
+ return next;
923
+ } catch {
924
+ const latest = svc.getState() as S;
925
+ if (latest.memberId !== s.memberId || latest.memberEmail !== s.memberEmail || latest.memberScope !== s.memberScope) {
926
+ return failed(latest, Object.assign(new Error("signed_out"), { code: "signed_out" }));
927
+ }
928
+ try {
929
+ if (!(await currentMemberMatches(s))) return failed(latest, Object.assign(new Error("signed_out"), { code: "signed_out" }));
930
+ } catch {
931
+ const current = await memberAccess().catch(() => undefined);
932
+ if (!current?.email || current.email !== s.memberEmail || current.scope !== s.memberScope) return failed(latest, Object.assign(new Error("signed_out"), { code: "signed_out" }));
933
+ }
934
+ return {
935
+ ...latest,
936
+ saving: false,
937
+ message: acceptedFields.size
938
+ ? copyParam("accountSettings.savePartialError", "Some updates were accepted, but we could not confirm the rest. Reload before trying again.")
939
+ : copyParam("accountSettings.saveError", "We could not confirm the update. Your edits are still here; reload before trying again."),
940
+ noMessage: false,
941
+ };
942
+ } finally {
943
+ profileMutationInFlight = false;
944
+ }
945
+ },
946
+ saveSlug: async (s): Promise<S | undefined> => {
947
+ if (slugMutationInFlight || !s.memberId || s.status !== "loaded") return undefined;
948
+ const slug = s.slug.trim().replace(/^\/+|\/+$/g, "");
949
+ if (!/^[a-zA-Z0-9_-]{1,80}$/.test(slug)) return { ...s, slugMessage: copyParam("accountSettings.slugInvalid", "Use letters, numbers, hyphens, or underscores for your profile URL."), noSlugMessage: false };
950
+ slugMutationInFlight = true;
951
+ try {
952
+ const current = await memberAccess();
953
+ if (!current.email || current.email !== s.memberEmail || current.scope !== s.memberScope) return failed(s, Object.assign(new Error("signed_out"), { code: "signed_out" }));
954
+ const response = await updateMySlug({ slug }) as WixMemberResponse;
955
+ const after = await memberAccess();
956
+ if (!after.email || after.email !== s.memberEmail || after.scope !== s.memberScope) return failed(s, Object.assign(new Error("signed_out"), { code: "signed_out" }));
957
+ const saved = response.member;
958
+ if (saved?.id !== s.memberId) throw new Error("The profile URL update was not confirmed for this member.");
959
+ const refreshed = await getMyMember({ fieldsets: ["FULL"] }) as WixMemberResponse;
960
+ const afterRefresh = await memberAccess();
961
+ if (refreshed.member?.id !== s.memberId || !afterRefresh.email || afterRefresh.email !== s.memberEmail || afterRefresh.scope !== s.memberScope) return failed(s, Object.assign(new Error("signed_out"), { code: "signed_out" }));
962
+ const latest = svc.getState() as S;
963
+ if (latest.memberId !== s.memberId || latest.memberEmail !== s.memberEmail || latest.memberScope !== s.memberScope) return latest;
964
+ const savedSlug = clean(refreshed.member.profile?.slug);
965
+ const savedProfileUrl = profileUrl(latest.siteUrl, savedSlug);
966
+ const keepNewerSlugDraft = latest.slug !== s.slug;
967
+ return {
968
+ ...latest,
969
+ slug: keepNewerSlugDraft ? latest.slug : savedSlug,
970
+ slugEditing: keepNewerSlugDraft ? latest.slugEditing : false,
971
+ original: { ...latest.original, slug: savedSlug },
972
+ profileUrl: savedProfileUrl,
973
+ hasProfileUrl: !!savedProfileUrl,
974
+ noSiteUrl: !savedProfileUrl,
975
+ dirty: changedProfileFields(latest).size > 0 || (keepNewerSlugDraft ? latest.slug : savedSlug) !== savedSlug,
976
+ slugMessage: copyParam("accountSettings.slugSaved", "Your profile URL was updated."),
977
+ noSlugMessage: false,
978
+ };
979
+ } catch {
980
+ const latest = svc.getState() as S;
981
+ if (latest.memberId !== s.memberId || latest.memberEmail !== s.memberEmail || latest.memberScope !== s.memberScope) return latest;
982
+ return { ...latest, slugMessage: copyParam("accountSettings.slugSaveError", "Your profile URL was not updated."), noSlugMessage: false };
983
+ } finally { slugMutationInFlight = false; }
984
+ },
985
+ setPrivate: async (s, arg): Promise<S | undefined> => {
986
+ if (privacyMutationInFlight || !s.memberId || !s.privacySupported || s.status !== "loaded") return undefined;
987
+ const supplied = arg as { status?: unknown; value?: unknown } | undefined;
988
+ const value = supplied?.value;
989
+ const nextStatus = supplied?.status === "PRIVATE" || supplied?.status === "PUBLIC"
990
+ ? supplied.status
991
+ : value === true ? "PRIVATE" : value === false ? "PUBLIC" : s.pendingPrivacyStatus;
992
+ if (!nextStatus) return undefined;
993
+ privacyMutationInFlight = true;
994
+ try {
995
+ const current = await memberAccess();
996
+ if (!current.email || current.email !== s.memberEmail || current.scope !== s.memberScope) return failed(s, Object.assign(new Error("signed_out"), { code: "signed_out" }));
997
+ const response = nextStatus === "PRIVATE" ? await leaveCommunity() : await joinCommunity();
998
+ const after = await memberAccess();
999
+ if (!after.email || after.email !== s.memberEmail || after.scope !== s.memberScope) return failed(s, Object.assign(new Error("signed_out"), { code: "signed_out" }));
1000
+ if (response.member?.id !== s.memberId || !response.member.privacyStatus) throw new Error("The profile privacy update was not confirmed for this member.");
1001
+ const refreshed = await getMyMember({ fieldsets: ["FULL"] }) as WixMemberResponse;
1002
+ const afterRefresh = await memberAccess();
1003
+ if (refreshed.member?.id !== s.memberId || !afterRefresh.email || afterRefresh.email !== s.memberEmail || afterRefresh.scope !== s.memberScope || !refreshed.member.privacyStatus) return failed(s, Object.assign(new Error("signed_out"), { code: "signed_out" }));
1004
+ // A stale read or a rejected write leaves the OLD status in the readback: that is not a success.
1005
+ if (refreshed.member.privacyStatus.toUpperCase() !== nextStatus) throw new Error("The profile privacy update was not confirmed by the readback.");
1006
+ const isPrivate = refreshed.member.privacyStatus.toUpperCase() === "PRIVATE";
1007
+ // The awaits above can outlast edits and a member change: apply the result to the CURRENT state.
1008
+ const latest = svc.getState() as S;
1009
+ if (latest.memberId !== s.memberId || latest.memberEmail !== s.memberEmail || latest.memberScope !== s.memberScope) return latest;
1010
+ return { ...latest, privacyStatus: refreshed.member.privacyStatus, isPrivate, notPrivate: !isPrivate, privacyActionLabel: copyParam(isPrivate ? "accountSettings.makePublicCta" : "accountSettings.makePrivateCta", isPrivate ? "Make profile public" : "Make profile private"), privacyConfirmVisible: false, noPrivacyConfirm: true, pendingPrivacyStatus: "", privacyPrompt: "", message: copyParam("accountSettings.privacySaved", "Your profile privacy was updated."), noMessage: false };
1011
+ } catch {
1012
+ const latest = svc.getState() as S;
1013
+ if (latest.memberId !== s.memberId || latest.memberEmail !== s.memberEmail || latest.memberScope !== s.memberScope) return latest;
1014
+ return { ...latest, message: copyParam("accountSettings.privacyError", "Your privacy setting was not updated."), noMessage: false };
1015
+ } finally { privacyMutationInFlight = false; }
1016
+ },
1017
+ requestPrivacyChange: (s) => {
1018
+ if (!s.privacySupported || s.status !== "loaded") return { ...s, privacyConfirmVisible: false, noPrivacyConfirm: true, pendingPrivacyStatus: "", privacyPrompt: "" };
1019
+ const pendingPrivacyStatus = s.isPrivate ? "PUBLIC" : "PRIVATE";
1020
+ return { ...s, privacyConfirmVisible: true, noPrivacyConfirm: false, pendingPrivacyStatus, privacyPrompt: pendingPrivacyStatus === "PRIVATE"
1021
+ ? copyParam("accountSettings.privacyConfirmPrivate", "Making your profile private removes you from the Wix Members Area community. Continue?")
1022
+ : copyParam("accountSettings.privacyConfirmPublic", "Making your profile public rejoins the Wix Members Area community. Continue?") };
1023
+ },
1024
+ cancelPrivacyChange: (s) => ({ ...s, privacyConfirmVisible: false, noPrivacyConfirm: true, pendingPrivacyStatus: "", privacyPrompt: "" }),
1025
+ setPasswordCurrent: (s, arg) => s.passwordCredentialSupported ? ({ ...s, passwordCurrent: rawText((arg as { value?: unknown } | undefined)?.value), passwordError: "", passwordNoError: true, passwordMessage: "", passwordNoMessage: true }) : s,
1026
+ setPasswordNew: (s, arg) => s.passwordCredentialSupported ? ({ ...s, passwordNew: rawText((arg as { value?: unknown } | undefined)?.value), passwordError: "", passwordNoError: true, passwordMessage: "", passwordNoMessage: true }) : s,
1027
+ setPasswordConfirm: (s, arg) => s.passwordCredentialSupported ? ({ ...s, passwordConfirm: rawText((arg as { value?: unknown } | undefined)?.value), passwordError: "", passwordNoError: true, passwordMessage: "", passwordNoMessage: true }) : s,
1028
+ openPasswordPanel: (s) => !s.passwordCredentialSupported ? { ...clearPassword(s), passwordPanelOpen: false, emailPanelOpen: false } : ({ ...s, passwordPanelOpen: true, emailPanelOpen: false, passwordSaving: passwordMutationInFlight || passwordResetInFlight, notPasswordSaving: !(passwordMutationInFlight || passwordResetInFlight), notPasswordChanging: !passwordMutationInFlight, passwordResetSending: passwordResetInFlight, notPasswordResetSending: !passwordResetInFlight, passwordError: "", passwordNoError: true, passwordMessage: "", passwordNoMessage: true }),
1029
+ closePasswordPanel: (s) => { passwordMutationVersion += 1; return { ...clearPassword(s), passwordPanelOpen: false, passwordSaving: false, notPasswordSaving: true, notPasswordChanging: true, passwordResetSending: false, notPasswordResetSending: true, passwordRequestVersion: passwordMutationVersion, passwordError: "", passwordNoError: true, passwordMessage: "", passwordNoMessage: true }; },
1030
+ openEmailPanel: (s) => { passwordMutationVersion += 1; return { ...clearPassword(s), passwordPanelOpen: false, emailPanelOpen: true, passwordSaving: false, notPasswordSaving: true, notPasswordChanging: true, passwordResetSending: false, notPasswordResetSending: true, passwordRequestVersion: passwordMutationVersion, passwordError: "", passwordNoError: true, passwordMessage: "", passwordNoMessage: true }; },
1031
+ closeEmailPanel: (s) => ({ ...clearPassword(s), emailPanelOpen: false }),
1032
+ changePassword: (s) => {
1033
+ if (!s.passwordCredentialSupported || passwordMutationInFlight || passwordResetInFlight || s.status !== "loaded" || !s.memberId || !s.passwordPanelOpen || s.passwordSaving) return s;
1034
+ if (!s.passwordCurrent || !s.passwordNew || !s.passwordConfirm) return {
1035
+ ...s, passwordError: copyParam("accountSettings.passwordRequired", "Enter your current password and choose a new password."), passwordNoError: false,
1036
+ };
1037
+ if (s.passwordNew !== s.passwordConfirm) return {
1038
+ ...s, passwordError: copyParam("accountSettings.passwordMismatch", "The new passwords do not match."), passwordNoError: false,
1039
+ };
1040
+ passwordMutationInFlight = true;
1041
+ const version = ++passwordMutationVersion;
1042
+ void performPasswordChange(version, s.memberId, s.memberEmail, s.memberScope, s.passwordCurrent, s.passwordNew);
1043
+ return { ...s, passwordSaving: true, notPasswordSaving: false, notPasswordChanging: false, passwordRequestVersion: version, passwordError: "", passwordNoError: true, passwordMessage: "", passwordNoMessage: true };
1044
+ },
1045
+ resolvePasswordChange: (s, arg) => {
1046
+ const result = arg as { status?: unknown; code?: unknown; version?: unknown } | undefined;
1047
+ if (result?.version !== passwordMutationVersion || result?.version !== s.passwordRequestVersion) return s.passwordSaving ? { ...s, passwordSaving: false, notPasswordSaving: true, notPasswordChanging: true } : s;
1048
+ const ok = result?.status === "ok";
1049
+ const code = clean(result?.code);
1050
+ const message = ok
1051
+ ? copyParam("accountSettings.passwordChanged", "Your password was changed.")
1052
+ : code === "reauthentication_failed"
1053
+ ? copyParam("accountSettings.passwordReauthenticationFailed", "We could not verify your current password.")
1054
+ : code === "account_changed"
1055
+ ? copyParam("accountSettings.passwordAccountChanged", "Your account changed. Sign in again before changing your password.")
1056
+ : code === "unavailable"
1057
+ ? copyParam("accountSettings.passwordUnavailable", "Password changes aren’t available in this session. Sign in again or use the website.")
1058
+ : copyParam("accountSettings.passwordChangeFailed", "Your password was not changed. Try again.");
1059
+ const cleanState = { ...clearPassword(s), passwordSaving: false, notPasswordSaving: true, notPasswordChanging: true, passwordError: ok ? "" : message, passwordNoError: ok, passwordMessage: ok ? message : "", passwordNoMessage: !ok };
1060
+ return ok ? { ...cleanState, passwordPanelOpen: false } : cleanState;
1061
+ },
1062
+ resolvePasswordReset: (s, arg) => {
1063
+ const result = arg as { version?: unknown; code?: unknown } | undefined;
1064
+ if (result?.version !== passwordMutationVersion || result?.version !== s.passwordRequestVersion) return s;
1065
+ const accountChanged = result?.code === "account_changed";
1066
+ const ok = !result?.code;
1067
+ const message = ok
1068
+ ? copyParam("accountSettings.passwordResetSent", "If this login email is registered, a reset link is on its way.")
1069
+ : accountChanged
1070
+ ? copyParam("accountSettings.passwordAccountChanged", "Your account changed. Sign in again before changing your password.")
1071
+ : copyParam("accountSettings.passwordResetFailed", "A reset link could not be sent. Try again.");
1072
+ return { ...clearPassword(s), passwordSaving: false, notPasswordSaving: true, passwordResetSending: false, notPasswordResetSending: true, passwordError: ok ? "" : message, passwordNoError: ok, passwordMessage: ok ? message : "", passwordNoMessage: !ok };
1073
+ },
1074
+ resetPassword: (s) => {
1075
+ if (!s.passwordCredentialSupported || passwordMutationInFlight || passwordResetInFlight || !s.loginEmail || s.status !== "loaded" || !s.passwordPanelOpen || s.passwordSaving) return s;
1076
+ passwordResetInFlight = true;
1077
+ const version = ++passwordMutationVersion;
1078
+ void performPasswordReset(version, s.memberId, s.memberEmail, s.memberScope);
1079
+ return { ...s, passwordSaving: true, notPasswordSaving: false, notPasswordChanging: true, passwordResetSending: true, notPasswordResetSending: false, passwordRequestVersion: version, passwordError: "", passwordNoError: true, passwordMessage: "", passwordNoMessage: true };
1080
+ },
1081
+ openSite: async (s): Promise<S | undefined> => {
1082
+ // A missing, malformed or placeholder configured site has no website handoff.
1083
+ if (!siteHandoffAvailable(s.siteUrl)) return s;
1084
+ try { await openUrl(s.siteUrl, "in_app_browser"); } catch { /* the handoff is best-effort */ }
1085
+ return s;
1086
+ },
1087
+ },
1088
+ });
1089
+
1090
+ svc.load();
1091
+ export { svc };