@businessdash/sdk 0.9.60 → 0.9.80
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/README.md +991 -295
- package/assets/biab-sw.js +92 -0
- package/dist/account-core/index.d.ts +126 -0
- package/dist/account-core/index.d.ts.map +1 -0
- package/dist/account-core/index.js +191 -0
- package/dist/account-core/index.js.map +1 -0
- package/dist/account.d.ts +11 -0
- package/dist/account.d.ts.map +1 -0
- package/dist/account.js +11 -0
- package/dist/account.js.map +1 -0
- package/dist/auth-handler.d.ts +1 -1
- package/dist/auth-links.d.ts +33 -0
- package/dist/auth-links.d.ts.map +1 -0
- package/dist/auth-links.js +64 -0
- package/dist/auth-links.js.map +1 -0
- package/dist/blog-core/index.d.ts +112 -0
- package/dist/blog-core/index.d.ts.map +1 -0
- package/dist/blog-core/index.js +330 -0
- package/dist/blog-core/index.js.map +1 -0
- package/dist/blog.d.ts +11 -0
- package/dist/blog.d.ts.map +1 -0
- package/dist/blog.js +11 -0
- package/dist/blog.js.map +1 -0
- package/dist/chat-core/index.d.ts +97 -0
- package/dist/chat-core/index.d.ts.map +1 -0
- package/dist/chat-core/index.js +237 -0
- package/dist/chat-core/index.js.map +1 -0
- package/dist/chat.d.ts +11 -0
- package/dist/chat.d.ts.map +1 -0
- package/dist/chat.js +11 -0
- package/dist/chat.js.map +1 -0
- package/dist/checkout-core/index.d.ts +65 -0
- package/dist/checkout-core/index.d.ts.map +1 -0
- package/dist/checkout-core/index.js +159 -0
- package/dist/checkout-core/index.js.map +1 -0
- package/dist/checkout.d.ts +10 -0
- package/dist/checkout.d.ts.map +1 -0
- package/dist/checkout.js +10 -0
- package/dist/checkout.js.map +1 -0
- package/dist/cli.js +133 -25
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts +622 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +888 -1
- package/dist/client.js.map +1 -1
- package/dist/consent-core/index.cjs +107 -0
- package/dist/consent-core/index.d.ts +103 -0
- package/dist/consent-core/index.d.ts.map +1 -0
- package/dist/consent-core/index.js +157 -0
- package/dist/consent-core/index.js.map +1 -0
- package/dist/contracts.d.ts +3526 -767
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +571 -4
- package/dist/contracts.js.map +1 -1
- package/dist/data-model-schema.d.ts +1 -1
- package/dist/data-model-schema.d.ts.map +1 -1
- package/dist/element.d.ts +30 -0
- package/dist/element.d.ts.map +1 -1
- package/dist/element.js +56 -0
- package/dist/element.js.map +1 -1
- package/dist/embed-protocol.d.ts +86 -0
- package/dist/embed-protocol.d.ts.map +1 -0
- package/dist/embed-protocol.js +60 -0
- package/dist/embed-protocol.js.map +1 -0
- package/dist/framework-support.d.ts +122 -0
- package/dist/framework-support.d.ts.map +1 -0
- package/dist/framework-support.js +323 -0
- package/dist/framework-support.js.map +1 -0
- package/dist/index.cjs +1355 -24
- package/dist/legal-core/index.cjs +115 -0
- package/dist/legal-core/index.d.ts +160 -0
- package/dist/legal-core/index.d.ts.map +1 -0
- package/dist/legal-core/index.js +220 -0
- package/dist/legal-core/index.js.map +1 -0
- package/dist/marketing-core/index.d.ts +66 -0
- package/dist/marketing-core/index.d.ts.map +1 -0
- package/dist/marketing-core/index.js +154 -0
- package/dist/marketing-core/index.js.map +1 -0
- package/dist/marketing.d.ts +7 -0
- package/dist/marketing.d.ts.map +1 -0
- package/dist/marketing.js +7 -0
- package/dist/marketing.js.map +1 -0
- package/dist/mcp-adapters/index.d.ts +135 -0
- package/dist/mcp-adapters/index.d.ts.map +1 -0
- package/dist/mcp-adapters/index.js +187 -0
- package/dist/mcp-adapters/index.js.map +1 -0
- package/dist/mcp-adapters.d.ts +7 -0
- package/dist/mcp-adapters.d.ts.map +1 -0
- package/dist/mcp-adapters.js +7 -0
- package/dist/mcp-adapters.js.map +1 -0
- package/dist/model3d-core/index.cjs +95 -0
- package/dist/model3d-core/index.d.ts +167 -0
- package/dist/model3d-core/index.d.ts.map +1 -0
- package/dist/model3d-core/index.js +158 -0
- package/dist/model3d-core/index.js.map +1 -0
- package/dist/notifications-core/index.cjs +315 -0
- package/dist/notifications-core/index.d.ts +229 -0
- package/dist/notifications-core/index.d.ts.map +1 -0
- package/dist/notifications-core/index.js +445 -0
- package/dist/notifications-core/index.js.map +1 -0
- package/dist/portal-core/index.d.ts +451 -0
- package/dist/portal-core/index.d.ts.map +1 -0
- package/dist/portal-core/index.js +819 -0
- package/dist/portal-core/index.js.map +1 -0
- package/dist/portal.d.ts +7 -0
- package/dist/portal.d.ts.map +1 -0
- package/dist/portal.js +7 -0
- package/dist/portal.js.map +1 -0
- package/dist/push-core/index.cjs +171 -0
- package/dist/push-core/index.d.ts +149 -0
- package/dist/push-core/index.d.ts.map +1 -0
- package/dist/push-core/index.js +231 -0
- package/dist/push-core/index.js.map +1 -0
- package/dist/react-marketing.d.ts +1 -17
- package/dist/react-marketing.d.ts.map +1 -1
- package/dist/react-marketing.js +4 -0
- package/dist/react-marketing.js.map +1 -1
- package/dist/react.d.ts +294 -101
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +302 -246
- package/dist/react.js.map +1 -1
- package/dist/seed-core/index.d.ts +354 -0
- package/dist/seed-core/index.d.ts.map +1 -0
- package/dist/seed-core/index.js +420 -0
- package/dist/seed-core/index.js.map +1 -0
- package/dist/seed.d.ts +7 -0
- package/dist/seed.d.ts.map +1 -0
- package/dist/seed.js +7 -0
- package/dist/seed.js.map +1 -0
- package/dist/seo-core/blog.d.ts +94 -0
- package/dist/seo-core/blog.d.ts.map +1 -0
- package/dist/seo-core/blog.js +149 -0
- package/dist/seo-core/blog.js.map +1 -0
- package/dist/seo-core/commerce.d.ts +64 -0
- package/dist/seo-core/commerce.d.ts.map +1 -0
- package/dist/seo-core/commerce.js +87 -0
- package/dist/seo-core/commerce.js.map +1 -0
- package/dist/seo-core/index.cjs +751 -0
- package/dist/seo-core/index.d.ts +226 -0
- package/dist/seo-core/index.d.ts.map +1 -0
- package/dist/seo-core/index.js +455 -0
- package/dist/seo-core/index.js.map +1 -0
- package/dist/seo-core/local.d.ts +181 -0
- package/dist/seo-core/local.d.ts.map +1 -0
- package/dist/seo-core/local.js +193 -0
- package/dist/seo-core/local.js.map +1 -0
- package/dist/sitemap-core/index.cjs +167 -0
- package/dist/sitemap-core/index.d.ts +155 -0
- package/dist/sitemap-core/index.d.ts.map +1 -0
- package/dist/sitemap-core/index.js +195 -0
- package/dist/sitemap-core/index.js.map +1 -0
- package/dist/social-core/index.d.ts +74 -0
- package/dist/social-core/index.d.ts.map +1 -0
- package/dist/social-core/index.js +72 -0
- package/dist/social-core/index.js.map +1 -0
- package/dist/social.d.ts +7 -0
- package/dist/social.d.ts.map +1 -0
- package/dist/social.js +7 -0
- package/dist/social.js.map +1 -0
- package/dist/solid-compat.d.ts +204 -0
- package/dist/solid-compat.d.ts.map +1 -0
- package/dist/solid-compat.js +221 -0
- package/dist/solid-compat.js.map +1 -0
- package/dist/store-core/index.d.ts +141 -0
- package/dist/store-core/index.d.ts.map +1 -0
- package/dist/store-core/index.js +277 -0
- package/dist/store-core/index.js.map +1 -0
- package/dist/store.d.ts +12 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +12 -0
- package/dist/store.js.map +1 -0
- package/dist/vanilla.d.ts +111 -0
- package/dist/vanilla.d.ts.map +1 -1
- package/dist/vanilla.js +206 -0
- package/dist/vanilla.js.map +1 -1
- package/dist/verify-core/index.d.ts +98 -0
- package/dist/verify-core/index.d.ts.map +1 -0
- package/dist/verify-core/index.js +186 -0
- package/dist/verify-core/index.js.map +1 -0
- package/dist/verify.d.ts +8 -0
- package/dist/verify.d.ts.map +1 -0
- package/dist/verify.js +8 -0
- package/dist/verify.js.map +1 -0
- package/openapi.json +4516 -1311
- package/package.json +128 -11
- package/src/account-core/index.ts +309 -0
- package/src/account.ts +11 -0
- package/src/angular/headless.ts +519 -0
- package/src/angular/index.ts +3 -0
- package/src/auth-links.ts +76 -0
- package/src/blog-core/index.ts +423 -0
- package/src/blog.ts +11 -0
- package/src/chat-core/index.ts +335 -0
- package/src/chat.ts +11 -0
- package/src/checkout-core/index.ts +225 -0
- package/src/checkout.ts +10 -0
- package/src/cli.ts +184 -28
- package/src/client.ts +1097 -0
- package/src/consent-core/index.ts +213 -0
- package/src/contracts.ts +768 -11
- package/src/data-model-schema.ts +1 -1
- package/src/element.ts +102 -0
- package/src/embed-protocol.ts +100 -0
- package/src/framework-support.ts +440 -0
- package/src/legal-core/index.ts +276 -0
- package/src/marketing-core/index.ts +209 -0
- package/src/marketing.ts +7 -0
- package/src/mcp-adapters/index.ts +264 -0
- package/src/mcp-adapters.ts +7 -0
- package/src/model3d-core/index.ts +248 -0
- package/src/notifications-core/index.ts +650 -0
- package/src/portal-core/index.ts +1564 -0
- package/src/portal.ts +7 -0
- package/src/push-core/index.ts +335 -0
- package/src/qwik.tsx +602 -2
- package/src/react-marketing.tsx +5 -0
- package/src/react.tsx +434 -319
- package/src/seed-core/index.ts +735 -0
- package/src/seed.ts +7 -0
- package/src/seo-core/blog.ts +216 -0
- package/src/seo-core/commerce.ts +138 -0
- package/src/seo-core/index.ts +605 -0
- package/src/seo-core/local.ts +333 -0
- package/src/sitemap-core/index.ts +296 -0
- package/src/social-core/index.ts +103 -0
- package/src/social.ts +7 -0
- package/src/solid-compat.tsx +347 -0
- package/src/solid-surfaces.tsx +1311 -0
- package/src/solid.tsx +82 -60
- package/src/store-core/index.ts +426 -0
- package/src/store.ts +12 -0
- package/src/svelte/SocialLinks.svelte +84 -0
- package/src/svelte/headless.ts +435 -0
- package/src/svelte/index.ts +9 -0
- package/src/vanilla.ts +367 -0
- package/src/verify-core/index.ts +288 -0
- package/src/verify.ts +8 -0
- package/src/vue/headless.ts +527 -0
- package/src/vue/index.ts +3 -0
|
@@ -0,0 +1,819 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@businessdash/sdk/portal` — the headless customer-portal controller.
|
|
3
|
+
*
|
|
4
|
+
* The portal is the largest surface in the SDK: 34 methods covering work,
|
|
5
|
+
* quotes, contracts, invoices, orders, downloads, shipping, reviews, referrals,
|
|
6
|
+
* profile and notification preferences. Until now the only way to render it was
|
|
7
|
+
* `<Dashboard>`, a BusinessDash-hosted page in an iframe — so a business could
|
|
8
|
+
* not put their own customer portal on their own domain in their own design.
|
|
9
|
+
*
|
|
10
|
+
* This is the native path. Four things about the surface shape the design, and
|
|
11
|
+
* all of them come from the audit in `Resources/BIAB-Customer-Portal-SDK-Audit.md`.
|
|
12
|
+
*
|
|
13
|
+
* ## 1. The session is not optional
|
|
14
|
+
*
|
|
15
|
+
* Every portal route requires a signed-in session token AND the scope
|
|
16
|
+
* `customer_portal:self`. Without `withSession` the client has no identity and
|
|
17
|
+
* every call is unauthenticated — which fails, but quietly enough to look like
|
|
18
|
+
* an empty portal. So the token is a REQUIRED constructor argument and the
|
|
19
|
+
* controller calls `withSession` itself. A consumer cannot forget it.
|
|
20
|
+
*
|
|
21
|
+
* ## 2. There are no list endpoints, except orders and payments
|
|
22
|
+
*
|
|
23
|
+
* Invoices, quotes, contracts and shipments have detail-by-id routes only.
|
|
24
|
+
* `getWork()` returns a summary — open job count, unpaid balance — so a portal
|
|
25
|
+
* can render counts, but "show me all my unpaid invoices" has no single call.
|
|
26
|
+
* This controller does NOT paper over that with a fake list built from
|
|
27
|
+
* repeated detail fetches: it exposes what exists, and the gap stays visible
|
|
28
|
+
* where someone can decide to close it in the API.
|
|
29
|
+
*
|
|
30
|
+
* ## 3. Nothing can be deleted
|
|
31
|
+
*
|
|
32
|
+
* Every write is a create or an edit. There is deliberately no delete action
|
|
33
|
+
* anywhere in this controller, because there is none in the API.
|
|
34
|
+
*
|
|
35
|
+
* ## 4. Reviews are moderated, not published
|
|
36
|
+
*
|
|
37
|
+
* `submitReview` lands `status: "pending"` for staff approval. The controller
|
|
38
|
+
* does NOT add the new review to `reviews` — showing it back immediately would
|
|
39
|
+
* tell the author their review is live when the public cannot see it.
|
|
40
|
+
*
|
|
41
|
+
* @module
|
|
42
|
+
*/
|
|
43
|
+
import { currentPushSubscription, pushSupport, subscribeToPush, unsubscribeFromPush, } from "../push-core/index.js";
|
|
44
|
+
function toError(cause, fallback) {
|
|
45
|
+
return cause instanceof Error ? cause : new Error(fallback);
|
|
46
|
+
}
|
|
47
|
+
export function createPortalController(options) {
|
|
48
|
+
const { onError } = options;
|
|
49
|
+
if (!options.sessionToken) {
|
|
50
|
+
throw new Error("createPortalController requires a `sessionToken`. Every customer-portal route is scoped to the signed-in customer; without one every call is unauthenticated and the portal renders empty rather than signed-out.");
|
|
51
|
+
}
|
|
52
|
+
const portal = options.client
|
|
53
|
+
.customerPortal()
|
|
54
|
+
.withSession(options.sessionToken);
|
|
55
|
+
/**
|
|
56
|
+
* A portal client pinned to another of the customer's orgs.
|
|
57
|
+
*
|
|
58
|
+
* Memoised because a dashboard listing five companies would otherwise build
|
|
59
|
+
* a client per render. The session token is the same — it identifies the
|
|
60
|
+
* PERSON; the org id only says which tenant's data to read, and the server
|
|
61
|
+
* still checks they belong to it.
|
|
62
|
+
*/
|
|
63
|
+
const orgClients = new Map();
|
|
64
|
+
const portalFor = (orgId) => {
|
|
65
|
+
let pinned = orgClients.get(orgId);
|
|
66
|
+
if (!pinned) {
|
|
67
|
+
pinned = options.client
|
|
68
|
+
.customerPortal(orgId)
|
|
69
|
+
.withSession(options.sessionToken);
|
|
70
|
+
orgClients.set(orgId, pinned);
|
|
71
|
+
}
|
|
72
|
+
return pinned;
|
|
73
|
+
};
|
|
74
|
+
let destroyed = false;
|
|
75
|
+
/** Guards a slow detail fetch landing after the customer navigated on. */
|
|
76
|
+
let detailToken = 0;
|
|
77
|
+
const subscribers = new Set();
|
|
78
|
+
const state = {
|
|
79
|
+
status: "idle",
|
|
80
|
+
context: null,
|
|
81
|
+
profile: null,
|
|
82
|
+
work: null,
|
|
83
|
+
push: null,
|
|
84
|
+
notificationPreferences: null,
|
|
85
|
+
notificationPreferencesByOrg: {},
|
|
86
|
+
otherOrgs: null,
|
|
87
|
+
orders: null,
|
|
88
|
+
payments: null,
|
|
89
|
+
reviews: null,
|
|
90
|
+
invoices: null,
|
|
91
|
+
quotes: null,
|
|
92
|
+
contracts: null,
|
|
93
|
+
shipments: null,
|
|
94
|
+
messages: null,
|
|
95
|
+
returnRequests: null,
|
|
96
|
+
productReviews: null,
|
|
97
|
+
subscription: null,
|
|
98
|
+
subscriberContent: null,
|
|
99
|
+
productReviewAwaitingModeration: false,
|
|
100
|
+
lastNotReceived: null,
|
|
101
|
+
referrals: { stats: null, history: null, payouts: null },
|
|
102
|
+
activeJob: null,
|
|
103
|
+
jobActivity: null,
|
|
104
|
+
jobEta: null,
|
|
105
|
+
jobComments: null,
|
|
106
|
+
activeOrder: null,
|
|
107
|
+
orderDownloads: null,
|
|
108
|
+
activeInvoice: null,
|
|
109
|
+
activeQuote: null,
|
|
110
|
+
activeContract: null,
|
|
111
|
+
shipmentTracking: null,
|
|
112
|
+
reviewAwaitingModeration: false,
|
|
113
|
+
error: null,
|
|
114
|
+
pending: {
|
|
115
|
+
overview: false,
|
|
116
|
+
orders: false,
|
|
117
|
+
payments: false,
|
|
118
|
+
reviews: false,
|
|
119
|
+
referrals: false,
|
|
120
|
+
detail: false,
|
|
121
|
+
action: false,
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
let cached = null;
|
|
125
|
+
function snapshot() {
|
|
126
|
+
if (!cached) {
|
|
127
|
+
cached = {
|
|
128
|
+
status: state.status,
|
|
129
|
+
context: state.context,
|
|
130
|
+
profile: state.profile,
|
|
131
|
+
work: state.work,
|
|
132
|
+
push: state.push,
|
|
133
|
+
notificationPreferences: state.notificationPreferences,
|
|
134
|
+
notificationPreferencesByOrg: state.notificationPreferencesByOrg,
|
|
135
|
+
otherOrgs: state.otherOrgs,
|
|
136
|
+
orders: state.orders,
|
|
137
|
+
payments: state.payments,
|
|
138
|
+
reviews: state.reviews,
|
|
139
|
+
invoices: state.invoices,
|
|
140
|
+
quotes: state.quotes,
|
|
141
|
+
contracts: state.contracts,
|
|
142
|
+
shipments: state.shipments,
|
|
143
|
+
messages: state.messages,
|
|
144
|
+
returnRequests: state.returnRequests,
|
|
145
|
+
productReviews: state.productReviews,
|
|
146
|
+
subscription: state.subscription,
|
|
147
|
+
subscriberContent: state.subscriberContent,
|
|
148
|
+
productReviewAwaitingModeration: state.productReviewAwaitingModeration,
|
|
149
|
+
lastNotReceived: state.lastNotReceived,
|
|
150
|
+
unreadMessageCount: (state.messages?.items ?? []).filter((m) => m.direction === "outbound" && m.readByCustomerAt === null).length,
|
|
151
|
+
referrals: state.referrals,
|
|
152
|
+
activeJob: state.activeJob,
|
|
153
|
+
jobActivity: state.jobActivity,
|
|
154
|
+
jobEta: state.jobEta,
|
|
155
|
+
jobComments: state.jobComments,
|
|
156
|
+
activeOrder: state.activeOrder,
|
|
157
|
+
orderDownloads: state.orderDownloads,
|
|
158
|
+
activeInvoice: state.activeInvoice,
|
|
159
|
+
activeQuote: state.activeQuote,
|
|
160
|
+
activeContract: state.activeContract,
|
|
161
|
+
shipmentTracking: state.shipmentTracking,
|
|
162
|
+
reviewAwaitingModeration: state.reviewAwaitingModeration,
|
|
163
|
+
error: state.error,
|
|
164
|
+
pending: state.pending,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
return cached;
|
|
168
|
+
}
|
|
169
|
+
function notify() {
|
|
170
|
+
if (destroyed)
|
|
171
|
+
return;
|
|
172
|
+
cached = null;
|
|
173
|
+
const next = snapshot();
|
|
174
|
+
for (const cb of subscribers)
|
|
175
|
+
cb(next);
|
|
176
|
+
}
|
|
177
|
+
function fail(cause, fallback) {
|
|
178
|
+
const error = toError(cause, fallback);
|
|
179
|
+
state.error = error;
|
|
180
|
+
if (state.status !== "ready")
|
|
181
|
+
state.status = "error";
|
|
182
|
+
onError?.(error);
|
|
183
|
+
}
|
|
184
|
+
/** Run a read that fills one slice of the snapshot. */
|
|
185
|
+
async function read(key, work, fallback) {
|
|
186
|
+
if (destroyed)
|
|
187
|
+
return;
|
|
188
|
+
state.pending[key] = true;
|
|
189
|
+
notify();
|
|
190
|
+
try {
|
|
191
|
+
await work();
|
|
192
|
+
if (!destroyed)
|
|
193
|
+
state.status = "ready";
|
|
194
|
+
}
|
|
195
|
+
catch (cause) {
|
|
196
|
+
if (!destroyed)
|
|
197
|
+
fail(cause, fallback);
|
|
198
|
+
}
|
|
199
|
+
finally {
|
|
200
|
+
if (!destroyed) {
|
|
201
|
+
state.pending[key] = false;
|
|
202
|
+
notify();
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/** Run a write. Returns false rather than throwing, so a form can branch. */
|
|
207
|
+
async function act(work, fallback, after) {
|
|
208
|
+
if (destroyed)
|
|
209
|
+
return null;
|
|
210
|
+
state.pending.action = true;
|
|
211
|
+
state.error = null;
|
|
212
|
+
notify();
|
|
213
|
+
try {
|
|
214
|
+
const result = await work();
|
|
215
|
+
if (destroyed)
|
|
216
|
+
return null;
|
|
217
|
+
after?.(result);
|
|
218
|
+
return result;
|
|
219
|
+
}
|
|
220
|
+
catch (cause) {
|
|
221
|
+
if (!destroyed)
|
|
222
|
+
fail(cause, fallback);
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
finally {
|
|
226
|
+
if (!destroyed) {
|
|
227
|
+
state.pending.action = false;
|
|
228
|
+
notify();
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/** Fill a detail slot, dropping the result if the customer moved on. */
|
|
233
|
+
async function detail(work, fallback) {
|
|
234
|
+
if (destroyed)
|
|
235
|
+
return;
|
|
236
|
+
const token = ++detailToken;
|
|
237
|
+
state.pending.detail = true;
|
|
238
|
+
state.error = null;
|
|
239
|
+
notify();
|
|
240
|
+
try {
|
|
241
|
+
await work(token);
|
|
242
|
+
}
|
|
243
|
+
catch (cause) {
|
|
244
|
+
if (!destroyed && token === detailToken)
|
|
245
|
+
fail(cause, fallback);
|
|
246
|
+
}
|
|
247
|
+
finally {
|
|
248
|
+
if (!destroyed && token === detailToken) {
|
|
249
|
+
state.pending.detail = false;
|
|
250
|
+
notify();
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return {
|
|
255
|
+
snapshot,
|
|
256
|
+
subscribe(callback) {
|
|
257
|
+
subscribers.add(callback);
|
|
258
|
+
return () => {
|
|
259
|
+
subscribers.delete(callback);
|
|
260
|
+
};
|
|
261
|
+
},
|
|
262
|
+
load() {
|
|
263
|
+
if (state.status === "idle")
|
|
264
|
+
state.status = "loading";
|
|
265
|
+
return read("overview", async () => {
|
|
266
|
+
// Context, profile and work are independent, so one failing
|
|
267
|
+
// should not hide the other two.
|
|
268
|
+
const [context, profile, work] = await Promise.allSettled([
|
|
269
|
+
portal.context(),
|
|
270
|
+
portal.getProfile(),
|
|
271
|
+
portal.getWork(),
|
|
272
|
+
]);
|
|
273
|
+
if (destroyed)
|
|
274
|
+
return;
|
|
275
|
+
if (context.status === "fulfilled")
|
|
276
|
+
state.context = context.value;
|
|
277
|
+
if (profile.status === "fulfilled")
|
|
278
|
+
state.profile = profile.value;
|
|
279
|
+
if (work.status === "fulfilled")
|
|
280
|
+
state.work = work.value;
|
|
281
|
+
const firstFailure = [context, profile, work].find((r) => r.status === "rejected");
|
|
282
|
+
if (firstFailure && firstFailure.status === "rejected") {
|
|
283
|
+
throw firstFailure.reason;
|
|
284
|
+
}
|
|
285
|
+
}, "Failed to load the portal.");
|
|
286
|
+
},
|
|
287
|
+
loadOrders(opts) {
|
|
288
|
+
return read("orders", async () => {
|
|
289
|
+
state.orders = await portal.listOrders(opts);
|
|
290
|
+
}, "Failed to load orders.");
|
|
291
|
+
},
|
|
292
|
+
loadInvoices(opts) {
|
|
293
|
+
return read("orders", async () => {
|
|
294
|
+
state.invoices = await portal.listInvoices(opts);
|
|
295
|
+
}, "Failed to load invoices.");
|
|
296
|
+
},
|
|
297
|
+
loadQuotes(opts) {
|
|
298
|
+
return read("orders", async () => {
|
|
299
|
+
state.quotes = await portal.listQuotes(opts);
|
|
300
|
+
}, "Failed to load quotes.");
|
|
301
|
+
},
|
|
302
|
+
loadContracts(opts) {
|
|
303
|
+
return read("orders", async () => {
|
|
304
|
+
state.contracts = await portal.listContracts(opts);
|
|
305
|
+
}, "Failed to load contracts.");
|
|
306
|
+
},
|
|
307
|
+
loadShipments(opts) {
|
|
308
|
+
return read("orders", async () => {
|
|
309
|
+
state.shipments = await portal.listShipments(opts);
|
|
310
|
+
}, "Failed to load shipments.");
|
|
311
|
+
},
|
|
312
|
+
loadMessages(opts) {
|
|
313
|
+
return read("orders", async () => {
|
|
314
|
+
state.messages = await portal.listMessages(opts);
|
|
315
|
+
}, "Failed to load your messages.");
|
|
316
|
+
},
|
|
317
|
+
async postMessage(input) {
|
|
318
|
+
const res = await act(() => portal.postMessage(input), "Failed to send your message.");
|
|
319
|
+
if (!res)
|
|
320
|
+
return false;
|
|
321
|
+
// Re-read rather than append: the stored row carries the author name
|
|
322
|
+
// and timestamp the server assigned, and a locally-appended message
|
|
323
|
+
// that later differs is the kind of thing a customer notices.
|
|
324
|
+
await read("orders", async () => {
|
|
325
|
+
state.messages = await portal.listMessages(input.jobId ? { jobId: input.jobId } : undefined);
|
|
326
|
+
}, "Message sent, but the thread could not be refreshed.");
|
|
327
|
+
return true;
|
|
328
|
+
},
|
|
329
|
+
async markMessagesRead() {
|
|
330
|
+
await act(() => portal.markMessagesRead(), "Failed to mark your messages read.", () => {
|
|
331
|
+
// Stamp locally so the badge clears immediately rather than
|
|
332
|
+
// waiting for a re-read the caller may never do.
|
|
333
|
+
if (state.messages) {
|
|
334
|
+
const now = new Date().toISOString();
|
|
335
|
+
state.messages = {
|
|
336
|
+
...state.messages,
|
|
337
|
+
items: state.messages.items.map((m) => m.direction === "outbound" && m.readByCustomerAt === null
|
|
338
|
+
? { ...m, readByCustomerAt: now }
|
|
339
|
+
: m),
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
},
|
|
344
|
+
loadPayments(opts) {
|
|
345
|
+
return read("payments", async () => {
|
|
346
|
+
state.payments = await portal.listPayments(opts);
|
|
347
|
+
}, "Failed to load payment history.");
|
|
348
|
+
},
|
|
349
|
+
loadReviews() {
|
|
350
|
+
return read("reviews", async () => {
|
|
351
|
+
state.reviews = await portal.listMyReviews();
|
|
352
|
+
}, "Failed to load your reviews.");
|
|
353
|
+
},
|
|
354
|
+
loadReferrals() {
|
|
355
|
+
return read("referrals", async () => {
|
|
356
|
+
const [stats, history, payouts] = await Promise.all([
|
|
357
|
+
portal.myReferralStats(),
|
|
358
|
+
portal.myReferralHistory(),
|
|
359
|
+
portal.myReferralPayouts(),
|
|
360
|
+
]);
|
|
361
|
+
if (destroyed)
|
|
362
|
+
return;
|
|
363
|
+
state.referrals = { stats, history, payouts };
|
|
364
|
+
}, "Failed to load referrals.");
|
|
365
|
+
},
|
|
366
|
+
loadPushConfig() {
|
|
367
|
+
return read(
|
|
368
|
+
// "overview" rather than a slice of its own: the device list is
|
|
369
|
+
// small, loads with the rest of a settings page, and one more
|
|
370
|
+
// pending flag is one more thing every binding has to thread.
|
|
371
|
+
"overview", async () => {
|
|
372
|
+
state.push = await portal.getPushConfig();
|
|
373
|
+
}, "Failed to load your notification devices.");
|
|
374
|
+
},
|
|
375
|
+
async enablePush(options) {
|
|
376
|
+
// Support is checked BEFORE the config fetch: on an iPhone in a
|
|
377
|
+
// normal tab there is nothing a key could enable, and a round trip
|
|
378
|
+
// that ends in "add this to your Home Screen" is a wasted one.
|
|
379
|
+
const support = pushSupport();
|
|
380
|
+
if (!support.supported) {
|
|
381
|
+
return {
|
|
382
|
+
ok: false,
|
|
383
|
+
reason: support.reason === "server" ? "unsupported" : support.reason,
|
|
384
|
+
message: support.message,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
const config = await act(() => portal.getPushConfig(), "Failed to start setting up notifications.");
|
|
388
|
+
if (!config) {
|
|
389
|
+
return {
|
|
390
|
+
ok: false,
|
|
391
|
+
reason: "failed",
|
|
392
|
+
message: "Failed to start setting up notifications.",
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
if (!config.enabled || !config.vapidPublicKey) {
|
|
396
|
+
// The platform has no VAPID pair configured. Nothing the visitor
|
|
397
|
+
// or the org can do about it, so say so plainly instead of
|
|
398
|
+
// failing at `subscribe()` with a DOMException nobody can act on.
|
|
399
|
+
return {
|
|
400
|
+
ok: false,
|
|
401
|
+
reason: "not-configured",
|
|
402
|
+
message: "Push notifications aren't available for this site yet.",
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
const subscribed = await subscribeToPush({
|
|
406
|
+
vapidPublicKey: config.vapidPublicKey,
|
|
407
|
+
...(options?.serviceWorkerPath === undefined
|
|
408
|
+
? {}
|
|
409
|
+
: { serviceWorkerPath: options.serviceWorkerPath }),
|
|
410
|
+
...(options?.scope === undefined ? {} : { scope: options.scope }),
|
|
411
|
+
});
|
|
412
|
+
if (!subscribed.ok) {
|
|
413
|
+
return {
|
|
414
|
+
ok: false,
|
|
415
|
+
reason: subscribed.reason,
|
|
416
|
+
message: subscribed.message,
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
const saved = await act(() => portal.subscribePush({
|
|
420
|
+
endpoint: subscribed.subscription.endpoint,
|
|
421
|
+
p256dh: subscribed.subscription.p256dh,
|
|
422
|
+
auth: subscribed.subscription.auth,
|
|
423
|
+
userAgent: subscribed.subscription.userAgent ?? null,
|
|
424
|
+
label: options?.label ?? null,
|
|
425
|
+
}), "Notifications were allowed, but we couldn't finish setting them up.");
|
|
426
|
+
if (!saved) {
|
|
427
|
+
// The browser now holds a subscription the server does not know
|
|
428
|
+
// about. Roll it back rather than leaving a device that looks
|
|
429
|
+
// enabled locally and receives nothing — a state whose only
|
|
430
|
+
// symptom is silence.
|
|
431
|
+
await unsubscribeFromPush().catch(() => undefined);
|
|
432
|
+
return {
|
|
433
|
+
ok: false,
|
|
434
|
+
reason: "failed",
|
|
435
|
+
message: "Notifications were allowed, but we couldn't finish setting them up. Try again.",
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
await this.loadPushConfig();
|
|
439
|
+
return { ok: true, deviceId: saved.id };
|
|
440
|
+
},
|
|
441
|
+
async disablePush() {
|
|
442
|
+
// Read the endpoint BEFORE tearing the local subscription down —
|
|
443
|
+
// it is what the server keys on, and once `unsubscribe()` has run
|
|
444
|
+
// there is nothing left to look the row up by.
|
|
445
|
+
const local = await currentPushSubscription().catch(() => null);
|
|
446
|
+
const torn = await unsubscribeFromPush().catch(() => ({
|
|
447
|
+
endpoint: null,
|
|
448
|
+
}));
|
|
449
|
+
const endpoint = torn.endpoint ?? local?.endpoint ?? null;
|
|
450
|
+
if (!endpoint) {
|
|
451
|
+
// Nothing registered here. Refresh anyway, since a device may
|
|
452
|
+
// have been removed from somewhere else.
|
|
453
|
+
await this.loadPushConfig();
|
|
454
|
+
return true;
|
|
455
|
+
}
|
|
456
|
+
const result = await act(() => portal.unsubscribePush({ endpoint }), "Failed to turn notifications off.");
|
|
457
|
+
if (result)
|
|
458
|
+
await this.loadPushConfig();
|
|
459
|
+
return Boolean(result?.ok);
|
|
460
|
+
},
|
|
461
|
+
async updatePushDevice(input) {
|
|
462
|
+
const result = await act(() => portal.updatePushDevice(input), "Failed to update that device.");
|
|
463
|
+
if (result?.ok)
|
|
464
|
+
await this.loadPushConfig();
|
|
465
|
+
return Boolean(result?.ok);
|
|
466
|
+
},
|
|
467
|
+
async forgetPushDevice(id) {
|
|
468
|
+
const result = await act(() => portal.unsubscribePush({ id }), "Failed to remove that device.");
|
|
469
|
+
if (result?.ok)
|
|
470
|
+
await this.loadPushConfig();
|
|
471
|
+
return Boolean(result?.ok);
|
|
472
|
+
},
|
|
473
|
+
loadNotificationPreferences() {
|
|
474
|
+
return read("overview", async () => {
|
|
475
|
+
state.notificationPreferences =
|
|
476
|
+
await portal.getNotificationPreferences();
|
|
477
|
+
}, "Failed to load notification preferences.");
|
|
478
|
+
},
|
|
479
|
+
loadOtherOrgs() {
|
|
480
|
+
return read("overview", async () => {
|
|
481
|
+
state.otherOrgs = await portal.myOtherCustomerOrgs();
|
|
482
|
+
}, "Failed to load your other companies.");
|
|
483
|
+
},
|
|
484
|
+
loadNotificationPreferencesFor(orgId) {
|
|
485
|
+
return read("overview", async () => {
|
|
486
|
+
const prefs = await portalFor(orgId).getNotificationPreferences();
|
|
487
|
+
// Replace the whole record rather than mutating in place, so
|
|
488
|
+
// framework bindings comparing snapshot identity actually see it.
|
|
489
|
+
state.notificationPreferencesByOrg = {
|
|
490
|
+
...state.notificationPreferencesByOrg,
|
|
491
|
+
[orgId]: prefs,
|
|
492
|
+
};
|
|
493
|
+
}, "Failed to load notification preferences for that company.");
|
|
494
|
+
},
|
|
495
|
+
async updateNotificationPreferencesFor(orgId, input) {
|
|
496
|
+
const res = await act(() => portalFor(orgId).updateNotificationPreferences(input), "Failed to update notification preferences for that company.");
|
|
497
|
+
if (res && !destroyed) {
|
|
498
|
+
// The server merges sparse writes into the stored matrix, so the
|
|
499
|
+
// authoritative result is what came BACK — echoing the request
|
|
500
|
+
// would show the customer a matrix the server never agreed to.
|
|
501
|
+
state.notificationPreferencesByOrg = {
|
|
502
|
+
...state.notificationPreferencesByOrg,
|
|
503
|
+
[orgId]: res,
|
|
504
|
+
};
|
|
505
|
+
notify();
|
|
506
|
+
}
|
|
507
|
+
return res !== null;
|
|
508
|
+
},
|
|
509
|
+
openJob(jobId) {
|
|
510
|
+
return detail(async (token) => {
|
|
511
|
+
const job = await portal.getJob(jobId);
|
|
512
|
+
if (destroyed || token !== detailToken)
|
|
513
|
+
return;
|
|
514
|
+
state.activeJob = job;
|
|
515
|
+
notify();
|
|
516
|
+
// Activity, ETA and comments are supporting detail. Any of them
|
|
517
|
+
// failing must not blank the job the customer asked for.
|
|
518
|
+
const [activity, eta, comments] = await Promise.allSettled([
|
|
519
|
+
portal.getJobActivity(jobId),
|
|
520
|
+
portal.getJobEta(jobId),
|
|
521
|
+
portal.listJobComments(jobId),
|
|
522
|
+
]);
|
|
523
|
+
if (destroyed || token !== detailToken)
|
|
524
|
+
return;
|
|
525
|
+
state.jobActivity =
|
|
526
|
+
activity.status === "fulfilled" ? activity.value : null;
|
|
527
|
+
state.jobEta = eta.status === "fulfilled" ? eta.value : null;
|
|
528
|
+
state.jobComments =
|
|
529
|
+
comments.status === "fulfilled" ? comments.value : null;
|
|
530
|
+
}, "Failed to load the job.");
|
|
531
|
+
},
|
|
532
|
+
openOrder(orderId) {
|
|
533
|
+
return detail(async (token) => {
|
|
534
|
+
const order = await portal.getOrder(orderId);
|
|
535
|
+
if (destroyed || token !== detailToken)
|
|
536
|
+
return;
|
|
537
|
+
state.activeOrder = order;
|
|
538
|
+
notify();
|
|
539
|
+
// Digital downloads only exist for some orders; a 404 here is
|
|
540
|
+
// normal and must not look like the order failed to load.
|
|
541
|
+
// Downloads and returns are both optional detail on an order —
|
|
542
|
+
// most orders have neither, so a 404 on either is normal and must
|
|
543
|
+
// not look like the order failed to load.
|
|
544
|
+
const [downloads, returns] = await Promise.allSettled([
|
|
545
|
+
portal.getDownloads(orderId),
|
|
546
|
+
portal.listReturnRequests(orderId),
|
|
547
|
+
]);
|
|
548
|
+
if (!destroyed && token === detailToken) {
|
|
549
|
+
state.orderDownloads =
|
|
550
|
+
downloads.status === "fulfilled" ? downloads.value : null;
|
|
551
|
+
state.returnRequests =
|
|
552
|
+
returns.status === "fulfilled" ? returns.value : null;
|
|
553
|
+
}
|
|
554
|
+
}, "Failed to load the order.");
|
|
555
|
+
},
|
|
556
|
+
openInvoice(invoiceId) {
|
|
557
|
+
return detail(async (token) => {
|
|
558
|
+
const invoice = await portal.getInvoice(invoiceId);
|
|
559
|
+
if (destroyed || token !== detailToken)
|
|
560
|
+
return;
|
|
561
|
+
state.activeInvoice = invoice;
|
|
562
|
+
}, "Failed to load the invoice.");
|
|
563
|
+
},
|
|
564
|
+
openQuote(quoteId) {
|
|
565
|
+
return detail(async (token) => {
|
|
566
|
+
// `viewQuote` both fetches and records that the customer looked,
|
|
567
|
+
// which the org can see. That is the right call from a portal:
|
|
568
|
+
// opening a quote IS viewing it.
|
|
569
|
+
const quote = await portal.viewQuote(quoteId);
|
|
570
|
+
if (destroyed || token !== detailToken)
|
|
571
|
+
return;
|
|
572
|
+
state.activeQuote = quote;
|
|
573
|
+
}, "Failed to load the quote.");
|
|
574
|
+
},
|
|
575
|
+
openContract(contractId) {
|
|
576
|
+
return detail(async (token) => {
|
|
577
|
+
const contract = await portal.getContract(contractId);
|
|
578
|
+
if (destroyed || token !== detailToken)
|
|
579
|
+
return;
|
|
580
|
+
state.activeContract = contract;
|
|
581
|
+
}, "Failed to load the contract.");
|
|
582
|
+
},
|
|
583
|
+
trackShipment(shipmentId) {
|
|
584
|
+
return detail(async (token) => {
|
|
585
|
+
const tracking = await portal.trackShipment(shipmentId);
|
|
586
|
+
if (destroyed || token !== detailToken)
|
|
587
|
+
return;
|
|
588
|
+
state.shipmentTracking = tracking;
|
|
589
|
+
}, "Failed to track the shipment.");
|
|
590
|
+
},
|
|
591
|
+
closeDetail() {
|
|
592
|
+
if (destroyed)
|
|
593
|
+
return;
|
|
594
|
+
detailToken += 1; // orphan anything in flight
|
|
595
|
+
state.activeJob = null;
|
|
596
|
+
state.jobActivity = null;
|
|
597
|
+
state.jobEta = null;
|
|
598
|
+
state.jobComments = null;
|
|
599
|
+
state.activeOrder = null;
|
|
600
|
+
state.orderDownloads = null;
|
|
601
|
+
state.returnRequests = null;
|
|
602
|
+
state.activeInvoice = null;
|
|
603
|
+
state.activeQuote = null;
|
|
604
|
+
state.activeContract = null;
|
|
605
|
+
state.shipmentTracking = null;
|
|
606
|
+
// Per-shipment, so it belongs to the detail view rather than the
|
|
607
|
+
// session — leaving it would show a stale claim on the next parcel.
|
|
608
|
+
state.lastNotReceived = null;
|
|
609
|
+
notify();
|
|
610
|
+
},
|
|
611
|
+
async postJobComment(jobId, input) {
|
|
612
|
+
const res = await act(() => portal.postJobComment(jobId, input), "Failed to post the comment.");
|
|
613
|
+
if (!res)
|
|
614
|
+
return false;
|
|
615
|
+
// Re-read rather than splice: the thread is the authority on what a
|
|
616
|
+
// comment looks like once the server has it.
|
|
617
|
+
await read("detail", async () => {
|
|
618
|
+
state.jobComments = await portal.listJobComments(jobId);
|
|
619
|
+
}, "Comment posted, but the thread could not be refreshed.");
|
|
620
|
+
return true;
|
|
621
|
+
},
|
|
622
|
+
async acceptQuote(quoteId) {
|
|
623
|
+
const res = await act(() => portal.acceptQuote(quoteId), "Failed to accept the quote.", () => {
|
|
624
|
+
// One-way state change; re-read so the UI shows the new status
|
|
625
|
+
// rather than the stale one it was rendered from.
|
|
626
|
+
void portal
|
|
627
|
+
.getQuote(quoteId)
|
|
628
|
+
.then((q) => {
|
|
629
|
+
if (!destroyed) {
|
|
630
|
+
state.activeQuote = q;
|
|
631
|
+
notify();
|
|
632
|
+
}
|
|
633
|
+
})
|
|
634
|
+
.catch(() => { });
|
|
635
|
+
});
|
|
636
|
+
return res !== null;
|
|
637
|
+
},
|
|
638
|
+
async rejectQuote(quoteId) {
|
|
639
|
+
const res = await act(() => portal.rejectQuote(quoteId), "Failed to reject the quote.", () => {
|
|
640
|
+
void portal
|
|
641
|
+
.getQuote(quoteId)
|
|
642
|
+
.then((q) => {
|
|
643
|
+
if (!destroyed) {
|
|
644
|
+
state.activeQuote = q;
|
|
645
|
+
notify();
|
|
646
|
+
}
|
|
647
|
+
})
|
|
648
|
+
.catch(() => { });
|
|
649
|
+
});
|
|
650
|
+
return res !== null;
|
|
651
|
+
},
|
|
652
|
+
async signContract(contractId) {
|
|
653
|
+
const res = await act(() => portal.signContract(contractId), "Failed to sign the contract.", () => {
|
|
654
|
+
// There is no unsign. Re-reading is the only way the UI learns
|
|
655
|
+
// the contract is now signed.
|
|
656
|
+
void portal
|
|
657
|
+
.getContract(contractId)
|
|
658
|
+
.then((c) => {
|
|
659
|
+
if (!destroyed) {
|
|
660
|
+
state.activeContract = c;
|
|
661
|
+
notify();
|
|
662
|
+
}
|
|
663
|
+
})
|
|
664
|
+
.catch(() => { });
|
|
665
|
+
});
|
|
666
|
+
return res !== null;
|
|
667
|
+
},
|
|
668
|
+
payInvoice(invoiceId, opts) {
|
|
669
|
+
return act(() => portal.payInvoice(invoiceId, opts), "Could not start the payment.");
|
|
670
|
+
},
|
|
671
|
+
async updateProfile(input) {
|
|
672
|
+
const res = await act(() => portal.updateProfile(input), "Failed to save your details.");
|
|
673
|
+
if (res === null)
|
|
674
|
+
return false;
|
|
675
|
+
await read("overview", async () => {
|
|
676
|
+
state.profile = await portal.getProfile();
|
|
677
|
+
}, "Saved, but your details could not be re-read.");
|
|
678
|
+
return true;
|
|
679
|
+
},
|
|
680
|
+
async updateNotificationPreferences(input) {
|
|
681
|
+
const res = await act(() => portal.updateNotificationPreferences(input), "Failed to save your notification settings.", (next) => {
|
|
682
|
+
state.notificationPreferences = next;
|
|
683
|
+
});
|
|
684
|
+
return res !== null;
|
|
685
|
+
},
|
|
686
|
+
async submitReview(input) {
|
|
687
|
+
const res = await act(() => portal.submitReview(input), "Failed to submit your review.", () => {
|
|
688
|
+
// Deliberately NOT added to `reviews`. It lands as `pending` for
|
|
689
|
+
// staff approval, and showing it back would tell the author it is
|
|
690
|
+
// live when the public cannot see it.
|
|
691
|
+
state.reviewAwaitingModeration = true;
|
|
692
|
+
});
|
|
693
|
+
return res !== null;
|
|
694
|
+
},
|
|
695
|
+
submitReturnRequest(orderId, input) {
|
|
696
|
+
return act(() => portal.submitReturnRequest(orderId, input), "Failed to submit the return request.", () => {
|
|
697
|
+
// Re-read so the customer immediately sees the request they just
|
|
698
|
+
// made, with its real status. Submitting into silence is what
|
|
699
|
+
// made this gap worth closing.
|
|
700
|
+
void portal
|
|
701
|
+
.listReturnRequests(orderId)
|
|
702
|
+
.then((res) => {
|
|
703
|
+
if (!destroyed) {
|
|
704
|
+
state.returnRequests = res;
|
|
705
|
+
notify();
|
|
706
|
+
}
|
|
707
|
+
})
|
|
708
|
+
.catch(() => { });
|
|
709
|
+
});
|
|
710
|
+
},
|
|
711
|
+
submitExchangeRequest(orderId, input) {
|
|
712
|
+
// Routed through the same method so the re-read, the error copy and
|
|
713
|
+
// the pending flag stay identical — an exchange is a return request
|
|
714
|
+
// with a different ask, not a separate flow.
|
|
715
|
+
return this.submitReturnRequest(orderId, {
|
|
716
|
+
...(input ?? {}),
|
|
717
|
+
kind: "exchange",
|
|
718
|
+
});
|
|
719
|
+
},
|
|
720
|
+
reportNotReceived(shipmentId, input) {
|
|
721
|
+
return act(() => portal.reportNotReceived(shipmentId, input), "Failed to report the parcel as not received.").then((res) => {
|
|
722
|
+
if (res && !destroyed) {
|
|
723
|
+
state.lastNotReceived = res;
|
|
724
|
+
notify();
|
|
725
|
+
}
|
|
726
|
+
return res;
|
|
727
|
+
});
|
|
728
|
+
},
|
|
729
|
+
loadDispatchStatus(jobId) {
|
|
730
|
+
return detail(async (token) => {
|
|
731
|
+
const eta = await portal.getDispatchStatus(jobId);
|
|
732
|
+
if (destroyed || token !== detailToken)
|
|
733
|
+
return;
|
|
734
|
+
state.jobEta = eta;
|
|
735
|
+
}, "Failed to load the dispatch status.");
|
|
736
|
+
},
|
|
737
|
+
loadSubscription() {
|
|
738
|
+
return read("overview", async () => {
|
|
739
|
+
state.subscription = await portal.getSubscription();
|
|
740
|
+
}, "Failed to load your subscription.");
|
|
741
|
+
},
|
|
742
|
+
loadSubscriberContent(opts) {
|
|
743
|
+
return read("overview", async () => {
|
|
744
|
+
state.subscriberContent = await portal.getSubscriberContent(opts);
|
|
745
|
+
}, "Failed to load your subscriber content.");
|
|
746
|
+
},
|
|
747
|
+
async cancelSubscription() {
|
|
748
|
+
const res = await act(() => portal.cancelSubscription(), "Failed to cancel your subscription.");
|
|
749
|
+
if (res && !destroyed) {
|
|
750
|
+
// Re-read rather than patching the cached row: the provider's
|
|
751
|
+
// webhook owns `status`, and guessing what it will write is how a
|
|
752
|
+
// portal ends up showing "cancelled" while the customer still has
|
|
753
|
+
// — and has paid for — access.
|
|
754
|
+
void portal
|
|
755
|
+
.getSubscription()
|
|
756
|
+
.then((next) => {
|
|
757
|
+
if (!destroyed) {
|
|
758
|
+
state.subscription = next;
|
|
759
|
+
notify();
|
|
760
|
+
}
|
|
761
|
+
})
|
|
762
|
+
.catch(() => { });
|
|
763
|
+
}
|
|
764
|
+
return res;
|
|
765
|
+
},
|
|
766
|
+
async resumeSubscription() {
|
|
767
|
+
const res = await act(() => portal.resumeSubscription(), "Failed to resume your subscription.");
|
|
768
|
+
if (res && !destroyed) {
|
|
769
|
+
void portal
|
|
770
|
+
.getSubscription()
|
|
771
|
+
.then((next) => {
|
|
772
|
+
if (!destroyed) {
|
|
773
|
+
state.subscription = next;
|
|
774
|
+
notify();
|
|
775
|
+
}
|
|
776
|
+
})
|
|
777
|
+
.catch(() => { });
|
|
778
|
+
}
|
|
779
|
+
return res;
|
|
780
|
+
},
|
|
781
|
+
async loadProductReviews() {
|
|
782
|
+
await act(() => portal.listProductReviews(), "Failed to load your product reviews.", () => { }).then((res) => {
|
|
783
|
+
if (res && !destroyed) {
|
|
784
|
+
state.productReviews = res;
|
|
785
|
+
notify();
|
|
786
|
+
}
|
|
787
|
+
});
|
|
788
|
+
},
|
|
789
|
+
async submitProductReview(input) {
|
|
790
|
+
const res = await act(() => portal.submitProductReview(input), "Failed to submit your review.", () => { });
|
|
791
|
+
if (res && !destroyed) {
|
|
792
|
+
// Only flag moderation when the org actually held it. Saying
|
|
793
|
+
// "awaiting review" for a review that is already live is its own
|
|
794
|
+
// kind of wrong.
|
|
795
|
+
state.productReviewAwaitingModeration = res.awaitingModeration;
|
|
796
|
+
// Re-read so the customer sees what they wrote, with its real
|
|
797
|
+
// approval state, rather than having to trust that it landed.
|
|
798
|
+
void portal
|
|
799
|
+
.listProductReviews()
|
|
800
|
+
.then((list) => {
|
|
801
|
+
if (!destroyed) {
|
|
802
|
+
state.productReviews = list;
|
|
803
|
+
notify();
|
|
804
|
+
}
|
|
805
|
+
})
|
|
806
|
+
.catch(() => { });
|
|
807
|
+
}
|
|
808
|
+
return res !== null;
|
|
809
|
+
},
|
|
810
|
+
contactCompany(input) {
|
|
811
|
+
return act(() => portal.contactCompany(input), "Failed to send your message.");
|
|
812
|
+
},
|
|
813
|
+
destroy() {
|
|
814
|
+
destroyed = true;
|
|
815
|
+
subscribers.clear();
|
|
816
|
+
},
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
//# sourceMappingURL=index.js.map
|