Skip to content

Commit d9a329f

Browse files
committed
update: bug fixes
1 parent 3c20ed2 commit d9a329f

File tree

12 files changed

+252
-179
lines changed

12 files changed

+252
-179
lines changed

app/entry.server.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,14 @@ export default async function handleRequest(
4040
</I18nextProvider>,
4141
{
4242
signal: request.signal,
43-
onError(error: unknown) {
43+
onError(
44+
error: any /* eslint-disable-line @typescript-eslint/no-explicit-any */,
45+
) {
4446
if (!request.signal.aborted) {
4547
// Log streaming rendering errors from inside the shell
46-
console.error(error);
48+
console.error("entry.server.onError", error);
4749
}
48-
// biome-ignore lint/style/noParameterAssign: It's ok
49-
status = 500;
50+
status = Number.isNaN(error.error?.status) ? 500 : error.error.status;
5051
},
5152
},
5253
);

app/routes/app.index.tsx

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Page, Text } from "@shopify/polaris";
22
import { useEffect } from "react";
33
import { useTranslation } from "react-i18next";
4-
import { data, useActionData, useLoaderData } from "react-router";
4+
import { data } from "react-router";
55

66
import type { Route } from "./+types/app.index";
77
import i18n from "~/i18n.server";
@@ -12,7 +12,7 @@ export async function loader({ context, request }: Route.LoaderArgs) {
1212
const shopify = createShopify(context);
1313
shopify.utils.log.debug("app.index.loader");
1414

15-
const client = await shopify.authorize(request);
15+
const client = await shopify.admin(request);
1616

1717
try {
1818
const { data, errors } = await client.request(/* GraphQL */ `
@@ -30,8 +30,16 @@ export async function loader({ context, request }: Route.LoaderArgs) {
3030
} catch (error) {
3131
shopify.utils.log.error("app.index.loader.error", error);
3232

33-
if (error instanceof ShopifyException && error?.type === "GRAPHQL") {
34-
return { errors: error.errors };
33+
if (error instanceof ShopifyException) {
34+
switch (error.type) {
35+
case "GRAPHQL":
36+
return { errors: error.errors };
37+
38+
default:
39+
return new Response(error.message, {
40+
status: error.status,
41+
});
42+
}
3543
}
3644

3745
const t = await i18n.getFixedT(request);
@@ -45,9 +53,10 @@ export async function loader({ context, request }: Route.LoaderArgs) {
4553
}
4654
}
4755

48-
export default function AppIndex() {
49-
const loaderData = useLoaderData<typeof loader>();
50-
const actionData = useActionData<typeof action>();
56+
export default function AppIndex({
57+
actionData,
58+
loaderData,
59+
}: Route.ComponentProps) {
5160
const { data, errors } = loaderData ?? actionData ?? {};
5261
console.log("app.index", data);
5362

app/routes/app.tsx

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,25 @@ import { APP_BRIDGE_URL } from "~/const";
1313
import { createShopify } from "~/shopify.server";
1414

1515
export async function loader({ context, request }: Route.LoaderArgs) {
16-
const shopify = createShopify(context);
17-
shopify.utils.log.debug("app");
16+
try {
17+
const shopify = createShopify(context);
18+
shopify.utils.log.debug("app");
1819

19-
const response = await shopify.authorize(request);
20-
if (response instanceof Response) throw response;
20+
await shopify.admin(request);
2121

22-
return {
23-
apiKey: shopify.config.apiKey,
24-
appDebug: shopify.config.appLogLevel === "debug",
25-
appUrl: shopify.config.appUrl,
26-
};
22+
return {
23+
apiKey: shopify.config.apiKey,
24+
appDebug: shopify.config.appLogLevel === "debug",
25+
appUrl: shopify.config.appUrl,
26+
};
27+
} catch (error: any /* eslint-disable-line @typescript-eslint/no-explicit-any */) {
28+
if (error instanceof Response) return error;
29+
30+
return new Response(error.message, {
31+
status: error.status,
32+
statusText: "Unauthorized",
33+
});
34+
}
2735
}
2836

2937
export default function App() {

app/routes/proxy.index.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
import { useTranslation } from "react-i18next";
22

33
import type { Route } from "./+types/proxy.index";
4+
import { createShopify } from "~/shopify.server";
5+
6+
export async function loader({ context, request }: Route.LoaderArgs) {
7+
const shopify = createShopify(context);
8+
shopify.utils.log.debug("proxy.index");
9+
10+
await shopify.proxy(request);
411

5-
export async function loader(_: Route.LoaderArgs) {
612
const data = {};
713
return { data };
814
}

app/routes/proxy.tsx

Lines changed: 13 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,23 @@
1-
import type { Crypto } from "@cloudflare/workers-types/experimental";
21
import { Outlet } from "react-router";
32

43
import type { Route } from "./+types/proxy";
54
import { createShopify } from "~/shopify.server";
65

76
export async function loader({ context, request }: Route.LoaderArgs) {
8-
const shopify = createShopify(context);
9-
10-
const url = new URL(request.url);
11-
12-
const param = url.searchParams.get("signature");
13-
if (param === null) {
14-
return new Response("Proxy param is missing", { status: 400 });
15-
}
16-
17-
url.searchParams.delete("signature");
18-
url.searchParams.sort();
19-
const params = url.searchParams.toString();
20-
21-
const encoder = new TextEncoder();
22-
const encodedKey = encoder.encode(shopify.config.apiSecretKey);
23-
const encodedData = encoder.encode(params);
24-
const hmacKey = await crypto.subtle.importKey(
25-
"raw",
26-
encodedKey,
27-
{
28-
name: "HMAC",
29-
hash: "SHA-256",
30-
},
31-
true,
32-
["sign", "verify"],
33-
);
34-
const signature = await crypto.subtle.sign("HMAC", hmacKey, encodedData);
35-
const hmac = btoa(String.fromCharCode(...new Uint8Array(signature))); // base64
36-
37-
const encodedBody = encoder.encode(hmac);
38-
const encodedParam = encoder.encode(param);
39-
if (encodedBody.byteLength !== encodedParam.byteLength) {
40-
return new Response("Encoded byte length mismatch", { status: 401 });
41-
}
42-
43-
const valid = (crypto as Crypto).subtle.timingSafeEqual(
44-
encodedBody,
45-
encodedParam,
46-
);
47-
if (!valid) {
48-
return new Response("Invalid hmac", { status: 401 });
7+
try {
8+
const shopify = createShopify(context);
9+
shopify.utils.log.debug("proxy");
10+
11+
const proxy = await shopify.proxy(request);
12+
shopify.utils.log.debug("proxy", { ...proxy });
13+
14+
return new Response(null, { status: 204 });
15+
} catch (error: any /* eslint-disable-line @typescript-eslint/no-explicit-any */) {
16+
return new Response(error.message, {
17+
status: error.status,
18+
statusText: "Unauthorized",
19+
});
4920
}
50-
51-
shopify.utils.log.debug("proxy", {
52-
params: Object.fromEntries(url.searchParams),
53-
});
54-
55-
return new Response(null, { status: 204 });
5621
}
5722

5823
export default function Proxy() {

app/routes/shopify.webhooks.tsx

Lines changed: 28 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,101 +1,42 @@
1-
import type { Crypto } from "@cloudflare/workers-types/experimental";
2-
31
import type { Route } from "./+types/shopify.webhooks";
42
import { createShopify } from "~/shopify.server";
53

64
export async function action({ context, request }: Route.ActionArgs) {
7-
const shopify = createShopify(context);
8-
9-
// validate.body
10-
const body = await request.text();
11-
if (body.length === 0) {
12-
return new Response("Webhook body is missing", { status: 400 });
13-
}
5+
try {
6+
const shopify = createShopify(context);
7+
shopify.utils.log.debug("shopify.webhooks");
148

15-
// validate.hmac
16-
const header = request.headers.get("X-Shopify-Hmac-Sha256");
17-
if (header === null) {
18-
return new Response("Webhook header is missing", { status: 400 });
19-
}
9+
const webhook = await shopify.webhook(request);
10+
shopify.utils.log.debug("shopify.webhooks", { ...webhook });
2011

21-
const encoder = new TextEncoder();
22-
const encodedKey = encoder.encode(shopify.config.apiSecretKey);
23-
const encodedData = encoder.encode(body);
24-
const hmacKey = await crypto.subtle.importKey(
25-
"raw",
26-
encodedKey,
27-
{
28-
name: "HMAC",
29-
hash: "SHA-256",
30-
},
31-
true,
32-
["sign", "verify"],
33-
);
34-
const signature = await crypto.subtle.sign("HMAC", hmacKey, encodedData);
35-
const hmac = btoa(String.fromCharCode(...new Uint8Array(signature))); // base64
12+
const session = await shopify.session.get(webhook.domain);
3613

37-
const encodedBody = encoder.encode(hmac);
38-
const encodedHeader = encoder.encode(header);
39-
if (encodedBody.byteLength !== encodedHeader.byteLength) {
40-
return new Response("Encoded byte length mismatch", { status: 401 });
41-
}
42-
43-
const valid = (crypto as Crypto).subtle.timingSafeEqual(
44-
encodedBody,
45-
encodedHeader,
46-
);
47-
if (!valid) {
48-
return new Response("Invalid hmac", { status: 401 });
49-
}
14+
switch (webhook.topic) {
15+
// app
16+
case "APP_UNINSTALLED": {
17+
if (!session) {
18+
break;
19+
}
20+
await shopify.session.delete(session.id);
5021

51-
// validate.headers
52-
const requiredHeaders = {
53-
apiVersion: "X-Shopify-API-Version",
54-
domain: "X-Shopify-Shop-Domain",
55-
hmac: "X-Shopify-Hmac-Sha256",
56-
topic: "X-Shopify-Topic",
57-
webhookId: "X-Shopify-Webhook-Id",
58-
};
59-
if (
60-
!Object.values(requiredHeaders).every((header) =>
61-
request.headers.has(header),
62-
)
63-
) {
64-
return new Response("Webhook headers are missing", { status: 400 });
65-
}
66-
const optionalHeaders = { subTopic: "X-Shopify-Sub-Topic" };
67-
const headers = { ...requiredHeaders, ...optionalHeaders };
68-
const webhook = Object.values(headers).reduce(
69-
(headers, header) => ({
70-
...headers,
71-
[header]: request.headers.get(header),
72-
}),
73-
{} as typeof headers,
74-
);
75-
shopify.utils.log.debug("shopify.webhooks", { ...webhook });
76-
77-
const session = await shopify.session.get(webhook.domain);
78-
79-
switch (webhook.topic) {
80-
// app
81-
case "APP_UNINSTALLED": {
82-
if (!session) {
8322
break;
8423
}
85-
await shopify.session.delete(session.id);
86-
87-
break;
24+
case "APP_PURCHASES_ONE_TIME_UPDATE":
25+
case "APP_SUBSCRIPTIONS_APPROACHING_CAPPED_AMOUNT":
26+
case "APP_SUBSCRIPTIONS_UPDATE":
27+
28+
// compliance
29+
case "CUSTOMERS_DATA_REQUEST": // eslint-disable-line no-fallthrough
30+
case "CUSTOMERS_REDACT":
31+
case "SHOP_REDACT":
32+
break;
8833
}
89-
case "APP_PURCHASES_ONE_TIME_UPDATE":
90-
case "APP_SUBSCRIPTIONS_APPROACHING_CAPPED_AMOUNT":
91-
case "APP_SUBSCRIPTIONS_UPDATE":
9234

93-
// compliance
94-
case "CUSTOMERS_DATA_REQUEST": // eslint-disable-line no-fallthrough
95-
case "CUSTOMERS_REDACT":
96-
case "SHOP_REDACT":
97-
break;
35+
return new Response(undefined, { status: 204 });
36+
} catch (error: any /* eslint-disable-line @typescript-eslint/no-explicit-any */) {
37+
return new Response(error.message, {
38+
status: error.status,
39+
statusText: "Unauthorized",
40+
});
9841
}
99-
100-
return new Response(undefined, { status: 204 });
10142
}

app/shopify.server.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,5 @@ const context = { cloudflare: { env } } as unknown as AppLoadContext;
88

99
test("createShopify", () => {
1010
const shopify = createShopify(context);
11-
expect(shopify.authorize).toBeDefined();
11+
expect(shopify.admin).toBeDefined();
1212
});

0 commit comments

Comments
 (0)