@huloglobal/vendure-plugin-geo-block 0.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/CHANGELOG.md +20 -0
- package/LICENSE +34 -0
- package/README.md +146 -0
- package/dist/geo-block.controller.d.ts +56 -0
- package/dist/geo-block.controller.d.ts.map +1 -0
- package/dist/geo-block.controller.js +236 -0
- package/dist/geo-block.controller.js.map +1 -0
- package/dist/geo-regions.d.ts +35 -0
- package/dist/geo-regions.d.ts.map +1 -0
- package/dist/geo-regions.js +62 -0
- package/dist/geo-regions.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin.d.ts +47 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +140 -0
- package/dist/plugin.js.map +1 -0
- package/dist/proxy-headers.d.ts +37 -0
- package/dist/proxy-headers.d.ts.map +1 -0
- package/dist/proxy-headers.js +81 -0
- package/dist/proxy-headers.js.map +1 -0
- package/package.json +41 -0
- package/ui/components/geo-block.component.ts +573 -0
- package/ui/geo-block.module.ts +17 -0
|
@@ -0,0 +1,573 @@
|
|
|
1
|
+
import { Component, OnInit, ChangeDetectorRef } from '@angular/core';
|
|
2
|
+
import { HttpClient } from '@angular/common/http';
|
|
3
|
+
import { NotificationService } from '@vendure/admin-ui/core';
|
|
4
|
+
|
|
5
|
+
interface ChannelRow {
|
|
6
|
+
id: number;
|
|
7
|
+
code: string;
|
|
8
|
+
token: string;
|
|
9
|
+
enabled: boolean;
|
|
10
|
+
allowedRegions: string[];
|
|
11
|
+
extraAllowed: string[];
|
|
12
|
+
blockedCountries: string[];
|
|
13
|
+
allowedGbRegions: string[];
|
|
14
|
+
resolved: { allowedCountries: string[] | null; blockedCountries: string[] };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface RegionDef { value: string; label: string; hint: string; countries: number; }
|
|
18
|
+
interface CountryDef { value: string; label: string; flag: string; }
|
|
19
|
+
|
|
20
|
+
@Component({
|
|
21
|
+
selector: 'ees-geo-block',
|
|
22
|
+
standalone: false,
|
|
23
|
+
template: `
|
|
24
|
+
<vdr-page-block>
|
|
25
|
+
<vdr-action-bar>
|
|
26
|
+
<vdr-ab-left><h2>Site access (geo-block)</h2></vdr-ab-left>
|
|
27
|
+
<vdr-ab-right>
|
|
28
|
+
<button class="btn btn-link" (click)="reload()" [disabled]="loading">
|
|
29
|
+
<clr-icon shape="refresh"></clr-icon> Refresh
|
|
30
|
+
</button>
|
|
31
|
+
</vdr-ab-right>
|
|
32
|
+
</vdr-action-bar>
|
|
33
|
+
</vdr-page-block>
|
|
34
|
+
|
|
35
|
+
<vdr-page-block>
|
|
36
|
+
<div class="card" *ngIf="!loading && current">
|
|
37
|
+
<div class="card-block">
|
|
38
|
+
<div class="chan-row">
|
|
39
|
+
<label class="lbl">Channel</label>
|
|
40
|
+
<select class="form-select" [(ngModel)]="currentToken" (ngModelChange)="onChannelChange()">
|
|
41
|
+
<option *ngFor="let c of channels" [value]="c.token">{{ c.code }}</option>
|
|
42
|
+
</select>
|
|
43
|
+
|
|
44
|
+
<span class="status-pill" [class.on]="current.enabled" [class.off]="!current.enabled">
|
|
45
|
+
{{ current.enabled ? 'GEO-BLOCK ON' : 'GEO-BLOCK OFF' }}
|
|
46
|
+
</span>
|
|
47
|
+
<button class="btn btn-sm btn-link" (click)="toggleEnabled()">
|
|
48
|
+
{{ current.enabled ? 'Turn off' : 'Turn on' }}
|
|
49
|
+
</button>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<p class="hint" *ngIf="!current.enabled">
|
|
53
|
+
Geo-block is <strong>off</strong> — everyone can visit the storefront, regardless of the settings below.
|
|
54
|
+
Use the toggle above to turn it on.
|
|
55
|
+
</p>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
</vdr-page-block>
|
|
59
|
+
|
|
60
|
+
<vdr-page-block *ngIf="!loading && current">
|
|
61
|
+
<div class="card">
|
|
62
|
+
<div class="card-block">
|
|
63
|
+
<h3 class="step-title">1 · Choose a mode</h3>
|
|
64
|
+
<p class="hint">Pick one — the rest of the page changes based on what you select.</p>
|
|
65
|
+
|
|
66
|
+
<div class="mode-grid">
|
|
67
|
+
<label class="mode-card" [class.active]="mode === 'specific'">
|
|
68
|
+
<input type="radio" name="mode" value="specific" [(ngModel)]="mode" (ngModelChange)="onModeChange()">
|
|
69
|
+
<div class="mode-title">Allow only specific places</div>
|
|
70
|
+
<div class="mode-body">Pick regions and / or individual countries. Everyone else is blocked. Best for “UK only” or “UK + Europe”.</div>
|
|
71
|
+
</label>
|
|
72
|
+
|
|
73
|
+
<label class="mode-card" [class.active]="mode === 'worldwide'">
|
|
74
|
+
<input type="radio" name="mode" value="worldwide" [(ngModel)]="mode" (ngModelChange)="onModeChange()">
|
|
75
|
+
<div class="mode-title">Allow worldwide, except blocked</div>
|
|
76
|
+
<div class="mode-body">Everyone is welcome <em>except</em> the countries you add to the block list. Best for “serve everyone but sanction-targeted countries”.</div>
|
|
77
|
+
</label>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
</vdr-page-block>
|
|
82
|
+
|
|
83
|
+
<vdr-page-block *ngIf="!loading && current && mode === 'specific'">
|
|
84
|
+
<div class="card">
|
|
85
|
+
<div class="card-block">
|
|
86
|
+
<h3 class="step-title">2 · Allowed regions <small>(one click)</small></h3>
|
|
87
|
+
<p class="hint">Each preset adds a whole group of countries. Tick as many as you want — they stack.</p>
|
|
88
|
+
|
|
89
|
+
<div class="preset-grid">
|
|
90
|
+
<label *ngFor="let r of regionDefs" class="preset-card" [class.active]="isRegionPicked(r.value)">
|
|
91
|
+
<input type="checkbox" [checked]="isRegionPicked(r.value)" (change)="toggleRegion(r.value)">
|
|
92
|
+
<div class="preset-label">{{ r.label }}</div>
|
|
93
|
+
<div class="preset-hint">{{ r.hint }}<span *ngIf="r.countries"> · {{ r.countries }} countries</span></div>
|
|
94
|
+
</label>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
</vdr-page-block>
|
|
99
|
+
|
|
100
|
+
<vdr-page-block *ngIf="!loading && current && mode === 'specific'">
|
|
101
|
+
<div class="card">
|
|
102
|
+
<div class="card-block">
|
|
103
|
+
<h3 class="step-title">3 · Add extra countries <small>(optional)</small></h3>
|
|
104
|
+
<p class="hint">Countries that aren't in any preset above — for example: add 🇯🇵 Japan or 🇮🇱 Israel while keeping “UK only” selected.</p>
|
|
105
|
+
|
|
106
|
+
<div class="chip-row">
|
|
107
|
+
<span class="chip" *ngFor="let cc of current.extraAllowed">
|
|
108
|
+
{{ flag(cc) }} {{ countryLabel(cc) }}
|
|
109
|
+
<button class="chip-x" (click)="removeExtra(cc)" title="Remove">×</button>
|
|
110
|
+
</span>
|
|
111
|
+
<span *ngIf="!current.extraAllowed.length" class="hint inline">None yet.</span>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<div class="picker">
|
|
115
|
+
<select class="form-select" [(ngModel)]="newExtra">
|
|
116
|
+
<option value="">— pick a country —</option>
|
|
117
|
+
<option *ngFor="let c of unpickedExtras()" [value]="c.value">{{ c.flag }} {{ c.label }}</option>
|
|
118
|
+
</select>
|
|
119
|
+
<button class="btn btn-secondary btn-sm" (click)="addExtra()" [disabled]="!newExtra">+ Add</button>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
</vdr-page-block>
|
|
124
|
+
|
|
125
|
+
<vdr-page-block *ngIf="!loading && current">
|
|
126
|
+
<div class="card">
|
|
127
|
+
<div class="card-block">
|
|
128
|
+
<h3 class="step-title">{{ mode === 'specific' ? '4' : '2' }} · Block specific countries</h3>
|
|
129
|
+
<p class="hint" *ngIf="mode === 'specific'">Countries in this list are always blocked, even if they would otherwise be allowed by a preset. Useful for sanctioned countries inside a region you allow (e.g. block 🇷🇺 Russia while allowing “Europe”).</p>
|
|
130
|
+
<p class="hint" *ngIf="mode === 'worldwide'">In worldwide mode this list is the <em>only</em> filter — everyone except these countries can visit.</p>
|
|
131
|
+
|
|
132
|
+
<div class="chip-row">
|
|
133
|
+
<span class="chip blocked" *ngFor="let cc of current.blockedCountries">
|
|
134
|
+
{{ flag(cc) }} {{ countryLabel(cc) }}
|
|
135
|
+
<button class="chip-x" (click)="removeBlocked(cc)" title="Remove">×</button>
|
|
136
|
+
</span>
|
|
137
|
+
<span *ngIf="!current.blockedCountries.length" class="hint inline">None yet.</span>
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
<div class="picker">
|
|
141
|
+
<select class="form-select" [(ngModel)]="newBlocked">
|
|
142
|
+
<option value="">— pick a country —</option>
|
|
143
|
+
<option *ngFor="let c of unpickedBlocked()" [value]="c.value">{{ c.flag }} {{ c.label }}</option>
|
|
144
|
+
</select>
|
|
145
|
+
<button class="btn btn-secondary btn-sm" (click)="addBlocked()" [disabled]="!newBlocked">+ Add</button>
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
</vdr-page-block>
|
|
150
|
+
|
|
151
|
+
<vdr-page-block *ngIf="!loading && current && mode === 'specific' && isUkResolved()">
|
|
152
|
+
<div class="card">
|
|
153
|
+
<div class="card-block">
|
|
154
|
+
<h3 class="step-title">5 · UK subdivisions <small>(only if 🇬🇧 is allowed)</small></h3>
|
|
155
|
+
<p class="hint">Tighten the UK further. Leave all four ticked (or all unchecked) to allow the whole of the UK.</p>
|
|
156
|
+
|
|
157
|
+
<div class="uk-row">
|
|
158
|
+
<label *ngFor="let r of ukRegions" class="uk-pill" [class.active]="current.allowedGbRegions.includes(r.value)">
|
|
159
|
+
<input type="checkbox"
|
|
160
|
+
[checked]="current.allowedGbRegions.includes(r.value)"
|
|
161
|
+
(change)="toggleUkRegion(r.value)">
|
|
162
|
+
{{ r.label }}
|
|
163
|
+
</label>
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
</vdr-page-block>
|
|
168
|
+
|
|
169
|
+
<vdr-page-block *ngIf="!loading && current">
|
|
170
|
+
<div class="card preview-card">
|
|
171
|
+
<div class="card-block">
|
|
172
|
+
<h3 class="step-title">Resolved allow-list <small>(what will happen when you save)</small></h3>
|
|
173
|
+
|
|
174
|
+
<div *ngIf="!current.enabled" class="preview-banner preview-off">
|
|
175
|
+
<strong>Geo-block is OFF</strong> — everyone can visit. Nothing below applies.
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
<div *ngIf="current.enabled">
|
|
179
|
+
<div class="preview-banner preview-allow">
|
|
180
|
+
<strong *ngIf="resolvedAllowed() === null">
|
|
181
|
+
✅ Allow visitors from anywhere
|
|
182
|
+
</strong>
|
|
183
|
+
<strong *ngIf="resolvedAllowed() !== null && resolvedAllowed()!.length">
|
|
184
|
+
✅ Allow visitors from {{ resolvedAllowed()!.length }} {{ resolvedAllowed()!.length === 1 ? 'country' : 'countries' }}
|
|
185
|
+
</strong>
|
|
186
|
+
<strong *ngIf="resolvedAllowed() !== null && !resolvedAllowed()!.length" class="warn">
|
|
187
|
+
⚠️ Nothing is allowed — every visitor will see the maintenance page.
|
|
188
|
+
</strong>
|
|
189
|
+
|
|
190
|
+
<div class="country-chips" *ngIf="resolvedAllowed() !== null && resolvedAllowed()!.length">
|
|
191
|
+
<span class="mini-chip" *ngFor="let cc of resolvedAllowed()!">{{ flag(cc) }} {{ cc }}</span>
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
|
|
195
|
+
<div class="preview-banner preview-block" *ngIf="current.blockedCountries.length">
|
|
196
|
+
<strong>🚫 Block {{ current.blockedCountries.length }} {{ current.blockedCountries.length === 1 ? 'country' : 'countries' }}</strong>
|
|
197
|
+
<div class="country-chips">
|
|
198
|
+
<span class="mini-chip blocked" *ngFor="let cc of current.blockedCountries">{{ flag(cc) }} {{ cc }}</span>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
<div class="preview-banner preview-uk" *ngIf="isUkResolved() && current.allowedGbRegions.length">
|
|
203
|
+
<strong>🏴 Within the UK, allow only:</strong>
|
|
204
|
+
<span class="mini-chip" *ngFor="let r of current.allowedGbRegions">{{ ukLabel(r) }}</span>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
</vdr-page-block>
|
|
210
|
+
|
|
211
|
+
<vdr-page-block *ngIf="!loading && current">
|
|
212
|
+
<div style="display:flex;gap:8px;align-items:center">
|
|
213
|
+
<button class="btn btn-primary" (click)="save()" [disabled]="saving">
|
|
214
|
+
{{ saving ? 'Saving…' : 'Save changes' }}
|
|
215
|
+
</button>
|
|
216
|
+
<button class="btn btn-link" (click)="reload()" [disabled]="saving">Discard</button>
|
|
217
|
+
<span class="hint inline" *ngIf="dirty">Unsaved changes</span>
|
|
218
|
+
</div>
|
|
219
|
+
</vdr-page-block>
|
|
220
|
+
`,
|
|
221
|
+
styles: [`
|
|
222
|
+
:host { color: var(--color-text-100, inherit); display: block; }
|
|
223
|
+
.chan-row { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
|
|
224
|
+
.chan-row .lbl { font-size: 12px; color: var(--color-component-color-300); }
|
|
225
|
+
.form-select {
|
|
226
|
+
padding: 6px 10px; border-radius: 4px; min-width: 200px;
|
|
227
|
+
border: 1px solid var(--color-component-border-200);
|
|
228
|
+
background: var(--color-component-bg-100);
|
|
229
|
+
color: var(--color-text-100, inherit);
|
|
230
|
+
}
|
|
231
|
+
.status-pill {
|
|
232
|
+
display: inline-block; padding: 3px 12px; border-radius: 12px;
|
|
233
|
+
font-size: 11px; font-weight: 700; letter-spacing: 0.5px;
|
|
234
|
+
}
|
|
235
|
+
.status-pill.on { background: #10b981; color: #fff; }
|
|
236
|
+
.status-pill.off { background: var(--color-component-bg-200); color: var(--color-component-color-300); }
|
|
237
|
+
|
|
238
|
+
.hint { font-size: 13px; color: var(--color-component-color-300); margin: 6px 0 12px; }
|
|
239
|
+
.hint.inline { display: inline; margin: 0; }
|
|
240
|
+
|
|
241
|
+
.step-title { font-size: 16px; font-weight: 600; margin-bottom: 4px; }
|
|
242
|
+
.step-title small { color: var(--color-component-color-300); font-weight: 400; }
|
|
243
|
+
|
|
244
|
+
.mode-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; }
|
|
245
|
+
.mode-card {
|
|
246
|
+
display: block; padding: 16px;
|
|
247
|
+
border: 2px solid var(--color-component-border-200);
|
|
248
|
+
border-radius: 8px; cursor: pointer;
|
|
249
|
+
background: var(--color-component-bg-100);
|
|
250
|
+
transition: border-color .15s;
|
|
251
|
+
}
|
|
252
|
+
.mode-card.active { border-color: var(--color-primary-500, #1d4ed8); }
|
|
253
|
+
.mode-card input { margin-right: 8px; }
|
|
254
|
+
.mode-card .mode-title { font-weight: 600; font-size: 14px; margin-bottom: 4px; }
|
|
255
|
+
.mode-card .mode-body { font-size: 12px; color: var(--color-component-color-300); line-height: 1.5; }
|
|
256
|
+
|
|
257
|
+
.preset-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 10px; }
|
|
258
|
+
.preset-card {
|
|
259
|
+
display: block; padding: 12px;
|
|
260
|
+
border: 1px solid var(--color-component-border-200);
|
|
261
|
+
border-radius: 6px; cursor: pointer;
|
|
262
|
+
background: var(--color-component-bg-100);
|
|
263
|
+
}
|
|
264
|
+
.preset-card.active { border-color: var(--color-primary-500, #1d4ed8); background: var(--color-component-bg-200); }
|
|
265
|
+
.preset-card input { float: right; }
|
|
266
|
+
.preset-label { font-weight: 600; font-size: 13px; }
|
|
267
|
+
.preset-hint { font-size: 11px; color: var(--color-component-color-300); margin-top: 4px; }
|
|
268
|
+
|
|
269
|
+
.chip-row { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; min-height: 30px; }
|
|
270
|
+
.chip {
|
|
271
|
+
display: inline-flex; align-items: center; gap: 6px;
|
|
272
|
+
padding: 4px 10px; border-radius: 14px;
|
|
273
|
+
background: #dbeafe; color: #1e3a8a; font-size: 12px;
|
|
274
|
+
border: 1px solid #93c5fd;
|
|
275
|
+
}
|
|
276
|
+
.chip.blocked { background: #fee2e2; color: #991b1b; border-color: #fca5a5; }
|
|
277
|
+
.chip-x {
|
|
278
|
+
background: transparent; border: none; cursor: pointer; padding: 0 0 0 2px;
|
|
279
|
+
color: inherit; font-size: 16px; line-height: 1;
|
|
280
|
+
}
|
|
281
|
+
.picker { display: flex; gap: 8px; align-items: center; }
|
|
282
|
+
|
|
283
|
+
.uk-row { display: flex; gap: 8px; flex-wrap: wrap; }
|
|
284
|
+
.uk-pill {
|
|
285
|
+
display: inline-flex; align-items: center; gap: 6px;
|
|
286
|
+
padding: 6px 14px; border-radius: 18px; cursor: pointer;
|
|
287
|
+
border: 1px solid var(--color-component-border-200);
|
|
288
|
+
background: var(--color-component-bg-100);
|
|
289
|
+
font-size: 13px;
|
|
290
|
+
}
|
|
291
|
+
.uk-pill.active { border-color: var(--color-primary-500, #1d4ed8); background: #dbeafe; color: #1e3a8a; }
|
|
292
|
+
|
|
293
|
+
.preview-card { border-left: 4px solid var(--color-primary-500, #1d4ed8); }
|
|
294
|
+
.preview-banner {
|
|
295
|
+
padding: 12px; border-radius: 6px; margin: 10px 0;
|
|
296
|
+
background: var(--color-component-bg-200);
|
|
297
|
+
}
|
|
298
|
+
.preview-banner .warn { color: #ef4444; }
|
|
299
|
+
.preview-banner .country-chips { margin-top: 8px; display: flex; flex-wrap: wrap; gap: 4px; }
|
|
300
|
+
.mini-chip {
|
|
301
|
+
display: inline-block; padding: 2px 6px; border-radius: 4px;
|
|
302
|
+
background: var(--color-component-bg-100); font-size: 11px;
|
|
303
|
+
border: 1px solid var(--color-component-border-200);
|
|
304
|
+
font-family: var(--clr-font-family-monospace, monospace);
|
|
305
|
+
}
|
|
306
|
+
.mini-chip.blocked { background: #fee2e2; color: #991b1b; border-color: #fca5a5; }
|
|
307
|
+
`],
|
|
308
|
+
})
|
|
309
|
+
export class GeoBlockComponent implements OnInit {
|
|
310
|
+
loading = true;
|
|
311
|
+
saving = false;
|
|
312
|
+
channels: ChannelRow[] = [];
|
|
313
|
+
currentToken = '';
|
|
314
|
+
current: ChannelRow | null = null;
|
|
315
|
+
/** UI mode derived from underlying fields. */
|
|
316
|
+
mode: 'specific' | 'worldwide' = 'specific';
|
|
317
|
+
dirty = false;
|
|
318
|
+
newExtra = '';
|
|
319
|
+
newBlocked = '';
|
|
320
|
+
|
|
321
|
+
/** Canonical region definitions — must match values in the channel
|
|
322
|
+
* customField options so existing rows round-trip cleanly. */
|
|
323
|
+
regionDefs: RegionDef[] = [
|
|
324
|
+
{ value: 'UK_ONLY', label: '🇬🇧 United Kingdom only', hint: 'Just GB.', countries: 1 },
|
|
325
|
+
{ value: 'BRITISH_ISLES', label: '🏴 British Isles', hint: 'UK, Ireland, Isle of Man, Channel Islands, Faroes.', countries: 6 },
|
|
326
|
+
{ value: 'EU', label: '🇪🇺 European Union (27)', hint: 'All current EU member states.', countries: 27 },
|
|
327
|
+
{ value: 'EEA', label: '🇪🇺 EEA', hint: 'EU + Iceland, Liechtenstein, Norway.', countries: 30 },
|
|
328
|
+
{ value: 'EUROPE', label: '🌍 All of Europe', hint: 'Whole continent incl. UK, RU, UA, micro-states.', countries: 51 },
|
|
329
|
+
{ value: 'NORTH_AMERICA', label: '🌎 North America', hint: 'US, Canada, Mexico.', countries: 3 },
|
|
330
|
+
{ value: 'OCEANIA', label: '🌏 Oceania', hint: 'Australia, New Zealand.', countries: 2 },
|
|
331
|
+
];
|
|
332
|
+
|
|
333
|
+
/** Curated country list — matches the dropdown options on the
|
|
334
|
+
* channel customField, plus a few common extras. */
|
|
335
|
+
countryDefs: CountryDef[] = [
|
|
336
|
+
{ value: 'GB', label: 'United Kingdom', flag: '🇬🇧' },
|
|
337
|
+
{ value: 'IE', label: 'Ireland', flag: '🇮🇪' },
|
|
338
|
+
{ value: 'FR', label: 'France', flag: '🇫🇷' },
|
|
339
|
+
{ value: 'DE', label: 'Germany', flag: '🇩🇪' },
|
|
340
|
+
{ value: 'NL', label: 'Netherlands', flag: '🇳🇱' },
|
|
341
|
+
{ value: 'BE', label: 'Belgium', flag: '🇧🇪' },
|
|
342
|
+
{ value: 'LU', label: 'Luxembourg', flag: '🇱🇺' },
|
|
343
|
+
{ value: 'ES', label: 'Spain', flag: '🇪🇸' },
|
|
344
|
+
{ value: 'PT', label: 'Portugal', flag: '🇵🇹' },
|
|
345
|
+
{ value: 'IT', label: 'Italy', flag: '🇮🇹' },
|
|
346
|
+
{ value: 'AT', label: 'Austria', flag: '🇦🇹' },
|
|
347
|
+
{ value: 'CH', label: 'Switzerland', flag: '🇨🇭' },
|
|
348
|
+
{ value: 'DK', label: 'Denmark', flag: '🇩🇰' },
|
|
349
|
+
{ value: 'SE', label: 'Sweden', flag: '🇸🇪' },
|
|
350
|
+
{ value: 'NO', label: 'Norway', flag: '🇳🇴' },
|
|
351
|
+
{ value: 'FI', label: 'Finland', flag: '🇫🇮' },
|
|
352
|
+
{ value: 'IS', label: 'Iceland', flag: '🇮🇸' },
|
|
353
|
+
{ value: 'PL', label: 'Poland', flag: '🇵🇱' },
|
|
354
|
+
{ value: 'CZ', label: 'Czechia', flag: '🇨🇿' },
|
|
355
|
+
{ value: 'US', label: 'United States', flag: '🇺🇸' },
|
|
356
|
+
{ value: 'CA', label: 'Canada', flag: '🇨🇦' },
|
|
357
|
+
{ value: 'AU', label: 'Australia', flag: '🇦🇺' },
|
|
358
|
+
{ value: 'NZ', label: 'New Zealand', flag: '🇳🇿' },
|
|
359
|
+
{ value: 'JP', label: 'Japan', flag: '🇯🇵' },
|
|
360
|
+
{ value: 'IL', label: 'Israel', flag: '🇮🇱' },
|
|
361
|
+
// Sanction-list defaults — useful in the block picker.
|
|
362
|
+
{ value: 'RU', label: 'Russia', flag: '🇷🇺' },
|
|
363
|
+
{ value: 'BY', label: 'Belarus', flag: '🇧🇾' },
|
|
364
|
+
{ value: 'UA', label: 'Ukraine', flag: '🇺🇦' },
|
|
365
|
+
{ value: 'IR', label: 'Iran', flag: '🇮🇷' },
|
|
366
|
+
{ value: 'KP', label: 'North Korea', flag: '🇰🇵' },
|
|
367
|
+
{ value: 'SY', label: 'Syria', flag: '🇸🇾' },
|
|
368
|
+
{ value: 'CU', label: 'Cuba', flag: '🇨🇺' },
|
|
369
|
+
{ value: 'CN', label: 'China', flag: '🇨🇳' },
|
|
370
|
+
];
|
|
371
|
+
|
|
372
|
+
ukRegions = [
|
|
373
|
+
{ value: 'ENG', label: '🏴 England' },
|
|
374
|
+
{ value: 'WLS', label: '🏴 Wales' },
|
|
375
|
+
{ value: 'SCT', label: '🏴 Scotland' },
|
|
376
|
+
{ value: 'NIR', label: '🇬🇧 Northern Ireland' },
|
|
377
|
+
];
|
|
378
|
+
|
|
379
|
+
constructor(
|
|
380
|
+
private http: HttpClient,
|
|
381
|
+
private notify: NotificationService,
|
|
382
|
+
private cdr: ChangeDetectorRef,
|
|
383
|
+
) {}
|
|
384
|
+
|
|
385
|
+
ngOnInit() { this.reload(); }
|
|
386
|
+
|
|
387
|
+
reload() {
|
|
388
|
+
this.loading = true;
|
|
389
|
+
this.dirty = false;
|
|
390
|
+
this.http.get<{ channels: ChannelRow[] }>('/ees/geo-block/admin/channels').subscribe({
|
|
391
|
+
next: (res) => {
|
|
392
|
+
this.channels = res.channels || [];
|
|
393
|
+
if (!this.currentToken && this.channels.length) {
|
|
394
|
+
this.currentToken = this.channels[0].token;
|
|
395
|
+
}
|
|
396
|
+
this.current = this.channels.find(c => c.token === this.currentToken) || null;
|
|
397
|
+
this.deriveMode();
|
|
398
|
+
this.loading = false;
|
|
399
|
+
this.cdr.markForCheck();
|
|
400
|
+
},
|
|
401
|
+
error: () => {
|
|
402
|
+
this.loading = false;
|
|
403
|
+
this.notify.error('Failed to load channels');
|
|
404
|
+
},
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
onChannelChange() {
|
|
409
|
+
this.current = this.channels.find(c => c.token === this.currentToken) || null;
|
|
410
|
+
this.dirty = false;
|
|
411
|
+
this.deriveMode();
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
private deriveMode() {
|
|
415
|
+
if (!this.current) return;
|
|
416
|
+
this.mode = this.current.allowedRegions.includes('WORLDWIDE') ? 'worldwide' : 'specific';
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
onModeChange() {
|
|
420
|
+
if (!this.current) return;
|
|
421
|
+
if (this.mode === 'worldwide') {
|
|
422
|
+
// Replace presets with just WORLDWIDE (the resolver short-circuits).
|
|
423
|
+
this.current.allowedRegions = ['WORLDWIDE'];
|
|
424
|
+
this.current.extraAllowed = [];
|
|
425
|
+
} else {
|
|
426
|
+
this.current.allowedRegions = this.current.allowedRegions.filter(r => r !== 'WORLDWIDE');
|
|
427
|
+
if (!this.current.allowedRegions.length && !this.current.extraAllowed.length) {
|
|
428
|
+
// Sensible default when switching back.
|
|
429
|
+
this.current.allowedRegions = ['UK_ONLY'];
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
this.markDirty();
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
toggleEnabled() {
|
|
436
|
+
if (!this.current) return;
|
|
437
|
+
this.current.enabled = !this.current.enabled;
|
|
438
|
+
this.markDirty();
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
isRegionPicked(r: string): boolean {
|
|
442
|
+
return !!this.current?.allowedRegions.includes(r);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
toggleRegion(r: string) {
|
|
446
|
+
if (!this.current) return;
|
|
447
|
+
if (this.isRegionPicked(r)) {
|
|
448
|
+
this.current.allowedRegions = this.current.allowedRegions.filter(x => x !== r);
|
|
449
|
+
} else {
|
|
450
|
+
this.current.allowedRegions = [...this.current.allowedRegions, r];
|
|
451
|
+
}
|
|
452
|
+
this.markDirty();
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
unpickedExtras(): CountryDef[] {
|
|
456
|
+
return this.countryDefs.filter(c => !this.current?.extraAllowed.includes(c.value));
|
|
457
|
+
}
|
|
458
|
+
unpickedBlocked(): CountryDef[] {
|
|
459
|
+
return this.countryDefs.filter(c => !this.current?.blockedCountries.includes(c.value));
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
addExtra() {
|
|
463
|
+
if (!this.current || !this.newExtra) return;
|
|
464
|
+
if (!this.current.extraAllowed.includes(this.newExtra)) {
|
|
465
|
+
this.current.extraAllowed = [...this.current.extraAllowed, this.newExtra];
|
|
466
|
+
this.markDirty();
|
|
467
|
+
}
|
|
468
|
+
this.newExtra = '';
|
|
469
|
+
}
|
|
470
|
+
removeExtra(cc: string) {
|
|
471
|
+
if (!this.current) return;
|
|
472
|
+
this.current.extraAllowed = this.current.extraAllowed.filter(c => c !== cc);
|
|
473
|
+
this.markDirty();
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
addBlocked() {
|
|
477
|
+
if (!this.current || !this.newBlocked) return;
|
|
478
|
+
if (!this.current.blockedCountries.includes(this.newBlocked)) {
|
|
479
|
+
this.current.blockedCountries = [...this.current.blockedCountries, this.newBlocked];
|
|
480
|
+
this.markDirty();
|
|
481
|
+
}
|
|
482
|
+
this.newBlocked = '';
|
|
483
|
+
}
|
|
484
|
+
removeBlocked(cc: string) {
|
|
485
|
+
if (!this.current) return;
|
|
486
|
+
this.current.blockedCountries = this.current.blockedCountries.filter(c => c !== cc);
|
|
487
|
+
this.markDirty();
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
toggleUkRegion(r: string) {
|
|
491
|
+
if (!this.current) return;
|
|
492
|
+
if (this.current.allowedGbRegions.includes(r)) {
|
|
493
|
+
this.current.allowedGbRegions = this.current.allowedGbRegions.filter(x => x !== r);
|
|
494
|
+
} else {
|
|
495
|
+
this.current.allowedGbRegions = [...this.current.allowedGbRegions, r];
|
|
496
|
+
}
|
|
497
|
+
this.markDirty();
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
isUkResolved(): boolean {
|
|
501
|
+
const allowed = this.resolvedAllowed();
|
|
502
|
+
return allowed === null || allowed.includes('GB');
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
flag(cc: string): string {
|
|
506
|
+
return this.countryDefs.find(c => c.value === cc)?.flag || cc;
|
|
507
|
+
}
|
|
508
|
+
countryLabel(cc: string): string {
|
|
509
|
+
return this.countryDefs.find(c => c.value === cc)?.label || cc;
|
|
510
|
+
}
|
|
511
|
+
ukLabel(r: string): string {
|
|
512
|
+
return this.ukRegions.find(u => u.value === r)?.label || r;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/** Re-compute the resolved allow-list locally so the preview matches
|
|
516
|
+
* exactly what the backend resolver will produce on save. */
|
|
517
|
+
resolvedAllowed(): string[] | null {
|
|
518
|
+
if (!this.current) return [];
|
|
519
|
+
const regions = this.current.allowedRegions.map(r => r.toUpperCase());
|
|
520
|
+
if (regions.includes('WORLDWIDE')) return null;
|
|
521
|
+
const RM = REGION_TO_COUNTRIES;
|
|
522
|
+
const set = new Set<string>();
|
|
523
|
+
for (const r of regions) {
|
|
524
|
+
const cs = RM[r as keyof typeof RM];
|
|
525
|
+
if (cs) for (const c of cs) set.add(c);
|
|
526
|
+
}
|
|
527
|
+
for (const c of this.current.extraAllowed) set.add(c.toUpperCase());
|
|
528
|
+
for (const c of this.current.blockedCountries) set.delete(c.toUpperCase());
|
|
529
|
+
return Array.from(set).sort();
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
private markDirty() { this.dirty = true; }
|
|
533
|
+
|
|
534
|
+
save() {
|
|
535
|
+
if (!this.current) return;
|
|
536
|
+
this.saving = true;
|
|
537
|
+
const body = {
|
|
538
|
+
token: this.current.token,
|
|
539
|
+
enabled: this.current.enabled,
|
|
540
|
+
allowedRegions: this.current.allowedRegions,
|
|
541
|
+
extraAllowed: this.current.extraAllowed,
|
|
542
|
+
blockedCountries: this.current.blockedCountries,
|
|
543
|
+
allowedGbRegions: this.current.allowedGbRegions,
|
|
544
|
+
};
|
|
545
|
+
this.http.post<any>('/ees/geo-block/admin/save', body).subscribe({
|
|
546
|
+
next: () => {
|
|
547
|
+
this.saving = false;
|
|
548
|
+
this.dirty = false;
|
|
549
|
+
this.notify.success('Geo-block settings saved');
|
|
550
|
+
},
|
|
551
|
+
error: (err) => {
|
|
552
|
+
this.saving = false;
|
|
553
|
+
this.notify.error(err?.error?.error || 'Save failed');
|
|
554
|
+
},
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Mirrors src/plugins/ees/geo-regions.ts so the live preview matches the
|
|
560
|
+
// backend resolver exactly. Kept in-file to avoid an admin-UI import of
|
|
561
|
+
// backend code.
|
|
562
|
+
const EU_27 = ['AT','BE','BG','HR','CY','CZ','DK','EE','FI','FR','DE','GR','HU','IE','IT','LV','LT','LU','MT','NL','PL','PT','RO','SK','SI','ES','SE'];
|
|
563
|
+
const EEA_EXTRA = ['IS','LI','NO'];
|
|
564
|
+
const NON_EU_EUROPE = ['GB','CH','NO','IS','LI','AL','AD','BA','BY','FO','GI','IM','JE','GG','MC','ME','MD','MK','RS','SM','UA','VA','XK','RU'];
|
|
565
|
+
const REGION_TO_COUNTRIES: Record<string, string[]> = {
|
|
566
|
+
EUROPE: Array.from(new Set([...EU_27, ...NON_EU_EUROPE])),
|
|
567
|
+
EU: EU_27,
|
|
568
|
+
EEA: Array.from(new Set([...EU_27, ...EEA_EXTRA])),
|
|
569
|
+
BRITISH_ISLES: ['GB','IE','IM','JE','GG','FO'],
|
|
570
|
+
UK_ONLY: ['GB'],
|
|
571
|
+
NORTH_AMERICA: ['US','CA','MX'],
|
|
572
|
+
OCEANIA: ['AU','NZ'],
|
|
573
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { NgModule } from '@angular/core';
|
|
2
|
+
import { RouterModule } from '@angular/router';
|
|
3
|
+
import { SharedModule } from '@vendure/admin-ui/core';
|
|
4
|
+
import { FormsModule } from '@angular/forms';
|
|
5
|
+
import { HttpClientModule } from '@angular/common/http';
|
|
6
|
+
import { GeoBlockComponent } from './components/geo-block.component';
|
|
7
|
+
|
|
8
|
+
@NgModule({
|
|
9
|
+
imports: [
|
|
10
|
+
SharedModule, FormsModule, HttpClientModule,
|
|
11
|
+
RouterModule.forChild([
|
|
12
|
+
{ path: '', pathMatch: 'full', component: GeoBlockComponent, data: { breadcrumb: 'Geo-block' } },
|
|
13
|
+
]),
|
|
14
|
+
],
|
|
15
|
+
declarations: [GeoBlockComponent],
|
|
16
|
+
})
|
|
17
|
+
export class GeoBlockModule {}
|