hookwright 1.6.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +96 -18
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -24,7 +24,7 @@ var init_package = __esm({
|
|
|
24
24
|
"package.json"() {
|
|
25
25
|
package_default = {
|
|
26
26
|
name: "hookwright",
|
|
27
|
-
version: "1.
|
|
27
|
+
version: "1.8.0",
|
|
28
28
|
description: "Build real, signed e-commerce webhooks from a live Shopify catalogue \u2014 interactive terminal UI, no backend",
|
|
29
29
|
keywords: [
|
|
30
30
|
"webhook",
|
|
@@ -163,6 +163,9 @@ var DEFAULT_CONFIG = {
|
|
|
163
163
|
shopify: {
|
|
164
164
|
domain: "",
|
|
165
165
|
accessToken: "",
|
|
166
|
+
// Storefront API token. Creating a checkout needs one: the admin checkout API
|
|
167
|
+
// was removed, so a fresh checkout can only be minted through the storefront.
|
|
168
|
+
storefrontAccessToken: "",
|
|
166
169
|
apiVersion: "2024-10",
|
|
167
170
|
// cached from the last successful connection, for display only
|
|
168
171
|
shop: null
|
|
@@ -911,7 +914,7 @@ function editableFieldsFor(eventName) {
|
|
|
911
914
|
}
|
|
912
915
|
var editableFields3 = editableFieldsFor(DEFAULT_EVENT);
|
|
913
916
|
function buildPayload3(ctx) {
|
|
914
|
-
const { config, items, phone, live } = ctx;
|
|
917
|
+
const { config, items, phone, live, delta } = ctx;
|
|
915
918
|
const eventName = ctx.eventName ?? DEFAULT_EVENT;
|
|
916
919
|
const now = ctx.now ?? /* @__PURE__ */ new Date();
|
|
917
920
|
const currency = ctx.currency || config.defaults.currency;
|
|
@@ -1022,16 +1025,18 @@ function buildPayload3(ctx) {
|
|
|
1022
1025
|
case "addtocart":
|
|
1023
1026
|
case "removefromcart": {
|
|
1024
1027
|
const added = eventName === "addtocart";
|
|
1025
|
-
const
|
|
1026
|
-
const
|
|
1028
|
+
const fallback = added ? items : items.slice(-1);
|
|
1029
|
+
const moved = delta?.length ? items.filter((item) => delta.includes(item)) : fallback;
|
|
1030
|
+
const untouched = items.filter((item) => !moved.includes(item));
|
|
1031
|
+
const cart = added ? items : untouched;
|
|
1027
1032
|
eventVal = {
|
|
1028
|
-
line_items:
|
|
1029
|
-
cart_value:
|
|
1030
|
-
recent_product_image: added ?
|
|
1033
|
+
line_items: cart.map(cartLine),
|
|
1034
|
+
cart_value: valueOf(cart),
|
|
1035
|
+
recent_product_image: added ? moved.map(cartLine)[0]?.image_url ?? null : null,
|
|
1031
1036
|
init_cart: false,
|
|
1032
|
-
empty_cart: !added &&
|
|
1033
|
-
items_added: added ?
|
|
1034
|
-
items_removed:
|
|
1037
|
+
empty_cart: !added && cart.length === 0,
|
|
1038
|
+
items_added: added ? moved.map(cartLine) : [],
|
|
1039
|
+
items_removed: added ? [] : moved.map(removedLine),
|
|
1035
1040
|
customer,
|
|
1036
1041
|
bik_customer_id: null
|
|
1037
1042
|
};
|
|
@@ -2491,6 +2496,33 @@ var ShopifyClient = class {
|
|
|
2491
2496
|
const { json } = await this.request("/checkouts.json", { query: { limit } });
|
|
2492
2497
|
return json?.checkouts ?? [];
|
|
2493
2498
|
}
|
|
2499
|
+
/**
|
|
2500
|
+
* Creates a real checkout through the storefront API and returns its recovery
|
|
2501
|
+
* link. The admin checkout API was removed, so this is the only way to mint a
|
|
2502
|
+
* new one; it needs a storefront token, which is separate from the admin one.
|
|
2503
|
+
*/
|
|
2504
|
+
async createCheckout({ storefrontAccessToken, lineItems, email }) {
|
|
2505
|
+
if (!storefrontAccessToken) throw new ShopifyError("Storefront access token missing", { hint: "run `hookwright configure` and add it, or the checkout event reuses an existing checkout" });
|
|
2506
|
+
const mutation = `mutation checkoutCreate($input: CheckoutCreateInput!) {
|
|
2507
|
+
checkoutCreate(input: $input) {
|
|
2508
|
+
checkout { id webUrl totalPrice { amount currencyCode } }
|
|
2509
|
+
checkoutUserErrors { field message }
|
|
2510
|
+
}
|
|
2511
|
+
}`;
|
|
2512
|
+
const input = { lineItems: lineItems.map(({ variantId, quantity }) => ({ variantId: `gid://shopify/ProductVariant/${variantId}`, quantity })) };
|
|
2513
|
+
if (email) input.email = email;
|
|
2514
|
+
const res = await fetch(`https://${this.domain}/api/2024-01/graphql.json`, {
|
|
2515
|
+
method: "POST",
|
|
2516
|
+
headers: { "Content-Type": "application/json", "X-Shopify-Storefront-Access-Token": storefrontAccessToken },
|
|
2517
|
+
body: JSON.stringify({ query: mutation, variables: { input } })
|
|
2518
|
+
});
|
|
2519
|
+
const body = await res.json();
|
|
2520
|
+
const errors = body.errors ?? body.data?.checkoutCreate?.checkoutUserErrors;
|
|
2521
|
+
if (errors?.length) throw new ShopifyError(`Shopify rejected the checkout: ${errors.map((e) => e.message).join("; ")}`, { hint: "the storefront token needs unauthenticated_write_checkouts" });
|
|
2522
|
+
const checkout = body.data?.checkoutCreate?.checkout;
|
|
2523
|
+
if (!checkout) throw new ShopifyError("Shopify returned no checkout", { hint: "check the storefront token and api version" });
|
|
2524
|
+
return checkout;
|
|
2525
|
+
}
|
|
2494
2526
|
/** Orders, newest first. */
|
|
2495
2527
|
async orders({ limit = 10, status = "any" } = {}) {
|
|
2496
2528
|
const { json } = await this.request("/orders.json", { query: { limit, status } });
|
|
@@ -2908,7 +2940,7 @@ function loadPayload(file) {
|
|
|
2908
2940
|
}
|
|
2909
2941
|
|
|
2910
2942
|
// src/core/build.mjs
|
|
2911
|
-
function draftPayload({ cfg, providerId = "cashfree-occ", items, discount = 0, phone, eventName, collection, envelope, live }) {
|
|
2943
|
+
function draftPayload({ cfg, providerId = "cashfree-occ", items, discount = 0, phone, eventName, collection, envelope, live, delta }) {
|
|
2912
2944
|
const provider = getProvider(providerId);
|
|
2913
2945
|
const event = eventName ?? provider.defaultEvent;
|
|
2914
2946
|
const resolvedPhone = phone ?? cfg.customer.phone;
|
|
@@ -2921,6 +2953,7 @@ function draftPayload({ cfg, providerId = "cashfree-occ", items, discount = 0, p
|
|
|
2921
2953
|
collection,
|
|
2922
2954
|
envelope,
|
|
2923
2955
|
live,
|
|
2956
|
+
delta,
|
|
2924
2957
|
phone: parsed.ok ? parsed.value : String(resolvedPhone ?? "")
|
|
2925
2958
|
});
|
|
2926
2959
|
const editable = provider.editableFieldsFor ? provider.editableFieldsFor(event) : provider.editableFields;
|
|
@@ -2936,7 +2969,7 @@ function applyEdits(payload, fields, edits) {
|
|
|
2936
2969
|
}
|
|
2937
2970
|
return payload;
|
|
2938
2971
|
}
|
|
2939
|
-
async function buildPayload8({ cfg, providerId = "cashfree-occ", items = [], discount = 0, phone, checkImageUrls = true, payload: prebuilt, eventName, collection, envelope, live }) {
|
|
2972
|
+
async function buildPayload8({ cfg, providerId = "cashfree-occ", items = [], discount = 0, phone, checkImageUrls = true, payload: prebuilt, eventName, collection, envelope, live, delta }) {
|
|
2940
2973
|
const provider0 = getProvider(providerId);
|
|
2941
2974
|
const needsItems = !provider0.selectionFor || ["cart", "product"].includes(provider0.selectionFor(eventName ?? provider0.defaultEvent));
|
|
2942
2975
|
if (needsItems && !items.length) throw new ConfigError("no products selected");
|
|
@@ -2945,7 +2978,7 @@ async function buildPayload8({ cfg, providerId = "cashfree-occ", items = [], dis
|
|
|
2945
2978
|
const parsed = toE164(resolvedPhone, cfg.customer.countryCode);
|
|
2946
2979
|
if (!parsed.ok) throw new ConfigError(`phone is unusable: ${parsed.reason}`);
|
|
2947
2980
|
const event = eventName ?? provider.defaultEvent;
|
|
2948
|
-
const payload = prebuilt ?? provider.buildPayload({ config: cfg, items, discount, phone: parsed.value, eventName: event, collection, envelope, live });
|
|
2981
|
+
const payload = prebuilt ?? provider.buildPayload({ config: cfg, items, discount, phone: parsed.value, eventName: event, collection, envelope, live, delta });
|
|
2949
2982
|
const body = JSON.stringify(payload);
|
|
2950
2983
|
const report = await runAll({ payload, body, provider, config: cfg, checkImageUrls, eventName: event });
|
|
2951
2984
|
const summary = provider.summarize ? provider.summarize(payload) : {};
|
|
@@ -2988,6 +3021,19 @@ function clientFor(config) {
|
|
|
2988
3021
|
});
|
|
2989
3022
|
}
|
|
2990
3023
|
async function liveCheckout({ config, items = [], client = clientFor(config) }) {
|
|
3024
|
+
const storefrontAccessToken = config.shopify?.storefrontAccessToken;
|
|
3025
|
+
const lineItems = items.map(({ variant, product, quantity }) => ({ variantId: Number(variant?.id ?? product?.id), quantity: Number(quantity) || 1 })).filter((l) => Number.isFinite(l.variantId) && l.variantId > 0);
|
|
3026
|
+
if (storefrontAccessToken && lineItems.length) {
|
|
3027
|
+
const checkout2 = await client.createCheckout({ storefrontAccessToken, lineItems, email: config.customer?.email || void 0 });
|
|
3028
|
+
return {
|
|
3029
|
+
kind: "checkout",
|
|
3030
|
+
created: true,
|
|
3031
|
+
url: checkout2.webUrl,
|
|
3032
|
+
cartValue: money2(checkout2.totalPrice?.amount ?? 0).toFixed(2),
|
|
3033
|
+
cartToken: /\/checkouts\/([^/?]+)/.exec(checkout2.webUrl ?? "")?.[1],
|
|
3034
|
+
lineItems: []
|
|
3035
|
+
};
|
|
3036
|
+
}
|
|
2991
3037
|
const checkouts = await client.abandonedCheckouts({ limit: 20 });
|
|
2992
3038
|
if (checkouts.length === 0) return null;
|
|
2993
3039
|
const wanted = new Set(items.map(({ variant }) => Number(variant?.id)).filter(Boolean));
|
|
@@ -2995,6 +3041,7 @@ async function liveCheckout({ config, items = [], client = clientFor(config) })
|
|
|
2995
3041
|
const checkout = matching ?? checkouts[0];
|
|
2996
3042
|
return {
|
|
2997
3043
|
kind: "checkout",
|
|
3044
|
+
created: false,
|
|
2998
3045
|
url: checkout.abandoned_checkout_url,
|
|
2999
3046
|
cartValue: money2(checkout.total_price ?? 0).toFixed(2),
|
|
3000
3047
|
cartToken: checkout.cart_token,
|
|
@@ -3224,6 +3271,7 @@ var STEP = {
|
|
|
3224
3271
|
PICK: "pick",
|
|
3225
3272
|
VARIANT: "variant",
|
|
3226
3273
|
QUANTITY: "quantity",
|
|
3274
|
+
DELTA: "delta",
|
|
3227
3275
|
DISCOUNT: "discount",
|
|
3228
3276
|
LIVE: "live",
|
|
3229
3277
|
MODE: "mode",
|
|
@@ -3248,6 +3296,8 @@ function Build({ config, providerId = "cashfree-occ", onDone }) {
|
|
|
3248
3296
|
const [cursor, setCursor] = useState(0);
|
|
3249
3297
|
const [items, setItems] = useState([]);
|
|
3250
3298
|
const [pendingVariant, setPendingVariant] = useState(null);
|
|
3299
|
+
const [delta, setDelta] = useState([]);
|
|
3300
|
+
const isDeltaEvent = providerId === "nitro" && (eventName === "addtocart" || eventName === "removefromcart");
|
|
3251
3301
|
const [draft, setDraft] = useState(null);
|
|
3252
3302
|
const [fieldIndex, setFieldIndex] = useState(0);
|
|
3253
3303
|
const [edits, setEdits] = useState({});
|
|
@@ -3281,6 +3331,8 @@ function Build({ config, providerId = "cashfree-occ", onDone }) {
|
|
|
3281
3331
|
setItems(items.slice(0, -1));
|
|
3282
3332
|
return stepInto(last);
|
|
3283
3333
|
}
|
|
3334
|
+
case STEP.DELTA:
|
|
3335
|
+
return setStep(STEP.PICK);
|
|
3284
3336
|
case STEP.LIVE:
|
|
3285
3337
|
case STEP.MODE:
|
|
3286
3338
|
return setStep(STEP.DISCOUNT);
|
|
@@ -3369,7 +3421,8 @@ function Build({ config, providerId = "cashfree-occ", onDone }) {
|
|
|
3369
3421
|
const advance = (nextItems, nextCursor) => {
|
|
3370
3422
|
if (nextCursor >= chosen.length) {
|
|
3371
3423
|
setItems(nextItems);
|
|
3372
|
-
if (
|
|
3424
|
+
if (isDeltaEvent) setStep(STEP.DELTA);
|
|
3425
|
+
else if (selection === "cart") setStep(STEP.DISCOUNT);
|
|
3373
3426
|
else startReview(0, collection, nextItems);
|
|
3374
3427
|
return;
|
|
3375
3428
|
}
|
|
@@ -3425,7 +3478,7 @@ function Build({ config, providerId = "cashfree-occ", onDone }) {
|
|
|
3425
3478
|
const nextItems = [...items, { product, variant: pendingVariant, quantity: qty }];
|
|
3426
3479
|
advance(nextItems, cursor + 1);
|
|
3427
3480
|
};
|
|
3428
|
-
const startReview = async (discount, chosenCollection = collection, chosenItems = items) => {
|
|
3481
|
+
const startReview = async (discount, chosenCollection = collection, chosenItems = items, chosenDelta = delta) => {
|
|
3429
3482
|
try {
|
|
3430
3483
|
let live = null;
|
|
3431
3484
|
if (needsLive(providerId, eventName)) {
|
|
@@ -3439,19 +3492,26 @@ function Build({ config, providerId = "cashfree-occ", onDone }) {
|
|
|
3439
3492
|
discount,
|
|
3440
3493
|
eventName,
|
|
3441
3494
|
collection: chosenCollection,
|
|
3442
|
-
live
|
|
3495
|
+
live,
|
|
3496
|
+
delta: chosenDelta
|
|
3443
3497
|
});
|
|
3444
|
-
setDraft({ ...next, discount, live });
|
|
3498
|
+
setDraft({ ...next, discount, live, delta: chosenDelta });
|
|
3445
3499
|
setStep(STEP.MODE);
|
|
3446
3500
|
} catch (err) {
|
|
3447
3501
|
setError({ message: err.message, hint: err.hint, failures: err.failures });
|
|
3448
3502
|
setStep(STEP.ERROR);
|
|
3449
3503
|
}
|
|
3450
3504
|
};
|
|
3505
|
+
const onDelta = (ids) => {
|
|
3506
|
+
const picked = items.filter((i) => ids.includes(String(i.variant?.id ?? i.product?.id)));
|
|
3507
|
+
if (!picked.length) return;
|
|
3508
|
+
setDelta(picked);
|
|
3509
|
+
startReview(0, collection, items, picked);
|
|
3510
|
+
};
|
|
3451
3511
|
const onDiscount = (raw) => startReview(Math.max(0, Number.parseFloat(raw) || 0));
|
|
3452
3512
|
const finalise = (payload) => {
|
|
3453
3513
|
setStep(STEP.BUILDING);
|
|
3454
|
-
buildPayload8({ cfg: config, providerId, items, discount: draft.discount, payload, eventName, collection, live: draft.live }).then(async (result) => {
|
|
3514
|
+
buildPayload8({ cfg: config, providerId, items, discount: draft.discount, payload, eventName, collection, live: draft.live, delta: draft.delta }).then(async (result) => {
|
|
3455
3515
|
setBuilt(result);
|
|
3456
3516
|
let req = null;
|
|
3457
3517
|
let command = "";
|
|
@@ -3571,6 +3631,24 @@ function Build({ config, providerId = "cashfree-occ", onDone }) {
|
|
|
3571
3631
|
}
|
|
3572
3632
|
) })
|
|
3573
3633
|
] }),
|
|
3634
|
+
step === STEP.DELTA && /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
|
|
3635
|
+
/* @__PURE__ */ jsxs8(Text8, { children: [
|
|
3636
|
+
eventName === "addtocart" ? "Which of those lines is the shopper adding now? " : "Which of those lines is the shopper removing? ",
|
|
3637
|
+
/* @__PURE__ */ jsx8(Text8, { color: palette.dim, children: "(space toggles \xB7 enter confirms)" })
|
|
3638
|
+
] }),
|
|
3639
|
+
/* @__PURE__ */ jsx8(Text8, { color: palette.dim, children: eventName === "addtocart" ? "The rest were already in the cart." : "The rest stay in the cart." }),
|
|
3640
|
+
/* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx8(
|
|
3641
|
+
MultiSelect,
|
|
3642
|
+
{
|
|
3643
|
+
visibleOptionCount: 10,
|
|
3644
|
+
options: items.map((i) => ({
|
|
3645
|
+
label: `${truncate(i.product.title, 40).padEnd(41)} x${String(i.quantity).padEnd(3)} ${money(i.variant?.price, currency)}`,
|
|
3646
|
+
value: String(i.variant?.id ?? i.product?.id)
|
|
3647
|
+
})),
|
|
3648
|
+
onSubmit: onDelta
|
|
3649
|
+
}
|
|
3650
|
+
) })
|
|
3651
|
+
] }),
|
|
3574
3652
|
step === STEP.VARIANT && /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
|
|
3575
3653
|
/* @__PURE__ */ jsxs8(Text8, { children: [
|
|
3576
3654
|
`Variant for `,
|