@moonbase.sh/storefront-api 3.1.0 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -2
- package/dist/index.cjs +196 -37
- package/dist/index.d.cts +127 -2
- package/dist/index.d.ts +127 -2
- package/dist/index.js +194 -37
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -76,6 +76,40 @@ const redeemed = await client.vouchers.redeem('MY-CODE')
|
|
|
76
76
|
console.log(redeemed)
|
|
77
77
|
```
|
|
78
78
|
|
|
79
|
+
### Enroll a customer in a group
|
|
80
|
+
|
|
81
|
+
Merchant-owned customer lists that have been opened to public sign-ups can be
|
|
82
|
+
named by ID on sign-up, on a newsletter subscription, and on an order:
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
await client.identity.signUp('Jane', 'jane@example.com', 'password', null, true, true, true, {
|
|
86
|
+
groupIds: ['newsletter-2026'],
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
await client.communications.subscribe({ email: 'jane@example.com', groupIds: ['newsletter-2026'] })
|
|
90
|
+
|
|
91
|
+
// On an order, after it has been pushed and before it is paid. Additive: each
|
|
92
|
+
// call adds to what the order already carries.
|
|
93
|
+
await client.orders.joinGroups(order.id, ['buyers-2026'])
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
None of those three report which groups were applied. An ID naming a list that
|
|
97
|
+
is closed, deleted, rule-driven or another tenant's is skipped in silence, so a
|
|
98
|
+
stale ID can never fail a registration or a sale. IDs are matched lowercase and
|
|
99
|
+
at most five per request are applied.
|
|
100
|
+
|
|
101
|
+
For a customer who is already signed in, joining is a request of its own, and it
|
|
102
|
+
is the one that does report failure:
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
await client.groups.join('beta') // throws NotFoundError if it isn't joinable
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Every reason it can be refused (no such group, deleted, closed, a segment,
|
|
109
|
+
another tenant's) arrives as the same `NotFoundError`, so that a signed-in
|
|
110
|
+
customer cannot use it to enumerate a merchant's group IDs. Joining twice
|
|
111
|
+
succeeds and changes nothing.
|
|
112
|
+
|
|
79
113
|
## Inventory flow
|
|
80
114
|
|
|
81
115
|
Most inventory endpoints require an authenticated user:
|
|
@@ -97,7 +131,7 @@ while (page.hasMore && page.next) {
|
|
|
97
131
|
|
|
98
132
|
## Endpoint groups
|
|
99
133
|
|
|
100
|
-
`client.storefront`, `client.identity`, `client.orders`, `client.vouchers`, `client.inventory`, `client.activationRequests`, `client.vendor`.
|
|
134
|
+
`client.storefront`, `client.identity`, `client.orders`, `client.vouchers`, `client.inventory`, `client.activationRequests`, `client.vendor`, `client.communications`, `client.groups`.
|
|
101
135
|
|
|
102
136
|
## Image renditions
|
|
103
137
|
|
|
@@ -122,7 +156,17 @@ host — is returned untouched and logs a warning, since it will be served at fu
|
|
|
122
156
|
|
|
123
157
|
## Token storage and logging
|
|
124
158
|
|
|
125
|
-
By default, credentials are stored in localStorage
|
|
159
|
+
By default, credentials are stored in localStorage, falling back to memory only when localStorage
|
|
160
|
+
cannot be reached at all. Even that check sits inside a `try`, because reading
|
|
161
|
+
`window.localStorage` itself throws in Firefox when the buyer has blocked cookies and site data.
|
|
162
|
+
|
|
163
|
+
Reachability, not writability, is the test on purpose: an origin that is merely full is still
|
|
164
|
+
holding the buyer's existing session, so it is used rather than discarded. Individual reads and
|
|
165
|
+
writes that fail degrade to an in-memory copy and warn through the configured logger. The session
|
|
166
|
+
stays valid for that page load; it just doesn't survive a reload, and
|
|
167
|
+
`client.tokenStore.isPersistent` goes false once that has happened if you want to tell the buyer.
|
|
168
|
+
A failure to *persist* credentials is never reported as a failure to sign in, so a locked-down
|
|
169
|
+
browser can't strand someone in a sign-in loop.
|
|
126
170
|
|
|
127
171
|
You can inject your own storage/logger implementations:
|
|
128
172
|
|
package/dist/index.cjs
CHANGED
|
@@ -49,9 +49,11 @@ __export(index_exports, {
|
|
|
49
49
|
SubscriptionStatus: () => SubscriptionStatus,
|
|
50
50
|
TokenStore: () => TokenStore,
|
|
51
51
|
imageWidths: () => imageWidths,
|
|
52
|
+
isLocalStorageAvailable: () => isLocalStorageAvailable,
|
|
52
53
|
isPromotionDisplay: () => isPromotionDisplay,
|
|
53
54
|
isTransformableImageUrl: () => isTransformableImageUrl,
|
|
54
55
|
maxImageDimension: () => maxImageDimension,
|
|
56
|
+
normalizeGroupIds: () => normalizeGroupIds,
|
|
55
57
|
objectToQuery: () => objectToQuery,
|
|
56
58
|
parseOffSiteReferrer: () => parseOffSiteReferrer,
|
|
57
59
|
problemDetailsSchema: () => problemDetailsSchema,
|
|
@@ -489,6 +491,19 @@ var ActivationRequestEndpoints = class {
|
|
|
489
491
|
}
|
|
490
492
|
};
|
|
491
493
|
|
|
494
|
+
// src/utils/groups.ts
|
|
495
|
+
function normalizeGroupIds(groupIds) {
|
|
496
|
+
if (!groupIds)
|
|
497
|
+
return void 0;
|
|
498
|
+
const normalized = [];
|
|
499
|
+
for (const groupId of groupIds) {
|
|
500
|
+
const trimmed = groupId == null ? void 0 : groupId.trim().toLowerCase();
|
|
501
|
+
if (trimmed && !normalized.includes(trimmed))
|
|
502
|
+
normalized.push(trimmed);
|
|
503
|
+
}
|
|
504
|
+
return normalized.length > 0 ? normalized : void 0;
|
|
505
|
+
}
|
|
506
|
+
|
|
492
507
|
// src/communications/schemas.ts
|
|
493
508
|
var schemas_exports4 = {};
|
|
494
509
|
__export(schemas_exports4, {
|
|
@@ -522,7 +537,8 @@ var CommunicationsEndpoints = class {
|
|
|
522
537
|
email: input.email,
|
|
523
538
|
name: ((_a = input.name) == null ? void 0 : _a.trim()) || null,
|
|
524
539
|
newsletter: (_b = input.newsletter) != null ? _b : true,
|
|
525
|
-
productUpdates: (_c = input.productUpdates) != null ? _c : true
|
|
540
|
+
productUpdates: (_c = input.productUpdates) != null ? _c : true,
|
|
541
|
+
groupIds: normalizeGroupIds(input.groupIds)
|
|
526
542
|
}
|
|
527
543
|
});
|
|
528
544
|
return response.data;
|
|
@@ -564,6 +580,37 @@ var CommunicationsEndpoints = class {
|
|
|
564
580
|
}
|
|
565
581
|
};
|
|
566
582
|
|
|
583
|
+
// src/groups/endpoints.ts
|
|
584
|
+
var GroupEndpoints = class {
|
|
585
|
+
constructor(api) {
|
|
586
|
+
this.api = api;
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Joins the signed-in customer to a merchant-owned group that has been opened
|
|
590
|
+
* to public sign-ups.
|
|
591
|
+
*
|
|
592
|
+
* The one group call that reports failure. Group IDs sent along with sign-up,
|
|
593
|
+
* a newsletter subscription or a checkout are passengers on a request that
|
|
594
|
+
* wanted something else, so an unusable one is skipped rather than allowed to
|
|
595
|
+
* fail the account, the subscription or the sale. Here joining *is* the
|
|
596
|
+
* request, so it throws.
|
|
597
|
+
*
|
|
598
|
+
* Every reason it can fail (no such group, deleted, closed, a segment, a
|
|
599
|
+
* system group, another tenant's) arrives as the same `NotFoundError`, so
|
|
600
|
+
* that a customer of one tenant can't enumerate that merchant's group IDs.
|
|
601
|
+
*
|
|
602
|
+
* Idempotent: joining a group the customer is already in succeeds and changes
|
|
603
|
+
* nothing.
|
|
604
|
+
*/
|
|
605
|
+
async join(groupId) {
|
|
606
|
+
await this.api.authenticatedFetch(
|
|
607
|
+
`/api/customer/groups/${encodeURIComponent(groupId)}/join`,
|
|
608
|
+
null,
|
|
609
|
+
{ method: "POST" }
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
};
|
|
613
|
+
|
|
567
614
|
// src/utils/errors.ts
|
|
568
615
|
var NotAuthorizedError = class extends Error {
|
|
569
616
|
constructor() {
|
|
@@ -757,17 +804,17 @@ var IdentityEndpoints = class {
|
|
|
757
804
|
throw new NotFoundError("User not found");
|
|
758
805
|
if (response.status >= 400)
|
|
759
806
|
await handleResponseProblem(response, this.logger);
|
|
807
|
+
let identity;
|
|
760
808
|
try {
|
|
761
|
-
|
|
762
|
-
const user = identitySchema.parse(data);
|
|
763
|
-
this.tokenStore.setUser(user);
|
|
764
|
-
return userSchema.parse(user);
|
|
809
|
+
identity = identitySchema.parse(await response.json());
|
|
765
810
|
} catch (err) {
|
|
766
811
|
this.logger.warn("Could not sign in user", { email, response, err });
|
|
767
812
|
throw new MoonbaseError("Bad response", "Could not sign in user", response.status);
|
|
768
813
|
}
|
|
814
|
+
this.tokenStore.setUser(identity);
|
|
815
|
+
return userSchema.parse(identity);
|
|
769
816
|
}
|
|
770
|
-
async signUp(name, email, password, address, acceptedPrivacyPolicy, acceptedTermsAndConditions, communicationOptIn) {
|
|
817
|
+
async signUp(name, email, password, address, acceptedPrivacyPolicy, acceptedTermsAndConditions, communicationOptIn, options) {
|
|
771
818
|
const path = `/api/customer/identity/sign-up?scheme=JWT&communicationOptIn=${communicationOptIn ? "true" : "false"}`;
|
|
772
819
|
const response = await fetch(`${this.api.baseUrl}${path}`, {
|
|
773
820
|
method: "POST",
|
|
@@ -782,7 +829,8 @@ var IdentityEndpoints = class {
|
|
|
782
829
|
password,
|
|
783
830
|
address,
|
|
784
831
|
acceptedPrivacyPolicy,
|
|
785
|
-
acceptedTermsAndConditions
|
|
832
|
+
acceptedTermsAndConditions,
|
|
833
|
+
groupIds: normalizeGroupIds(options == null ? void 0 : options.groupIds)
|
|
786
834
|
})
|
|
787
835
|
});
|
|
788
836
|
if (response.status >= 400)
|
|
@@ -792,14 +840,15 @@ var IdentityEndpoints = class {
|
|
|
792
840
|
const parsed = pendingActivationSchema.parse(data);
|
|
793
841
|
return { status: "activation-pending", email: parsed.email };
|
|
794
842
|
}
|
|
843
|
+
let identity;
|
|
795
844
|
try {
|
|
796
|
-
|
|
797
|
-
this.tokenStore.setUser(identity);
|
|
798
|
-
return { status: "signed-in", user: userSchema.parse(identity) };
|
|
845
|
+
identity = identitySchema.parse(data);
|
|
799
846
|
} catch (err) {
|
|
800
847
|
this.logger.warn("Could not sign up user", { email, response, err });
|
|
801
848
|
throw new MoonbaseError("Bad response", "Could not sign up user", response.status);
|
|
802
849
|
}
|
|
850
|
+
this.tokenStore.setUser(identity);
|
|
851
|
+
return { status: "signed-in", user: userSchema.parse(identity) };
|
|
803
852
|
}
|
|
804
853
|
async signOut() {
|
|
805
854
|
this.tokenStore.clear();
|
|
@@ -1079,6 +1128,15 @@ var MoonbaseApi = class {
|
|
|
1079
1128
|
status: response.status,
|
|
1080
1129
|
duration: finishedAt.getTime() - startedAt.getTime()
|
|
1081
1130
|
});
|
|
1131
|
+
if (response.type === "opaqueredirect" || response.status === 0) {
|
|
1132
|
+
const err = new MoonbaseError(
|
|
1133
|
+
"Unexpected redirect",
|
|
1134
|
+
`The Moonbase API redirected ${path}, which the SDK is not configured to follow`,
|
|
1135
|
+
response.status
|
|
1136
|
+
);
|
|
1137
|
+
this.reportRequestProblem(path, request, response, err);
|
|
1138
|
+
throw err;
|
|
1139
|
+
}
|
|
1082
1140
|
if (response.status >= 400) {
|
|
1083
1141
|
try {
|
|
1084
1142
|
await handleResponseProblem(response, this.logger);
|
|
@@ -1494,6 +1552,28 @@ var OrderEndpoints = class {
|
|
|
1494
1552
|
const response = await this.api.fetch(`/api/customer/orders/${orderId}/coupons`, openOrderSchema, { method: "DELETE" });
|
|
1495
1553
|
return response.data;
|
|
1496
1554
|
}
|
|
1555
|
+
/**
|
|
1556
|
+
* Records merchant-owned customer groups the buyer should join once this order
|
|
1557
|
+
* completes. The order has to exist server-side first, so call it after a
|
|
1558
|
+
* `pushContent`, and before the payment rather than after: the enrollment
|
|
1559
|
+
* rides on the order until the payment webhook settles it.
|
|
1560
|
+
*
|
|
1561
|
+
* Additive rather than a replacement: each call adds to what the order already
|
|
1562
|
+
* carries, and the accumulated set is capped server-side. Groups the merchant
|
|
1563
|
+
* has not opened to public sign-ups are skipped in silence, so a stale ID in a
|
|
1564
|
+
* checkout form can never fail a sale.
|
|
1565
|
+
*
|
|
1566
|
+
* Kept apart from the marketing opt-in on purpose: a checkout can offer both
|
|
1567
|
+
* independently, and merging them would opt a buyer who only asked to join a
|
|
1568
|
+
* list into email they never asked for.
|
|
1569
|
+
*/
|
|
1570
|
+
async joinGroups(orderId, groupIds) {
|
|
1571
|
+
const response = await this.api.fetch(`/api/customer/orders/${orderId}/groups`, openOrderSchema, {
|
|
1572
|
+
method: "POST",
|
|
1573
|
+
body: { groupIds: normalizeGroupIds(groupIds) }
|
|
1574
|
+
});
|
|
1575
|
+
return response.data;
|
|
1576
|
+
}
|
|
1497
1577
|
};
|
|
1498
1578
|
|
|
1499
1579
|
// src/storefront/endpoints.ts
|
|
@@ -1543,28 +1623,81 @@ var ConsoleLogger = class {
|
|
|
1543
1623
|
};
|
|
1544
1624
|
|
|
1545
1625
|
// src/utils/store.ts
|
|
1626
|
+
function isLocalStorageAvailable() {
|
|
1627
|
+
try {
|
|
1628
|
+
return typeof window !== "undefined" && !!window.localStorage;
|
|
1629
|
+
} catch (e) {
|
|
1630
|
+
return false;
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1546
1633
|
var LocalStorageStore = class {
|
|
1634
|
+
constructor(onDegraded) {
|
|
1635
|
+
this.onDegraded = onDegraded;
|
|
1636
|
+
// Values whose write to localStorage failed. Read back in preference to
|
|
1637
|
+
// localStorage so the rest of the session sees what it just wrote.
|
|
1638
|
+
this.fallback = /* @__PURE__ */ new Map();
|
|
1639
|
+
this.degraded = false;
|
|
1640
|
+
}
|
|
1641
|
+
get isDegraded() {
|
|
1642
|
+
return this.degraded;
|
|
1643
|
+
}
|
|
1547
1644
|
get(key) {
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1645
|
+
if (this.fallback.has(key))
|
|
1646
|
+
return this.fallback.get(key);
|
|
1647
|
+
try {
|
|
1648
|
+
const item = localStorage.getItem(key);
|
|
1649
|
+
if (item) {
|
|
1650
|
+
return JSON.parse(item);
|
|
1651
|
+
}
|
|
1652
|
+
return null;
|
|
1653
|
+
} catch (err) {
|
|
1654
|
+
this.reportDegraded(err);
|
|
1655
|
+
return null;
|
|
1551
1656
|
}
|
|
1552
|
-
return null;
|
|
1553
1657
|
}
|
|
1554
1658
|
set(key, item) {
|
|
1555
|
-
|
|
1659
|
+
try {
|
|
1660
|
+
localStorage.setItem(key, JSON.stringify(item));
|
|
1661
|
+
this.fallback.delete(key);
|
|
1662
|
+
} catch (err) {
|
|
1663
|
+
this.reportDegraded(err);
|
|
1664
|
+
this.fallback.set(key, item);
|
|
1665
|
+
}
|
|
1556
1666
|
}
|
|
1557
1667
|
remove(key) {
|
|
1558
|
-
|
|
1668
|
+
try {
|
|
1669
|
+
localStorage.removeItem(key);
|
|
1670
|
+
this.fallback.delete(key);
|
|
1671
|
+
return;
|
|
1672
|
+
} catch (err) {
|
|
1673
|
+
this.reportDegraded(err);
|
|
1674
|
+
}
|
|
1675
|
+
this.fallback.set(key, null);
|
|
1676
|
+
try {
|
|
1677
|
+
localStorage.setItem(key, "");
|
|
1678
|
+
} catch (e) {
|
|
1679
|
+
}
|
|
1559
1680
|
}
|
|
1560
1681
|
listen(key, callback) {
|
|
1561
|
-
if (typeof window
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1682
|
+
if (typeof window === "undefined")
|
|
1683
|
+
return;
|
|
1684
|
+
window.addEventListener("storage", (event) => {
|
|
1685
|
+
if (event.key !== key)
|
|
1686
|
+
return;
|
|
1687
|
+
this.fallback.delete(key);
|
|
1688
|
+
try {
|
|
1689
|
+
callback(event.newValue ? JSON.parse(event.newValue) : null);
|
|
1690
|
+
} catch (err) {
|
|
1691
|
+
console.warn(`Moonbase could not parse the stored value for ${key}`, err);
|
|
1692
|
+
}
|
|
1693
|
+
});
|
|
1694
|
+
}
|
|
1695
|
+
reportDegraded(err) {
|
|
1696
|
+
var _a;
|
|
1697
|
+
if (this.degraded)
|
|
1698
|
+
return;
|
|
1699
|
+
this.degraded = true;
|
|
1700
|
+
(_a = this.onDegraded) == null ? void 0 : _a.call(this, err);
|
|
1568
1701
|
}
|
|
1569
1702
|
};
|
|
1570
1703
|
var InMemoryStore = class {
|
|
@@ -1602,8 +1735,11 @@ var _TokenStore = class _TokenStore {
|
|
|
1602
1735
|
this.tokens = null;
|
|
1603
1736
|
this.refreshTimeoutId = null;
|
|
1604
1737
|
this.refreshPromise = null;
|
|
1605
|
-
var _a;
|
|
1606
|
-
this.
|
|
1738
|
+
var _a, _b;
|
|
1739
|
+
this.logger = (_a = configuration.logger) != null ? _a : new ConsoleLogger();
|
|
1740
|
+
this.store = (_b = configuration.store) != null ? _b : isLocalStorageAvailable() ? new LocalStorageStore((err) => this.warnAboutStorage(err)) : new InMemoryStore();
|
|
1741
|
+
if (typeof window !== "undefined" && !configuration.store && !(this.store instanceof LocalStorageStore))
|
|
1742
|
+
this.warnAboutStorage();
|
|
1607
1743
|
const storedToken = this.store.get(_TokenStore.storageKey);
|
|
1608
1744
|
if (storedToken) {
|
|
1609
1745
|
this.tokens = {
|
|
@@ -1618,18 +1754,17 @@ var _TokenStore = class _TokenStore {
|
|
|
1618
1754
|
return this.tokens;
|
|
1619
1755
|
return null;
|
|
1620
1756
|
}
|
|
1757
|
+
get isPersistent() {
|
|
1758
|
+
return this.store instanceof LocalStorageStore && !this.store.isDegraded;
|
|
1759
|
+
}
|
|
1621
1760
|
async getAccessToken() {
|
|
1622
1761
|
var _a, _b, _c;
|
|
1623
|
-
if (this.isExpired) {
|
|
1624
|
-
|
|
1625
|
-
const tokens2 = await this.refreshPromise;
|
|
1626
|
-
return (_a = tokens2 == null ? void 0 : tokens2.accessToken) != null ? _a : null;
|
|
1627
|
-
}
|
|
1628
|
-
this.refreshPromise = this.refreshTokens();
|
|
1629
|
-
const tokens = await this.refreshPromise;
|
|
1630
|
-
return (_b = tokens == null ? void 0 : tokens.accessToken) != null ? _b : null;
|
|
1762
|
+
if (!this.isExpired) {
|
|
1763
|
+
return ((_a = this.tokens) == null ? void 0 : _a.accessToken) || null;
|
|
1631
1764
|
}
|
|
1632
|
-
|
|
1765
|
+
const pending = (_b = this.refreshPromise) != null ? _b : this.startRefresh();
|
|
1766
|
+
const tokens = await pending;
|
|
1767
|
+
return (_c = tokens == null ? void 0 : tokens.accessToken) != null ? _c : null;
|
|
1633
1768
|
}
|
|
1634
1769
|
setUser(user) {
|
|
1635
1770
|
const identity = user;
|
|
@@ -1654,7 +1789,7 @@ var _TokenStore = class _TokenStore {
|
|
|
1654
1789
|
if (this.refreshTimeoutId != null)
|
|
1655
1790
|
window.clearTimeout(this.refreshTimeoutId);
|
|
1656
1791
|
this.refreshTimeoutId = window.setTimeout(() => {
|
|
1657
|
-
this.
|
|
1792
|
+
this.startRefresh().catch((err) => this.logger.warn("Could not refresh access token", err));
|
|
1658
1793
|
}, 10 * 60 * 1e3);
|
|
1659
1794
|
}
|
|
1660
1795
|
return this.tokens;
|
|
@@ -1669,10 +1804,25 @@ var _TokenStore = class _TokenStore {
|
|
|
1669
1804
|
get isExpired() {
|
|
1670
1805
|
return this.tokens != null && this.tokens.expiresAt < /* @__PURE__ */ new Date();
|
|
1671
1806
|
}
|
|
1807
|
+
/**
|
|
1808
|
+
* Start a refresh and hold it so concurrent callers join it rather than firing
|
|
1809
|
+
* their own. The slot is cleared as soon as it settles: a retained promise
|
|
1810
|
+
* would hand every later expiry the same stale access token, and a rejected one
|
|
1811
|
+
* that stayed put would re-throw on every subsequent request (including the
|
|
1812
|
+
* anonymous ones) for the rest of the session.
|
|
1813
|
+
*/
|
|
1814
|
+
startRefresh() {
|
|
1815
|
+
const pending = this.refreshTokens().finally(() => {
|
|
1816
|
+
if (this.refreshPromise === pending)
|
|
1817
|
+
this.refreshPromise = null;
|
|
1818
|
+
});
|
|
1819
|
+
this.refreshPromise = pending;
|
|
1820
|
+
return pending;
|
|
1821
|
+
}
|
|
1672
1822
|
async refreshTokens() {
|
|
1673
1823
|
if (!this.tokens)
|
|
1674
1824
|
throw new Error("No tokens found to refresh");
|
|
1675
|
-
const response = await fetch(`${this.configuration.endpoint}/api/customer/identity/refresh?token=${this.tokens.refreshToken}`, {
|
|
1825
|
+
const response = await fetch(`${this.configuration.endpoint}/api/customer/identity/refresh?token=${encodeURIComponent(this.tokens.refreshToken)}`, {
|
|
1676
1826
|
method: "POST",
|
|
1677
1827
|
headers: {
|
|
1678
1828
|
"Accept": "application/json",
|
|
@@ -1694,12 +1844,18 @@ var _TokenStore = class _TokenStore {
|
|
|
1694
1844
|
}
|
|
1695
1845
|
handleStorageUpdate(updatedTokens) {
|
|
1696
1846
|
this.tokens = updatedTokens;
|
|
1697
|
-
if (this.refreshTimeoutId != null)
|
|
1847
|
+
if (this.refreshTimeoutId != null && typeof window !== "undefined")
|
|
1698
1848
|
window.clearTimeout(this.refreshTimeoutId);
|
|
1699
1849
|
if (this.tokens) {
|
|
1700
1850
|
this.tokens.expiresAt = new Date(this.tokens.expiresAt);
|
|
1701
1851
|
}
|
|
1702
1852
|
}
|
|
1853
|
+
warnAboutStorage(err) {
|
|
1854
|
+
this.logger.warn(
|
|
1855
|
+
"Moonbase could not persist credentials to localStorage: this browser is blocking or has exhausted site storage. Sign-in will work for this page load but will not survive a reload.",
|
|
1856
|
+
err
|
|
1857
|
+
);
|
|
1858
|
+
}
|
|
1703
1859
|
};
|
|
1704
1860
|
_TokenStore.storageKey = "moonbase_auth";
|
|
1705
1861
|
var TokenStore = _TokenStore;
|
|
@@ -2394,6 +2550,7 @@ var MoonbaseClient = class {
|
|
|
2394
2550
|
this.storefront = new StorefrontEndpoints(this.api, this.configuration);
|
|
2395
2551
|
this.identity = new IdentityEndpoints(this.api, this.tokenStore, this.logger);
|
|
2396
2552
|
this.communications = new CommunicationsEndpoints(this.api);
|
|
2553
|
+
this.groups = new GroupEndpoints(this.api);
|
|
2397
2554
|
this.vouchers = new VoucherEndpoints(this.api);
|
|
2398
2555
|
this.orders = new OrderEndpoints(this.api);
|
|
2399
2556
|
this.inventory = new InventoryEndpoints(this.api, this.configuration);
|
|
@@ -2432,9 +2589,11 @@ var MoonbaseClient = class {
|
|
|
2432
2589
|
SubscriptionStatus,
|
|
2433
2590
|
TokenStore,
|
|
2434
2591
|
imageWidths,
|
|
2592
|
+
isLocalStorageAvailable,
|
|
2435
2593
|
isPromotionDisplay,
|
|
2436
2594
|
isTransformableImageUrl,
|
|
2437
2595
|
maxImageDimension,
|
|
2596
|
+
normalizeGroupIds,
|
|
2438
2597
|
objectToQuery,
|
|
2439
2598
|
parseOffSiteReferrer,
|
|
2440
2599
|
problemDetailsSchema,
|
package/dist/index.d.cts
CHANGED
|
@@ -26,12 +26,47 @@ interface IStore {
|
|
|
26
26
|
set: <TItem>(key: string, item: TItem) => void;
|
|
27
27
|
remove: (key: string) => void;
|
|
28
28
|
listen: <TItem>(key: string, callback: (item: TItem | null) => void) => void;
|
|
29
|
+
/**
|
|
30
|
+
* True once any storage operation has failed and the store has fallen back to
|
|
31
|
+
* memory. Optional so custom stores don't have to implement it.
|
|
32
|
+
*/
|
|
33
|
+
readonly isDegraded?: boolean;
|
|
29
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Whether `localStorage` can be reached at all.
|
|
37
|
+
*
|
|
38
|
+
* Deliberately *not* a write probe. Writability is not the right question here,
|
|
39
|
+
* because `LocalStorageStore` degrades per operation when a write fails, so a
|
|
40
|
+
* storage that reads fine but is at quota is still worth using: it is holding the
|
|
41
|
+
* buyer's existing session, and refusing it would throw away a perfectly readable
|
|
42
|
+
* sign-in and send them back to the login form. The only thing that makes
|
|
43
|
+
* `localStorage` unusable is not being able to touch it, which is what Firefox
|
|
44
|
+
* does (`SecurityError` from the property getter) when the buyer has blocked
|
|
45
|
+
* cookies and site data. That is why even the truthiness check sits inside a
|
|
46
|
+
* `try`.
|
|
47
|
+
*/
|
|
48
|
+
declare function isLocalStorageAvailable(): boolean;
|
|
49
|
+
/**
|
|
50
|
+
* `localStorage`-backed store that degrades to memory instead of throwing.
|
|
51
|
+
*
|
|
52
|
+
* Storage can start failing at any point in a session, not just at construction:
|
|
53
|
+
* the origin hits quota, an extension clears site data, the profile's storage
|
|
54
|
+
* file goes corrupt. Since this is where the auth tokens live, an unhandled
|
|
55
|
+
* throw here surfaces as a failed sign-in for a buyer whose credentials were
|
|
56
|
+
* accepted. So every operation falls back to an in-memory copy. The session
|
|
57
|
+
* keeps working; it just stops surviving a reload.
|
|
58
|
+
*/
|
|
30
59
|
declare class LocalStorageStore implements IStore {
|
|
60
|
+
private readonly onDegraded?;
|
|
61
|
+
private readonly fallback;
|
|
62
|
+
private degraded;
|
|
63
|
+
constructor(onDegraded?: ((err: unknown) => void) | undefined);
|
|
64
|
+
get isDegraded(): boolean;
|
|
31
65
|
get<TItem>(key: string): TItem | null;
|
|
32
66
|
set<TItem>(key: string, item: TItem): void;
|
|
33
67
|
remove(key: string): void;
|
|
34
68
|
listen<TItem>(key: string, callback: (item: TItem | null) => void): void;
|
|
69
|
+
private reportDegraded;
|
|
35
70
|
}
|
|
36
71
|
declare class InMemoryStore implements IStore {
|
|
37
72
|
private readonly store;
|
|
@@ -47,6 +82,7 @@ type IdentityWithExpiry = Identity & {
|
|
|
47
82
|
};
|
|
48
83
|
interface ITokenStore {
|
|
49
84
|
get user(): User | null;
|
|
85
|
+
get isPersistent(): boolean;
|
|
50
86
|
getAccessToken: () => Promise<string | null>;
|
|
51
87
|
setUser: (user: Identity | User) => User | null;
|
|
52
88
|
clear: () => void;
|
|
@@ -57,15 +93,26 @@ declare class TokenStore implements ITokenStore {
|
|
|
57
93
|
private refreshTimeoutId;
|
|
58
94
|
private refreshPromise;
|
|
59
95
|
private readonly store;
|
|
96
|
+
private readonly logger;
|
|
60
97
|
static readonly storageKey = "moonbase_auth";
|
|
61
98
|
constructor(configuration: MoonbaseConfiguration);
|
|
62
99
|
get user(): User | null;
|
|
100
|
+
get isPersistent(): boolean;
|
|
63
101
|
getAccessToken(): Promise<string | null>;
|
|
64
102
|
setUser(user: Identity | User): IdentityWithExpiry | null;
|
|
65
103
|
clear(): null;
|
|
66
104
|
private get isExpired();
|
|
105
|
+
/**
|
|
106
|
+
* Start a refresh and hold it so concurrent callers join it rather than firing
|
|
107
|
+
* their own. The slot is cleared as soon as it settles: a retained promise
|
|
108
|
+
* would hand every later expiry the same stale access token, and a rejected one
|
|
109
|
+
* that stayed put would re-throw on every subsequent request (including the
|
|
110
|
+
* anonymous ones) for the rest of the session.
|
|
111
|
+
*/
|
|
112
|
+
private startRefresh;
|
|
67
113
|
private refreshTokens;
|
|
68
114
|
private handleStorageUpdate;
|
|
115
|
+
private warnAboutStorage;
|
|
69
116
|
}
|
|
70
117
|
|
|
71
118
|
type HttpMethods = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
|
|
@@ -1076,6 +1123,13 @@ interface SubscribeInput {
|
|
|
1076
1123
|
name?: string | null;
|
|
1077
1124
|
newsletter?: boolean;
|
|
1078
1125
|
productUpdates?: boolean;
|
|
1126
|
+
/**
|
|
1127
|
+
* Merchant-owned customer groups to enroll the subscriber into. Applied on
|
|
1128
|
+
* the same terms as the opt-in beside them: on a tenant that requires double
|
|
1129
|
+
* opt-in they land when the confirmation link is clicked, not here. Groups the
|
|
1130
|
+
* merchant has not opened to public sign-ups are skipped in silence.
|
|
1131
|
+
*/
|
|
1132
|
+
groupIds?: string[];
|
|
1079
1133
|
}
|
|
1080
1134
|
interface CommunicationPreferencesInput {
|
|
1081
1135
|
newsletter: boolean;
|
|
@@ -1092,6 +1146,29 @@ declare class CommunicationsEndpoints {
|
|
|
1092
1146
|
unsubscribe(email: string, token: string): Promise<void>;
|
|
1093
1147
|
}
|
|
1094
1148
|
|
|
1149
|
+
declare class GroupEndpoints {
|
|
1150
|
+
private api;
|
|
1151
|
+
constructor(api: MoonbaseApi);
|
|
1152
|
+
/**
|
|
1153
|
+
* Joins the signed-in customer to a merchant-owned group that has been opened
|
|
1154
|
+
* to public sign-ups.
|
|
1155
|
+
*
|
|
1156
|
+
* The one group call that reports failure. Group IDs sent along with sign-up,
|
|
1157
|
+
* a newsletter subscription or a checkout are passengers on a request that
|
|
1158
|
+
* wanted something else, so an unusable one is skipped rather than allowed to
|
|
1159
|
+
* fail the account, the subscription or the sale. Here joining *is* the
|
|
1160
|
+
* request, so it throws.
|
|
1161
|
+
*
|
|
1162
|
+
* Every reason it can fail (no such group, deleted, closed, a segment, a
|
|
1163
|
+
* system group, another tenant's) arrives as the same `NotFoundError`, so
|
|
1164
|
+
* that a customer of one tenant can't enumerate that merchant's group IDs.
|
|
1165
|
+
*
|
|
1166
|
+
* Idempotent: joining a group the customer is already in succeeds and changes
|
|
1167
|
+
* nothing.
|
|
1168
|
+
*/
|
|
1169
|
+
join(groupId: string): Promise<void>;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1095
1172
|
declare const addressSchema: z.ZodObject<{
|
|
1096
1173
|
countryCode: z.ZodString;
|
|
1097
1174
|
streetAddress1: z.ZodString;
|
|
@@ -1584,6 +1661,17 @@ type SignUpResult = {
|
|
|
1584
1661
|
status: 'activation-pending';
|
|
1585
1662
|
email: string;
|
|
1586
1663
|
};
|
|
1664
|
+
interface SignUpOptions {
|
|
1665
|
+
/**
|
|
1666
|
+
* Merchant-owned customer groups to enroll the new account into.
|
|
1667
|
+
*
|
|
1668
|
+
* Only groups the merchant has opened to public sign-ups are applied, and the
|
|
1669
|
+
* response does not say which ones were: an ID naming a group that is closed,
|
|
1670
|
+
* deleted, rule-driven or another tenant's is skipped in silence, so a stale
|
|
1671
|
+
* ID left in a form can never fail a registration.
|
|
1672
|
+
*/
|
|
1673
|
+
groupIds?: string[];
|
|
1674
|
+
}
|
|
1587
1675
|
|
|
1588
1676
|
declare class IdentityEndpoints {
|
|
1589
1677
|
private api;
|
|
@@ -1592,7 +1680,7 @@ declare class IdentityEndpoints {
|
|
|
1592
1680
|
constructor(api: MoonbaseApi, tokenStore: ITokenStore, logger: ILogger);
|
|
1593
1681
|
get(): Promise<User>;
|
|
1594
1682
|
signIn(email: string, password: string): Promise<User>;
|
|
1595
|
-
signUp(name: string, email: string, password: string, address: Address | null | undefined, acceptedPrivacyPolicy: boolean, acceptedTermsAndConditions: boolean, communicationOptIn?: boolean): Promise<SignUpResult>;
|
|
1683
|
+
signUp(name: string, email: string, password: string, address: Address | null | undefined, acceptedPrivacyPolicy: boolean, acceptedTermsAndConditions: boolean, communicationOptIn?: boolean, options?: SignUpOptions): Promise<SignUpResult>;
|
|
1596
1684
|
signOut(): Promise<void>;
|
|
1597
1685
|
update(name: string | null, email: string, emailConfirmationToken?: string, communicationPreferences?: CommunicationPreferences): Promise<{
|
|
1598
1686
|
needsEmailConfirmationToken: boolean;
|
|
@@ -24142,6 +24230,22 @@ declare class OrderEndpoints {
|
|
|
24142
24230
|
addCouponCode(orderId: string, code: string): Promise<OpenOrder>;
|
|
24143
24231
|
removeCouponCode(orderId: string, code: string): Promise<OpenOrder>;
|
|
24144
24232
|
clearCouponCodes(orderId: string): Promise<OpenOrder>;
|
|
24233
|
+
/**
|
|
24234
|
+
* Records merchant-owned customer groups the buyer should join once this order
|
|
24235
|
+
* completes. The order has to exist server-side first, so call it after a
|
|
24236
|
+
* `pushContent`, and before the payment rather than after: the enrollment
|
|
24237
|
+
* rides on the order until the payment webhook settles it.
|
|
24238
|
+
*
|
|
24239
|
+
* Additive rather than a replacement: each call adds to what the order already
|
|
24240
|
+
* carries, and the accumulated set is capped server-side. Groups the merchant
|
|
24241
|
+
* has not opened to public sign-ups are skipped in silence, so a stale ID in a
|
|
24242
|
+
* checkout form can never fail a sale.
|
|
24243
|
+
*
|
|
24244
|
+
* Kept apart from the marketing opt-in on purpose: a checkout can offer both
|
|
24245
|
+
* independently, and merging them would opt a buyer who only asked to join a
|
|
24246
|
+
* list into email they never asked for.
|
|
24247
|
+
*/
|
|
24248
|
+
joinGroups(orderId: string, groupIds: string[]): Promise<OpenOrder>;
|
|
24145
24249
|
}
|
|
24146
24250
|
|
|
24147
24251
|
declare class StorefrontEndpoints {
|
|
@@ -46885,6 +46989,26 @@ declare class MoonbaseError extends Error {
|
|
|
46885
46989
|
constructor(title: string, detail: string | undefined, status?: number | undefined, errors?: Record<string, string> | undefined);
|
|
46886
46990
|
}
|
|
46887
46991
|
|
|
46992
|
+
/**
|
|
46993
|
+
* Cleans up merchant-authored group IDs before they go on the wire.
|
|
46994
|
+
*
|
|
46995
|
+
* Deliberately normalizes without validating. The server sanitizes these itself,
|
|
46996
|
+
* dropping IDs that are too long, malformed, closed, or belonging to another
|
|
46997
|
+
* tenant, and capping the request at five. It does so in silence, so the
|
|
46998
|
+
* endpoints can't be used to enumerate a tenant's groups. Re-implementing those
|
|
46999
|
+
* rules here would only mean a merchant's typo disappears in the one place they
|
|
47000
|
+
* can't see it, so anything that survives trimming is sent and the server has
|
|
47001
|
+
* the last word.
|
|
47002
|
+
*
|
|
47003
|
+
* Lowercasing is the exception, and it earns its place by making the de-dupe
|
|
47004
|
+
* correct: group IDs are lowercase server-side, so `VIP` and `vip` are one
|
|
47005
|
+
* group and should only cost one of the five slots.
|
|
47006
|
+
*
|
|
47007
|
+
* Returns `undefined` rather than `[]` when nothing survives, so the field is
|
|
47008
|
+
* omitted from the request body entirely for every caller not using this.
|
|
47009
|
+
*/
|
|
47010
|
+
declare function normalizeGroupIds(groupIds?: readonly string[] | null): string[] | undefined;
|
|
47011
|
+
|
|
46888
47012
|
/**
|
|
46889
47013
|
* Builds URLs for on-the-fly renditions of images stored in Moonbase's asset CDN.
|
|
46890
47014
|
*
|
|
@@ -47025,6 +47149,7 @@ declare class MoonbaseClient {
|
|
|
47025
47149
|
storefront: StorefrontEndpoints;
|
|
47026
47150
|
identity: IdentityEndpoints;
|
|
47027
47151
|
communications: CommunicationsEndpoints;
|
|
47152
|
+
groups: GroupEndpoints;
|
|
47028
47153
|
vouchers: VoucherEndpoints;
|
|
47029
47154
|
activationRequests: ActivationRequestEndpoints;
|
|
47030
47155
|
inventory: InventoryEndpoints;
|
|
@@ -47032,4 +47157,4 @@ declare class MoonbaseClient {
|
|
|
47032
47157
|
orders: OrderEndpoints;
|
|
47033
47158
|
}
|
|
47034
47159
|
|
|
47035
|
-
export { type Activation, ActivationMethod, type ActivationRequest, ActivationRequestFulfillmentType, ActivationRequestStatus, ActivationStatus, type Address, Architecture, type BundleLineItem, type CartCatalog, type CartOrder, type CartTotal, type CartTotalCondition, CartUtils, type CommunicationPreferences, type CommunicationPreferencesInput, type CommunicationPreferencesView, type CompletedOrder, ConnectableAccountProvider, type ConnectedAccount, ConsoleLogger, CycleLength, type Discount, DiscountUtils, type Download, type DownloadManifest, type ILogger, type IRecurrence, type IStore, type ITokenStore, type Identity, type ImageFit, type ImageFormat, type ImageTransform, ImageUtils, InMemoryStore, type License, LicenseStatus, type LineItem, type LineItemTotal, LocalStorageStore, LogLevel, MarketingConsentType, type MilestoneProgress, type Money, type MoneyCollection, MoneyCollectionUtils, MoonbaseApi, MoonbaseClient, type MoonbaseConfiguration, MoonbaseError, NotAuthenticatedError, NotAuthorizedError, NotFoundError, type OfferCondition, type OfferConditionContext, type OfferProgress, type OfferScope, OfferUtils, type OpenOrder, type Order, OrderStatus, type OrderTotal, type OwnedProduct, type Page, Platform, type PricingTier, type PricingVariation, type ProblemDetails, type ProductLineItem, type PromotionDisplay, type Quantifiable, type RawPropertyArray, type RawPropertyObject, type RawPropertyValue, type SignUpResult, type SingleOrMultiple, type Storefront, type StorefrontBundle, type StorefrontOffer, type StorefrontOfferTarget, type StorefrontProduct, type StorefrontPromotion, type StorefrontPromotionCta, type StorefrontPromotionTarget, type SubscribeInput, type SubscribeResponse, type Subscription, SubscriptionStatus, type TaxEstimate, TokenStore, type UrchinTrackingModule, type User, type UserAccountConfirmed, type UserAccountConfirmedStatus, type Vendor, type Voucher, imageWidths, isPromotionDisplay, isTransformableImageUrl, maxImageDimension, objectToQuery, parseOffSiteReferrer, problemDetailsSchema, promotionDisplays, resolveUtm, schemas, utmToObject };
|
|
47160
|
+
export { type Activation, ActivationMethod, type ActivationRequest, ActivationRequestFulfillmentType, ActivationRequestStatus, ActivationStatus, type Address, Architecture, type BundleLineItem, type CartCatalog, type CartOrder, type CartTotal, type CartTotalCondition, CartUtils, type CommunicationPreferences, type CommunicationPreferencesInput, type CommunicationPreferencesView, type CompletedOrder, ConnectableAccountProvider, type ConnectedAccount, ConsoleLogger, CycleLength, type Discount, DiscountUtils, type Download, type DownloadManifest, type ILogger, type IRecurrence, type IStore, type ITokenStore, type Identity, type ImageFit, type ImageFormat, type ImageTransform, ImageUtils, InMemoryStore, type License, LicenseStatus, type LineItem, type LineItemTotal, LocalStorageStore, LogLevel, MarketingConsentType, type MilestoneProgress, type Money, type MoneyCollection, MoneyCollectionUtils, MoonbaseApi, MoonbaseClient, type MoonbaseConfiguration, MoonbaseError, NotAuthenticatedError, NotAuthorizedError, NotFoundError, type OfferCondition, type OfferConditionContext, type OfferProgress, type OfferScope, OfferUtils, type OpenOrder, type Order, OrderStatus, type OrderTotal, type OwnedProduct, type Page, Platform, type PricingTier, type PricingVariation, type ProblemDetails, type ProductLineItem, type PromotionDisplay, type Quantifiable, type RawPropertyArray, type RawPropertyObject, type RawPropertyValue, type SignUpOptions, type SignUpResult, type SingleOrMultiple, type Storefront, type StorefrontBundle, type StorefrontOffer, type StorefrontOfferTarget, type StorefrontProduct, type StorefrontPromotion, type StorefrontPromotionCta, type StorefrontPromotionTarget, type SubscribeInput, type SubscribeResponse, type Subscription, SubscriptionStatus, type TaxEstimate, TokenStore, type UrchinTrackingModule, type User, type UserAccountConfirmed, type UserAccountConfirmedStatus, type Vendor, type Voucher, imageWidths, isLocalStorageAvailable, isPromotionDisplay, isTransformableImageUrl, maxImageDimension, normalizeGroupIds, objectToQuery, parseOffSiteReferrer, problemDetailsSchema, promotionDisplays, resolveUtm, schemas, utmToObject };
|
package/dist/index.d.ts
CHANGED
|
@@ -26,12 +26,47 @@ interface IStore {
|
|
|
26
26
|
set: <TItem>(key: string, item: TItem) => void;
|
|
27
27
|
remove: (key: string) => void;
|
|
28
28
|
listen: <TItem>(key: string, callback: (item: TItem | null) => void) => void;
|
|
29
|
+
/**
|
|
30
|
+
* True once any storage operation has failed and the store has fallen back to
|
|
31
|
+
* memory. Optional so custom stores don't have to implement it.
|
|
32
|
+
*/
|
|
33
|
+
readonly isDegraded?: boolean;
|
|
29
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Whether `localStorage` can be reached at all.
|
|
37
|
+
*
|
|
38
|
+
* Deliberately *not* a write probe. Writability is not the right question here,
|
|
39
|
+
* because `LocalStorageStore` degrades per operation when a write fails, so a
|
|
40
|
+
* storage that reads fine but is at quota is still worth using: it is holding the
|
|
41
|
+
* buyer's existing session, and refusing it would throw away a perfectly readable
|
|
42
|
+
* sign-in and send them back to the login form. The only thing that makes
|
|
43
|
+
* `localStorage` unusable is not being able to touch it, which is what Firefox
|
|
44
|
+
* does (`SecurityError` from the property getter) when the buyer has blocked
|
|
45
|
+
* cookies and site data. That is why even the truthiness check sits inside a
|
|
46
|
+
* `try`.
|
|
47
|
+
*/
|
|
48
|
+
declare function isLocalStorageAvailable(): boolean;
|
|
49
|
+
/**
|
|
50
|
+
* `localStorage`-backed store that degrades to memory instead of throwing.
|
|
51
|
+
*
|
|
52
|
+
* Storage can start failing at any point in a session, not just at construction:
|
|
53
|
+
* the origin hits quota, an extension clears site data, the profile's storage
|
|
54
|
+
* file goes corrupt. Since this is where the auth tokens live, an unhandled
|
|
55
|
+
* throw here surfaces as a failed sign-in for a buyer whose credentials were
|
|
56
|
+
* accepted. So every operation falls back to an in-memory copy. The session
|
|
57
|
+
* keeps working; it just stops surviving a reload.
|
|
58
|
+
*/
|
|
30
59
|
declare class LocalStorageStore implements IStore {
|
|
60
|
+
private readonly onDegraded?;
|
|
61
|
+
private readonly fallback;
|
|
62
|
+
private degraded;
|
|
63
|
+
constructor(onDegraded?: ((err: unknown) => void) | undefined);
|
|
64
|
+
get isDegraded(): boolean;
|
|
31
65
|
get<TItem>(key: string): TItem | null;
|
|
32
66
|
set<TItem>(key: string, item: TItem): void;
|
|
33
67
|
remove(key: string): void;
|
|
34
68
|
listen<TItem>(key: string, callback: (item: TItem | null) => void): void;
|
|
69
|
+
private reportDegraded;
|
|
35
70
|
}
|
|
36
71
|
declare class InMemoryStore implements IStore {
|
|
37
72
|
private readonly store;
|
|
@@ -47,6 +82,7 @@ type IdentityWithExpiry = Identity & {
|
|
|
47
82
|
};
|
|
48
83
|
interface ITokenStore {
|
|
49
84
|
get user(): User | null;
|
|
85
|
+
get isPersistent(): boolean;
|
|
50
86
|
getAccessToken: () => Promise<string | null>;
|
|
51
87
|
setUser: (user: Identity | User) => User | null;
|
|
52
88
|
clear: () => void;
|
|
@@ -57,15 +93,26 @@ declare class TokenStore implements ITokenStore {
|
|
|
57
93
|
private refreshTimeoutId;
|
|
58
94
|
private refreshPromise;
|
|
59
95
|
private readonly store;
|
|
96
|
+
private readonly logger;
|
|
60
97
|
static readonly storageKey = "moonbase_auth";
|
|
61
98
|
constructor(configuration: MoonbaseConfiguration);
|
|
62
99
|
get user(): User | null;
|
|
100
|
+
get isPersistent(): boolean;
|
|
63
101
|
getAccessToken(): Promise<string | null>;
|
|
64
102
|
setUser(user: Identity | User): IdentityWithExpiry | null;
|
|
65
103
|
clear(): null;
|
|
66
104
|
private get isExpired();
|
|
105
|
+
/**
|
|
106
|
+
* Start a refresh and hold it so concurrent callers join it rather than firing
|
|
107
|
+
* their own. The slot is cleared as soon as it settles: a retained promise
|
|
108
|
+
* would hand every later expiry the same stale access token, and a rejected one
|
|
109
|
+
* that stayed put would re-throw on every subsequent request (including the
|
|
110
|
+
* anonymous ones) for the rest of the session.
|
|
111
|
+
*/
|
|
112
|
+
private startRefresh;
|
|
67
113
|
private refreshTokens;
|
|
68
114
|
private handleStorageUpdate;
|
|
115
|
+
private warnAboutStorage;
|
|
69
116
|
}
|
|
70
117
|
|
|
71
118
|
type HttpMethods = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
|
|
@@ -1076,6 +1123,13 @@ interface SubscribeInput {
|
|
|
1076
1123
|
name?: string | null;
|
|
1077
1124
|
newsletter?: boolean;
|
|
1078
1125
|
productUpdates?: boolean;
|
|
1126
|
+
/**
|
|
1127
|
+
* Merchant-owned customer groups to enroll the subscriber into. Applied on
|
|
1128
|
+
* the same terms as the opt-in beside them: on a tenant that requires double
|
|
1129
|
+
* opt-in they land when the confirmation link is clicked, not here. Groups the
|
|
1130
|
+
* merchant has not opened to public sign-ups are skipped in silence.
|
|
1131
|
+
*/
|
|
1132
|
+
groupIds?: string[];
|
|
1079
1133
|
}
|
|
1080
1134
|
interface CommunicationPreferencesInput {
|
|
1081
1135
|
newsletter: boolean;
|
|
@@ -1092,6 +1146,29 @@ declare class CommunicationsEndpoints {
|
|
|
1092
1146
|
unsubscribe(email: string, token: string): Promise<void>;
|
|
1093
1147
|
}
|
|
1094
1148
|
|
|
1149
|
+
declare class GroupEndpoints {
|
|
1150
|
+
private api;
|
|
1151
|
+
constructor(api: MoonbaseApi);
|
|
1152
|
+
/**
|
|
1153
|
+
* Joins the signed-in customer to a merchant-owned group that has been opened
|
|
1154
|
+
* to public sign-ups.
|
|
1155
|
+
*
|
|
1156
|
+
* The one group call that reports failure. Group IDs sent along with sign-up,
|
|
1157
|
+
* a newsletter subscription or a checkout are passengers on a request that
|
|
1158
|
+
* wanted something else, so an unusable one is skipped rather than allowed to
|
|
1159
|
+
* fail the account, the subscription or the sale. Here joining *is* the
|
|
1160
|
+
* request, so it throws.
|
|
1161
|
+
*
|
|
1162
|
+
* Every reason it can fail (no such group, deleted, closed, a segment, a
|
|
1163
|
+
* system group, another tenant's) arrives as the same `NotFoundError`, so
|
|
1164
|
+
* that a customer of one tenant can't enumerate that merchant's group IDs.
|
|
1165
|
+
*
|
|
1166
|
+
* Idempotent: joining a group the customer is already in succeeds and changes
|
|
1167
|
+
* nothing.
|
|
1168
|
+
*/
|
|
1169
|
+
join(groupId: string): Promise<void>;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1095
1172
|
declare const addressSchema: z.ZodObject<{
|
|
1096
1173
|
countryCode: z.ZodString;
|
|
1097
1174
|
streetAddress1: z.ZodString;
|
|
@@ -1584,6 +1661,17 @@ type SignUpResult = {
|
|
|
1584
1661
|
status: 'activation-pending';
|
|
1585
1662
|
email: string;
|
|
1586
1663
|
};
|
|
1664
|
+
interface SignUpOptions {
|
|
1665
|
+
/**
|
|
1666
|
+
* Merchant-owned customer groups to enroll the new account into.
|
|
1667
|
+
*
|
|
1668
|
+
* Only groups the merchant has opened to public sign-ups are applied, and the
|
|
1669
|
+
* response does not say which ones were: an ID naming a group that is closed,
|
|
1670
|
+
* deleted, rule-driven or another tenant's is skipped in silence, so a stale
|
|
1671
|
+
* ID left in a form can never fail a registration.
|
|
1672
|
+
*/
|
|
1673
|
+
groupIds?: string[];
|
|
1674
|
+
}
|
|
1587
1675
|
|
|
1588
1676
|
declare class IdentityEndpoints {
|
|
1589
1677
|
private api;
|
|
@@ -1592,7 +1680,7 @@ declare class IdentityEndpoints {
|
|
|
1592
1680
|
constructor(api: MoonbaseApi, tokenStore: ITokenStore, logger: ILogger);
|
|
1593
1681
|
get(): Promise<User>;
|
|
1594
1682
|
signIn(email: string, password: string): Promise<User>;
|
|
1595
|
-
signUp(name: string, email: string, password: string, address: Address | null | undefined, acceptedPrivacyPolicy: boolean, acceptedTermsAndConditions: boolean, communicationOptIn?: boolean): Promise<SignUpResult>;
|
|
1683
|
+
signUp(name: string, email: string, password: string, address: Address | null | undefined, acceptedPrivacyPolicy: boolean, acceptedTermsAndConditions: boolean, communicationOptIn?: boolean, options?: SignUpOptions): Promise<SignUpResult>;
|
|
1596
1684
|
signOut(): Promise<void>;
|
|
1597
1685
|
update(name: string | null, email: string, emailConfirmationToken?: string, communicationPreferences?: CommunicationPreferences): Promise<{
|
|
1598
1686
|
needsEmailConfirmationToken: boolean;
|
|
@@ -24142,6 +24230,22 @@ declare class OrderEndpoints {
|
|
|
24142
24230
|
addCouponCode(orderId: string, code: string): Promise<OpenOrder>;
|
|
24143
24231
|
removeCouponCode(orderId: string, code: string): Promise<OpenOrder>;
|
|
24144
24232
|
clearCouponCodes(orderId: string): Promise<OpenOrder>;
|
|
24233
|
+
/**
|
|
24234
|
+
* Records merchant-owned customer groups the buyer should join once this order
|
|
24235
|
+
* completes. The order has to exist server-side first, so call it after a
|
|
24236
|
+
* `pushContent`, and before the payment rather than after: the enrollment
|
|
24237
|
+
* rides on the order until the payment webhook settles it.
|
|
24238
|
+
*
|
|
24239
|
+
* Additive rather than a replacement: each call adds to what the order already
|
|
24240
|
+
* carries, and the accumulated set is capped server-side. Groups the merchant
|
|
24241
|
+
* has not opened to public sign-ups are skipped in silence, so a stale ID in a
|
|
24242
|
+
* checkout form can never fail a sale.
|
|
24243
|
+
*
|
|
24244
|
+
* Kept apart from the marketing opt-in on purpose: a checkout can offer both
|
|
24245
|
+
* independently, and merging them would opt a buyer who only asked to join a
|
|
24246
|
+
* list into email they never asked for.
|
|
24247
|
+
*/
|
|
24248
|
+
joinGroups(orderId: string, groupIds: string[]): Promise<OpenOrder>;
|
|
24145
24249
|
}
|
|
24146
24250
|
|
|
24147
24251
|
declare class StorefrontEndpoints {
|
|
@@ -46885,6 +46989,26 @@ declare class MoonbaseError extends Error {
|
|
|
46885
46989
|
constructor(title: string, detail: string | undefined, status?: number | undefined, errors?: Record<string, string> | undefined);
|
|
46886
46990
|
}
|
|
46887
46991
|
|
|
46992
|
+
/**
|
|
46993
|
+
* Cleans up merchant-authored group IDs before they go on the wire.
|
|
46994
|
+
*
|
|
46995
|
+
* Deliberately normalizes without validating. The server sanitizes these itself,
|
|
46996
|
+
* dropping IDs that are too long, malformed, closed, or belonging to another
|
|
46997
|
+
* tenant, and capping the request at five. It does so in silence, so the
|
|
46998
|
+
* endpoints can't be used to enumerate a tenant's groups. Re-implementing those
|
|
46999
|
+
* rules here would only mean a merchant's typo disappears in the one place they
|
|
47000
|
+
* can't see it, so anything that survives trimming is sent and the server has
|
|
47001
|
+
* the last word.
|
|
47002
|
+
*
|
|
47003
|
+
* Lowercasing is the exception, and it earns its place by making the de-dupe
|
|
47004
|
+
* correct: group IDs are lowercase server-side, so `VIP` and `vip` are one
|
|
47005
|
+
* group and should only cost one of the five slots.
|
|
47006
|
+
*
|
|
47007
|
+
* Returns `undefined` rather than `[]` when nothing survives, so the field is
|
|
47008
|
+
* omitted from the request body entirely for every caller not using this.
|
|
47009
|
+
*/
|
|
47010
|
+
declare function normalizeGroupIds(groupIds?: readonly string[] | null): string[] | undefined;
|
|
47011
|
+
|
|
46888
47012
|
/**
|
|
46889
47013
|
* Builds URLs for on-the-fly renditions of images stored in Moonbase's asset CDN.
|
|
46890
47014
|
*
|
|
@@ -47025,6 +47149,7 @@ declare class MoonbaseClient {
|
|
|
47025
47149
|
storefront: StorefrontEndpoints;
|
|
47026
47150
|
identity: IdentityEndpoints;
|
|
47027
47151
|
communications: CommunicationsEndpoints;
|
|
47152
|
+
groups: GroupEndpoints;
|
|
47028
47153
|
vouchers: VoucherEndpoints;
|
|
47029
47154
|
activationRequests: ActivationRequestEndpoints;
|
|
47030
47155
|
inventory: InventoryEndpoints;
|
|
@@ -47032,4 +47157,4 @@ declare class MoonbaseClient {
|
|
|
47032
47157
|
orders: OrderEndpoints;
|
|
47033
47158
|
}
|
|
47034
47159
|
|
|
47035
|
-
export { type Activation, ActivationMethod, type ActivationRequest, ActivationRequestFulfillmentType, ActivationRequestStatus, ActivationStatus, type Address, Architecture, type BundleLineItem, type CartCatalog, type CartOrder, type CartTotal, type CartTotalCondition, CartUtils, type CommunicationPreferences, type CommunicationPreferencesInput, type CommunicationPreferencesView, type CompletedOrder, ConnectableAccountProvider, type ConnectedAccount, ConsoleLogger, CycleLength, type Discount, DiscountUtils, type Download, type DownloadManifest, type ILogger, type IRecurrence, type IStore, type ITokenStore, type Identity, type ImageFit, type ImageFormat, type ImageTransform, ImageUtils, InMemoryStore, type License, LicenseStatus, type LineItem, type LineItemTotal, LocalStorageStore, LogLevel, MarketingConsentType, type MilestoneProgress, type Money, type MoneyCollection, MoneyCollectionUtils, MoonbaseApi, MoonbaseClient, type MoonbaseConfiguration, MoonbaseError, NotAuthenticatedError, NotAuthorizedError, NotFoundError, type OfferCondition, type OfferConditionContext, type OfferProgress, type OfferScope, OfferUtils, type OpenOrder, type Order, OrderStatus, type OrderTotal, type OwnedProduct, type Page, Platform, type PricingTier, type PricingVariation, type ProblemDetails, type ProductLineItem, type PromotionDisplay, type Quantifiable, type RawPropertyArray, type RawPropertyObject, type RawPropertyValue, type SignUpResult, type SingleOrMultiple, type Storefront, type StorefrontBundle, type StorefrontOffer, type StorefrontOfferTarget, type StorefrontProduct, type StorefrontPromotion, type StorefrontPromotionCta, type StorefrontPromotionTarget, type SubscribeInput, type SubscribeResponse, type Subscription, SubscriptionStatus, type TaxEstimate, TokenStore, type UrchinTrackingModule, type User, type UserAccountConfirmed, type UserAccountConfirmedStatus, type Vendor, type Voucher, imageWidths, isPromotionDisplay, isTransformableImageUrl, maxImageDimension, objectToQuery, parseOffSiteReferrer, problemDetailsSchema, promotionDisplays, resolveUtm, schemas, utmToObject };
|
|
47160
|
+
export { type Activation, ActivationMethod, type ActivationRequest, ActivationRequestFulfillmentType, ActivationRequestStatus, ActivationStatus, type Address, Architecture, type BundleLineItem, type CartCatalog, type CartOrder, type CartTotal, type CartTotalCondition, CartUtils, type CommunicationPreferences, type CommunicationPreferencesInput, type CommunicationPreferencesView, type CompletedOrder, ConnectableAccountProvider, type ConnectedAccount, ConsoleLogger, CycleLength, type Discount, DiscountUtils, type Download, type DownloadManifest, type ILogger, type IRecurrence, type IStore, type ITokenStore, type Identity, type ImageFit, type ImageFormat, type ImageTransform, ImageUtils, InMemoryStore, type License, LicenseStatus, type LineItem, type LineItemTotal, LocalStorageStore, LogLevel, MarketingConsentType, type MilestoneProgress, type Money, type MoneyCollection, MoneyCollectionUtils, MoonbaseApi, MoonbaseClient, type MoonbaseConfiguration, MoonbaseError, NotAuthenticatedError, NotAuthorizedError, NotFoundError, type OfferCondition, type OfferConditionContext, type OfferProgress, type OfferScope, OfferUtils, type OpenOrder, type Order, OrderStatus, type OrderTotal, type OwnedProduct, type Page, Platform, type PricingTier, type PricingVariation, type ProblemDetails, type ProductLineItem, type PromotionDisplay, type Quantifiable, type RawPropertyArray, type RawPropertyObject, type RawPropertyValue, type SignUpOptions, type SignUpResult, type SingleOrMultiple, type Storefront, type StorefrontBundle, type StorefrontOffer, type StorefrontOfferTarget, type StorefrontProduct, type StorefrontPromotion, type StorefrontPromotionCta, type StorefrontPromotionTarget, type SubscribeInput, type SubscribeResponse, type Subscription, SubscriptionStatus, type TaxEstimate, TokenStore, type UrchinTrackingModule, type User, type UserAccountConfirmed, type UserAccountConfirmedStatus, type Vendor, type Voucher, imageWidths, isLocalStorageAvailable, isPromotionDisplay, isTransformableImageUrl, maxImageDimension, normalizeGroupIds, objectToQuery, parseOffSiteReferrer, problemDetailsSchema, promotionDisplays, resolveUtm, schemas, utmToObject };
|
package/dist/index.js
CHANGED
|
@@ -431,6 +431,19 @@ var ActivationRequestEndpoints = class {
|
|
|
431
431
|
}
|
|
432
432
|
};
|
|
433
433
|
|
|
434
|
+
// src/utils/groups.ts
|
|
435
|
+
function normalizeGroupIds(groupIds) {
|
|
436
|
+
if (!groupIds)
|
|
437
|
+
return void 0;
|
|
438
|
+
const normalized = [];
|
|
439
|
+
for (const groupId of groupIds) {
|
|
440
|
+
const trimmed = groupId == null ? void 0 : groupId.trim().toLowerCase();
|
|
441
|
+
if (trimmed && !normalized.includes(trimmed))
|
|
442
|
+
normalized.push(trimmed);
|
|
443
|
+
}
|
|
444
|
+
return normalized.length > 0 ? normalized : void 0;
|
|
445
|
+
}
|
|
446
|
+
|
|
434
447
|
// src/communications/schemas.ts
|
|
435
448
|
var schemas_exports4 = {};
|
|
436
449
|
__export(schemas_exports4, {
|
|
@@ -464,7 +477,8 @@ var CommunicationsEndpoints = class {
|
|
|
464
477
|
email: input.email,
|
|
465
478
|
name: ((_a = input.name) == null ? void 0 : _a.trim()) || null,
|
|
466
479
|
newsletter: (_b = input.newsletter) != null ? _b : true,
|
|
467
|
-
productUpdates: (_c = input.productUpdates) != null ? _c : true
|
|
480
|
+
productUpdates: (_c = input.productUpdates) != null ? _c : true,
|
|
481
|
+
groupIds: normalizeGroupIds(input.groupIds)
|
|
468
482
|
}
|
|
469
483
|
});
|
|
470
484
|
return response.data;
|
|
@@ -506,6 +520,37 @@ var CommunicationsEndpoints = class {
|
|
|
506
520
|
}
|
|
507
521
|
};
|
|
508
522
|
|
|
523
|
+
// src/groups/endpoints.ts
|
|
524
|
+
var GroupEndpoints = class {
|
|
525
|
+
constructor(api) {
|
|
526
|
+
this.api = api;
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Joins the signed-in customer to a merchant-owned group that has been opened
|
|
530
|
+
* to public sign-ups.
|
|
531
|
+
*
|
|
532
|
+
* The one group call that reports failure. Group IDs sent along with sign-up,
|
|
533
|
+
* a newsletter subscription or a checkout are passengers on a request that
|
|
534
|
+
* wanted something else, so an unusable one is skipped rather than allowed to
|
|
535
|
+
* fail the account, the subscription or the sale. Here joining *is* the
|
|
536
|
+
* request, so it throws.
|
|
537
|
+
*
|
|
538
|
+
* Every reason it can fail (no such group, deleted, closed, a segment, a
|
|
539
|
+
* system group, another tenant's) arrives as the same `NotFoundError`, so
|
|
540
|
+
* that a customer of one tenant can't enumerate that merchant's group IDs.
|
|
541
|
+
*
|
|
542
|
+
* Idempotent: joining a group the customer is already in succeeds and changes
|
|
543
|
+
* nothing.
|
|
544
|
+
*/
|
|
545
|
+
async join(groupId) {
|
|
546
|
+
await this.api.authenticatedFetch(
|
|
547
|
+
`/api/customer/groups/${encodeURIComponent(groupId)}/join`,
|
|
548
|
+
null,
|
|
549
|
+
{ method: "POST" }
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
};
|
|
553
|
+
|
|
509
554
|
// src/utils/errors.ts
|
|
510
555
|
var NotAuthorizedError = class extends Error {
|
|
511
556
|
constructor() {
|
|
@@ -699,17 +744,17 @@ var IdentityEndpoints = class {
|
|
|
699
744
|
throw new NotFoundError("User not found");
|
|
700
745
|
if (response.status >= 400)
|
|
701
746
|
await handleResponseProblem(response, this.logger);
|
|
747
|
+
let identity;
|
|
702
748
|
try {
|
|
703
|
-
|
|
704
|
-
const user = identitySchema.parse(data);
|
|
705
|
-
this.tokenStore.setUser(user);
|
|
706
|
-
return userSchema.parse(user);
|
|
749
|
+
identity = identitySchema.parse(await response.json());
|
|
707
750
|
} catch (err) {
|
|
708
751
|
this.logger.warn("Could not sign in user", { email, response, err });
|
|
709
752
|
throw new MoonbaseError("Bad response", "Could not sign in user", response.status);
|
|
710
753
|
}
|
|
754
|
+
this.tokenStore.setUser(identity);
|
|
755
|
+
return userSchema.parse(identity);
|
|
711
756
|
}
|
|
712
|
-
async signUp(name, email, password, address, acceptedPrivacyPolicy, acceptedTermsAndConditions, communicationOptIn) {
|
|
757
|
+
async signUp(name, email, password, address, acceptedPrivacyPolicy, acceptedTermsAndConditions, communicationOptIn, options) {
|
|
713
758
|
const path = `/api/customer/identity/sign-up?scheme=JWT&communicationOptIn=${communicationOptIn ? "true" : "false"}`;
|
|
714
759
|
const response = await fetch(`${this.api.baseUrl}${path}`, {
|
|
715
760
|
method: "POST",
|
|
@@ -724,7 +769,8 @@ var IdentityEndpoints = class {
|
|
|
724
769
|
password,
|
|
725
770
|
address,
|
|
726
771
|
acceptedPrivacyPolicy,
|
|
727
|
-
acceptedTermsAndConditions
|
|
772
|
+
acceptedTermsAndConditions,
|
|
773
|
+
groupIds: normalizeGroupIds(options == null ? void 0 : options.groupIds)
|
|
728
774
|
})
|
|
729
775
|
});
|
|
730
776
|
if (response.status >= 400)
|
|
@@ -734,14 +780,15 @@ var IdentityEndpoints = class {
|
|
|
734
780
|
const parsed = pendingActivationSchema.parse(data);
|
|
735
781
|
return { status: "activation-pending", email: parsed.email };
|
|
736
782
|
}
|
|
783
|
+
let identity;
|
|
737
784
|
try {
|
|
738
|
-
|
|
739
|
-
this.tokenStore.setUser(identity);
|
|
740
|
-
return { status: "signed-in", user: userSchema.parse(identity) };
|
|
785
|
+
identity = identitySchema.parse(data);
|
|
741
786
|
} catch (err) {
|
|
742
787
|
this.logger.warn("Could not sign up user", { email, response, err });
|
|
743
788
|
throw new MoonbaseError("Bad response", "Could not sign up user", response.status);
|
|
744
789
|
}
|
|
790
|
+
this.tokenStore.setUser(identity);
|
|
791
|
+
return { status: "signed-in", user: userSchema.parse(identity) };
|
|
745
792
|
}
|
|
746
793
|
async signOut() {
|
|
747
794
|
this.tokenStore.clear();
|
|
@@ -1021,6 +1068,15 @@ var MoonbaseApi = class {
|
|
|
1021
1068
|
status: response.status,
|
|
1022
1069
|
duration: finishedAt.getTime() - startedAt.getTime()
|
|
1023
1070
|
});
|
|
1071
|
+
if (response.type === "opaqueredirect" || response.status === 0) {
|
|
1072
|
+
const err = new MoonbaseError(
|
|
1073
|
+
"Unexpected redirect",
|
|
1074
|
+
`The Moonbase API redirected ${path}, which the SDK is not configured to follow`,
|
|
1075
|
+
response.status
|
|
1076
|
+
);
|
|
1077
|
+
this.reportRequestProblem(path, request, response, err);
|
|
1078
|
+
throw err;
|
|
1079
|
+
}
|
|
1024
1080
|
if (response.status >= 400) {
|
|
1025
1081
|
try {
|
|
1026
1082
|
await handleResponseProblem(response, this.logger);
|
|
@@ -1436,6 +1492,28 @@ var OrderEndpoints = class {
|
|
|
1436
1492
|
const response = await this.api.fetch(`/api/customer/orders/${orderId}/coupons`, openOrderSchema, { method: "DELETE" });
|
|
1437
1493
|
return response.data;
|
|
1438
1494
|
}
|
|
1495
|
+
/**
|
|
1496
|
+
* Records merchant-owned customer groups the buyer should join once this order
|
|
1497
|
+
* completes. The order has to exist server-side first, so call it after a
|
|
1498
|
+
* `pushContent`, and before the payment rather than after: the enrollment
|
|
1499
|
+
* rides on the order until the payment webhook settles it.
|
|
1500
|
+
*
|
|
1501
|
+
* Additive rather than a replacement: each call adds to what the order already
|
|
1502
|
+
* carries, and the accumulated set is capped server-side. Groups the merchant
|
|
1503
|
+
* has not opened to public sign-ups are skipped in silence, so a stale ID in a
|
|
1504
|
+
* checkout form can never fail a sale.
|
|
1505
|
+
*
|
|
1506
|
+
* Kept apart from the marketing opt-in on purpose: a checkout can offer both
|
|
1507
|
+
* independently, and merging them would opt a buyer who only asked to join a
|
|
1508
|
+
* list into email they never asked for.
|
|
1509
|
+
*/
|
|
1510
|
+
async joinGroups(orderId, groupIds) {
|
|
1511
|
+
const response = await this.api.fetch(`/api/customer/orders/${orderId}/groups`, openOrderSchema, {
|
|
1512
|
+
method: "POST",
|
|
1513
|
+
body: { groupIds: normalizeGroupIds(groupIds) }
|
|
1514
|
+
});
|
|
1515
|
+
return response.data;
|
|
1516
|
+
}
|
|
1439
1517
|
};
|
|
1440
1518
|
|
|
1441
1519
|
// src/storefront/endpoints.ts
|
|
@@ -1485,28 +1563,81 @@ var ConsoleLogger = class {
|
|
|
1485
1563
|
};
|
|
1486
1564
|
|
|
1487
1565
|
// src/utils/store.ts
|
|
1566
|
+
function isLocalStorageAvailable() {
|
|
1567
|
+
try {
|
|
1568
|
+
return typeof window !== "undefined" && !!window.localStorage;
|
|
1569
|
+
} catch (e) {
|
|
1570
|
+
return false;
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1488
1573
|
var LocalStorageStore = class {
|
|
1574
|
+
constructor(onDegraded) {
|
|
1575
|
+
this.onDegraded = onDegraded;
|
|
1576
|
+
// Values whose write to localStorage failed. Read back in preference to
|
|
1577
|
+
// localStorage so the rest of the session sees what it just wrote.
|
|
1578
|
+
this.fallback = /* @__PURE__ */ new Map();
|
|
1579
|
+
this.degraded = false;
|
|
1580
|
+
}
|
|
1581
|
+
get isDegraded() {
|
|
1582
|
+
return this.degraded;
|
|
1583
|
+
}
|
|
1489
1584
|
get(key) {
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1585
|
+
if (this.fallback.has(key))
|
|
1586
|
+
return this.fallback.get(key);
|
|
1587
|
+
try {
|
|
1588
|
+
const item = localStorage.getItem(key);
|
|
1589
|
+
if (item) {
|
|
1590
|
+
return JSON.parse(item);
|
|
1591
|
+
}
|
|
1592
|
+
return null;
|
|
1593
|
+
} catch (err) {
|
|
1594
|
+
this.reportDegraded(err);
|
|
1595
|
+
return null;
|
|
1493
1596
|
}
|
|
1494
|
-
return null;
|
|
1495
1597
|
}
|
|
1496
1598
|
set(key, item) {
|
|
1497
|
-
|
|
1599
|
+
try {
|
|
1600
|
+
localStorage.setItem(key, JSON.stringify(item));
|
|
1601
|
+
this.fallback.delete(key);
|
|
1602
|
+
} catch (err) {
|
|
1603
|
+
this.reportDegraded(err);
|
|
1604
|
+
this.fallback.set(key, item);
|
|
1605
|
+
}
|
|
1498
1606
|
}
|
|
1499
1607
|
remove(key) {
|
|
1500
|
-
|
|
1608
|
+
try {
|
|
1609
|
+
localStorage.removeItem(key);
|
|
1610
|
+
this.fallback.delete(key);
|
|
1611
|
+
return;
|
|
1612
|
+
} catch (err) {
|
|
1613
|
+
this.reportDegraded(err);
|
|
1614
|
+
}
|
|
1615
|
+
this.fallback.set(key, null);
|
|
1616
|
+
try {
|
|
1617
|
+
localStorage.setItem(key, "");
|
|
1618
|
+
} catch (e) {
|
|
1619
|
+
}
|
|
1501
1620
|
}
|
|
1502
1621
|
listen(key, callback) {
|
|
1503
|
-
if (typeof window
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1622
|
+
if (typeof window === "undefined")
|
|
1623
|
+
return;
|
|
1624
|
+
window.addEventListener("storage", (event) => {
|
|
1625
|
+
if (event.key !== key)
|
|
1626
|
+
return;
|
|
1627
|
+
this.fallback.delete(key);
|
|
1628
|
+
try {
|
|
1629
|
+
callback(event.newValue ? JSON.parse(event.newValue) : null);
|
|
1630
|
+
} catch (err) {
|
|
1631
|
+
console.warn(`Moonbase could not parse the stored value for ${key}`, err);
|
|
1632
|
+
}
|
|
1633
|
+
});
|
|
1634
|
+
}
|
|
1635
|
+
reportDegraded(err) {
|
|
1636
|
+
var _a;
|
|
1637
|
+
if (this.degraded)
|
|
1638
|
+
return;
|
|
1639
|
+
this.degraded = true;
|
|
1640
|
+
(_a = this.onDegraded) == null ? void 0 : _a.call(this, err);
|
|
1510
1641
|
}
|
|
1511
1642
|
};
|
|
1512
1643
|
var InMemoryStore = class {
|
|
@@ -1544,8 +1675,11 @@ var _TokenStore = class _TokenStore {
|
|
|
1544
1675
|
this.tokens = null;
|
|
1545
1676
|
this.refreshTimeoutId = null;
|
|
1546
1677
|
this.refreshPromise = null;
|
|
1547
|
-
var _a;
|
|
1548
|
-
this.
|
|
1678
|
+
var _a, _b;
|
|
1679
|
+
this.logger = (_a = configuration.logger) != null ? _a : new ConsoleLogger();
|
|
1680
|
+
this.store = (_b = configuration.store) != null ? _b : isLocalStorageAvailable() ? new LocalStorageStore((err) => this.warnAboutStorage(err)) : new InMemoryStore();
|
|
1681
|
+
if (typeof window !== "undefined" && !configuration.store && !(this.store instanceof LocalStorageStore))
|
|
1682
|
+
this.warnAboutStorage();
|
|
1549
1683
|
const storedToken = this.store.get(_TokenStore.storageKey);
|
|
1550
1684
|
if (storedToken) {
|
|
1551
1685
|
this.tokens = {
|
|
@@ -1560,18 +1694,17 @@ var _TokenStore = class _TokenStore {
|
|
|
1560
1694
|
return this.tokens;
|
|
1561
1695
|
return null;
|
|
1562
1696
|
}
|
|
1697
|
+
get isPersistent() {
|
|
1698
|
+
return this.store instanceof LocalStorageStore && !this.store.isDegraded;
|
|
1699
|
+
}
|
|
1563
1700
|
async getAccessToken() {
|
|
1564
1701
|
var _a, _b, _c;
|
|
1565
|
-
if (this.isExpired) {
|
|
1566
|
-
|
|
1567
|
-
const tokens2 = await this.refreshPromise;
|
|
1568
|
-
return (_a = tokens2 == null ? void 0 : tokens2.accessToken) != null ? _a : null;
|
|
1569
|
-
}
|
|
1570
|
-
this.refreshPromise = this.refreshTokens();
|
|
1571
|
-
const tokens = await this.refreshPromise;
|
|
1572
|
-
return (_b = tokens == null ? void 0 : tokens.accessToken) != null ? _b : null;
|
|
1702
|
+
if (!this.isExpired) {
|
|
1703
|
+
return ((_a = this.tokens) == null ? void 0 : _a.accessToken) || null;
|
|
1573
1704
|
}
|
|
1574
|
-
|
|
1705
|
+
const pending = (_b = this.refreshPromise) != null ? _b : this.startRefresh();
|
|
1706
|
+
const tokens = await pending;
|
|
1707
|
+
return (_c = tokens == null ? void 0 : tokens.accessToken) != null ? _c : null;
|
|
1575
1708
|
}
|
|
1576
1709
|
setUser(user) {
|
|
1577
1710
|
const identity = user;
|
|
@@ -1596,7 +1729,7 @@ var _TokenStore = class _TokenStore {
|
|
|
1596
1729
|
if (this.refreshTimeoutId != null)
|
|
1597
1730
|
window.clearTimeout(this.refreshTimeoutId);
|
|
1598
1731
|
this.refreshTimeoutId = window.setTimeout(() => {
|
|
1599
|
-
this.
|
|
1732
|
+
this.startRefresh().catch((err) => this.logger.warn("Could not refresh access token", err));
|
|
1600
1733
|
}, 10 * 60 * 1e3);
|
|
1601
1734
|
}
|
|
1602
1735
|
return this.tokens;
|
|
@@ -1611,10 +1744,25 @@ var _TokenStore = class _TokenStore {
|
|
|
1611
1744
|
get isExpired() {
|
|
1612
1745
|
return this.tokens != null && this.tokens.expiresAt < /* @__PURE__ */ new Date();
|
|
1613
1746
|
}
|
|
1747
|
+
/**
|
|
1748
|
+
* Start a refresh and hold it so concurrent callers join it rather than firing
|
|
1749
|
+
* their own. The slot is cleared as soon as it settles: a retained promise
|
|
1750
|
+
* would hand every later expiry the same stale access token, and a rejected one
|
|
1751
|
+
* that stayed put would re-throw on every subsequent request (including the
|
|
1752
|
+
* anonymous ones) for the rest of the session.
|
|
1753
|
+
*/
|
|
1754
|
+
startRefresh() {
|
|
1755
|
+
const pending = this.refreshTokens().finally(() => {
|
|
1756
|
+
if (this.refreshPromise === pending)
|
|
1757
|
+
this.refreshPromise = null;
|
|
1758
|
+
});
|
|
1759
|
+
this.refreshPromise = pending;
|
|
1760
|
+
return pending;
|
|
1761
|
+
}
|
|
1614
1762
|
async refreshTokens() {
|
|
1615
1763
|
if (!this.tokens)
|
|
1616
1764
|
throw new Error("No tokens found to refresh");
|
|
1617
|
-
const response = await fetch(`${this.configuration.endpoint}/api/customer/identity/refresh?token=${this.tokens.refreshToken}`, {
|
|
1765
|
+
const response = await fetch(`${this.configuration.endpoint}/api/customer/identity/refresh?token=${encodeURIComponent(this.tokens.refreshToken)}`, {
|
|
1618
1766
|
method: "POST",
|
|
1619
1767
|
headers: {
|
|
1620
1768
|
"Accept": "application/json",
|
|
@@ -1636,12 +1784,18 @@ var _TokenStore = class _TokenStore {
|
|
|
1636
1784
|
}
|
|
1637
1785
|
handleStorageUpdate(updatedTokens) {
|
|
1638
1786
|
this.tokens = updatedTokens;
|
|
1639
|
-
if (this.refreshTimeoutId != null)
|
|
1787
|
+
if (this.refreshTimeoutId != null && typeof window !== "undefined")
|
|
1640
1788
|
window.clearTimeout(this.refreshTimeoutId);
|
|
1641
1789
|
if (this.tokens) {
|
|
1642
1790
|
this.tokens.expiresAt = new Date(this.tokens.expiresAt);
|
|
1643
1791
|
}
|
|
1644
1792
|
}
|
|
1793
|
+
warnAboutStorage(err) {
|
|
1794
|
+
this.logger.warn(
|
|
1795
|
+
"Moonbase could not persist credentials to localStorage: this browser is blocking or has exhausted site storage. Sign-in will work for this page load but will not survive a reload.",
|
|
1796
|
+
err
|
|
1797
|
+
);
|
|
1798
|
+
}
|
|
1645
1799
|
};
|
|
1646
1800
|
_TokenStore.storageKey = "moonbase_auth";
|
|
1647
1801
|
var TokenStore = _TokenStore;
|
|
@@ -2336,6 +2490,7 @@ var MoonbaseClient = class {
|
|
|
2336
2490
|
this.storefront = new StorefrontEndpoints(this.api, this.configuration);
|
|
2337
2491
|
this.identity = new IdentityEndpoints(this.api, this.tokenStore, this.logger);
|
|
2338
2492
|
this.communications = new CommunicationsEndpoints(this.api);
|
|
2493
|
+
this.groups = new GroupEndpoints(this.api);
|
|
2339
2494
|
this.vouchers = new VoucherEndpoints(this.api);
|
|
2340
2495
|
this.orders = new OrderEndpoints(this.api);
|
|
2341
2496
|
this.inventory = new InventoryEndpoints(this.api, this.configuration);
|
|
@@ -2373,9 +2528,11 @@ export {
|
|
|
2373
2528
|
SubscriptionStatus,
|
|
2374
2529
|
TokenStore,
|
|
2375
2530
|
imageWidths,
|
|
2531
|
+
isLocalStorageAvailable,
|
|
2376
2532
|
isPromotionDisplay,
|
|
2377
2533
|
isTransformableImageUrl,
|
|
2378
2534
|
maxImageDimension,
|
|
2535
|
+
normalizeGroupIds,
|
|
2379
2536
|
objectToQuery,
|
|
2380
2537
|
parseOffSiteReferrer,
|
|
2381
2538
|
problemDetailsSchema,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moonbase.sh/storefront-api",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.3.0",
|
|
5
5
|
"description": "Package to let you build storefronts with Moonbase.sh as payment and delivery provider",
|
|
6
6
|
"author": "Tobias Lønnerød Madsen <m@dsen.tv>",
|
|
7
7
|
"license": "MIT",
|