notify-me-wl 1.4.1 → 1.4.3
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/CLAUDE.md +199 -0
- package/README.md +8 -296
- package/index.js +232 -3
- package/package.json +1 -1
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# CLAUDE.md — The Wishlist Company (TWC / Wishlist.io)
|
|
2
|
+
|
|
3
|
+
This file gives Claude Code persistent context about the TWC platform so every session starts informed. Update it as the platform evolves.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## This Repository
|
|
8
|
+
|
|
9
|
+
**Repo:** `notify-me_coming-soon-wl-widget`
|
|
10
|
+
**GitHub:** https://github.com/The-Wishlist-Co/notify-me_coming-soon-wl-widget
|
|
11
|
+
|
|
12
|
+
Customer-facing widget for 'Notify Me' and 'Coming Soon' wishlist functionality. Embedded on retailer product pages.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Platform Overview
|
|
19
|
+
|
|
20
|
+
**Wishlist.io** is a SaaS retail platform focused on capturing and activating consumer intent data, primarily in fashion retail. It surfaces wishlist and engagement data to frontline retail staff and powers personalised outreach via messaging templates.
|
|
21
|
+
|
|
22
|
+
**GitHub org:** https://github.com/The-Wishlist-Co
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Service Architecture
|
|
27
|
+
|
|
28
|
+
Microservices stack deployed on **AWS EKS**, service mesh via **Istio + Consul**. Netflix OSS for gateway/resilience. All services are **Spring Boot / JHipster**, Java 13.
|
|
29
|
+
|
|
30
|
+
| Service | Role |
|
|
31
|
+
|---|---|
|
|
32
|
+
| `twcgateway` | API gateway (Zuul) |
|
|
33
|
+
| `wsservice` | Wishlist core service |
|
|
34
|
+
| `customerservice` | Customer profile service |
|
|
35
|
+
| `productsvc` | Product catalogue service |
|
|
36
|
+
| `eventcollector` | Event ingestion |
|
|
37
|
+
| `inventorysvc` | Inventory service |
|
|
38
|
+
| `merchantsvc` | Merchant/retailer management |
|
|
39
|
+
| `ordersvc` | Order service |
|
|
40
|
+
|
|
41
|
+
**Observability:** OpenTelemetry + Datadog. Logs via Filebeat → Elasticsearch (`twc-ops` cluster, ECK on EKS, Elasticsearch 8.3.3, 4-node).
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Critical Infrastructure Constraints
|
|
46
|
+
|
|
47
|
+
### Java / JVM (ALL services)
|
|
48
|
+
|
|
49
|
+
All services run Java 13 in containers and **cannot auto-detect CPU limits correctly via cgroups**. Every service deployment MUST include:
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
-XX:ActiveProcessorCount=N
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
in `JAVA_OPTS`, where N matches the container's CPU allocation. Omitting this causes Undertow thread pool over-sizing → Consul health check failures → Hystrix circuit breaker trips → 500 SHORTCIRCUIT errors at the gateway.
|
|
56
|
+
|
|
57
|
+
Affected services (apply to all eight): `twcgateway`, `wsservice`, `customerservice`, `productsvc`, `eventcollector`, `inventorysvc`, `merchantsvc`, `ordersvc`.
|
|
58
|
+
|
|
59
|
+
### Elasticsearch / ECK
|
|
60
|
+
|
|
61
|
+
- Cluster: `twc-ops`, 4 nodes on EKS via ECK operator
|
|
62
|
+
- **Memory:** each node must be configured with **4Gi** (`-Xms2g -Xmx2g`). Nodes with 2Gi will OOM-kill under load.
|
|
63
|
+
- **ILM:** Filebeat data stream indices require an ILM policy with **3-day retention** to prevent disk exhaustion.
|
|
64
|
+
- Yellow cluster state will block ECK rolling restarts — resolve unassigned shards before patching the CR.
|
|
65
|
+
|
|
66
|
+
### EKS Scaling Lambda
|
|
67
|
+
|
|
68
|
+
A Lambda (`eks_node_scaleup`) has hardcoded node group values and will override manual EKS scaling changes. Do not rely on manual scaling being durable — any fix must go into the Lambda source.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Data Architecture
|
|
73
|
+
|
|
74
|
+
### ClickHouse (Analytics / Reporting)
|
|
75
|
+
|
|
76
|
+
Used for conversion reporting and analytics. All ETL queries follow strict conventions — see Query Conventions below.
|
|
77
|
+
|
|
78
|
+
### DynamoDB (Operational)
|
|
79
|
+
|
|
80
|
+
Key tables:
|
|
81
|
+
|
|
82
|
+
| Table | PK | SK | Notes |
|
|
83
|
+
|---|---|---|---|
|
|
84
|
+
| `TWCTEMPLATE_TOPIC` | `tenantId` | `topicId` | Soft-delete via `active` flag |
|
|
85
|
+
| `MESSAGE` | — | — | `messageTopic` stored directly at send time — single source of truth |
|
|
86
|
+
|
|
87
|
+
`messageTopic` is captured on the MESSAGE record at send time and is **not** looked up from `TWCTEMPLATE_TOPIC` at query time. This is intentional.
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## ClickHouse Query Conventions
|
|
92
|
+
|
|
93
|
+
These conventions are non-negotiable. All new queries must follow them.
|
|
94
|
+
|
|
95
|
+
### Named Queries (Conversion Reporting Dashboard)
|
|
96
|
+
|
|
97
|
+
| Query ID | Description |
|
|
98
|
+
|---|---|
|
|
99
|
+
| `CONV_KPI_SUMMARY` | Top-level KPI summary |
|
|
100
|
+
| `CONV_TREND_DAILY` | Daily trend over period |
|
|
101
|
+
| `CONV_BREAKDOWN_STORE` | Breakdown by store |
|
|
102
|
+
| `CONV_BREAKDOWN_STAFF` | Breakdown by staff |
|
|
103
|
+
| `CONV_BREAKDOWN_TOPIC` | Breakdown by message topic |
|
|
104
|
+
| `CONV_BREAKDOWN_TEMPLATE_TAG` | Breakdown by template tag |
|
|
105
|
+
| `CONV_BREAKDOWN_CUSTOMER_TAG` | Breakdown by customer tag |
|
|
106
|
+
| `CONV_BREAKDOWN_CHANNEL` | Breakdown by channel |
|
|
107
|
+
|
|
108
|
+
### API Parameter Naming Convention
|
|
109
|
+
|
|
110
|
+
| Parameter | Maps to |
|
|
111
|
+
|---|---|
|
|
112
|
+
| `value1` | channel |
|
|
113
|
+
| `value2` | topic |
|
|
114
|
+
| `value3` | templateTag |
|
|
115
|
+
| `value4` | customerTag |
|
|
116
|
+
| `param5` | storeRefs (array) |
|
|
117
|
+
| `param6` | staffRefs (array) |
|
|
118
|
+
|
|
119
|
+
### Filter Syntax Rules
|
|
120
|
+
|
|
121
|
+
**`storeRef` and `staffRef` are ALWAYS multi-select.** These are never single-value filters.
|
|
122
|
+
|
|
123
|
+
```sql
|
|
124
|
+
-- ✅ CORRECT — always use IN with empty() guard
|
|
125
|
+
AND (empty({param5: Array(String)}) OR storeRef IN {param5: Array(String)})
|
|
126
|
+
AND (empty({param6: Array(String)}) OR staffRef IN {param6: Array(String)})
|
|
127
|
+
|
|
128
|
+
-- ❌ WRONG — never use equality for store or staff
|
|
129
|
+
AND storeRef = {param5: String}
|
|
130
|
+
AND staffRef = {param6: String}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
All other filters (`value1`–`value4`) follow the same array pattern if they are multi-select dropdowns.
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Integrations
|
|
138
|
+
|
|
139
|
+
### Shopify
|
|
140
|
+
|
|
141
|
+
- Custom pixel integration on `twc-fashion-demo.myshopify.com` (pixel `87425273`)
|
|
142
|
+
- Pixels require `analytics.subscribe('init', ...)` capture and `Content-Type: application/json` header
|
|
143
|
+
- App updates auto-deploy to all merchants unless new OAuth permission scopes are required
|
|
144
|
+
|
|
145
|
+
### Gorgias
|
|
146
|
+
|
|
147
|
+
Backend proxy integration surfacing support ticket data on customer profile screens.
|
|
148
|
+
|
|
149
|
+
- Per-customer credentials stored encrypted (AES-256) in `gorgias_credentials` table
|
|
150
|
+
- Ticket data cached in `gorgias_ticket_cache`
|
|
151
|
+
- Cache TTLs: 15 min (summary), 2 min (full ticket list)
|
|
152
|
+
- Five API endpoints for credential management and ticket retrieval
|
|
153
|
+
|
|
154
|
+
### Emarsys
|
|
155
|
+
|
|
156
|
+
Marketing automation platform integration. Customer data synced from TWC to Emarsys for campaign targeting. Webhook updates received via `emarsys-webhook-updates` Lambda.
|
|
157
|
+
|
|
158
|
+
### Newstore
|
|
159
|
+
|
|
160
|
+
POS platform integration. TWC builds plugins/webviews for the Newstore POS environment. Webhooks received via `twc-newstore-webhook`. Proxy via `infinityAPI` for Triquestra Infinity POS.
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Development Conventions
|
|
165
|
+
|
|
166
|
+
### General
|
|
167
|
+
|
|
168
|
+
- Full-stack ownership: data modelling, API design, backend, infra/DevOps, frontend
|
|
169
|
+
- Prefer explicit over implicit — document intent in code comments for complex query logic
|
|
170
|
+
- All schema changes should consider soft-delete patterns (see `TWCTEMPLATE_TOPIC` above)
|
|
171
|
+
- Default branch: `main`
|
|
172
|
+
- Feature branches cut from `main`, kebab-case description (e.g. `suppression-change`)
|
|
173
|
+
- PR back to `main` when complete
|
|
174
|
+
|
|
175
|
+
### Code Review Checklist (for Claude Code to apply)
|
|
176
|
+
|
|
177
|
+
Before suggesting or approving any change, verify:
|
|
178
|
+
|
|
179
|
+
- [ ] All ClickHouse queries touching store/staff use `IN` + `empty()` guard, not `=`
|
|
180
|
+
- [ ] Any new or modified Spring Boot service includes `-XX:ActiveProcessorCount` in JAVA_OPTS config
|
|
181
|
+
- [ ] DynamoDB table designs include `tenantId` as PK for all multi-tenant tables
|
|
182
|
+
- [ ] New Elasticsearch indices have an ILM policy assigned
|
|
183
|
+
- [ ] API parameter names follow the `value1`–`value4` / `param5`/`param6` convention
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## Key External References
|
|
188
|
+
|
|
189
|
+
- Anthropic Claude Code docs: https://docs.claude.com/en/docs/claude-code/overview
|
|
190
|
+
- ECK operator docs: https://www.elastic.co/guide/en/cloud-on-k8s/current/index.html
|
|
191
|
+
- Shopify custom pixels: https://shopify.dev/docs/api/web-pixels-api
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
*Last updated: May 2026. Keep this file current as the platform evolves — it is the primary context source for all Claude Code sessions.*
|
|
196
|
+
|
|
197
|
+
## References
|
|
198
|
+
- **Engineering Standards:** https://github.com/The-Wishlist-Co/twc-api-specs/blob/main/standards/TWC-STANDARDS.md
|
|
199
|
+
- **API Specifications:** https://github.com/The-Wishlist-Co/twc-api-specs/blob/main/apis/
|
package/README.md
CHANGED
|
@@ -29,6 +29,14 @@ Add a button to your HTML where you want the widget to appear. The button should
|
|
|
29
29
|
|
|
30
30
|
- `data-fields`: A JSON array specifying the fields to include in the form. Possible values are `"mobile"`, `"firstName"`, and `"lastName"`.
|
|
31
31
|
- `data-type`: Specifies the type of the widget. Possible values are `"notify-me"` and `"coming-soon"`.
|
|
32
|
+
- `data-auth`: Auth mode. `"token"` (default) uses the bundled server-issued access token. `"proxy"` uses the Shopify App Proxy at `/apps/twc-sdk/auth/token` to obtain a tenant-scoped token (requires the customer to be logged in to the storefront).
|
|
33
|
+
- `data-tenant`: The TWC tenant sent as the `X-Twc-Tenant` header. Used in both auth modes. Falls back to the bundled `TENANT_ID` if omitted.
|
|
34
|
+
|
|
35
|
+
### Proxy auth mode
|
|
36
|
+
|
|
37
|
+
When `data-auth="proxy"` is set, the widget does not use the bundled access token. Instead, on form submit it lazily fetches a tenant-scoped access token from the same-origin Shopify App Proxy endpoint `/apps/twc-sdk/auth/token` (Shopify signs and forwards the request). The token is cached for the page session and reused across submissions.
|
|
38
|
+
|
|
39
|
+
Because the proxy only issues a token for a logged-in customer, if the token cannot be obtained the popup stays open and shows an inline "Please log in to your account to continue." message; submission is blocked until a token is available.
|
|
32
40
|
|
|
33
41
|
### Example
|
|
34
42
|
|
|
@@ -60,301 +68,5 @@ https://api.au-sandbox.thewishlist.io/services/wsservice/api/wishlist/items/cust
|
|
|
60
68
|
https://api.au-aws.thewishlist.io/services/wsservice/api/wishlist/items/customerInterest
|
|
61
69
|
```
|
|
62
70
|
|
|
63
|
-
|
|
64
|
-
### Full Script
|
|
65
|
-
|
|
66
|
-
```javascript
|
|
67
|
-
// Access token for the API
|
|
68
|
-
const ACCESS_TOKEN = "123"; // Replace `ACCESS_TOKEN` with your actual access token to authenticate API requests.
|
|
69
|
-
const TENANT_ID = "victoria-woods"; // Replace `TENANT_ID` with your actual tenant ID.
|
|
70
|
-
|
|
71
|
-
document.addEventListener("DOMContentLoaded", function () {
|
|
72
|
-
const wrapper = document.getElementById("notification-widget");
|
|
73
|
-
if (!wrapper) return;
|
|
74
|
-
|
|
75
|
-
// Define and apply styles for the popup and button
|
|
76
|
-
const styles = `
|
|
77
|
-
.notification-btn {
|
|
78
|
-
background-color: #fff;
|
|
79
|
-
border: 1px solid black;
|
|
80
|
-
color: black;
|
|
81
|
-
padding: 12px 24px;
|
|
82
|
-
text-align: center;
|
|
83
|
-
width: 100%;
|
|
84
|
-
cursor: pointer;
|
|
85
|
-
text-transform: uppercase;
|
|
86
|
-
}
|
|
87
|
-
#popup-body.overlay {
|
|
88
|
-
position: fixed;
|
|
89
|
-
z-index: 9998;
|
|
90
|
-
top: 0;
|
|
91
|
-
bottom: 0;
|
|
92
|
-
left: 0;
|
|
93
|
-
right: 0;
|
|
94
|
-
background: rgba(0, 0, 0, 0.7);
|
|
95
|
-
transition: opacity 200ms;
|
|
96
|
-
visibility: hidden;
|
|
97
|
-
opacity: 0;
|
|
98
|
-
}
|
|
99
|
-
#popup-wrapper {
|
|
100
|
-
margin: 70px auto;
|
|
101
|
-
z-index: 9999;
|
|
102
|
-
padding: 15px;
|
|
103
|
-
background: #fff;
|
|
104
|
-
border-radius: 5px;
|
|
105
|
-
max-width: 360px;
|
|
106
|
-
position: relative;
|
|
107
|
-
}
|
|
108
|
-
#popup-close {
|
|
109
|
-
position: absolute;
|
|
110
|
-
top: 5px;
|
|
111
|
-
right: 15px;
|
|
112
|
-
transition: all 200ms;
|
|
113
|
-
font-size: 30px;
|
|
114
|
-
font-weight: bold;
|
|
115
|
-
text-decoration: none;
|
|
116
|
-
color: #333;
|
|
117
|
-
cursor: pointer;
|
|
118
|
-
}
|
|
119
|
-
#popup-close:hover {
|
|
120
|
-
color: #d80606;
|
|
121
|
-
}
|
|
122
|
-
#popup-title {
|
|
123
|
-
margin: 0;
|
|
124
|
-
font-size: 20px;
|
|
125
|
-
max-width: 350px;
|
|
126
|
-
text-transform: uppercase;
|
|
127
|
-
}
|
|
128
|
-
#popup-text {
|
|
129
|
-
margin: 10px 0;
|
|
130
|
-
font-size: 14px;
|
|
131
|
-
}
|
|
132
|
-
#popup-form {
|
|
133
|
-
display: flex;
|
|
134
|
-
flex-direction: column;
|
|
135
|
-
gap: 10px;
|
|
136
|
-
}
|
|
137
|
-
#popup-form input {
|
|
138
|
-
padding: 0.475em 1em;
|
|
139
|
-
border: 1px solid #caced1;
|
|
140
|
-
border-radius: 0.25rem;
|
|
141
|
-
font-size: 1.15rem;
|
|
142
|
-
max-width: 100%;
|
|
143
|
-
}
|
|
144
|
-
#popup-form input::placeholder {
|
|
145
|
-
font-size: 1rem;
|
|
146
|
-
}
|
|
147
|
-
#popup-form button {
|
|
148
|
-
font-size: 0.85rem;
|
|
149
|
-
padding: 0.975em 1em;
|
|
150
|
-
font-weight: 600;
|
|
151
|
-
border: none;
|
|
152
|
-
text-transform: uppercase;
|
|
153
|
-
background: #aca475;
|
|
154
|
-
color: #fff;
|
|
155
|
-
cursor: pointer;
|
|
156
|
-
}
|
|
157
|
-
.custom-select {
|
|
158
|
-
position: relative;
|
|
159
|
-
}
|
|
160
|
-
.custom-select select {
|
|
161
|
-
appearance: none;
|
|
162
|
-
width: 100%;
|
|
163
|
-
font-size: 1.15rem;
|
|
164
|
-
padding: 0.475em 1em;
|
|
165
|
-
background-color: #fff;
|
|
166
|
-
border: 1px solid #caced1;
|
|
167
|
-
border-radius: 0.25rem;
|
|
168
|
-
color: #000;
|
|
169
|
-
cursor: pointer;
|
|
170
|
-
}
|
|
171
|
-
.custom-select::before,
|
|
172
|
-
.custom-select::after {
|
|
173
|
-
--size: 0.3rem;
|
|
174
|
-
content: "";
|
|
175
|
-
position: absolute;
|
|
176
|
-
right: 1rem;
|
|
177
|
-
pointer-events: none;
|
|
178
|
-
}
|
|
179
|
-
.custom-select::before {
|
|
180
|
-
border-left: var(--size) solid transparent;
|
|
181
|
-
border-right: var(--size) solid transparent;
|
|
182
|
-
border-bottom: var(--size) solid black;
|
|
183
|
-
top: 40%;
|
|
184
|
-
}
|
|
185
|
-
.custom-select::after {
|
|
186
|
-
border-left: var(--size) solid transparent;
|
|
187
|
-
border-right: var(--size) solid transparent;
|
|
188
|
-
border-top: var(--size) solid black;
|
|
189
|
-
top: 55%;
|
|
190
|
-
}
|
|
191
|
-
`;
|
|
192
|
-
|
|
193
|
-
const styleSheet = document.createElement("style");
|
|
194
|
-
styleSheet.type = "text/css";
|
|
195
|
-
styleSheet.innerText = styles;
|
|
196
|
-
document.head.appendChild(styleSheet);
|
|
197
|
-
|
|
198
|
-
// Create and append the overlay for the popup
|
|
199
|
-
const overlay = document.createElement("div");
|
|
200
|
-
overlay.innerHTML = `
|
|
201
|
-
<div id="popup-body" class="overlay">
|
|
202
|
-
<div id="popup-wrapper">
|
|
203
|
-
<h3 id="popup-title"></h3>
|
|
204
|
-
<span id="popup-close">×</span>
|
|
205
|
-
<div id="popup-text"></div>
|
|
206
|
-
<form id="popup-form">
|
|
207
|
-
<div class="custom-select">
|
|
208
|
-
<select name="select-size" required>
|
|
209
|
-
</select>
|
|
210
|
-
</div>
|
|
211
|
-
<input name="email" placeholder="Email" type="email" required />
|
|
212
|
-
</form>
|
|
213
|
-
</div>
|
|
214
|
-
</div>
|
|
215
|
-
`;
|
|
216
|
-
|
|
217
|
-
wrapper.appendChild(overlay);
|
|
218
|
-
|
|
219
|
-
const popupBody = document.getElementById("popup-body");
|
|
220
|
-
const popupClose = document.getElementById("popup-close");
|
|
221
|
-
const popupOpenButton = document.getElementById("popup-open");
|
|
222
|
-
const popupTitle = document.getElementById("popup-title");
|
|
223
|
-
const popupText = document.getElementById("popup-text");
|
|
224
|
-
const sizeSelect = document.querySelector("select[name='select-size']");
|
|
225
|
-
const form = overlay.querySelector("#popup-form");
|
|
226
|
-
|
|
227
|
-
if (!popupBody || !popupClose || !popupOpenButton || !popupTitle || !sizeSelect) return;
|
|
228
|
-
|
|
229
|
-
// Parse fields and type to be included in the form from data attribute
|
|
230
|
-
const fields = JSON.parse(popupOpenButton.getAttribute("data-fields") || '["email"]');
|
|
231
|
-
const type = popupOpenButton.getAttribute("data-type") || "notify-me";
|
|
232
|
-
const typeConfig = {
|
|
233
|
-
"notify-me": {
|
|
234
|
-
text: "Register to receive a notification as soon as this item is back in stock",
|
|
235
|
-
buttonText: "Notify me",
|
|
236
|
-
},
|
|
237
|
-
"coming-soon": {
|
|
238
|
-
text: "Register your interest to hear more about this item",
|
|
239
|
-
buttonText: "Register Interest",
|
|
240
|
-
},
|
|
241
|
-
};
|
|
242
|
-
|
|
243
|
-
// Get product data from Shopify's global variable
|
|
244
|
-
const productData = window?.currentProduct;
|
|
245
|
-
|
|
246
|
-
// Map for form fields
|
|
247
|
-
popupText.innerText = typeConfig[type].text;
|
|
248
|
-
|
|
249
|
-
const fieldMap = {
|
|
250
|
-
email: `<input name="email" placeholder="Email" type="email" required />`,
|
|
251
|
-
mobile: `<input name="mobile" placeholder="Mobile" type="tel" required />`,
|
|
252
|
-
firstName: `<input name="firstName" placeholder="First name" type="text" required />`,
|
|
253
|
-
lastName: `<input name="lastName" placeholder="Last name" type="text" required />`,
|
|
254
|
-
};
|
|
255
|
-
|
|
256
|
-
// Add fields to the form based on the parsed fields
|
|
257
|
-
fields.forEach((field) => {
|
|
258
|
-
if (fieldMap[field]) {
|
|
259
|
-
form.insertAdjacentHTML("beforeend", fieldMap[field]);
|
|
260
|
-
}
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
form.insertAdjacentHTML("beforeend", `<div id="checkbox-wrapper"></div>`);
|
|
264
|
-
const checkboxWrapper = document.getElementById("checkbox-wrapper");
|
|
265
|
-
checkboxWrapper.insertAdjacentHTML("beforeend", `<input type="checkbox" name="mailList" id="mailList" />`);
|
|
266
|
-
checkboxWrapper.insertAdjacentHTML("beforeend", `<label for="mailList">Subscribe to our mailing list</label>`);
|
|
267
|
-
form.insertAdjacentHTML("beforeend", `<button type="submit">${typeConfig[type].buttonText}</button>`);
|
|
268
|
-
|
|
269
|
-
// Show/hide the popup
|
|
270
|
-
function showPopup() {
|
|
271
|
-
if (!productData) {
|
|
272
|
-
alert("Product data not found");
|
|
273
|
-
return;
|
|
274
|
-
}
|
|
275
|
-
// Set the popup title and populate the size dropdown
|
|
276
|
-
popupTitle.innerHTML = productData.title;
|
|
277
|
-
productData.variants.forEach((variant) => {
|
|
278
|
-
const option = document.createElement("option");
|
|
279
|
-
option.value = variant.title;
|
|
280
|
-
option.textContent = variant.title;
|
|
281
|
-
sizeSelect.appendChild(option);
|
|
282
|
-
});
|
|
283
|
-
popupBody.style.visibility = "visible";
|
|
284
|
-
popupBody.style.opacity = 1;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
function hidePopup() {
|
|
288
|
-
popupBody.style.visibility = "hidden";
|
|
289
|
-
popupBody.style.opacity = 0;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
popupOpenButton.addEventListener("click", showPopup);
|
|
293
|
-
popupClose.addEventListener("click", hidePopup);
|
|
294
|
-
|
|
295
|
-
// Form submission handler
|
|
296
|
-
if (form) {
|
|
297
|
-
form.addEventListener("submit", function (event) {
|
|
298
|
-
event.preventDefault();
|
|
299
|
-
|
|
300
|
-
const selectedSize = sizeSelect.value;
|
|
301
|
-
const selectedVariant = productData.variants.find((variant) => variant.title === selectedSize);
|
|
302
|
-
|
|
303
|
-
if (selectedVariant) {
|
|
304
|
-
const formData = {
|
|
305
|
-
variantRef: selectedVariant.id,
|
|
306
|
-
email: form.querySelector("input[name='email']").value,
|
|
307
|
-
subscribe: form.querySelector("input[name='mailList']").checked,
|
|
308
|
-
};
|
|
309
|
-
|
|
310
|
-
if (type === "coming-soon") {
|
|
311
|
-
formData.comingSoon = true;
|
|
312
|
-
// formData.productRef = productData.id;
|
|
313
|
-
} else {
|
|
314
|
-
formData.notifyMe = true;
|
|
315
|
-
}
|
|
316
|
-
if (fields.includes("firstName")) {
|
|
317
|
-
formData.firstName = form.querySelector("input[name='firstName']").value;
|
|
318
|
-
}
|
|
319
|
-
if (fields.includes("lastName")) {
|
|
320
|
-
formData.lastName = form.querySelector("input[name='lastName']").value;
|
|
321
|
-
}
|
|
322
|
-
if (fields.includes("mobile")) {
|
|
323
|
-
formData.mobile = form.querySelector("input[name='mobile']").value;
|
|
324
|
-
// formData.phone = form.querySelector("input[name='mobile']").value;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
let url = `https://api.au-sandbox.thewishlist.io/services/wsservice/api/wishlist/items/customerInterest`;
|
|
328
|
-
|
|
329
|
-
fetch(url, {
|
|
330
|
-
method: "POST",
|
|
331
|
-
headers: {
|
|
332
|
-
"Content-Type": "application/json",
|
|
333
|
-
Authorization: ACCESS_TOKEN,
|
|
334
|
-
"X-Twc-Tenant": TENANT_ID,
|
|
335
|
-
},
|
|
336
|
-
body: JSON.stringify(formData),
|
|
337
|
-
})
|
|
338
|
-
.then((response) => {
|
|
339
|
-
if (response.ok) {
|
|
340
|
-
alert("Form submitted");
|
|
341
|
-
hidePopup();
|
|
342
|
-
} else {
|
|
343
|
-
alert("Form submission failed");
|
|
344
|
-
}
|
|
345
|
-
})
|
|
346
|
-
.catch((error) => {
|
|
347
|
-
console.error("Error submitting form:", error);
|
|
348
|
-
alert("Form submission failed");
|
|
349
|
-
});
|
|
350
|
-
} else {
|
|
351
|
-
alert("Selected variant not found");
|
|
352
|
-
}
|
|
353
|
-
});
|
|
354
|
-
}
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
```
|
|
358
|
-
|
|
359
71
|
Replace `ACCESS_TOKEN` with your actual access token to authenticate API requests.
|
|
360
72
|
Replace `TENANT_ID` with your actual tenant to authenticate API requests.
|
package/index.js
CHANGED
|
@@ -3,6 +3,189 @@ const ACCESS_TOKEN =
|
|
|
3
3
|
'Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJRWlJkS3JabXJmMEk3WkhXRUtqNWRLTEhQanFubWJFeV9iNmpSbHdya1drIn0.eyJleHAiOjE3MjMwMzI3NzIsImlhdCI6MTcyMzAyOTE3MiwianRpIjoiNDJiNGYwM2QtMDNiNS00NmZlLTk5YmItZDQ2NTdhNjk5NGNiIiwiaXNzIjoiaHR0cHM6Ly9hdXRoLmF1LWF3cy50aGV3aXNobGlzdC5pby9hdXRoL3JlYWxtcy90d2NNYWluIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6IjI2YTIyNTAyLWRhNzQtNDhkMC1iZWFiLTgzY2E0YTlmMDdlOSIsInR5cCI6IkJlYXJlciIsImF6cCI6InR3Yy1wb3MtY2xpZW50Iiwic2Vzc2lvbl9zdGF0ZSI6IjQxYWQwMDc4LWVmYTItNGVjMi1hM2I2LTIzYjBkNDM3YjEzOCIsImFjciI6IjEiLCJhbGxvd2VkLW9yaWdpbnMiOlsiaHR0cHM6Ly9sb2NhbGhvc3QiXSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbInR3Yy1wb3MtdXNlciIsIm9mZmxpbmVfYWNjZXNzIiwidHdjLXN0b3JlLW93bmVyIiwidW1hX2F1dGhvcml6YXRpb24iXX0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6InRlbmFudGlkIHN0b3JlIHByb2ZpbGUgZW1haWwiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsInRlbmFudGlkIjoidmlrdG9yaWEtd29vZHMiLCJuYW1lIjoiTWF0dCBIYW1wc2hpcmUiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJtYXR0QHRoZXdpc2hsaXN0LmlvIiwic3RvcmUiOiIyMDUiLCJnaXZlbl9uYW1lIjoiTWF0dCIsImZhbWlseV9uYW1lIjoiSGFtcHNoaXJlIiwiZW1haWwiOiJtYXR0QHRoZXdpc2hsaXN0LmlvIn0.iZkMgwH74njjXUWvImkJomHYr91Lr8ZGrZGwslEGcV3vbNuoNc5CocvNWW476o-LoSh-LsKf-MLiYN1XvOuPDF3fGoGCEbMh6_M0RJcrhVWogkj81fx4ukvDPCFIjgoDCV9WIuehV9dsSWa7E0irZeE6MUVhLwRIaTzKtxgzUUKrAqBtI_HKpyo8TUGQBiYlrc85QFUyuoKbKg-QaRn_SObRLDB8ooIBJvIlgklXQt1ZYBM2HUOc5L1bAQwfzcrWEvl6eYiQHXCSPqS0rPGoaGC6v5ydBo9VMxtVHGladDHLrO3Gt2BnIGMBoYrKTAmt7j0KABwPyB3CmAIwj_pOBQ'; // Replace `ACCESS_TOKEN` with your actual access token to authenticate API requests.
|
|
4
4
|
const TENANT_ID = 'victoria-woods'; // Replace `TENANT_ID` with your actual tenant ID.
|
|
5
5
|
|
|
6
|
+
// Session-cached access token obtained from the Shopify App Proxy (proxy auth mode).
|
|
7
|
+
let cachedProxyToken = null;
|
|
8
|
+
|
|
9
|
+
// Fetch a tenant-scoped access token from the Shopify App Proxy.
|
|
10
|
+
// The path is relative/same-origin, so Shopify's App Proxy layer appends and
|
|
11
|
+
// signs `shop`, `logged_in_customer_id`, `timestamp`, and `signature`.
|
|
12
|
+
// Returns the access token string, or null on failure (e.g. customer not logged in).
|
|
13
|
+
async function getProxyAccessToken() {
|
|
14
|
+
if (cachedProxyToken) return cachedProxyToken;
|
|
15
|
+
try {
|
|
16
|
+
const response = await fetch('/apps/twc-sdk/auth/token', {
|
|
17
|
+
method: 'GET',
|
|
18
|
+
headers: { Accept: 'application/json' },
|
|
19
|
+
});
|
|
20
|
+
const body = await response.json().catch(() => null);
|
|
21
|
+
if (
|
|
22
|
+
response.ok &&
|
|
23
|
+
body &&
|
|
24
|
+
body.success &&
|
|
25
|
+
body.data &&
|
|
26
|
+
body.data.access_token
|
|
27
|
+
) {
|
|
28
|
+
cachedProxyToken = body.data.access_token;
|
|
29
|
+
return cachedProxyToken;
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
} catch (error) {
|
|
33
|
+
console.error('Error fetching proxy access token:', error);
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const COUNTRY_CODES = [
|
|
39
|
+
{ code: 'AF', name: 'Afghanistan' }, { code: 'AX', name: 'Åland Islands' },
|
|
40
|
+
{ code: 'AL', name: 'Albania' }, { code: 'DZ', name: 'Algeria' },
|
|
41
|
+
{ code: 'AS', name: 'American Samoa' }, { code: 'AD', name: 'Andorra' },
|
|
42
|
+
{ code: 'AO', name: 'Angola' }, { code: 'AI', name: 'Anguilla' },
|
|
43
|
+
{ code: 'AG', name: 'Antigua and Barbuda' }, { code: 'AR', name: 'Argentina' },
|
|
44
|
+
{ code: 'AM', name: 'Armenia' }, { code: 'AW', name: 'Aruba' },
|
|
45
|
+
{ code: 'AU', name: 'Australia' }, { code: 'AT', name: 'Austria' },
|
|
46
|
+
{ code: 'AZ', name: 'Azerbaijan' }, { code: 'BS', name: 'Bahamas' },
|
|
47
|
+
{ code: 'BH', name: 'Bahrain' }, { code: 'BD', name: 'Bangladesh' },
|
|
48
|
+
{ code: 'BB', name: 'Barbados' }, { code: 'BY', name: 'Belarus' },
|
|
49
|
+
{ code: 'BE', name: 'Belgium' }, { code: 'BZ', name: 'Belize' },
|
|
50
|
+
{ code: 'BJ', name: 'Benin' }, { code: 'BM', name: 'Bermuda' },
|
|
51
|
+
{ code: 'BT', name: 'Bhutan' }, { code: 'BO', name: 'Bolivia' },
|
|
52
|
+
{ code: 'BA', name: 'Bosnia and Herzegovina' }, { code: 'BW', name: 'Botswana' },
|
|
53
|
+
{ code: 'BR', name: 'Brazil' }, { code: 'BN', name: 'Brunei' },
|
|
54
|
+
{ code: 'BG', name: 'Bulgaria' }, { code: 'BF', name: 'Burkina Faso' },
|
|
55
|
+
{ code: 'BI', name: 'Burundi' }, { code: 'CV', name: 'Cabo Verde' },
|
|
56
|
+
{ code: 'KH', name: 'Cambodia' }, { code: 'CM', name: 'Cameroon' },
|
|
57
|
+
{ code: 'CA', name: 'Canada' }, { code: 'KY', name: 'Cayman Islands' },
|
|
58
|
+
{ code: 'CF', name: 'Central African Republic' }, { code: 'TD', name: 'Chad' },
|
|
59
|
+
{ code: 'CL', name: 'Chile' }, { code: 'CN', name: 'China' },
|
|
60
|
+
{ code: 'CO', name: 'Colombia' }, { code: 'KM', name: 'Comoros' },
|
|
61
|
+
{ code: 'CG', name: 'Congo' }, { code: 'CD', name: 'Congo, DR' },
|
|
62
|
+
{ code: 'CK', name: 'Cook Islands' }, { code: 'CR', name: 'Costa Rica' },
|
|
63
|
+
{ code: 'HR', name: 'Croatia' }, { code: 'CU', name: 'Cuba' },
|
|
64
|
+
{ code: 'CW', name: 'Curaçao' }, { code: 'CY', name: 'Cyprus' },
|
|
65
|
+
{ code: 'CZ', name: 'Czech Republic' }, { code: 'DK', name: 'Denmark' },
|
|
66
|
+
{ code: 'DJ', name: 'Djibouti' }, { code: 'DM', name: 'Dominica' },
|
|
67
|
+
{ code: 'DO', name: 'Dominican Republic' }, { code: 'EC', name: 'Ecuador' },
|
|
68
|
+
{ code: 'EG', name: 'Egypt' }, { code: 'SV', name: 'El Salvador' },
|
|
69
|
+
{ code: 'GQ', name: 'Equatorial Guinea' }, { code: 'ER', name: 'Eritrea' },
|
|
70
|
+
{ code: 'EE', name: 'Estonia' }, { code: 'SZ', name: 'Eswatini' },
|
|
71
|
+
{ code: 'ET', name: 'Ethiopia' }, { code: 'FO', name: 'Faroe Islands' },
|
|
72
|
+
{ code: 'FJ', name: 'Fiji' }, { code: 'FI', name: 'Finland' },
|
|
73
|
+
{ code: 'FR', name: 'France' }, { code: 'GF', name: 'French Guiana' },
|
|
74
|
+
{ code: 'PF', name: 'French Polynesia' }, { code: 'GA', name: 'Gabon' },
|
|
75
|
+
{ code: 'GM', name: 'Gambia' }, { code: 'GE', name: 'Georgia' },
|
|
76
|
+
{ code: 'DE', name: 'Germany' }, { code: 'GH', name: 'Ghana' },
|
|
77
|
+
{ code: 'GI', name: 'Gibraltar' }, { code: 'GR', name: 'Greece' },
|
|
78
|
+
{ code: 'GL', name: 'Greenland' }, { code: 'GD', name: 'Grenada' },
|
|
79
|
+
{ code: 'GP', name: 'Guadeloupe' }, { code: 'GU', name: 'Guam' },
|
|
80
|
+
{ code: 'GT', name: 'Guatemala' }, { code: 'GN', name: 'Guinea' },
|
|
81
|
+
{ code: 'GW', name: 'Guinea-Bissau' }, { code: 'GY', name: 'Guyana' },
|
|
82
|
+
{ code: 'HT', name: 'Haiti' }, { code: 'HN', name: 'Honduras' },
|
|
83
|
+
{ code: 'HK', name: 'Hong Kong' }, { code: 'HU', name: 'Hungary' },
|
|
84
|
+
{ code: 'IS', name: 'Iceland' }, { code: 'IN', name: 'India' },
|
|
85
|
+
{ code: 'ID', name: 'Indonesia' }, { code: 'IR', name: 'Iran' },
|
|
86
|
+
{ code: 'IQ', name: 'Iraq' }, { code: 'IE', name: 'Ireland' },
|
|
87
|
+
{ code: 'IM', name: 'Isle of Man' }, { code: 'IL', name: 'Israel' },
|
|
88
|
+
{ code: 'IT', name: 'Italy' }, { code: 'JM', name: 'Jamaica' },
|
|
89
|
+
{ code: 'JP', name: 'Japan' }, { code: 'JE', name: 'Jersey' },
|
|
90
|
+
{ code: 'JO', name: 'Jordan' }, { code: 'KZ', name: 'Kazakhstan' },
|
|
91
|
+
{ code: 'KE', name: 'Kenya' }, { code: 'KI', name: 'Kiribati' },
|
|
92
|
+
{ code: 'KR', name: 'Korea, South' }, { code: 'XK', name: 'Kosovo' },
|
|
93
|
+
{ code: 'KW', name: 'Kuwait' }, { code: 'KG', name: 'Kyrgyzstan' },
|
|
94
|
+
{ code: 'LA', name: 'Laos' }, { code: 'LV', name: 'Latvia' },
|
|
95
|
+
{ code: 'LB', name: 'Lebanon' }, { code: 'LS', name: 'Lesotho' },
|
|
96
|
+
{ code: 'LR', name: 'Liberia' }, { code: 'LY', name: 'Libya' },
|
|
97
|
+
{ code: 'LI', name: 'Liechtenstein' }, { code: 'LT', name: 'Lithuania' },
|
|
98
|
+
{ code: 'LU', name: 'Luxembourg' }, { code: 'MO', name: 'Macao' },
|
|
99
|
+
{ code: 'MG', name: 'Madagascar' }, { code: 'MW', name: 'Malawi' },
|
|
100
|
+
{ code: 'MY', name: 'Malaysia' }, { code: 'MV', name: 'Maldives' },
|
|
101
|
+
{ code: 'ML', name: 'Mali' }, { code: 'MT', name: 'Malta' },
|
|
102
|
+
{ code: 'MH', name: 'Marshall Islands' }, { code: 'MQ', name: 'Martinique' },
|
|
103
|
+
{ code: 'MR', name: 'Mauritania' }, { code: 'MU', name: 'Mauritius' },
|
|
104
|
+
{ code: 'MX', name: 'Mexico' }, { code: 'FM', name: 'Micronesia' },
|
|
105
|
+
{ code: 'MD', name: 'Moldova' }, { code: 'MC', name: 'Monaco' },
|
|
106
|
+
{ code: 'MN', name: 'Mongolia' }, { code: 'ME', name: 'Montenegro' },
|
|
107
|
+
{ code: 'MS', name: 'Montserrat' }, { code: 'MA', name: 'Morocco' },
|
|
108
|
+
{ code: 'MZ', name: 'Mozambique' }, { code: 'MM', name: 'Myanmar' },
|
|
109
|
+
{ code: 'NA', name: 'Namibia' }, { code: 'NR', name: 'Nauru' },
|
|
110
|
+
{ code: 'NP', name: 'Nepal' }, { code: 'NL', name: 'Netherlands' },
|
|
111
|
+
{ code: 'NC', name: 'New Caledonia' }, { code: 'NZ', name: 'New Zealand' },
|
|
112
|
+
{ code: 'NI', name: 'Nicaragua' }, { code: 'NE', name: 'Niger' },
|
|
113
|
+
{ code: 'NG', name: 'Nigeria' }, { code: 'NU', name: 'Niue' },
|
|
114
|
+
{ code: 'MK', name: 'North Macedonia' }, { code: 'NO', name: 'Norway' },
|
|
115
|
+
{ code: 'OM', name: 'Oman' }, { code: 'PK', name: 'Pakistan' },
|
|
116
|
+
{ code: 'PW', name: 'Palau' }, { code: 'PS', name: 'Palestine' },
|
|
117
|
+
{ code: 'PA', name: 'Panama' }, { code: 'PG', name: 'Papua New Guinea' },
|
|
118
|
+
{ code: 'PY', name: 'Paraguay' }, { code: 'PE', name: 'Peru' },
|
|
119
|
+
{ code: 'PH', name: 'Philippines' }, { code: 'PL', name: 'Poland' },
|
|
120
|
+
{ code: 'PT', name: 'Portugal' }, { code: 'PR', name: 'Puerto Rico' },
|
|
121
|
+
{ code: 'QA', name: 'Qatar' }, { code: 'RE', name: 'Réunion' },
|
|
122
|
+
{ code: 'RO', name: 'Romania' },
|
|
123
|
+
{ code: 'RW', name: 'Rwanda' }, { code: 'KN', name: 'Saint Kitts and Nevis' },
|
|
124
|
+
{ code: 'LC', name: 'Saint Lucia' }, { code: 'VC', name: 'Saint Vincent and the Grenadines' },
|
|
125
|
+
{ code: 'WS', name: 'Samoa' }, { code: 'SM', name: 'San Marino' },
|
|
126
|
+
{ code: 'SA', name: 'Saudi Arabia' }, { code: 'SN', name: 'Senegal' },
|
|
127
|
+
{ code: 'RS', name: 'Serbia' }, { code: 'SC', name: 'Seychelles' },
|
|
128
|
+
{ code: 'SL', name: 'Sierra Leone' }, { code: 'SG', name: 'Singapore' },
|
|
129
|
+
{ code: 'SK', name: 'Slovakia' }, { code: 'SI', name: 'Slovenia' },
|
|
130
|
+
{ code: 'SB', name: 'Solomon Islands' }, { code: 'SO', name: 'Somalia' },
|
|
131
|
+
{ code: 'ZA', name: 'South Africa' }, { code: 'SS', name: 'South Sudan' },
|
|
132
|
+
{ code: 'ES', name: 'Spain' }, { code: 'LK', name: 'Sri Lanka' },
|
|
133
|
+
{ code: 'SD', name: 'Sudan' }, { code: 'SR', name: 'Suriname' },
|
|
134
|
+
{ code: 'SE', name: 'Sweden' }, { code: 'CH', name: 'Switzerland' },
|
|
135
|
+
{ code: 'SY', name: 'Syria' }, { code: 'TW', name: 'Taiwan' },
|
|
136
|
+
{ code: 'TJ', name: 'Tajikistan' }, { code: 'TZ', name: 'Tanzania' },
|
|
137
|
+
{ code: 'TH', name: 'Thailand' }, { code: 'TL', name: 'Timor-Leste' },
|
|
138
|
+
{ code: 'TG', name: 'Togo' }, { code: 'TO', name: 'Tonga' },
|
|
139
|
+
{ code: 'TT', name: 'Trinidad and Tobago' }, { code: 'TN', name: 'Tunisia' },
|
|
140
|
+
{ code: 'TR', name: 'Turkey' }, { code: 'TM', name: 'Turkmenistan' },
|
|
141
|
+
{ code: 'TC', name: 'Turks and Caicos Islands' }, { code: 'TV', name: 'Tuvalu' },
|
|
142
|
+
{ code: 'UG', name: 'Uganda' }, { code: 'UA', name: 'Ukraine' },
|
|
143
|
+
{ code: 'AE', name: 'United Arab Emirates' }, { code: 'GB', name: 'United Kingdom' },
|
|
144
|
+
{ code: 'US', name: 'United States' }, { code: 'UY', name: 'Uruguay' },
|
|
145
|
+
{ code: 'UZ', name: 'Uzbekistan' }, { code: 'VU', name: 'Vanuatu' },
|
|
146
|
+
{ code: 'VE', name: 'Venezuela' }, { code: 'VN', name: 'Vietnam' },
|
|
147
|
+
{ code: 'VG', name: 'Virgin Islands, British' }, { code: 'VI', name: 'Virgin Islands, U.S.' },
|
|
148
|
+
{ code: 'YE', name: 'Yemen' }, { code: 'ZM', name: 'Zambia' },
|
|
149
|
+
{ code: 'ZW', name: 'Zimbabwe' },
|
|
150
|
+
];
|
|
151
|
+
|
|
152
|
+
// Detect country/province/market context using priority fallback chain
|
|
153
|
+
function detectCountryContext() {
|
|
154
|
+
const ctx = { countryCode: null, provinceCode: null, marketId: null };
|
|
155
|
+
|
|
156
|
+
// Priority 1: Shopify localization/country context
|
|
157
|
+
const shopify = window.Shopify;
|
|
158
|
+
if (shopify) {
|
|
159
|
+
if (typeof shopify.country === 'string') ctx.countryCode = shopify.country;
|
|
160
|
+
const mkt = shopify.markets?.currentMarket;
|
|
161
|
+
if (mkt) {
|
|
162
|
+
if (mkt.countryCode) ctx.countryCode = mkt.countryCode;
|
|
163
|
+
if (mkt.id != null) ctx.marketId = String(mkt.id);
|
|
164
|
+
}
|
|
165
|
+
if (!ctx.countryCode && typeof shopify.locale === 'string') {
|
|
166
|
+
const parts = shopify.locale.split('-');
|
|
167
|
+
if (parts.length > 1) ctx.countryCode = parts[parts.length - 1].toUpperCase();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Priority 2: Existing customer shipping country
|
|
172
|
+
if (!ctx.countryCode || !ctx.provinceCode) {
|
|
173
|
+
const addr = window.customer?.default_address;
|
|
174
|
+
if (addr) {
|
|
175
|
+
if (!ctx.countryCode && addr.country_code) ctx.countryCode = addr.country_code;
|
|
176
|
+
if (!ctx.provinceCode && addr.province_code) ctx.provinceCode = addr.province_code;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Priority 3: Browser locale fallback
|
|
181
|
+
if (!ctx.countryCode) {
|
|
182
|
+
const parts = (navigator.language || '').split('-');
|
|
183
|
+
if (parts.length > 1) ctx.countryCode = parts[parts.length - 1].toUpperCase();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return ctx;
|
|
187
|
+
}
|
|
188
|
+
|
|
6
189
|
document.addEventListener('DOMContentLoaded', function () {
|
|
7
190
|
const wrapper = document.getElementById('notification-widget');
|
|
8
191
|
if (!wrapper) return;
|
|
@@ -147,6 +330,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
147
330
|
<h3 id="popup-title"></h3>
|
|
148
331
|
<span id="popup-close">×</span>
|
|
149
332
|
<div id="popup-text"></div>
|
|
333
|
+
<div id="popup-login-msg" style="display:none; color:#d80606; font-size:14px; margin:10px 0;">Please log in to your account to continue.</div>
|
|
150
334
|
<form id="popup-form">
|
|
151
335
|
<div class="custom-select">
|
|
152
336
|
<select name="select-size" required>
|
|
@@ -165,6 +349,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
165
349
|
const popupOpenButton = document.getElementById('popup-open');
|
|
166
350
|
const popupTitle = document.getElementById('popup-title');
|
|
167
351
|
const popupText = document.getElementById('popup-text');
|
|
352
|
+
const loginMsg = document.getElementById('popup-login-msg');
|
|
168
353
|
const sizeSelect = document.querySelector("select[name='select-size']");
|
|
169
354
|
const form = overlay.querySelector('#popup-form');
|
|
170
355
|
|
|
@@ -182,6 +367,10 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
182
367
|
popupOpenButton.getAttribute('data-fields') || '["email"]',
|
|
183
368
|
);
|
|
184
369
|
const type = popupOpenButton.getAttribute('data-type') || 'notify-me';
|
|
370
|
+
// Auth mode: 'token' (default, hardcoded ACCESS_TOKEN) or 'proxy' (Shopify App Proxy).
|
|
371
|
+
const authMode = popupOpenButton.getAttribute('data-auth') || 'token';
|
|
372
|
+
// Tenant for X-Twc-Tenant header; used in both auth modes.
|
|
373
|
+
const tenant = popupOpenButton.getAttribute('data-tenant') || TENANT_ID;
|
|
185
374
|
const typeConfig = {
|
|
186
375
|
'notify-me': {
|
|
187
376
|
text: 'Register to receive a notification as soon as this item is back in stock',
|
|
@@ -196,14 +385,25 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
196
385
|
// Get product data from Shopify's global variable
|
|
197
386
|
const productData = window?.currentProduct;
|
|
198
387
|
|
|
388
|
+
const countryCtx = detectCountryContext();
|
|
389
|
+
const resolvedMarketId =
|
|
390
|
+
popupOpenButton.getAttribute('data-market-id') || countryCtx.marketId;
|
|
391
|
+
|
|
199
392
|
// Map for form fields
|
|
200
393
|
popupText.innerText = typeConfig[type].text;
|
|
201
394
|
|
|
395
|
+
const countryOptions = COUNTRY_CODES.map(
|
|
396
|
+
(c) =>
|
|
397
|
+
`<option value="${c.code}"${c.code === countryCtx.countryCode ? ' selected' : ''}>${c.name}</option>`,
|
|
398
|
+
).join('');
|
|
399
|
+
|
|
202
400
|
const fieldMap = {
|
|
203
401
|
email: `<input name="email" placeholder="Email" type="email" required />`,
|
|
204
402
|
mobile: `<input name="mobile" placeholder="Mobile" type="tel" required />`,
|
|
205
403
|
firstName: `<input name="firstName" placeholder="First name" type="text" required />`,
|
|
206
404
|
lastName: `<input name="lastName" placeholder="Last name" type="text" required />`,
|
|
405
|
+
countryCode: `<div class="custom-select"><select name="countryCode"><option value="">Select country</option>${countryOptions}</select></div>`,
|
|
406
|
+
provinceCode: `<input name="provinceCode" placeholder="State / Province code" type="text" value="${countryCtx.provinceCode || ''}" />`,
|
|
207
407
|
};
|
|
208
408
|
|
|
209
409
|
// Add fields to the form based on the parsed fields
|
|
@@ -256,7 +456,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
256
456
|
|
|
257
457
|
// Form submission handler
|
|
258
458
|
if (form) {
|
|
259
|
-
form.addEventListener('submit', function (event) {
|
|
459
|
+
form.addEventListener('submit', async function (event) {
|
|
260
460
|
event.preventDefault();
|
|
261
461
|
|
|
262
462
|
const selectedSize = sizeSelect.value;
|
|
@@ -293,14 +493,43 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
293
493
|
// formData.phone = form.querySelector("input[name='mobile']").value;
|
|
294
494
|
}
|
|
295
495
|
|
|
496
|
+
// Country / province / market — manual fields take precedence over auto-detected values
|
|
497
|
+
if (fields.includes('countryCode')) {
|
|
498
|
+
const selected = form.querySelector("select[name='countryCode']").value;
|
|
499
|
+
if (selected) formData.countryCode = selected;
|
|
500
|
+
} else if (countryCtx.countryCode) {
|
|
501
|
+
formData.countryCode = countryCtx.countryCode;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (fields.includes('provinceCode')) {
|
|
505
|
+
const val = form.querySelector("input[name='provinceCode']").value;
|
|
506
|
+
if (val) formData.provinceCode = val;
|
|
507
|
+
} else if (countryCtx.provinceCode) {
|
|
508
|
+
formData.provinceCode = countryCtx.provinceCode;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (resolvedMarketId) formData.marketId = resolvedMarketId;
|
|
512
|
+
|
|
513
|
+
// Resolve the Authorization token based on auth mode.
|
|
514
|
+
let authToken = ACCESS_TOKEN;
|
|
515
|
+
if (authMode === 'proxy') {
|
|
516
|
+
const proxyToken = await getProxyAccessToken();
|
|
517
|
+
if (!proxyToken) {
|
|
518
|
+
loginMsg.style.display = 'block';
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
loginMsg.style.display = 'none';
|
|
522
|
+
authToken = `Bearer ${proxyToken}`;
|
|
523
|
+
}
|
|
524
|
+
|
|
296
525
|
let url = `https://api.au-sandbox.thewishlist.io/services/wsservice/api/wishlist/items/customerInterest`;
|
|
297
526
|
|
|
298
527
|
fetch(url, {
|
|
299
528
|
method: 'POST',
|
|
300
529
|
headers: {
|
|
301
530
|
'Content-Type': 'application/json',
|
|
302
|
-
Authorization:
|
|
303
|
-
'X-Twc-Tenant':
|
|
531
|
+
Authorization: authToken,
|
|
532
|
+
'X-Twc-Tenant': tenant,
|
|
304
533
|
},
|
|
305
534
|
body: JSON.stringify(formData),
|
|
306
535
|
})
|