create-shopify-firebase-app 1.2.1 → 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/README.md +9 -13
- package/lib/index.js +676 -370
- 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,86 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Products - {{APP_NAME}}</title>
|
|
7
|
+
<link rel="stylesheet" href="/css/app.css">
|
|
8
|
+
<script src="https://cdn.shopify.com/shopifycloud/app-bridge.js"></script>
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<ui-nav-menu>
|
|
12
|
+
<a href="/" rel="home">Dashboard</a>
|
|
13
|
+
<a href="/products">Products</a>
|
|
14
|
+
<a href="/settings">Settings</a>
|
|
15
|
+
<a href="/polaris">Components</a>
|
|
16
|
+
</ui-nav-menu>
|
|
17
|
+
|
|
18
|
+
<ui-title-bar title="Products">
|
|
19
|
+
<button variant="primary" onclick="openResourcePicker()">Resource Picker</button>
|
|
20
|
+
</ui-title-bar>
|
|
21
|
+
|
|
22
|
+
<div class="page">
|
|
23
|
+
<!-- Search bar -->
|
|
24
|
+
<div class="card">
|
|
25
|
+
<div class="search-wrapper" style="margin-bottom: 0;">
|
|
26
|
+
<span class="search-icon">🔍</span>
|
|
27
|
+
<input
|
|
28
|
+
type="text"
|
|
29
|
+
class="search-input"
|
|
30
|
+
id="search-input"
|
|
31
|
+
placeholder="Search products by title..."
|
|
32
|
+
autocomplete="off"
|
|
33
|
+
>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<!-- Action bar -->
|
|
38
|
+
<div class="flex items-center justify-between mb-4">
|
|
39
|
+
<p class="text-secondary text-sm" id="results-count"></p>
|
|
40
|
+
<button class="btn btn-sm" onclick="openResourcePicker()">
|
|
41
|
+
🔎 Use Resource Picker
|
|
42
|
+
</button>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<!-- Results container -->
|
|
46
|
+
<div id="products-container">
|
|
47
|
+
<div class="card">
|
|
48
|
+
<div class="empty-state">
|
|
49
|
+
<div class="empty-state-icon">🛒</div>
|
|
50
|
+
<h3>Search for products</h3>
|
|
51
|
+
<p>Enter a search term above to find products in your store, or use the Resource Picker to browse.</p>
|
|
52
|
+
<button class="btn btn-primary" onclick="document.getElementById('search-input').focus()">
|
|
53
|
+
Start searching
|
|
54
|
+
</button>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<!-- Product detail modal -->
|
|
61
|
+
<ui-modal id="product-detail-modal" variant="large">
|
|
62
|
+
<div id="product-detail-content" style="padding: 16px;">
|
|
63
|
+
<div class="loading-state">
|
|
64
|
+
<div class="spinner"></div>
|
|
65
|
+
<p>Loading product details...</p>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
<ui-title-bar title="Product Details">
|
|
69
|
+
<button variant="primary" onclick="document.getElementById('product-detail-modal').hide()">Done</button>
|
|
70
|
+
</ui-title-bar>
|
|
71
|
+
</ui-modal>
|
|
72
|
+
|
|
73
|
+
<!-- Resource picker result modal -->
|
|
74
|
+
<ui-modal id="picker-result-modal">
|
|
75
|
+
<div id="picker-result-content" style="padding: 16px;">
|
|
76
|
+
<p class="text-secondary">No products selected.</p>
|
|
77
|
+
</div>
|
|
78
|
+
<ui-title-bar title="Selected Products">
|
|
79
|
+
<button variant="primary" onclick="document.getElementById('picker-result-modal').hide()">Close</button>
|
|
80
|
+
</ui-title-bar>
|
|
81
|
+
</ui-modal>
|
|
82
|
+
|
|
83
|
+
<script src="/js/app.js"></script>
|
|
84
|
+
<script src="/js/pages/products.js"></script>
|
|
85
|
+
</body>
|
|
86
|
+
</html>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Settings - {{APP_NAME}}</title>
|
|
7
|
+
<link rel="stylesheet" href="/css/app.css">
|
|
8
|
+
<script src="https://cdn.shopify.com/shopifycloud/app-bridge.js"></script>
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<ui-nav-menu>
|
|
12
|
+
<a href="/" rel="home">Dashboard</a>
|
|
13
|
+
<a href="/products">Products</a>
|
|
14
|
+
<a href="/settings">Settings</a>
|
|
15
|
+
<a href="/polaris">Components</a>
|
|
16
|
+
</ui-nav-menu>
|
|
17
|
+
|
|
18
|
+
<ui-title-bar title="Settings">
|
|
19
|
+
<button variant="primary" onclick="saveSettings()">Save</button>
|
|
20
|
+
</ui-title-bar>
|
|
21
|
+
|
|
22
|
+
<ui-save-bar id="save-bar">
|
|
23
|
+
<button id="save-btn" variant="primary"></button>
|
|
24
|
+
<button id="discard-btn"></button>
|
|
25
|
+
</ui-save-bar>
|
|
26
|
+
|
|
27
|
+
<div class="page page-narrow">
|
|
28
|
+
<!-- Settings form -->
|
|
29
|
+
<div id="settings-container">
|
|
30
|
+
<div class="loading-state">
|
|
31
|
+
<div class="spinner"></div>
|
|
32
|
+
<p>Loading settings...</p>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<script src="/js/app.js"></script>
|
|
38
|
+
<script src="/js/pages/settings.js"></script>
|
|
39
|
+
</body>
|
|
40
|
+
</html>
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
import { Router, Request, Response } from "express";
|
|
2
|
-
import { verifySessionToken } from "./verify-token";
|
|
3
|
-
import { getAccessToken } from "./auth";
|
|
4
|
-
|
|
5
|
-
export const adminApiRouter = Router();
|
|
6
|
-
|
|
7
|
-
// All admin routes require session token verification
|
|
8
|
-
adminApiRouter.use(verifySessionToken);
|
|
9
|
-
|
|
10
|
-
// Shopify API version — update when Shopify releases new versions
|
|
11
|
-
// Docs: https://shopify.dev/docs/api/usage/versioning
|
|
12
|
-
const API_VERSION = "2026-01";
|
|
13
|
-
|
|
14
|
-
// ─── Get shop info ───────────────────────────────────────────────────────
|
|
15
|
-
adminApiRouter.get("/shop", async (req: Request, res: Response) => {
|
|
16
|
-
const shop = (req as any).shopDomain;
|
|
17
|
-
const accessToken = await getAccessToken(shop);
|
|
18
|
-
|
|
19
|
-
if (!accessToken) {
|
|
20
|
-
res.status(401).json({ error: "Shop not authenticated" });
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
try {
|
|
25
|
-
const response = await fetch(
|
|
26
|
-
`https://${shop}/admin/api/${API_VERSION}/graphql.json`,
|
|
27
|
-
{
|
|
28
|
-
method: "POST",
|
|
29
|
-
headers: {
|
|
30
|
-
"Content-Type": "application/json",
|
|
31
|
-
"X-Shopify-Access-Token": accessToken,
|
|
32
|
-
},
|
|
33
|
-
body: JSON.stringify({
|
|
34
|
-
query: `{ shop { name email myshopifyDomain plan { displayName } } }`,
|
|
35
|
-
}),
|
|
36
|
-
},
|
|
37
|
-
);
|
|
38
|
-
|
|
39
|
-
const data = (await response.json()) as any;
|
|
40
|
-
res.json({ shop: data.data?.shop });
|
|
41
|
-
} catch (err: any) {
|
|
42
|
-
console.error("Shop info error:", err);
|
|
43
|
-
res.status(500).json({ error: err.message });
|
|
44
|
-
}
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
// ─── Search products (example) ───────────────────────────────────────────
|
|
48
|
-
adminApiRouter.get("/products/search", async (req: Request, res: Response) => {
|
|
49
|
-
const shop = (req as any).shopDomain;
|
|
50
|
-
const query = (req.query.q as string) || "";
|
|
51
|
-
const accessToken = await getAccessToken(shop);
|
|
52
|
-
|
|
53
|
-
if (!accessToken) {
|
|
54
|
-
res.status(401).json({ error: "Shop not authenticated" });
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
try {
|
|
59
|
-
const response = await fetch(
|
|
60
|
-
`https://${shop}/admin/api/${API_VERSION}/graphql.json`,
|
|
61
|
-
{
|
|
62
|
-
method: "POST",
|
|
63
|
-
headers: {
|
|
64
|
-
"Content-Type": "application/json",
|
|
65
|
-
"X-Shopify-Access-Token": accessToken,
|
|
66
|
-
},
|
|
67
|
-
body: JSON.stringify({
|
|
68
|
-
query: `
|
|
69
|
-
query SearchProducts($query: String!) {
|
|
70
|
-
products(first: 10, query: $query) {
|
|
71
|
-
edges {
|
|
72
|
-
node {
|
|
73
|
-
id
|
|
74
|
-
title
|
|
75
|
-
handle
|
|
76
|
-
status
|
|
77
|
-
featuredImage { url }
|
|
78
|
-
variants(first: 1) {
|
|
79
|
-
edges { node { id price } }
|
|
80
|
-
}
|
|
81
|
-
priceRangeV2 {
|
|
82
|
-
minVariantPrice { amount currencyCode }
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
`,
|
|
89
|
-
variables: { query },
|
|
90
|
-
}),
|
|
91
|
-
},
|
|
92
|
-
);
|
|
93
|
-
|
|
94
|
-
const data = (await response.json()) as any;
|
|
95
|
-
const products = (data.data?.products?.edges || []).map((edge: any) => ({
|
|
96
|
-
id: edge.node.id,
|
|
97
|
-
title: edge.node.title,
|
|
98
|
-
handle: edge.node.handle,
|
|
99
|
-
status: edge.node.status,
|
|
100
|
-
image: edge.node.featuredImage?.url || null,
|
|
101
|
-
variantId: edge.node.variants.edges[0]?.node.id || null,
|
|
102
|
-
price: edge.node.priceRangeV2?.minVariantPrice?.amount,
|
|
103
|
-
currency: edge.node.priceRangeV2?.minVariantPrice?.currencyCode,
|
|
104
|
-
}));
|
|
105
|
-
|
|
106
|
-
res.json({ products });
|
|
107
|
-
} catch (err: any) {
|
|
108
|
-
console.error("Product search error:", err);
|
|
109
|
-
res.status(500).json({ error: err.message });
|
|
110
|
-
}
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
// ──────────────────────────────────────────────────────────────────────────
|
|
114
|
-
// HOW TO ADD A NEW ADMIN API ROUTE:
|
|
115
|
-
//
|
|
116
|
-
// adminApiRouter.post("/my-endpoint", async (req, res) => {
|
|
117
|
-
// const shop = (req as any).shopDomain;
|
|
118
|
-
// const accessToken = await getAccessToken(shop);
|
|
119
|
-
// // Call Shopify Admin API, write to Firestore, etc.
|
|
120
|
-
// res.json({ success: true });
|
|
121
|
-
// });
|
|
122
|
-
//
|
|
123
|
-
// All routes are automatically protected by JWT session token verification.
|
|
124
|
-
// Deploy: firebase deploy --only functions:api
|
|
125
|
-
// ──────────────────────────────────────────────────────────────────────────
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* App Bridge helper — handles authentication, API calls, and navigation.
|
|
3
|
-
*
|
|
4
|
-
* Security is handled by App Bridge (session tokens) and server-side
|
|
5
|
-
* verification. No manual iframe checks needed.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
(function () {
|
|
9
|
-
"use strict";
|
|
10
|
-
|
|
11
|
-
window.App = {
|
|
12
|
-
ready: false,
|
|
13
|
-
shop: null,
|
|
14
|
-
host: null,
|
|
15
|
-
|
|
16
|
-
async init() {
|
|
17
|
-
const params = new URLSearchParams(window.location.search);
|
|
18
|
-
this.host = params.get("host");
|
|
19
|
-
this.shop = params.get("shop");
|
|
20
|
-
|
|
21
|
-
await this._waitForShopify();
|
|
22
|
-
|
|
23
|
-
if (!window.shopify) {
|
|
24
|
-
document.getElementById("app").innerHTML = `
|
|
25
|
-
<div style="text-align:center;padding:60px 20px;color:#6d7175;">
|
|
26
|
-
<h2 style="color:#1a1a1a;">Loading...</h2>
|
|
27
|
-
<p>If this persists, reload from the Shopify admin.</p>
|
|
28
|
-
</div>
|
|
29
|
-
`;
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
this.ready = true;
|
|
34
|
-
window.dispatchEvent(new Event("app-ready"));
|
|
35
|
-
},
|
|
36
|
-
|
|
37
|
-
_waitForShopify() {
|
|
38
|
-
return new Promise((resolve) => {
|
|
39
|
-
if (window.shopify) return resolve();
|
|
40
|
-
let attempts = 0;
|
|
41
|
-
const interval = setInterval(() => {
|
|
42
|
-
if (window.shopify || ++attempts > 50) {
|
|
43
|
-
clearInterval(interval);
|
|
44
|
-
resolve();
|
|
45
|
-
}
|
|
46
|
-
}, 100);
|
|
47
|
-
});
|
|
48
|
-
},
|
|
49
|
-
|
|
50
|
-
async getSessionToken() {
|
|
51
|
-
if (!window.shopify) throw new Error("App Bridge not loaded");
|
|
52
|
-
return await window.shopify.idToken();
|
|
53
|
-
},
|
|
54
|
-
|
|
55
|
-
async apiFetch(path, options = {}) {
|
|
56
|
-
const token = await this.getSessionToken();
|
|
57
|
-
const resp = await fetch(path, {
|
|
58
|
-
...options,
|
|
59
|
-
headers: {
|
|
60
|
-
"Content-Type": "application/json",
|
|
61
|
-
Authorization: `Bearer ${token}`,
|
|
62
|
-
...(options.headers || {}),
|
|
63
|
-
},
|
|
64
|
-
});
|
|
65
|
-
if (!resp.ok) {
|
|
66
|
-
const errText = await resp.text();
|
|
67
|
-
throw new Error(`API Error ${resp.status}: ${errText}`);
|
|
68
|
-
}
|
|
69
|
-
return resp.json();
|
|
70
|
-
},
|
|
71
|
-
|
|
72
|
-
navigate(page) {
|
|
73
|
-
const url = new URL(page, window.location.origin);
|
|
74
|
-
if (this.host) url.searchParams.set("host", this.host);
|
|
75
|
-
if (this.shop) url.searchParams.set("shop", this.shop);
|
|
76
|
-
window.location.href = url.pathname + url.search;
|
|
77
|
-
},
|
|
78
|
-
|
|
79
|
-
showToast(message) {
|
|
80
|
-
if (window.shopify?.toast) {
|
|
81
|
-
window.shopify.toast.show(message);
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
const existing = document.querySelector(".toast");
|
|
85
|
-
if (existing) existing.remove();
|
|
86
|
-
const toast = document.createElement("div");
|
|
87
|
-
toast.className = "toast show";
|
|
88
|
-
toast.textContent = message;
|
|
89
|
-
document.body.appendChild(toast);
|
|
90
|
-
setTimeout(() => {
|
|
91
|
-
toast.classList.remove("show");
|
|
92
|
-
setTimeout(() => toast.remove(), 300);
|
|
93
|
-
}, 3000);
|
|
94
|
-
},
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
document.addEventListener("DOMContentLoaded", () => window.App.init());
|
|
98
|
-
})();
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|