create-shopify-firebase-app 1.3.0 → 2.0.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/lib/index.js +672 -456
- package/package.json +2 -2
- package/templates/js/functions/package.json +24 -0
- package/templates/js/functions/src/admin-api.js +292 -0
- package/templates/js/functions/src/auth.js +147 -0
- package/templates/js/functions/src/config.js +14 -0
- package/templates/js/functions/src/firebase.js +12 -0
- package/templates/js/functions/src/index.js +93 -0
- package/templates/js/functions/src/proxy.js +60 -0
- package/templates/js/functions/src/verify-token.js +39 -0
- package/templates/js/functions/src/webhooks.js +111 -0
- package/templates/{firebase.json → shared/firebase.json} +1 -0
- package/templates/shopify.app.toml +1 -1
- package/templates/ts/functions/src/admin-api.ts +290 -0
- package/templates/web/css/app.css +1287 -47
- package/templates/web/index.html +84 -49
- package/templates/web/js/app.js +177 -0
- package/templates/web/js/pages/home.js +90 -0
- package/templates/web/js/pages/polaris-demo.js +190 -0
- package/templates/web/js/pages/products.js +319 -0
- package/templates/web/js/pages/settings.js +241 -0
- package/templates/web/polaris.html +1149 -0
- package/templates/web/products.html +86 -0
- package/templates/web/settings.html +40 -0
- package/templates/functions/src/admin-api.ts +0 -125
- package/templates/web/js/bridge.js +0 -98
- /package/templates/{env.example → shared/env.example} +0 -0
- /package/templates/{extensions → shared/extensions}/theme-block/assets/app-block.css +0 -0
- /package/templates/{extensions → shared/extensions}/theme-block/assets/app-block.js +0 -0
- /package/templates/{extensions → shared/extensions}/theme-block/blocks/app-block.liquid +0 -0
- /package/templates/{extensions → shared/extensions}/theme-block/locales/en.default.json +0 -0
- /package/templates/{extensions → shared/extensions}/theme-block/shopify.extension.toml +0 -0
- /package/templates/{firestore.indexes.json → shared/firestore.indexes.json} +0 -0
- /package/templates/{firestore.rules → shared/firestore.rules} +0 -0
- /package/templates/{gitignore → shared/gitignore} +0 -0
- /package/templates/{functions → ts/functions}/package.json +0 -0
- /package/templates/{functions → ts/functions}/src/auth.ts +0 -0
- /package/templates/{functions → ts/functions}/src/config.ts +0 -0
- /package/templates/{functions → ts/functions}/src/firebase.ts +0 -0
- /package/templates/{functions → ts/functions}/src/index.ts +0 -0
- /package/templates/{functions → ts/functions}/src/proxy.ts +0 -0
- /package/templates/{functions → ts/functions}/src/verify-token.ts +0 -0
- /package/templates/{functions → ts/functions}/src/webhooks.ts +0 -0
- /package/templates/{functions → ts/functions}/tsconfig.json +0 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const jwt = require("jsonwebtoken");
|
|
2
|
+
const { getConfig } = require("./config");
|
|
3
|
+
|
|
4
|
+
// Middleware: verify App Bridge session token.
|
|
5
|
+
// The embedded dashboard sends Authorization: Bearer <jwt> with every request.
|
|
6
|
+
// Docs: https://shopify.dev/docs/apps/build/authentication/session-tokens
|
|
7
|
+
function verifySessionToken(req, res, next) {
|
|
8
|
+
const authHeader = req.headers.authorization;
|
|
9
|
+
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
10
|
+
res.status(401).json({ error: "Missing Authorization header" });
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const token = authHeader.split(" ")[1];
|
|
15
|
+
const config = getConfig();
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const decoded = jwt.verify(token, config.apiSecret, {
|
|
19
|
+
algorithms: ["HS256"],
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
if (decoded.aud !== config.apiKey) {
|
|
23
|
+
res.status(403).json({ error: "Token audience mismatch" });
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Extract shop from issuer URL
|
|
28
|
+
const issUrl = new URL(decoded.iss);
|
|
29
|
+
req.shopDomain = issUrl.hostname;
|
|
30
|
+
req.sessionToken = decoded;
|
|
31
|
+
|
|
32
|
+
next();
|
|
33
|
+
} catch (err) {
|
|
34
|
+
console.error("Session token verification failed:", err);
|
|
35
|
+
res.status(401).json({ error: "Invalid session token" });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = { verifySessionToken };
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
const crypto = require("crypto");
|
|
2
|
+
const { getConfig } = require("./config");
|
|
3
|
+
const { db } = require("./firebase");
|
|
4
|
+
|
|
5
|
+
// ─── Verify Shopify Webhook HMAC ─────────────────────────────────────────
|
|
6
|
+
function verifyWebhookHmac(rawBody, hmacHeader) {
|
|
7
|
+
const config = getConfig();
|
|
8
|
+
const hash = crypto
|
|
9
|
+
.createHmac("sha256", config.apiSecret)
|
|
10
|
+
.update(rawBody)
|
|
11
|
+
.digest("base64");
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
return crypto.timingSafeEqual(Buffer.from(hash), Buffer.from(hmacHeader));
|
|
15
|
+
} catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Standalone webhook handler — no Express, no middleware.
|
|
22
|
+
*
|
|
23
|
+
* Why standalone? Webhooks must respond 200 within 5 seconds.
|
|
24
|
+
* Skipping Express middleware means faster cold starts and less overhead.
|
|
25
|
+
* Uses req.rawBody (provided natively by Firebase v2) for HMAC verification.
|
|
26
|
+
*/
|
|
27
|
+
async function webhookHandler(req, res) {
|
|
28
|
+
// Only accept POST
|
|
29
|
+
if (req.method !== "POST") {
|
|
30
|
+
res.status(405).send("Method not allowed");
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const hmac = req.headers["x-shopify-hmac-sha256"];
|
|
35
|
+
const topic = req.headers["x-shopify-topic"];
|
|
36
|
+
const shop = req.headers["x-shopify-shop-domain"];
|
|
37
|
+
|
|
38
|
+
// Verify HMAC using rawBody (Buffer, provided by Firebase v2)
|
|
39
|
+
if (req.rawBody && hmac && !verifyWebhookHmac(req.rawBody, hmac)) {
|
|
40
|
+
console.error("Webhook HMAC verification failed");
|
|
41
|
+
res.status(401).send("Unauthorized");
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
console.log(`Webhook: ${topic} from ${shop}`);
|
|
46
|
+
|
|
47
|
+
// Parse body
|
|
48
|
+
let body = {};
|
|
49
|
+
try {
|
|
50
|
+
body = JSON.parse(req.rawBody?.toString("utf8") || "{}");
|
|
51
|
+
} catch {
|
|
52
|
+
// Non-JSON webhook payloads are rare but valid
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
switch (topic) {
|
|
56
|
+
// ── App lifecycle ──────────────────────────────────────────────────
|
|
57
|
+
case "app/uninstalled": {
|
|
58
|
+
await db.collection("shopSessions").doc(shop).delete();
|
|
59
|
+
console.log(`Session cleaned up for ${shop}`);
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ── GDPR mandatory webhooks (required for App Store) ───────────────
|
|
64
|
+
case "customers/data_request": {
|
|
65
|
+
// Customer requested their data. Export within 30 days if you store any.
|
|
66
|
+
console.log(`Customer data request: ${shop}`);
|
|
67
|
+
// TODO: implement if you store customer data
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
case "customers/redact": {
|
|
72
|
+
// Customer requested deletion. Delete within 30 days.
|
|
73
|
+
console.log(`Customer redact: ${shop}`);
|
|
74
|
+
// TODO: implement if you store customer data
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
case "shop/redact": {
|
|
79
|
+
// 48h after uninstall. Delete ALL shop data.
|
|
80
|
+
console.log(`Shop redact: ${shop}`);
|
|
81
|
+
// TODO: delete all data for this shop from Firestore
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
default:
|
|
86
|
+
console.log(`Unhandled webhook: ${topic}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Always respond 200 quickly — do heavy work asynchronously
|
|
90
|
+
res.status(200).send("OK");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
94
|
+
// HOW TO ADD A NEW WEBHOOK HANDLER:
|
|
95
|
+
//
|
|
96
|
+
// 1. Register the topic in shopify.app.toml:
|
|
97
|
+
// [[webhooks.subscriptions]]
|
|
98
|
+
// topics = [ "orders/create" ]
|
|
99
|
+
// uri = "{{APP_URL}}/webhooks"
|
|
100
|
+
//
|
|
101
|
+
// 2. Add a case to the switch above:
|
|
102
|
+
// case "orders/create": {
|
|
103
|
+
// const order = body;
|
|
104
|
+
// // Your logic here
|
|
105
|
+
// break;
|
|
106
|
+
// }
|
|
107
|
+
//
|
|
108
|
+
// 3. Deploy: firebase deploy --only functions:webhooks
|
|
109
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
module.exports = { webhookHandler };
|
|
@@ -22,7 +22,7 @@ api_version = "2026-01"
|
|
|
22
22
|
topics = [ "app/uninstalled" ]
|
|
23
23
|
uri = "{{APP_URL}}/webhooks"
|
|
24
24
|
|
|
25
|
-
# GDPR mandatory webhooks
|
|
25
|
+
# GDPR mandatory webhooks
|
|
26
26
|
[webhooks.privacy_compliance]
|
|
27
27
|
customer_deletion_url = "{{APP_URL}}/webhooks"
|
|
28
28
|
customer_data_request_url = "{{APP_URL}}/webhooks"
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { Router, Request, Response } from "express";
|
|
2
|
+
import { verifySessionToken } from "./verify-token";
|
|
3
|
+
import { getAccessToken } from "./auth";
|
|
4
|
+
import { db } from "./firebase";
|
|
5
|
+
|
|
6
|
+
export const adminApiRouter = Router();
|
|
7
|
+
|
|
8
|
+
// All admin routes require session token verification
|
|
9
|
+
adminApiRouter.use(verifySessionToken);
|
|
10
|
+
|
|
11
|
+
// Shopify API version — update when Shopify releases new versions
|
|
12
|
+
// Docs: https://shopify.dev/docs/api/usage/versioning
|
|
13
|
+
const API_VERSION = "2026-01";
|
|
14
|
+
|
|
15
|
+
// Default app settings — returned when no settings are saved yet
|
|
16
|
+
const DEFAULT_SETTINGS = {
|
|
17
|
+
greeting: "Welcome to our app!",
|
|
18
|
+
theme: "auto",
|
|
19
|
+
notifications: true,
|
|
20
|
+
customCss: "",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// ─── Get shop info ───────────────────────────────────────────────────────
|
|
24
|
+
adminApiRouter.get("/shop", async (req: Request, res: Response) => {
|
|
25
|
+
const shop = (req as any).shopDomain;
|
|
26
|
+
const accessToken = await getAccessToken(shop);
|
|
27
|
+
|
|
28
|
+
if (!accessToken) {
|
|
29
|
+
res.status(401).json({ error: "Shop not authenticated" });
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const response = await fetch(
|
|
35
|
+
`https://${shop}/admin/api/${API_VERSION}/graphql.json`,
|
|
36
|
+
{
|
|
37
|
+
method: "POST",
|
|
38
|
+
headers: {
|
|
39
|
+
"Content-Type": "application/json",
|
|
40
|
+
"X-Shopify-Access-Token": accessToken,
|
|
41
|
+
},
|
|
42
|
+
body: JSON.stringify({
|
|
43
|
+
query: `{
|
|
44
|
+
shop {
|
|
45
|
+
name
|
|
46
|
+
email
|
|
47
|
+
myshopifyDomain
|
|
48
|
+
url
|
|
49
|
+
primaryDomain { url host }
|
|
50
|
+
plan { displayName partnerDevelopment shopifyPlus }
|
|
51
|
+
currencyCode
|
|
52
|
+
ianaTimezone
|
|
53
|
+
billingAddress { country countryCodeV2 }
|
|
54
|
+
productCount: productsCount { count }
|
|
55
|
+
}
|
|
56
|
+
}`,
|
|
57
|
+
}),
|
|
58
|
+
},
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const data = (await response.json()) as any;
|
|
62
|
+
res.json({ shop: data.data?.shop });
|
|
63
|
+
} catch (err: any) {
|
|
64
|
+
console.error("Shop info error:", err);
|
|
65
|
+
res.status(500).json({ error: err.message });
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// ─── Search products ────────────────────────────────────────────────────
|
|
70
|
+
// NOTE: This route MUST be defined before /products/:id so Express
|
|
71
|
+
// doesn't match "search" as a product ID.
|
|
72
|
+
adminApiRouter.get("/products/search", async (req: Request, res: Response) => {
|
|
73
|
+
const shop = (req as any).shopDomain;
|
|
74
|
+
const query = (req.query.q as string) || "";
|
|
75
|
+
const accessToken = await getAccessToken(shop);
|
|
76
|
+
|
|
77
|
+
if (!accessToken) {
|
|
78
|
+
res.status(401).json({ error: "Shop not authenticated" });
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const response = await fetch(
|
|
84
|
+
`https://${shop}/admin/api/${API_VERSION}/graphql.json`,
|
|
85
|
+
{
|
|
86
|
+
method: "POST",
|
|
87
|
+
headers: {
|
|
88
|
+
"Content-Type": "application/json",
|
|
89
|
+
"X-Shopify-Access-Token": accessToken,
|
|
90
|
+
},
|
|
91
|
+
body: JSON.stringify({
|
|
92
|
+
query: `
|
|
93
|
+
query SearchProducts($query: String!) {
|
|
94
|
+
products(first: 10, query: $query) {
|
|
95
|
+
edges {
|
|
96
|
+
node {
|
|
97
|
+
id
|
|
98
|
+
title
|
|
99
|
+
handle
|
|
100
|
+
status
|
|
101
|
+
featuredImage { url }
|
|
102
|
+
variants(first: 1) {
|
|
103
|
+
edges { node { id price } }
|
|
104
|
+
}
|
|
105
|
+
priceRangeV2 {
|
|
106
|
+
minVariantPrice { amount currencyCode }
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
`,
|
|
113
|
+
variables: { query },
|
|
114
|
+
}),
|
|
115
|
+
},
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
const data = (await response.json()) as any;
|
|
119
|
+
const products = (data.data?.products?.edges || []).map((edge: any) => ({
|
|
120
|
+
id: edge.node.id,
|
|
121
|
+
title: edge.node.title,
|
|
122
|
+
handle: edge.node.handle,
|
|
123
|
+
status: edge.node.status,
|
|
124
|
+
image: edge.node.featuredImage?.url || null,
|
|
125
|
+
variantId: edge.node.variants.edges[0]?.node.id || null,
|
|
126
|
+
price: edge.node.priceRangeV2?.minVariantPrice?.amount,
|
|
127
|
+
currency: edge.node.priceRangeV2?.minVariantPrice?.currencyCode,
|
|
128
|
+
}));
|
|
129
|
+
|
|
130
|
+
res.json({ products });
|
|
131
|
+
} catch (err: any) {
|
|
132
|
+
console.error("Product search error:", err);
|
|
133
|
+
res.status(500).json({ error: err.message });
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// ─── Get product detail ─────────────────────────────────────────────────
|
|
138
|
+
adminApiRouter.get("/products/:id", async (req: Request, res: Response) => {
|
|
139
|
+
const shop = (req as any).shopDomain;
|
|
140
|
+
const accessToken = await getAccessToken(shop);
|
|
141
|
+
|
|
142
|
+
if (!accessToken) {
|
|
143
|
+
res.status(401).json({ error: "Shop not authenticated" });
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const productId = req.params.id;
|
|
148
|
+
// Support both raw numeric IDs and full GID format
|
|
149
|
+
const gid = productId.startsWith("gid://")
|
|
150
|
+
? productId
|
|
151
|
+
: `gid://shopify/Product/${productId}`;
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
const response = await fetch(
|
|
155
|
+
`https://${shop}/admin/api/${API_VERSION}/graphql.json`,
|
|
156
|
+
{
|
|
157
|
+
method: "POST",
|
|
158
|
+
headers: {
|
|
159
|
+
"Content-Type": "application/json",
|
|
160
|
+
"X-Shopify-Access-Token": accessToken,
|
|
161
|
+
},
|
|
162
|
+
body: JSON.stringify({
|
|
163
|
+
query: `
|
|
164
|
+
query GetProduct($id: ID!) {
|
|
165
|
+
product(id: $id) {
|
|
166
|
+
id
|
|
167
|
+
title
|
|
168
|
+
handle
|
|
169
|
+
description
|
|
170
|
+
status
|
|
171
|
+
vendor
|
|
172
|
+
productType
|
|
173
|
+
tags
|
|
174
|
+
totalInventory
|
|
175
|
+
priceRangeV2 {
|
|
176
|
+
maxVariantPrice { amount currencyCode }
|
|
177
|
+
minVariantPrice { amount currencyCode }
|
|
178
|
+
}
|
|
179
|
+
featuredImage { url altText }
|
|
180
|
+
images(first: 5) {
|
|
181
|
+
edges { node { url altText } }
|
|
182
|
+
}
|
|
183
|
+
variants(first: 20) {
|
|
184
|
+
edges {
|
|
185
|
+
node {
|
|
186
|
+
id
|
|
187
|
+
title
|
|
188
|
+
price
|
|
189
|
+
sku
|
|
190
|
+
inventoryQuantity
|
|
191
|
+
selectedOptions { name value }
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
`,
|
|
198
|
+
variables: { id: gid },
|
|
199
|
+
}),
|
|
200
|
+
},
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
const data = (await response.json()) as any;
|
|
204
|
+
const product = data.data?.product;
|
|
205
|
+
|
|
206
|
+
if (!product) {
|
|
207
|
+
res.status(404).json({ error: "Product not found" });
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
res.json({ product });
|
|
212
|
+
} catch (err: any) {
|
|
213
|
+
console.error("Product detail error:", err);
|
|
214
|
+
res.status(500).json({ error: err.message });
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// ─── Get app settings ───────────────────────────────────────────────────
|
|
219
|
+
adminApiRouter.get("/settings", async (req: Request, res: Response) => {
|
|
220
|
+
const shop = (req as any).shopDomain;
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
const doc = await db.collection("appSettings").doc(shop).get();
|
|
224
|
+
|
|
225
|
+
if (!doc.exists) {
|
|
226
|
+
res.json({ settings: { ...DEFAULT_SETTINGS } });
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Merge with defaults so new keys are always present
|
|
231
|
+
res.json({ settings: { ...DEFAULT_SETTINGS, ...doc.data() } });
|
|
232
|
+
} catch (err: any) {
|
|
233
|
+
console.error("Get settings error:", err);
|
|
234
|
+
res.status(500).json({ error: err.message });
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// ─── Save app settings ──────────────────────────────────────────────────
|
|
239
|
+
adminApiRouter.post("/settings", async (req: Request, res: Response) => {
|
|
240
|
+
const shop = (req as any).shopDomain;
|
|
241
|
+
const body = req.body;
|
|
242
|
+
|
|
243
|
+
if (!body || typeof body !== "object") {
|
|
244
|
+
res.status(400).json({ error: "Request body must be a JSON object" });
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Only allow known setting keys
|
|
249
|
+
const allowedKeys = Object.keys(DEFAULT_SETTINGS);
|
|
250
|
+
const settings: Record<string, any> = {};
|
|
251
|
+
|
|
252
|
+
for (const key of allowedKeys) {
|
|
253
|
+
if (key in body) {
|
|
254
|
+
settings[key] = body[key];
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (Object.keys(settings).length === 0) {
|
|
259
|
+
res.status(400).json({
|
|
260
|
+
error: "No valid settings provided",
|
|
261
|
+
allowedKeys,
|
|
262
|
+
});
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
await db.collection("appSettings").doc(shop).set(settings, { merge: true });
|
|
268
|
+
|
|
269
|
+
// Return the full merged settings
|
|
270
|
+
const doc = await db.collection("appSettings").doc(shop).get();
|
|
271
|
+
res.json({ settings: { ...DEFAULT_SETTINGS, ...doc.data() } });
|
|
272
|
+
} catch (err: any) {
|
|
273
|
+
console.error("Save settings error:", err);
|
|
274
|
+
res.status(500).json({ error: err.message });
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
279
|
+
// HOW TO ADD A NEW ADMIN API ROUTE:
|
|
280
|
+
//
|
|
281
|
+
// adminApiRouter.post("/my-endpoint", async (req, res) => {
|
|
282
|
+
// const shop = (req as any).shopDomain;
|
|
283
|
+
// const accessToken = await getAccessToken(shop);
|
|
284
|
+
// // Call Shopify Admin API, write to Firestore, etc.
|
|
285
|
+
// res.json({ success: true });
|
|
286
|
+
// });
|
|
287
|
+
//
|
|
288
|
+
// All routes are automatically protected by JWT session token verification.
|
|
289
|
+
// Deploy: firebase deploy --only functions:api
|
|
290
|
+
// ──────────────────────────────────────────────────────────────────────────
|