@replohq/sdk 2.18.2 → 2.19.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/_vendor/replo-utils/lib/crypto.d.ts +5 -0
- package/_vendor/schemas/generated/consent.d.ts +13 -1
- package/analytics/analytics-provider.js +3 -1
- package/analytics/get-analytics-sinks.js +7 -4
- package/analytics/replo-pixel-script.d.ts +5 -5
- package/analytics/replo-pixel-script.js +23 -22
- package/cart/cart-actions.js +42 -13
- package/cart/checkout-actions.js +1 -1
- package/cart/checkout-session-actions.js +2 -2
- package/cart/gateways/cart-gateway.d.ts +29 -8
- package/cart/gateways/cookie-cart-gateway.d.ts +4 -2
- package/cart/gateways/cookie-cart-gateway.js +4 -4
- package/cart/gateways/resolver-cart-gateway.js +28 -16
- package/cart/gateways/stripe-cart-gateway.js +1 -1
- package/cart/start-stripe-checkout.js +1 -1
- package/consent/consent-platform.d.ts +20 -5
- package/consent/consent-platform.js +94 -11
- package/consent/consent-store.d.ts +7 -2
- package/consent/consent-store.js +16 -4
- package/consent/inject-script-descriptors.d.ts +8 -1
- package/consent/inject-script-descriptors.js +23 -0
- package/consent/replo-scripts.d.ts +6 -2
- package/consent/replo-scripts.js +35 -27
- package/consent/script-registration-store.d.ts +2 -0
- package/consent/script-snippets.d.ts +1 -1
- package/consent/types.d.ts +18 -4
- package/consent/usercentrics-consent.d.ts +15 -0
- package/consent/usercentrics-consent.js +96 -0
- package/consent/window-api.js +1 -1
- package/gateways/canopy-api.d.ts +21 -15
- package/gateways/canopy-api.js +105 -54
- package/lib/buildMetadata.js +3 -3
- package/lib/buyer-ip.d.ts +6 -0
- package/lib/buyer-ip.js +7 -0
- package/loaders/invoke-loader-action.js +6 -1
- package/loaders/invoke-loader-server.js +5 -1
- package/loaders/invoke-loader.js +5 -1
- package/loaders/loader-output.d.ts +2 -1
- package/loaders/loader-output.js +7 -3
- package/package.json +4 -4
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import * as v from "valibot";
|
|
2
|
+
const usercentricsDetailsSchema = v.object({
|
|
3
|
+
consent: v.optional(v.object({ type: v.optional(v.string()) })),
|
|
4
|
+
services: v.record(
|
|
5
|
+
v.string(),
|
|
6
|
+
v.object({
|
|
7
|
+
name: v.string(),
|
|
8
|
+
essential: v.optional(v.boolean()),
|
|
9
|
+
consent: v.object({ given: v.boolean() })
|
|
10
|
+
})
|
|
11
|
+
)
|
|
12
|
+
});
|
|
13
|
+
const usercentricsBaseInfoSchema = v.array(
|
|
14
|
+
v.object({
|
|
15
|
+
name: v.string(),
|
|
16
|
+
isEssential: v.optional(v.boolean()),
|
|
17
|
+
consent: v.object({
|
|
18
|
+
status: v.boolean(),
|
|
19
|
+
history: v.optional(v.array(v.object({ type: v.optional(v.string()) })))
|
|
20
|
+
})
|
|
21
|
+
})
|
|
22
|
+
);
|
|
23
|
+
function usercentricsCategories(services) {
|
|
24
|
+
const optionalServices = services.filter((service) => !service.essential);
|
|
25
|
+
const acceptedAll = optionalServices.length > 0 && optionalServices.every((service) => service.given);
|
|
26
|
+
return {
|
|
27
|
+
necessary: true,
|
|
28
|
+
analytics: acceptedAll,
|
|
29
|
+
marketing: acceptedAll,
|
|
30
|
+
preferences: acceptedAll,
|
|
31
|
+
sale_of_data: acceptedAll
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
async function readUsercentricsServices() {
|
|
35
|
+
if (window.__ucCmp?.getConsentDetails) {
|
|
36
|
+
const details = v.safeParse(
|
|
37
|
+
usercentricsDetailsSchema,
|
|
38
|
+
await window.__ucCmp.getConsentDetails()
|
|
39
|
+
);
|
|
40
|
+
if (!details.success) {
|
|
41
|
+
console.warn("[Replo] Unreadable Usercentrics consent details");
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
services: Object.values(details.output.services).map((service) => ({
|
|
46
|
+
name: service.name,
|
|
47
|
+
given: service.consent.given,
|
|
48
|
+
essential: service.essential === true
|
|
49
|
+
})),
|
|
50
|
+
isExplicit: details.output.consent?.type === "EXPLICIT"
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const rawBaseInfo = window.UC_UI?.getServicesBaseInfo?.();
|
|
54
|
+
if (rawBaseInfo === void 0) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
const baseInfo = v.safeParse(usercentricsBaseInfoSchema, rawBaseInfo);
|
|
58
|
+
if (!baseInfo.success) {
|
|
59
|
+
console.warn("[Replo] Unreadable Usercentrics service info");
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
services: baseInfo.output.map((service) => ({
|
|
64
|
+
name: service.name,
|
|
65
|
+
given: service.consent.status,
|
|
66
|
+
essential: service.isEssential === true
|
|
67
|
+
})),
|
|
68
|
+
isExplicit: baseInfo.output.some((service) => {
|
|
69
|
+
return service.consent.history?.at(-1)?.type === "explicit";
|
|
70
|
+
})
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
async function readUsercentricsChange(previous) {
|
|
74
|
+
const answer = await readUsercentricsServices();
|
|
75
|
+
if (!answer) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
const categories = usercentricsCategories(answer.services);
|
|
79
|
+
const grantedUsercentricsServices = answer.services.filter((service) => service.given).map((service) => service.name).sort();
|
|
80
|
+
const isUnchanged = categories.analytics === previous.categories.analytics && grantedUsercentricsServices.join() === previous.grantedUsercentricsServices.join();
|
|
81
|
+
const isDecided = answer.isExplicit || answer.services.some((service) => service.given && !service.essential);
|
|
82
|
+
if (isUnchanged && (previous.decidedAt !== null || !isDecided)) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
categories,
|
|
87
|
+
decidedAt: isDecided ? Date.now() : null,
|
|
88
|
+
grantedUsercentricsServices,
|
|
89
|
+
hasDowngrade: previous.grantedUsercentricsServices.some((name) => {
|
|
90
|
+
return !grantedUsercentricsServices.includes(name);
|
|
91
|
+
})
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
export {
|
|
95
|
+
readUsercentricsChange
|
|
96
|
+
};
|
package/consent/window-api.js
CHANGED
|
@@ -30,7 +30,7 @@ const api = {
|
|
|
30
30
|
saleOfDataAllowed: () => isAllowed("sale_of_data"),
|
|
31
31
|
shouldShowBanner: () => {
|
|
32
32
|
const state = consentStore.getSnapshot();
|
|
33
|
-
if (state.cmp
|
|
33
|
+
if (state.cmp && state.cmp !== "replo") {
|
|
34
34
|
return false;
|
|
35
35
|
}
|
|
36
36
|
return state.mode !== "off" && state.decidedAt === null;
|
package/gateways/canopy-api.d.ts
CHANGED
|
@@ -89,16 +89,13 @@ export type CheckoutAddress = {
|
|
|
89
89
|
};
|
|
90
90
|
export declare function shouldIncludeInactiveProducts(): boolean;
|
|
91
91
|
declare class CanopyApiGateway {
|
|
92
|
-
private apiHost;
|
|
93
|
-
private baseUrl;
|
|
94
|
-
constructor(apiHost: string);
|
|
95
92
|
private static readonly REQUEST_TIMEOUT_MILLISECONDS;
|
|
96
|
-
static setup(): Promise<CanopyApiGateway>;
|
|
97
93
|
private _post;
|
|
98
94
|
private _get;
|
|
99
|
-
invokeLoader({ loaderKey, args, }: {
|
|
95
|
+
invokeLoader({ loaderKey, args, buyerIp, }: {
|
|
100
96
|
loaderKey: string;
|
|
101
97
|
args: Record<string, unknown>;
|
|
98
|
+
buyerIp: string | null;
|
|
102
99
|
}): Promise<{
|
|
103
100
|
data: unknown;
|
|
104
101
|
meta?: {
|
|
@@ -121,10 +118,12 @@ declare class CanopyApiGateway {
|
|
|
121
118
|
}[];
|
|
122
119
|
}): Promise<ReconciledCartLine[]>;
|
|
123
120
|
private _cartPost;
|
|
124
|
-
cartGet({ cartId }: {
|
|
121
|
+
cartGet({ cartId, buyerIp, }: {
|
|
125
122
|
cartId: string;
|
|
123
|
+
buyerIp: string | null;
|
|
126
124
|
}): Promise<Cart | null>;
|
|
127
|
-
cartCreate(options
|
|
125
|
+
cartCreate({ buyerIp, ...options }: {
|
|
126
|
+
buyerIp: string | null;
|
|
128
127
|
lines?: {
|
|
129
128
|
merchandiseId: string;
|
|
130
129
|
quantity: number;
|
|
@@ -141,8 +140,9 @@ declare class CanopyApiGateway {
|
|
|
141
140
|
}[];
|
|
142
141
|
country?: StorefrontCountryCode;
|
|
143
142
|
}): Promise<Cart>;
|
|
144
|
-
cartAddLines({ cartId, lines, }: {
|
|
143
|
+
cartAddLines({ cartId, lines, buyerIp, }: {
|
|
145
144
|
cartId: string;
|
|
145
|
+
buyerIp: string | null;
|
|
146
146
|
lines: {
|
|
147
147
|
merchandiseId: string;
|
|
148
148
|
quantity: number;
|
|
@@ -153,8 +153,9 @@ declare class CanopyApiGateway {
|
|
|
153
153
|
sellingPlanId?: string | null;
|
|
154
154
|
}[];
|
|
155
155
|
}): Promise<Cart>;
|
|
156
|
-
cartUpdateLines({ cartId, lines, }: {
|
|
156
|
+
cartUpdateLines({ cartId, lines, buyerIp, }: {
|
|
157
157
|
cartId: string;
|
|
158
|
+
buyerIp: string | null;
|
|
158
159
|
lines: {
|
|
159
160
|
lineId: string;
|
|
160
161
|
quantity: number;
|
|
@@ -165,24 +166,29 @@ declare class CanopyApiGateway {
|
|
|
165
166
|
sellingPlanId?: string | null;
|
|
166
167
|
}[];
|
|
167
168
|
}): Promise<Cart>;
|
|
168
|
-
cartRemoveLines({ cartId, lineIds, }: {
|
|
169
|
+
cartRemoveLines({ cartId, lineIds, buyerIp, }: {
|
|
169
170
|
cartId: string;
|
|
171
|
+
buyerIp: string | null;
|
|
170
172
|
lineIds: string[];
|
|
171
173
|
}): Promise<Cart>;
|
|
172
|
-
cartUpdateDiscountCodes({ cartId, discountCodes, }: {
|
|
174
|
+
cartUpdateDiscountCodes({ cartId, discountCodes, buyerIp, }: {
|
|
173
175
|
cartId: string;
|
|
176
|
+
buyerIp: string | null;
|
|
174
177
|
discountCodes: string[];
|
|
175
178
|
}): Promise<Cart>;
|
|
176
|
-
cartUpdateBuyerIdentity({ cartId, country, }: {
|
|
179
|
+
cartUpdateBuyerIdentity({ cartId, country, buyerIp, }: {
|
|
177
180
|
cartId: string;
|
|
181
|
+
buyerIp: string | null;
|
|
178
182
|
country: StorefrontCountryCode;
|
|
179
183
|
}): Promise<Cart>;
|
|
180
|
-
cartUpdateNote({ cartId, note, }: {
|
|
184
|
+
cartUpdateNote({ cartId, note, buyerIp, }: {
|
|
181
185
|
cartId: string;
|
|
186
|
+
buyerIp: string | null;
|
|
182
187
|
note: string;
|
|
183
188
|
}): Promise<Cart>;
|
|
184
|
-
cartReplaceAttributes({ cartId, attributes, }: {
|
|
189
|
+
cartReplaceAttributes({ cartId, attributes, buyerIp, }: {
|
|
185
190
|
cartId: string;
|
|
191
|
+
buyerIp: string | null;
|
|
186
192
|
attributes: {
|
|
187
193
|
key: string;
|
|
188
194
|
value: string;
|
|
@@ -223,5 +229,5 @@ declare class CanopyApiGateway {
|
|
|
223
229
|
variantId: string;
|
|
224
230
|
}): Promise<AddCrossSellOutcome>;
|
|
225
231
|
}
|
|
226
|
-
export declare function getCanopyApiGateway():
|
|
232
|
+
export declare function getCanopyApiGateway(): CanopyApiGateway;
|
|
227
233
|
export {};
|
package/gateways/canopy-api.js
CHANGED
|
@@ -92,19 +92,12 @@ const addCrossSellOutcomeSchema = z.discriminatedUnion("type", [
|
|
|
92
92
|
function shouldIncludeInactiveProducts() {
|
|
93
93
|
return Boolean(process.env.IS_REPLO_SANDBOX);
|
|
94
94
|
}
|
|
95
|
+
function buyerIpHeaders(buyerIp) {
|
|
96
|
+
return buyerIp ? { "x-replo-buyer-ip": buyerIp } : {};
|
|
97
|
+
}
|
|
95
98
|
class CanopyApiGateway {
|
|
96
|
-
apiHost;
|
|
97
|
-
baseUrl;
|
|
98
|
-
constructor(apiHost) {
|
|
99
|
-
this.apiHost = apiHost;
|
|
100
|
-
this.baseUrl = buildBaseUrl(apiHost);
|
|
101
|
-
}
|
|
102
99
|
static REQUEST_TIMEOUT_MILLISECONDS = 1e4;
|
|
103
|
-
|
|
104
|
-
const env = await getEnv();
|
|
105
|
-
return new CanopyApiGateway(env.CANOPY_API_HOST);
|
|
106
|
-
}
|
|
107
|
-
async _post(url, body) {
|
|
100
|
+
async _post(url, body, headers) {
|
|
108
101
|
const abortController = new AbortController();
|
|
109
102
|
const timeoutId = setTimeout(() => {
|
|
110
103
|
abortController.abort();
|
|
@@ -112,7 +105,7 @@ class CanopyApiGateway {
|
|
|
112
105
|
try {
|
|
113
106
|
return await fetch(url, {
|
|
114
107
|
method: "POST",
|
|
115
|
-
headers: { "Content-Type": "application/json" },
|
|
108
|
+
headers: { "Content-Type": "application/json", ...headers },
|
|
116
109
|
body: JSON.stringify(body),
|
|
117
110
|
signal: abortController.signal
|
|
118
111
|
});
|
|
@@ -121,7 +114,7 @@ class CanopyApiGateway {
|
|
|
121
114
|
if (process.env.NODE_ENV !== "production") {
|
|
122
115
|
message += `
|
|
123
116
|
|
|
124
|
-
apiHost: ${
|
|
117
|
+
apiHost: ${(await getEnv()).CANOPY_API_HOST}
|
|
125
118
|
If you're running locally, make sure the canopy-api cloudflare worker is running. You may need to restart the dev server.`;
|
|
126
119
|
}
|
|
127
120
|
throw new UnknownCanopyApiError({ message, cause: error });
|
|
@@ -141,7 +134,7 @@ If you're running locally, make sure the canopy-api cloudflare worker is running
|
|
|
141
134
|
if (process.env.NODE_ENV !== "production") {
|
|
142
135
|
message += `
|
|
143
136
|
|
|
144
|
-
apiHost: ${
|
|
137
|
+
apiHost: ${(await getEnv()).CANOPY_API_HOST}
|
|
145
138
|
If you're running locally, make sure the canopy-api cloudflare worker is running. You may need to restart the dev server.`;
|
|
146
139
|
}
|
|
147
140
|
throw new UnknownCanopyApiError({ message, cause: error });
|
|
@@ -151,15 +144,20 @@ If you're running locally, make sure the canopy-api cloudflare worker is running
|
|
|
151
144
|
}
|
|
152
145
|
async invokeLoader({
|
|
153
146
|
loaderKey,
|
|
154
|
-
args
|
|
147
|
+
args,
|
|
148
|
+
buyerIp
|
|
155
149
|
}) {
|
|
156
150
|
const env = await getEnv();
|
|
157
|
-
const response = await this._post(
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
151
|
+
const response = await this._post(
|
|
152
|
+
`${buildBaseUrl(env.CANOPY_API_HOST)}/api/v1/loaders/invoke`,
|
|
153
|
+
{
|
|
154
|
+
projectId: env.PROJECT_ID,
|
|
155
|
+
loaderKey,
|
|
156
|
+
args,
|
|
157
|
+
includeInactiveProducts: shouldIncludeInactiveProducts()
|
|
158
|
+
},
|
|
159
|
+
buyerIpHeaders(buyerIp)
|
|
160
|
+
);
|
|
163
161
|
if (!response.ok) {
|
|
164
162
|
const errorBody = await response.text().catch(() => "");
|
|
165
163
|
throw new CanopyApiGatewayError({
|
|
@@ -174,8 +172,9 @@ If you're running locally, make sure the canopy-api cloudflare worker is running
|
|
|
174
172
|
}) {
|
|
175
173
|
const env = await getEnv();
|
|
176
174
|
const response = await this._post(
|
|
177
|
-
`${
|
|
178
|
-
{ projectId: env.PROJECT_ID, lines }
|
|
175
|
+
`${buildBaseUrl(env.CANOPY_API_HOST)}/api/v1/product-catalog/reconcile-cart-lines`,
|
|
176
|
+
{ projectId: env.PROJECT_ID, lines },
|
|
177
|
+
{}
|
|
179
178
|
);
|
|
180
179
|
if (!response.ok) {
|
|
181
180
|
const errorBody = await response.text().catch(() => "");
|
|
@@ -186,9 +185,13 @@ If you're running locally, make sure the canopy-api cloudflare worker is running
|
|
|
186
185
|
}
|
|
187
186
|
return reconcileCartLinesResponseSchema.parse(await response.json()).lines;
|
|
188
187
|
}
|
|
189
|
-
async _cartPost(
|
|
188
|
+
async _cartPost({
|
|
189
|
+
path,
|
|
190
|
+
body,
|
|
191
|
+
buyerIp
|
|
192
|
+
}) {
|
|
190
193
|
const env = await getEnv();
|
|
191
|
-
const url = `${
|
|
194
|
+
const url = `${buildBaseUrl(env.CANOPY_API_HOST)}/api/v1/cart${path}`;
|
|
192
195
|
const abortController = new AbortController();
|
|
193
196
|
const timeoutId = setTimeout(() => {
|
|
194
197
|
abortController.abort();
|
|
@@ -198,7 +201,8 @@ If you're running locally, make sure the canopy-api cloudflare worker is running
|
|
|
198
201
|
response = await fetch(url, {
|
|
199
202
|
method: "POST",
|
|
200
203
|
headers: {
|
|
201
|
-
"Content-Type": "application/json"
|
|
204
|
+
"Content-Type": "application/json",
|
|
205
|
+
...buyerIpHeaders(buyerIp)
|
|
202
206
|
},
|
|
203
207
|
body: JSON.stringify({ ...body, projectId: env.PROJECT_ID }),
|
|
204
208
|
signal: abortController.signal
|
|
@@ -221,9 +225,12 @@ If you're running locally, make sure the canopy-api cloudflare worker is running
|
|
|
221
225
|
const json = await response.json();
|
|
222
226
|
return cartSchema.parse(json.data);
|
|
223
227
|
}
|
|
224
|
-
async cartGet({
|
|
228
|
+
async cartGet({
|
|
229
|
+
cartId,
|
|
230
|
+
buyerIp
|
|
231
|
+
}) {
|
|
225
232
|
try {
|
|
226
|
-
return await this._cartPost("/get", { cartId });
|
|
233
|
+
return await this._cartPost({ path: "/get", body: { cartId }, buyerIp });
|
|
227
234
|
} catch (error) {
|
|
228
235
|
if (error instanceof CanopyApiGatewayError && error.statusCode === 404) {
|
|
229
236
|
return null;
|
|
@@ -231,55 +238,97 @@ If you're running locally, make sure the canopy-api cloudflare worker is running
|
|
|
231
238
|
throw error;
|
|
232
239
|
}
|
|
233
240
|
}
|
|
234
|
-
async cartCreate(
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
241
|
+
async cartCreate({
|
|
242
|
+
buyerIp,
|
|
243
|
+
...options
|
|
244
|
+
}) {
|
|
245
|
+
return this._cartPost({
|
|
246
|
+
path: "/create",
|
|
247
|
+
body: {
|
|
248
|
+
lines: options.lines,
|
|
249
|
+
discountCodes: options.discountCodes,
|
|
250
|
+
attributes: options.attributes,
|
|
251
|
+
country: options.country
|
|
252
|
+
},
|
|
253
|
+
buyerIp
|
|
240
254
|
});
|
|
241
255
|
}
|
|
242
256
|
async cartAddLines({
|
|
243
257
|
cartId,
|
|
244
|
-
lines
|
|
258
|
+
lines,
|
|
259
|
+
buyerIp
|
|
245
260
|
}) {
|
|
246
|
-
return this._cartPost(
|
|
261
|
+
return this._cartPost({
|
|
262
|
+
path: "/add-lines",
|
|
263
|
+
body: { cartId, lines },
|
|
264
|
+
buyerIp
|
|
265
|
+
});
|
|
247
266
|
}
|
|
248
267
|
async cartUpdateLines({
|
|
249
268
|
cartId,
|
|
250
|
-
lines
|
|
269
|
+
lines,
|
|
270
|
+
buyerIp
|
|
251
271
|
}) {
|
|
252
|
-
return this._cartPost(
|
|
272
|
+
return this._cartPost({
|
|
273
|
+
path: "/update-lines",
|
|
274
|
+
body: { cartId, lines },
|
|
275
|
+
buyerIp
|
|
276
|
+
});
|
|
253
277
|
}
|
|
254
278
|
async cartRemoveLines({
|
|
255
279
|
cartId,
|
|
256
|
-
lineIds
|
|
280
|
+
lineIds,
|
|
281
|
+
buyerIp
|
|
257
282
|
}) {
|
|
258
|
-
return this._cartPost(
|
|
283
|
+
return this._cartPost({
|
|
284
|
+
path: "/remove-lines",
|
|
285
|
+
body: { cartId, lineIds },
|
|
286
|
+
buyerIp
|
|
287
|
+
});
|
|
259
288
|
}
|
|
260
289
|
async cartUpdateDiscountCodes({
|
|
261
290
|
cartId,
|
|
262
|
-
discountCodes
|
|
291
|
+
discountCodes,
|
|
292
|
+
buyerIp
|
|
263
293
|
}) {
|
|
264
|
-
return this._cartPost(
|
|
294
|
+
return this._cartPost({
|
|
295
|
+
path: "/update-discount-codes",
|
|
296
|
+
body: { cartId, discountCodes },
|
|
297
|
+
buyerIp
|
|
298
|
+
});
|
|
265
299
|
}
|
|
266
300
|
async cartUpdateBuyerIdentity({
|
|
267
301
|
cartId,
|
|
268
|
-
country
|
|
302
|
+
country,
|
|
303
|
+
buyerIp
|
|
269
304
|
}) {
|
|
270
|
-
return this._cartPost(
|
|
305
|
+
return this._cartPost({
|
|
306
|
+
path: "/update-buyer-identity",
|
|
307
|
+
body: { cartId, country },
|
|
308
|
+
buyerIp
|
|
309
|
+
});
|
|
271
310
|
}
|
|
272
311
|
async cartUpdateNote({
|
|
273
312
|
cartId,
|
|
274
|
-
note
|
|
313
|
+
note,
|
|
314
|
+
buyerIp
|
|
275
315
|
}) {
|
|
276
|
-
return this._cartPost(
|
|
316
|
+
return this._cartPost({
|
|
317
|
+
path: "/update-note",
|
|
318
|
+
body: { cartId, note },
|
|
319
|
+
buyerIp
|
|
320
|
+
});
|
|
277
321
|
}
|
|
278
322
|
async cartReplaceAttributes({
|
|
279
323
|
cartId,
|
|
280
|
-
attributes
|
|
324
|
+
attributes,
|
|
325
|
+
buyerIp
|
|
281
326
|
}) {
|
|
282
|
-
return this._cartPost(
|
|
327
|
+
return this._cartPost({
|
|
328
|
+
path: "/replace-attributes",
|
|
329
|
+
body: { cartId, attributes },
|
|
330
|
+
buyerIp
|
|
331
|
+
});
|
|
283
332
|
}
|
|
284
333
|
async _stripeCheckoutPost({
|
|
285
334
|
path,
|
|
@@ -288,8 +337,9 @@ If you're running locally, make sure the canopy-api cloudflare worker is running
|
|
|
288
337
|
}) {
|
|
289
338
|
const env = await getEnv();
|
|
290
339
|
const response = await this._post(
|
|
291
|
-
`${
|
|
292
|
-
{ projectId: env.PROJECT_ID, ...body }
|
|
340
|
+
`${buildBaseUrl(env.CANOPY_API_HOST)}/api/v1/stripe-checkout/${path}`,
|
|
341
|
+
{ projectId: env.PROJECT_ID, ...body },
|
|
342
|
+
{}
|
|
293
343
|
);
|
|
294
344
|
if (!response.ok) {
|
|
295
345
|
throw new CanopyApiGatewayError({
|
|
@@ -308,7 +358,7 @@ If you're running locally, make sure the canopy-api cloudflare worker is running
|
|
|
308
358
|
}) {
|
|
309
359
|
const env = await getEnv();
|
|
310
360
|
const response = await this._post(
|
|
311
|
-
`${
|
|
361
|
+
`${buildBaseUrl(env.CANOPY_API_HOST)}/api/v1/stripe-checkout/create-session`,
|
|
312
362
|
{
|
|
313
363
|
cartId: checkoutAttemptId,
|
|
314
364
|
checkoutAttemptId,
|
|
@@ -317,7 +367,8 @@ If you're running locally, make sure the canopy-api cloudflare worker is running
|
|
|
317
367
|
successUrl,
|
|
318
368
|
cancelUrl,
|
|
319
369
|
...uiMode === "form" ? { uiMode } : {}
|
|
320
|
-
}
|
|
370
|
+
},
|
|
371
|
+
{}
|
|
321
372
|
);
|
|
322
373
|
if (!response.ok) {
|
|
323
374
|
const restart = response.status === 409 ? restartCheckoutSchema.safeParse(await response.json()) : null;
|
|
@@ -362,8 +413,8 @@ If you're running locally, make sure the canopy-api cloudflare worker is running
|
|
|
362
413
|
});
|
|
363
414
|
}
|
|
364
415
|
}
|
|
365
|
-
|
|
366
|
-
return CanopyApiGateway
|
|
416
|
+
function getCanopyApiGateway() {
|
|
417
|
+
return new CanopyApiGateway();
|
|
367
418
|
}
|
|
368
419
|
export {
|
|
369
420
|
CanopyApiGatewayError,
|
package/lib/buildMetadata.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export const BUILD_METADATA = {
|
|
2
2
|
"packageName": "@replohq/sdk",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.19.0",
|
|
4
4
|
"branch": "HEAD",
|
|
5
|
-
"commit": "
|
|
6
|
-
"builtAt": "2026-11-
|
|
5
|
+
"commit": "377fd6358bd4e3d0d32ff03942701a9cede4e0e9",
|
|
6
|
+
"builtAt": "2026-11-06T19:28:13.532Z"
|
|
7
7
|
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shopper's IP, for canopy to pass to Shopify so it throttles and
|
|
3
|
+
* bot-checks each shopper separately; canopy only sees this site's server.
|
|
4
|
+
* Server actions only: it reads the incoming request.
|
|
5
|
+
*/
|
|
6
|
+
export declare function readBuyerIp(): Promise<string | null>;
|
package/lib/buyer-ip.js
ADDED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
"use server";
|
|
2
|
+
import { readBuyerIp } from "../lib/buyer-ip";
|
|
2
3
|
import { fetchValidatedLoaderOutput } from "./loader-output";
|
|
3
4
|
async function invokeLoaderAction({
|
|
4
5
|
loaderKey,
|
|
5
6
|
args
|
|
6
7
|
}) {
|
|
7
|
-
return fetchValidatedLoaderOutput({
|
|
8
|
+
return fetchValidatedLoaderOutput({
|
|
9
|
+
loaderKey,
|
|
10
|
+
args,
|
|
11
|
+
buyerIp: await readBuyerIp()
|
|
12
|
+
});
|
|
8
13
|
}
|
|
9
14
|
export {
|
|
10
15
|
invokeLoaderAction
|
|
@@ -49,7 +49,11 @@ async function invokeLoaderServer({
|
|
|
49
49
|
try {
|
|
50
50
|
return {
|
|
51
51
|
kind: "data",
|
|
52
|
-
data: await fetchValidatedLoaderOutput({
|
|
52
|
+
data: await fetchValidatedLoaderOutput({
|
|
53
|
+
loaderKey,
|
|
54
|
+
args,
|
|
55
|
+
buyerIp: null
|
|
56
|
+
})
|
|
53
57
|
};
|
|
54
58
|
} catch (error) {
|
|
55
59
|
if (error instanceof LoaderOutputError) {
|
package/loaders/invoke-loader.js
CHANGED
|
@@ -13,7 +13,11 @@ async function invokeUntypedLoader({
|
|
|
13
13
|
try {
|
|
14
14
|
if (process.env.NEXT_RUNTIME) {
|
|
15
15
|
const { fetchValidatedLoaderOutput } = await import("./loader-output");
|
|
16
|
-
return await fetchValidatedLoaderOutput({
|
|
16
|
+
return await fetchValidatedLoaderOutput({
|
|
17
|
+
loaderKey,
|
|
18
|
+
args,
|
|
19
|
+
buyerIp: null
|
|
20
|
+
});
|
|
17
21
|
}
|
|
18
22
|
const { invokeLoaderAction } = await import("./invoke-loader-action");
|
|
19
23
|
return await invokeLoaderAction({ loaderKey, args });
|
|
@@ -25,7 +25,8 @@ export declare class LoaderOutputError extends CanopyError {
|
|
|
25
25
|
* before any request, since JSON would drop them and canopy-api would reject
|
|
26
26
|
* the rest. The log matters because `PrefetchedLoaders` swallows rejections.
|
|
27
27
|
*/
|
|
28
|
-
export declare function fetchValidatedLoaderOutput({ loaderKey, args, }: {
|
|
28
|
+
export declare function fetchValidatedLoaderOutput({ loaderKey, args, buyerIp, }: {
|
|
29
29
|
loaderKey: string;
|
|
30
30
|
args: Record<string, unknown>;
|
|
31
|
+
buyerIp: string | null;
|
|
31
32
|
}): Promise<unknown>;
|
package/loaders/loader-output.js
CHANGED
|
@@ -92,15 +92,19 @@ class LoaderOutputError extends CanopyError {
|
|
|
92
92
|
}
|
|
93
93
|
async function fetchValidatedLoaderOutput({
|
|
94
94
|
loaderKey,
|
|
95
|
-
args
|
|
95
|
+
args,
|
|
96
|
+
buyerIp
|
|
96
97
|
}) {
|
|
97
98
|
const argsError = getLoaderArgsError({ loaderKey, args });
|
|
98
99
|
if (argsError !== null) {
|
|
99
100
|
console.error(argsError.message);
|
|
100
101
|
throw argsError;
|
|
101
102
|
}
|
|
102
|
-
const
|
|
103
|
-
|
|
103
|
+
const { data } = await getCanopyApiGateway().invokeLoader({
|
|
104
|
+
loaderKey,
|
|
105
|
+
args,
|
|
106
|
+
buyerIp
|
|
107
|
+
});
|
|
104
108
|
const knownKey = dataLoaderKeySchema.safeParse(loaderKey);
|
|
105
109
|
if (!knownKey.success) {
|
|
106
110
|
return data;
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@replohq/sdk",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.19.0",
|
|
4
4
|
"reploBuild": {
|
|
5
5
|
"packageName": "@replohq/sdk",
|
|
6
|
-
"version": "2.
|
|
6
|
+
"version": "2.19.0",
|
|
7
7
|
"branch": "HEAD",
|
|
8
|
-
"commit": "
|
|
9
|
-
"builtAt": "2026-11-
|
|
8
|
+
"commit": "377fd6358bd4e3d0d32ff03942701a9cede4e0e9",
|
|
9
|
+
"builtAt": "2026-11-06T19:28:13.532Z"
|
|
10
10
|
},
|
|
11
11
|
"description": "Replo SDK — cart, analytics, and data loaders for agent-built Next.js sites.",
|
|
12
12
|
"license": "SEE LICENSE IN LICENSE",
|