@tokenoftrust/cli 1.0.0 → 1.1.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/bin/tot.mjs +57 -4
- package/package.json +2 -1
- package/src/activity-log.mjs +112 -0
- package/src/auth.mjs +1 -1
- package/src/commands/dev.mjs +274 -17
- package/src/commands/feedback.mjs +176 -0
- package/src/commands/login.mjs +39 -2
- package/src/commands/logout.mjs +39 -0
- package/src/commands/start.mjs +173 -15
- package/src/oauth.mjs +77 -0
- package/src/sample.mjs +202 -0
- package/src/token-store.mjs +17 -1
- package/template/sample-store/content/chrome.html +152 -0
- package/template/sample-store/content/chrome.json +94 -0
- package/template/sample-store/content/home.html +194 -0
- package/template/sample-store/content/home.json +50 -0
- package/template/sample-store/content/pages/about.json +10 -0
- package/template/sample-store/content/pages/privacy.json +6 -0
- package/template/sample-store/content/pages/shipping-returns.json +6 -0
- package/template/sample-store/content/pages-html/blogs/news.html +68 -0
- package/template/sample-store/content/pages-html/pages/about-us.html +95 -0
- package/template/sample-store/content/pages-html/pages/contact-us.html +68 -0
- package/template/sample-store/content/pages-html/pages/privacy-policy.html +65 -0
- package/template/sample-store/content/pages-html/pages/shipping-returns.html +77 -0
- package/template/sample-store/content/themes/giant-navy.json +73 -0
- package/template/sample-store/public/img/hero-suicide-bunny.jpg +0 -0
- package/template/sample-store/public/img/hero.webp +0 -0
- package/template/sample-store/public/img/og.jpg +0 -0
- package/template/sample-store/public/logo-wordmark.png +0 -0
- package/template/sample-store/public/pages/home.css +120 -0
- package/template/sample-store/public/pages/mkt.css +185 -0
- package/template/sample-store/public/pages/page.css +155 -0
- package/template/sample-store/public/themes/giant-navy.css +76 -0
- package/template/sample-store/theme.json +39 -0
package/src/sample.mjs
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sample-store scaffolding — the ZERO-LOGIN, NO-MCP offline quickstart.
|
|
3
|
+
*
|
|
4
|
+
* The free first taste: `npx @tokenoftrust/cli dev --sample` (or `tot start`
|
|
5
|
+
* with no session) materializes a real, editable tenant checkout on disk from a
|
|
6
|
+
* template BUNDLED in this package, then runs it natively (no Docker, no MCP, no
|
|
7
|
+
* network). It STILL visibly does the ToT thing — the age-gate overlay and the
|
|
8
|
+
* nicotine-warning bar render — because the sample reuses a registered,
|
|
9
|
+
* regulated tenant's identity so the runner's tenant registry resolves it with
|
|
10
|
+
* its `compliance` block on.
|
|
11
|
+
*
|
|
12
|
+
* WHY GIANTVAPES (not a novel synthetic domain): compliance + catalog are
|
|
13
|
+
* resolved by the RUNNER's tenant registry (apps/storefront/src/config/tenants.ts),
|
|
14
|
+
* keyed by domain. `giantvapes.com` is a registered 21+/PACT vape store WITH a
|
|
15
|
+
* `compliance` block and a 50-product catalog fixture baked into the runner, so
|
|
16
|
+
* the sample lights up age-gate + nicotine warning + a populated catalog with
|
|
17
|
+
* zero server. A truly novel domain (e.g. "sample.example") is unclaimed → the
|
|
18
|
+
* runner falls back to HOME_TENANT (siteType "marketing", NO compliance, empty
|
|
19
|
+
* catalog), which would defeat the whole "the free taste still does compliance"
|
|
20
|
+
* goal — so we deliberately borrow giantvapes' identity for the sample. It's the
|
|
21
|
+
* smallest of the vapor tenants (448K), so it keeps the npm tarball light.
|
|
22
|
+
*
|
|
23
|
+
* Dependency-free (node:fs + node:path only).
|
|
24
|
+
*/
|
|
25
|
+
import {
|
|
26
|
+
cpSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync,
|
|
27
|
+
} from "node:fs";
|
|
28
|
+
import { fileURLToPath } from "node:url";
|
|
29
|
+
import { dirname, join, resolve } from "node:path";
|
|
30
|
+
|
|
31
|
+
const here = dirname(fileURLToPath(import.meta.url)); // packages/cli/src
|
|
32
|
+
|
|
33
|
+
/** The tenant identity the sample borrows so compliance + catalog resolve (see module doc). */
|
|
34
|
+
export const SAMPLE_TENANT = "giantvapes";
|
|
35
|
+
/** Dotted scope → the runner's path-prefix route + registry lookup (must contain a dot per context.mjs). */
|
|
36
|
+
export const SAMPLE_SCOPE = "giantvapes.com";
|
|
37
|
+
/** Default directory name scaffolded when the user doesn't name one. */
|
|
38
|
+
export const SAMPLE_DIR_NAME = "sample-store";
|
|
39
|
+
|
|
40
|
+
/** Absolute path to the bundled template tree (shipped in the npm tarball via package.json `files`). */
|
|
41
|
+
export function sampleTemplateDir() {
|
|
42
|
+
return resolve(here, "..", "template", "sample-store");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The `.tot/config.json` a scaffolded sample checkout carries. Same shape a real
|
|
47
|
+
* `tenant_checkout` produces ({tenant, scope, mappings[]}) plus a `sample: true`
|
|
48
|
+
* marker so the CLI can recognize a re-run inside a scaffolded sample and reuse
|
|
49
|
+
* it. `repo` fields mirror the colocated identity map (tot-dev grafts
|
|
50
|
+
* structurally and ignores `repo`, but including them keeps `tot validate` clean).
|
|
51
|
+
*/
|
|
52
|
+
export function sampleConfig() {
|
|
53
|
+
return {
|
|
54
|
+
tenant: SAMPLE_TENANT,
|
|
55
|
+
scope: SAMPLE_SCOPE,
|
|
56
|
+
sample: true,
|
|
57
|
+
mappings: [
|
|
58
|
+
{ workspace: "theme.json", repo: `tenants/${SAMPLE_TENANT}/theme.json`, kind: "file" },
|
|
59
|
+
{ workspace: "content/", repo: `tenants/${SAMPLE_TENANT}/content/`, kind: "tree" },
|
|
60
|
+
{ workspace: "public/", repo: `tenants/${SAMPLE_TENANT}/public/`, kind: "tree" },
|
|
61
|
+
],
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Is `dir` a checkout we scaffolded earlier (so a re-run reuses it instead of erroring)? */
|
|
66
|
+
export function isSampleCheckout(dir) {
|
|
67
|
+
const p = join(dir, ".tot", "config.json");
|
|
68
|
+
if (!existsSync(p)) return false;
|
|
69
|
+
try {
|
|
70
|
+
return JSON.parse(readFileSync(p, "utf8")).sample === true;
|
|
71
|
+
} catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** A directory is "empty enough" to scaffold into if it's absent or has no entries. */
|
|
77
|
+
function isEmptyDir(dir) {
|
|
78
|
+
if (!existsSync(dir)) return true;
|
|
79
|
+
try {
|
|
80
|
+
return readdirSync(dir).length === 0;
|
|
81
|
+
} catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Materialize a runnable sample checkout at `destDir`: copy the bundled template
|
|
88
|
+
* (content/ public/ theme.json) in, then write .tot/config.json. Idempotent —
|
|
89
|
+
* re-running against an existing sample checkout is a no-op that returns it. Refuses
|
|
90
|
+
* to clobber a non-sample, non-empty directory.
|
|
91
|
+
*
|
|
92
|
+
* @param {string} destDir
|
|
93
|
+
* @param {{ force?: boolean, log?: (m: string) => void }} [opts]
|
|
94
|
+
* @returns {{ dir: string, tenant: string, scope: string, created: boolean }}
|
|
95
|
+
*/
|
|
96
|
+
export function scaffoldSample(destDir, { force = false, log = () => {} } = {}) {
|
|
97
|
+
const dir = resolve(destDir);
|
|
98
|
+
|
|
99
|
+
if (isSampleCheckout(dir)) {
|
|
100
|
+
log(` ✓ reusing existing sample checkout ${dir}`);
|
|
101
|
+
return { dir, tenant: SAMPLE_TENANT, scope: SAMPLE_SCOPE, created: false };
|
|
102
|
+
}
|
|
103
|
+
if (!force && !isEmptyDir(dir)) {
|
|
104
|
+
const err = new Error(
|
|
105
|
+
`${dir} isn't empty and isn't a sample checkout — scaffold into an empty directory (or pass a new --workspace)`,
|
|
106
|
+
);
|
|
107
|
+
err.code = "ENOTEMPTY_SAMPLE";
|
|
108
|
+
throw err;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const template = sampleTemplateDir();
|
|
112
|
+
if (!existsSync(template)) {
|
|
113
|
+
throw new Error(`bundled sample template missing at ${template} — is the package built/installed correctly?`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
mkdirSync(dir, { recursive: true });
|
|
117
|
+
for (const entry of ["content", "public", "theme.json"]) {
|
|
118
|
+
const from = join(template, entry);
|
|
119
|
+
if (existsSync(from)) cpSync(from, join(dir, entry), { recursive: true });
|
|
120
|
+
}
|
|
121
|
+
mkdirSync(join(dir, ".tot"), { recursive: true });
|
|
122
|
+
writeFileSync(join(dir, ".tot", "config.json"), JSON.stringify(sampleConfig(), null, 2) + "\n");
|
|
123
|
+
|
|
124
|
+
log(` ✓ scaffolded a sample store → ${dir}`);
|
|
125
|
+
return { dir, tenant: SAMPLE_TENANT, scope: SAMPLE_SCOPE, created: true };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Resolve where the RENDERER comes from for the offline sample run — WITHOUT
|
|
130
|
+
* calling the entitlement-gated `dev_renderer_artifact` MCP tool (that's the whole
|
|
131
|
+
* point: the free taste needs no login). This is the clean SEAM the sibling WS3b
|
|
132
|
+
* task fills for public renderer delivery.
|
|
133
|
+
*
|
|
134
|
+
* Resolution order (first hit wins), all offline/no-MCP:
|
|
135
|
+
* 1. TOT_RUNNER_DIR — an already-extracted, installed runner tree. Used as-is.
|
|
136
|
+
* 2. TOT_RUNNER_TARBALL — a local .tar.gz (or file://) path to extract+install.
|
|
137
|
+
* 3. TOT_RUNNER_URL — a PUBLIC https tarball URL. ← the seam WS3b bakes a
|
|
138
|
+
* default into (a well-known public CDN URL), so a bare
|
|
139
|
+
* `npx … dev --sample` on a clean machine Just Works.
|
|
140
|
+
* 4. cache reuse — a runner a prior authenticated `tot dev` already
|
|
141
|
+
* downloaded under ~/.tot/cache/renderer/<version>/.
|
|
142
|
+
* Piggybacks on it with no network at all.
|
|
143
|
+
* 5. monorepo — inside the storefront monorepo the runner IS the tree;
|
|
144
|
+
* run its in-tree scripts/tot-dev.mjs directly.
|
|
145
|
+
* 6. none — nothing local + no public URL yet → caller emits the
|
|
146
|
+
* exact seam WS3b must fill.
|
|
147
|
+
*
|
|
148
|
+
* Pure/deterministic given its inputs (env + fs probes) — no network, no MCP.
|
|
149
|
+
* @param {{ env?: NodeJS.ProcessEnv, mode?: string, repoRoot?: string|null, cacheRoot?: string }} [opts]
|
|
150
|
+
* @returns {{ kind: "dir"|"tarball"|"none", dir?: string, source?: string, isUrl?: boolean, why: string }}
|
|
151
|
+
*/
|
|
152
|
+
export function resolveRendererSource({ env = process.env, mode = "loose", repoRoot = null, cacheRoot } = {}) {
|
|
153
|
+
if (env.TOT_RUNNER_DIR) {
|
|
154
|
+
return { kind: "dir", dir: resolve(env.TOT_RUNNER_DIR), why: "TOT_RUNNER_DIR" };
|
|
155
|
+
}
|
|
156
|
+
if (env.TOT_RUNNER_TARBALL) {
|
|
157
|
+
return { kind: "tarball", source: env.TOT_RUNNER_TARBALL, isUrl: false, why: "TOT_RUNNER_TARBALL" };
|
|
158
|
+
}
|
|
159
|
+
if (env.TOT_RUNNER_URL) {
|
|
160
|
+
return { kind: "tarball", source: env.TOT_RUNNER_URL, isUrl: true, why: "TOT_RUNNER_URL" };
|
|
161
|
+
}
|
|
162
|
+
const cached = newestCachedRunner(cacheRoot);
|
|
163
|
+
if (cached) {
|
|
164
|
+
return { kind: "dir", dir: cached, why: "cached runner (prior `tot dev`)" };
|
|
165
|
+
}
|
|
166
|
+
if (mode === "monorepo" && repoRoot && existsSync(join(repoRoot, "scripts", "tot-dev.mjs"))) {
|
|
167
|
+
return { kind: "dir", dir: repoRoot, why: "in-tree monorepo runner" };
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
kind: "none",
|
|
171
|
+
why:
|
|
172
|
+
"no local runner and no public renderer URL configured yet (WS3b seam) — set TOT_RUNNER_URL / TOT_RUNNER_TARBALL / TOT_RUNNER_DIR, or run once inside the storefront monorepo",
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Newest fully-installed runner under the renderer cache (dirs carry a
|
|
178
|
+
* `.tot-cache-complete` marker written by dev.mjs). Lets the sample path reuse a
|
|
179
|
+
* runner a prior authenticated run already fetched, with no network. Returns the
|
|
180
|
+
* absolute dir or null.
|
|
181
|
+
*/
|
|
182
|
+
function newestCachedRunner(cacheRoot) {
|
|
183
|
+
if (!cacheRoot || !existsSync(cacheRoot)) return null;
|
|
184
|
+
let best = null;
|
|
185
|
+
let bestMtime = -1;
|
|
186
|
+
for (const name of readdirSync(cacheRoot)) {
|
|
187
|
+
const dir = join(cacheRoot, name);
|
|
188
|
+
const marker = join(dir, ".tot-cache-complete");
|
|
189
|
+
if (!existsSync(marker)) continue;
|
|
190
|
+
let mtime;
|
|
191
|
+
try {
|
|
192
|
+
mtime = statSync(marker).mtimeMs;
|
|
193
|
+
} catch {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (mtime > bestMtime) {
|
|
197
|
+
bestMtime = mtime;
|
|
198
|
+
best = dir;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return best;
|
|
202
|
+
}
|
package/src/token-store.mjs
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* `TOT_HOME` overrides the home dir (used by tests to point at a temp dir).
|
|
16
16
|
*/
|
|
17
17
|
import {
|
|
18
|
-
readFileSync, writeFileSync, mkdirSync, renameSync, chmodSync,
|
|
18
|
+
readFileSync, writeFileSync, mkdirSync, renameSync, chmodSync, existsSync, rmSync,
|
|
19
19
|
} from "node:fs";
|
|
20
20
|
import { homedir } from "node:os";
|
|
21
21
|
import { join, dirname } from "node:path";
|
|
@@ -63,3 +63,19 @@ export function isExpired(creds, { now = Date.now(), skewMs = 60_000 } = {}) {
|
|
|
63
63
|
if (!creds || !creds.expiresAt) return false;
|
|
64
64
|
return now >= creds.expiresAt - skewMs;
|
|
65
65
|
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Delete the cached session (the whole "signed in" state), idempotently. Returns
|
|
69
|
+
* true if a credential file was actually removed, false if there was nothing to
|
|
70
|
+
* remove. Never throws — "already signed out" is success. Local-only: this clears
|
|
71
|
+
* the machine's cache; it does NOT revoke the grant server-side.
|
|
72
|
+
*/
|
|
73
|
+
export function clearCredentials(filePath) {
|
|
74
|
+
const existed = existsSync(filePath);
|
|
75
|
+
try {
|
|
76
|
+
rmSync(filePath, { force: true });
|
|
77
|
+
} catch {
|
|
78
|
+
/* best-effort: a missing/locked file still means "not signed in here" */
|
|
79
|
+
}
|
|
80
|
+
return existed;
|
|
81
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
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
|
+
<link rel="icon" type="image/png" href="/tenants/giantvapes/logo-wordmark.png">
|
|
7
|
+
<link rel="stylesheet" href="/tenants/giantvapes/themes/giant-navy.css">
|
|
8
|
+
<link rel="stylesheet" href="/tenants/giantvapes/pages/mkt.css">
|
|
9
|
+
<!--PAGE_HEAD-->
|
|
10
|
+
</head>
|
|
11
|
+
<body>
|
|
12
|
+
<a class="skip-link" href="#main">Skip to main content</a>
|
|
13
|
+
|
|
14
|
+
<!-- Top FDA warning bar -->
|
|
15
|
+
<div class="topbar-warning"><p>WARNING: This product contains nicotine. Nicotine is an addictive chemical.</p></div>
|
|
16
|
+
|
|
17
|
+
<!-- Announcement: scrolling marquee -->
|
|
18
|
+
<div class="announce-marquee" aria-label="Free US shipping on orders over $100">
|
|
19
|
+
<div class="announce-marquee__track" aria-hidden="true">
|
|
20
|
+
<span>FREE U.S. Shipping on Orders Over $100!</span><span>FREE U.S. Shipping on Orders Over $100!</span><span>FREE U.S. Shipping on Orders Over $100!</span><span>FREE U.S. Shipping on Orders Over $100!</span>
|
|
21
|
+
<span>FREE U.S. Shipping on Orders Over $100!</span><span>FREE U.S. Shipping on Orders Over $100!</span><span>FREE U.S. Shipping on Orders Over $100!</span><span>FREE U.S. Shipping on Orders Over $100!</span>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<!-- Utility nav -->
|
|
26
|
+
<div class="utility-nav">
|
|
27
|
+
<div class="container">
|
|
28
|
+
<ul>
|
|
29
|
+
<li><a href="/blogs/news">News</a></li>
|
|
30
|
+
<li><a href="/pages/about-us">About</a></li>
|
|
31
|
+
<li><a href="/pages/contact-us">Contact</a></li>
|
|
32
|
+
<li><a href="/pages/faq">FAQ</a></li>
|
|
33
|
+
<li><a href="/pages/order-tracking">Track Order</a></li>
|
|
34
|
+
</ul>
|
|
35
|
+
<div class="utility-nav__right">
|
|
36
|
+
<span class="sel">English</span>
|
|
37
|
+
<span class="sel">United States (USD $)</span>
|
|
38
|
+
<span class="social">
|
|
39
|
+
<a href="https://instagram.com/giantvapes" aria-label="Instagram" rel="noopener"><svg viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="5"/><circle cx="12" cy="12" r="4"/><circle cx="17.5" cy="6.5" r="1.1"/></svg></a>
|
|
40
|
+
<a href="https://facebook.com/giantvapes" aria-label="Facebook" rel="noopener"><svg viewBox="0 0 24 24"><path d="M14 8h2V5h-2a4 4 0 0 0-4 4v2H8v3h2v6h3v-6h2.2l.8-3H13V9a1 1 0 0 1 1-1z"/></svg></a>
|
|
41
|
+
</span>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<!-- Header -->
|
|
47
|
+
<header class="site-header">
|
|
48
|
+
<div class="container header-row">
|
|
49
|
+
<a class="brand" href="/" aria-label="Giant Vapes home"><img src="/tenants/giantvapes/logo-wordmark.png" alt="Giant Vapes" width="200" height="106"></a>
|
|
50
|
+
<form class="header-search" action="/search" method="get" role="search">
|
|
51
|
+
<label class="sr-only" for="hdr-q">Search for anything</label>
|
|
52
|
+
<input id="hdr-q" type="search" name="q" placeholder="Search e-liquids, disposables, hardware" autocomplete="off">
|
|
53
|
+
<button type="submit" aria-label="Search"><svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg></button>
|
|
54
|
+
</form>
|
|
55
|
+
<div class="header-actions">
|
|
56
|
+
<a href="/account" aria-label="Account"><svg viewBox="0 0 24 24"><circle cx="12" cy="8" r="4"/><path d="M4 21c0-4.4 3.6-7 8-7s8 2.6 8 7"/></svg></a>
|
|
57
|
+
<a class="cart" href="/cart" aria-label="Cart"><svg viewBox="0 0 24 24"><path d="M6 6h15l-1.5 9h-12z"/><circle cx="9" cy="20" r="1.4"/><circle cx="18" cy="20" r="1.4"/><path d="M6 6L5 3H2"/></svg><span class="count">0</span></a>
|
|
58
|
+
<button class="hamburger" id="hamburger" aria-label="Toggle navigation menu" aria-expanded="false" aria-controls="primaryNav"><span></span><span></span><span></span></button>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
<nav class="primary-nav-bar" aria-label="Primary">
|
|
62
|
+
<div class="container">
|
|
63
|
+
<nav class="primary-nav" id="primaryNav">
|
|
64
|
+
<ul>
|
|
65
|
+
<li><a href="/collections/all">E-Liquid<span class="caret">▾</span></a></li>
|
|
66
|
+
<li><a href="/collections/salt-nic">Salt Nic</a></li>
|
|
67
|
+
<li><a href="/collections/all-disposables">Disposables<span class="caret">▾</span></a></li>
|
|
68
|
+
<li><a href="/collections/vaporesso">Hardware<span class="caret">▾</span></a></li>
|
|
69
|
+
<li><a href="/collections/nicotine-pouches">Nic Pouches</a></li>
|
|
70
|
+
<li><a href="/collections/new-arrivals">New Arrivals</a></li>
|
|
71
|
+
<li><a class="is-flag" href="/collections/clearance">Clearance<span class="new-badge">SALE</span></a></li>
|
|
72
|
+
<li><a href="/collections/all">Brands<span class="caret">▾</span></a></li>
|
|
73
|
+
</ul>
|
|
74
|
+
</nav>
|
|
75
|
+
</div>
|
|
76
|
+
</nav>
|
|
77
|
+
</header>
|
|
78
|
+
|
|
79
|
+
<!--PAGE_BODY-->
|
|
80
|
+
|
|
81
|
+
<!-- Compliance strip -->
|
|
82
|
+
<div class="compliance-strip">
|
|
83
|
+
<div class="container"><p>WARNING: This product contains nicotine. Nicotine is an addictive chemical.</p></div>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<!-- Footer -->
|
|
87
|
+
<footer class="site-footer">
|
|
88
|
+
<div class="container">
|
|
89
|
+
<div class="footer-grid">
|
|
90
|
+
<div class="footer-brand">
|
|
91
|
+
<img src="/tenants/giantvapes/logo-wordmark.png" alt="Giant Vapes" width="200" height="106">
|
|
92
|
+
<p>Premium e-liquid, salts, and disposables — shipped fast.<br>Serving vapers online since 2013.</p>
|
|
93
|
+
</div>
|
|
94
|
+
<div class="footer-col">
|
|
95
|
+
<h4>Shop</h4>
|
|
96
|
+
<ul>
|
|
97
|
+
<li><a href="/collections/all">E-Liquid</a></li>
|
|
98
|
+
<li><a href="/collections/salt-nic">Salt Nic</a></li>
|
|
99
|
+
<li><a href="/collections/all-disposables">Disposables</a></li>
|
|
100
|
+
<li><a href="/collections/vaporesso">Hardware</a></li>
|
|
101
|
+
<li><a href="/collections/nicotine-pouches">Nic Pouches</a></li>
|
|
102
|
+
<li><a href="/collections/clearance">Clearance</a></li>
|
|
103
|
+
</ul>
|
|
104
|
+
</div>
|
|
105
|
+
<div class="footer-col">
|
|
106
|
+
<h4>Support</h4>
|
|
107
|
+
<ul>
|
|
108
|
+
<li><a href="/search">Search</a></li>
|
|
109
|
+
<li><a href="/pages/about-us">About Us</a></li>
|
|
110
|
+
<li><a href="/pages/contact-us">Contact Us</a></li>
|
|
111
|
+
<li><a href="/pages/faq">FAQ</a></li>
|
|
112
|
+
<li><a href="/pages/order-tracking">Order Tracking</a></li>
|
|
113
|
+
<li><a href="/blogs/news">Blog</a></li>
|
|
114
|
+
</ul>
|
|
115
|
+
</div>
|
|
116
|
+
<div class="footer-col">
|
|
117
|
+
<h4>Company</h4>
|
|
118
|
+
<ul>
|
|
119
|
+
<li><a href="/pages/shipping-returns">Shipping & Returns</a></li>
|
|
120
|
+
<li><a href="/pages/privacy-policy">Privacy Policy</a></li>
|
|
121
|
+
<li><a href="/pages/giant-distribution">Wholesale</a></li>
|
|
122
|
+
</ul>
|
|
123
|
+
<form class="footer-news" action="/api/newsletter" method="post">
|
|
124
|
+
<input type="hidden" name="source" value="footer">
|
|
125
|
+
<label class="sr-only" for="foot-email">Email</label>
|
|
126
|
+
<input id="foot-email" type="email" name="email" placeholder="Email" required>
|
|
127
|
+
<button type="submit" aria-label="Subscribe">→</button>
|
|
128
|
+
</form>
|
|
129
|
+
<div class="pay-badges" aria-label="Accepted payments">
|
|
130
|
+
<span>AMEX</span><span>Discover</span><span>Mastercard</span><span>Visa</span><span>PayPal</span>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
<div class="footer-warning">
|
|
135
|
+
<p><strong>WARNING:</strong> This product contains nicotine. Nicotine is an addictive chemical. Products intended for adults of legal smoking age (21+) only.</p>
|
|
136
|
+
</div>
|
|
137
|
+
<div class="footer-bottom">
|
|
138
|
+
<p>© Giant Vapes, LLC. All rights reserved.</p>
|
|
139
|
+
<p>Must be 21 or older to purchase. Not for sale to minors.</p>
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
</footer>
|
|
143
|
+
|
|
144
|
+
<script>
|
|
145
|
+
(function () {
|
|
146
|
+
var burger = document.getElementById('hamburger');
|
|
147
|
+
var nav = document.getElementById('primaryNav');
|
|
148
|
+
if (burger && nav) burger.addEventListener('click', function () { var o = nav.classList.toggle('open'); burger.setAttribute('aria-expanded', o ? 'true' : 'false'); });
|
|
149
|
+
})();
|
|
150
|
+
</script>
|
|
151
|
+
</body>
|
|
152
|
+
</html>
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
{
|
|
2
|
+
"announcement": "FREE U.S. Shipping on Orders Over $100!",
|
|
3
|
+
"nav": [
|
|
4
|
+
{
|
|
5
|
+
"label": "E-Liquid",
|
|
6
|
+
"href": "/collections/fruity-e-liquids",
|
|
7
|
+
"children": [
|
|
8
|
+
{
|
|
9
|
+
"label": "Fruity E-Liquids",
|
|
10
|
+
"href": "/collections/fruity-e-liquids"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"label": "Nicotine Salts",
|
|
14
|
+
"href": "/collections/nicotine-salts"
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"label": "Fruity E-Liquids",
|
|
20
|
+
"href": "/collections/fruity-e-liquids"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"label": "Nicotine Salts",
|
|
24
|
+
"href": "/collections/nicotine-salts"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"label": "Disposables",
|
|
28
|
+
"href": "/collections/all-disposables"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"label": "About",
|
|
32
|
+
"href": "/about"
|
|
33
|
+
}
|
|
34
|
+
],
|
|
35
|
+
"footer": {
|
|
36
|
+
"tagline": "Premium e-liquid, salts, and disposables — shipped fast.",
|
|
37
|
+
"columns": [
|
|
38
|
+
{
|
|
39
|
+
"title": "Shop",
|
|
40
|
+
"links": [
|
|
41
|
+
{
|
|
42
|
+
"label": "Fruity E-Liquids",
|
|
43
|
+
"href": "/collections/fruity-e-liquids"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"label": "Nicotine Salts",
|
|
47
|
+
"href": "/collections/nicotine-salts"
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"label": "Disposables",
|
|
51
|
+
"href": "/collections/all-disposables"
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"label": "Search",
|
|
55
|
+
"href": "/search"
|
|
56
|
+
}
|
|
57
|
+
]
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"title": "Company",
|
|
61
|
+
"links": [
|
|
62
|
+
{
|
|
63
|
+
"label": "About",
|
|
64
|
+
"href": "/about"
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"label": "Shipping & Returns",
|
|
68
|
+
"href": "/shipping-returns"
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"label": "Privacy",
|
|
72
|
+
"href": "/privacy"
|
|
73
|
+
}
|
|
74
|
+
]
|
|
75
|
+
}
|
|
76
|
+
],
|
|
77
|
+
"social": [
|
|
78
|
+
{
|
|
79
|
+
"label": "Instagram",
|
|
80
|
+
"href": "https://instagram.com"
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"label": "Newsletter",
|
|
84
|
+
"href": "#newsletter"
|
|
85
|
+
}
|
|
86
|
+
]
|
|
87
|
+
},
|
|
88
|
+
"disclaimer": "<p>This product can expose you to chemicals including Nicotine, which is known to the State of California to cause birth defects or other reproductive harm. For more information go to <a href=\"http://www.p65warnings.ca.gov\">www.P65Warnings.ca.gov</a><p></p>\n<p>Giant Vapes will not be held liable for any injury, damage, or defect, permanent or temporary that may be caused by the improper use of these products. Please have a great understanding of the items you are using and how to store, use and care for your products properly. Terms and Conditions.</p>\n<p><b>WARNING:</b> This product can expose you to chemicals including Nicotine, which is known to the State of California to cause birth defects or other reproductive harm. For more information go to <a href=\"http://www.p65warnings.ca.gov\">www.P65Warnings.ca.gov</a></p>\n<p>This consumable nicotine product is not eligible for return, refund or exchange.</p>",
|
|
89
|
+
"disclaimers": {
|
|
90
|
+
"Vape Juice": "<p>This product can expose you to chemicals including Nicotine, which is known to the State of California to cause birth defects or other reproductive harm. For more information go to <a href=\"http://www.p65warnings.ca.gov\">www.P65Warnings.ca.gov</a><p></p>\n<p>Giant Vapes will not be held liable for any injury, damage, or defect, permanent or temporary that may be caused by the improper use of these products. Please have a great understanding of the items you are using and how to store, use and care for your products properly. Terms and Conditions.</p>\n<p><b>WARNING:</b> This product can expose you to chemicals including Nicotine, which is known to the State of California to cause birth defects or other reproductive harm. For more information go to <a href=\"http://www.p65warnings.ca.gov\">www.P65Warnings.ca.gov</a></p>\n<p>This consumable nicotine product is not eligible for return, refund or exchange.</p>",
|
|
91
|
+
"Nicotine Salt": "<p><b>WARNING: Do not use this product in sub-ohm tanks or with sub-ohm coils.</b> This eliquid contains nicotine salt and has a substantially higher amount of nicotine per milliliter than other eliquids. This eliquid is designed <b>only</b> for use with high-resistance coils (1.0 ohm or higher) and ultra low output/low wattage vaping devices.<p></p>\n<p>This product can expose you to chemicals including Nicotine, which is known to the State of California to cause birth defects or other reproductive harm. For more information go to <a href=\"http://www.p65warnings.ca.gov\">www.P65Warnings.ca.gov</a><p></p>\n<p>Giant Vapes will not be held liable for any injury, damage, or defect, permanent or temporary that may be caused by the improper use of these products. Please have a great understanding of the items you are using and how to store, use and care for your products properly. Terms and Conditions.</p>\n<p><b>WARNING:</b> This product can expose you to chemicals including Nicotine, which is known to the State of California to cause birth defects or other reproductive harm. For more information go to <a href=\"http://www.p65warnings.ca.gov\">www.P65Warnings.ca.gov</a></p>\n<p>This consumable nicotine product is not eligible for return, refund or exchange.</p>",
|
|
92
|
+
"Vape Disposable": "<p>There is always risk involved when using tobacco and nicotine products and/or rechargeable batteries, at any time and under any circumstances. Giant Vapes is not held responsible for any damage for any modification of the batteries, chargers, devices and other products that are sold on GiantVapes.com.</p>\n<p>Please Note: There is always risk involved when using rechargeable batteries at anytime and under any circumstances. Giant Vapes, LLC is not held responsible for any damage for any modification of the batteries, chargers, devices and other products that are sold on GiantVapes.com.</p>\n<p>Giant Vapes, LLC will not be held liable for any injury, damage, or defect, permanent or temporary that may be caused by the improper use of a Li-ion (Lithium-ion), LiPo (Lithium-ion Polymer) nor any rechargeable battery/batteries as well as chargers. Please have a great understanding of the batteries/chargers you are using and how to care for them properly. You can see a list of our recommendations for proper handling of rechargeable batteries in our terms and conditions</p>\n<p>This product can expose you to chemicals including Nicotine, which is known to the State of California to cause birth defects or other reproductive harm. For more information go to <a href=\"http://www.p65warnings.ca.gov\">www.P65Warnings.ca.gov</a><p></p>\n<p>Giant Vapes will not be held liable for any injury, damage, or defect, permanent or temporary that may be caused by the improper use of these products. Please have a great understanding of the items you are using and how to store, use and care for your products properly. Terms and Conditions.</p>\n<p><b>WARNING:</b> This product can expose you to chemicals including Nicotine, which is known to the State of California to cause birth defects or other reproductive harm. For more information go to <a href=\"http://www.p65warnings.ca.gov\">www.P65Warnings.ca.gov</a></p>"
|
|
93
|
+
}
|
|
94
|
+
}
|