@dev-crew-berlin/enter-js-utils 0.98.12 → 0.99.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/dist/lib/email-handlebars.d.ts +31 -0
- package/dist/lib/email-handlebars.js +436 -0
- package/dist/lib/email-handlebars.test.js +339 -0
- package/dist/lib/email-links.d.ts +46 -0
- package/dist/lib/email-links.js +68 -0
- package/dist/lib/email-links.test.js +348 -0
- package/dist/lib/index.d.ts +2 -0
- package/dist/lib/index.js +3 -1
- package/dist/lib/tokens.d.ts +20 -0
- package/dist/lib/tokens.js +48 -0
- package/dist/lib/tokens.test.js +508 -0
- package/package.json +2 -1
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { renderHandlebars } from './email-handlebars';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Mirrors enter-core/tests/unittests/app/render_templates_test.py, which
|
|
6
|
+
* characterizes the *actual* pybars implementation these helpers are
|
|
7
|
+
* ported from -- same cases, same expected outputs (verified against real
|
|
8
|
+
* pybars/babel output there), so the two engines are checked to agree
|
|
9
|
+
* rather than silently drifting apart. Two documented, intentional
|
|
10
|
+
* deviations: `escape` does real HTML-escaping instead of reproducing a
|
|
11
|
+
* legacy bytes-repr bug, and date/shortdate/longdate/fulldate assume UTC
|
|
12
|
+
* input (see lib/email-handlebars.ts for why).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const baseOptions = {
|
|
16
|
+
linkOptions: {
|
|
17
|
+
instanceName: 'test-instance',
|
|
18
|
+
jwtSecret: 'test-secret',
|
|
19
|
+
domain: 'test.frontend.example'
|
|
20
|
+
},
|
|
21
|
+
linkContext: {
|
|
22
|
+
attendeeId: 'att-1',
|
|
23
|
+
emailName: 'welcome',
|
|
24
|
+
trackingEnabled: true
|
|
25
|
+
},
|
|
26
|
+
resolvePartial: async () => {
|
|
27
|
+
throw new Error('unexpected partial lookup');
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
function renderEn(source, data = {}, extra = {}) {
|
|
31
|
+
return renderHandlebars(source, data, {
|
|
32
|
+
...baseOptions,
|
|
33
|
+
lang: 'en',
|
|
34
|
+
languages: ['en', 'de'],
|
|
35
|
+
...extra
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
function renderDe(source, data = {}) {
|
|
39
|
+
return renderHandlebars(source, data, {
|
|
40
|
+
...baseOptions,
|
|
41
|
+
lang: 'de',
|
|
42
|
+
languages: ['en', 'de']
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
describe('translate helper (_)', () => {
|
|
46
|
+
it('picks the current language by default', async () => {
|
|
47
|
+
expect(await renderDe('{{_ label}}', {
|
|
48
|
+
label: {
|
|
49
|
+
en: 'Hello',
|
|
50
|
+
de: 'Hallo'
|
|
51
|
+
}
|
|
52
|
+
})).toBe('Hallo');
|
|
53
|
+
});
|
|
54
|
+
it('an explicit language argument overrides the current one', async () => {
|
|
55
|
+
expect(await renderDe('{{_ label "en"}}', {
|
|
56
|
+
label: {
|
|
57
|
+
en: 'Hello',
|
|
58
|
+
de: 'Hallo'
|
|
59
|
+
}
|
|
60
|
+
})).toBe('Hello');
|
|
61
|
+
});
|
|
62
|
+
it('passes non-object values through unchanged', async () => {
|
|
63
|
+
expect(await renderEn('{{_ label}}', {
|
|
64
|
+
label: 'plain string'
|
|
65
|
+
})).toBe('plain string');
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
describe('upper/lower helpers', () => {
|
|
69
|
+
it('upper translates then uppercases', async () => {
|
|
70
|
+
expect(await renderEn('{{upper label}}', {
|
|
71
|
+
label: {
|
|
72
|
+
en: 'hello'
|
|
73
|
+
}
|
|
74
|
+
})).toBe('HELLO');
|
|
75
|
+
});
|
|
76
|
+
it('lower translates then lowercases', async () => {
|
|
77
|
+
expect(await renderEn('{{lower label}}', {
|
|
78
|
+
label: {
|
|
79
|
+
en: 'HELLO'
|
|
80
|
+
}
|
|
81
|
+
})).toBe('hello');
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
describe('language block helper', () => {
|
|
85
|
+
it('renders the block only for the matching language', async () => {
|
|
86
|
+
const template = '{{#en}}Hi{{/en}}{{#de}}Hallo{{/de}}';
|
|
87
|
+
expect(await renderEn(template, {})).toBe('Hi');
|
|
88
|
+
expect(await renderDe(template, {})).toBe('Hallo');
|
|
89
|
+
});
|
|
90
|
+
it('inline args translate and concatenate', async () => {
|
|
91
|
+
expect(await renderEn('{{en "Attendee management by"}}', {})).toBe('Attendee management by');
|
|
92
|
+
expect(await renderDe('{{en "Attendee management by"}}', {})).toBe('');
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
describe('eq helper', () => {
|
|
96
|
+
it('takes the true branch when equal', async () => {
|
|
97
|
+
expect(await renderEn('{{#eq a b}}yes{{else}}no{{/eq}}', {
|
|
98
|
+
a: 'x',
|
|
99
|
+
b: 'x'
|
|
100
|
+
})).toBe('yes');
|
|
101
|
+
});
|
|
102
|
+
it('takes the false branch when not equal', async () => {
|
|
103
|
+
expect(await renderEn('{{#eq a b}}yes{{else}}no{{/eq}}', {
|
|
104
|
+
a: 'x',
|
|
105
|
+
b: 'y'
|
|
106
|
+
})).toBe('no');
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
describe('custom if helper', () => {
|
|
110
|
+
it.each([['==', 5, 5, 'yes'], ['!=', 5, 5, 'yes'],
|
|
111
|
+
// mirrors the legacy bug: != behaves like ==
|
|
112
|
+
['>', 5, 3, 'yes'], ['<', 3, 5, 'yes'], ['>=', 5, 5, 'yes'], ['<=', 5, 5, 'yes'], ['in', 'b', 'abc', 'yes']])('operator %s', async (operator, left, right, expected) => {
|
|
113
|
+
const template = `{{#if a "${operator}" b}}yes{{else}}no{{/if}}`;
|
|
114
|
+
expect(await renderEn(template, {
|
|
115
|
+
a: left,
|
|
116
|
+
b: right
|
|
117
|
+
})).toBe(expected);
|
|
118
|
+
});
|
|
119
|
+
it('falls back to else on a type mismatch', async () => {
|
|
120
|
+
expect(await renderEn('{{#if a ">" b}}yes{{else}}no{{/if}}', {
|
|
121
|
+
a: 'not-a-number',
|
|
122
|
+
b: 3
|
|
123
|
+
})).toBe('no');
|
|
124
|
+
});
|
|
125
|
+
it('single-argument truthiness', async () => {
|
|
126
|
+
expect(await renderEn('{{#if x}}Y{{else}}N{{/if}}', {
|
|
127
|
+
x: true
|
|
128
|
+
})).toBe('Y');
|
|
129
|
+
expect(await renderEn('{{#if x}}Y{{else}}N{{/if}}', {
|
|
130
|
+
x: false
|
|
131
|
+
})).toBe('N');
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
describe('if_in helper', () => {
|
|
135
|
+
it('true when the value is in the list', async () => {
|
|
136
|
+
expect(await renderEn('{{#if_in list "x"}}yes{{else}}no{{/if_in}}', {
|
|
137
|
+
list: ['x', 'y']
|
|
138
|
+
})).toBe('yes');
|
|
139
|
+
});
|
|
140
|
+
it('false when missing', async () => {
|
|
141
|
+
expect(await renderEn('{{#if_in list "z"}}yes{{else}}no{{/if_in}}', {
|
|
142
|
+
list: ['x', 'y']
|
|
143
|
+
})).toBe('no');
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
describe('join helper', () => {
|
|
147
|
+
it('joins a list with a separator', async () => {
|
|
148
|
+
expect(await renderEn('{{join list ", "}}', {
|
|
149
|
+
list: ['a', 'b', 'c']
|
|
150
|
+
})).toBe('a, b, c');
|
|
151
|
+
});
|
|
152
|
+
it('joins with a distinct last separator', async () => {
|
|
153
|
+
expect(await renderEn('{{join list ", " " and "}}', {
|
|
154
|
+
list: ['a', 'b', 'c']
|
|
155
|
+
})).toBe('a, b and c');
|
|
156
|
+
});
|
|
157
|
+
it('a single-item list is returned unchanged', async () => {
|
|
158
|
+
expect(await renderEn('{{join list ", " " and "}}', {
|
|
159
|
+
list: ['a']
|
|
160
|
+
})).toBe('a');
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
describe('calc helper', () => {
|
|
164
|
+
it('addition preserves max decimal precision', async () => {
|
|
165
|
+
expect(await renderEn('{{calc a "+" b}}', {
|
|
166
|
+
a: '1.5',
|
|
167
|
+
b: '2.25'
|
|
168
|
+
})).toBe('3.75');
|
|
169
|
+
});
|
|
170
|
+
it('division round-half-to-even on a whole-number result shows .0', async () => {
|
|
171
|
+
expect(await renderEn('{{calc a "/" b}}', {
|
|
172
|
+
a: '10',
|
|
173
|
+
b: '4'
|
|
174
|
+
})).toBe('2.0');
|
|
175
|
+
});
|
|
176
|
+
it('non-numeric input renders empty', async () => {
|
|
177
|
+
expect(await renderEn('{{calc a "+" b}}', {
|
|
178
|
+
a: 'abc',
|
|
179
|
+
b: '1'
|
|
180
|
+
})).toBe('');
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
describe('count helper', () => {
|
|
184
|
+
it('counts list length', async () => {
|
|
185
|
+
expect(await renderEn('{{count list}}', {
|
|
186
|
+
list: ['a', 'b', 'c']
|
|
187
|
+
})).toBe('3');
|
|
188
|
+
});
|
|
189
|
+
it('non-countable value renders empty', async () => {
|
|
190
|
+
expect(await renderEn('{{count x}}', {
|
|
191
|
+
x: 5
|
|
192
|
+
})).toBe('');
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
describe('get helper', () => {
|
|
196
|
+
it('returns an existing key', async () => {
|
|
197
|
+
expect(await renderEn('{{get obj "key"}}', {
|
|
198
|
+
obj: {
|
|
199
|
+
key: 'val'
|
|
200
|
+
}
|
|
201
|
+
})).toBe('val');
|
|
202
|
+
});
|
|
203
|
+
it('returns the provided default when missing', async () => {
|
|
204
|
+
expect(await renderEn('{{get obj "missing" "fallback"}}', {
|
|
205
|
+
obj: {
|
|
206
|
+
key: 'val'
|
|
207
|
+
}
|
|
208
|
+
})).toBe('fallback');
|
|
209
|
+
});
|
|
210
|
+
it('returns empty when missing and no default', async () => {
|
|
211
|
+
expect(await renderEn('{{get obj "missing"}}', {
|
|
212
|
+
obj: {
|
|
213
|
+
key: 'val'
|
|
214
|
+
}
|
|
215
|
+
})).toBe('');
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
describe('range helper', () => {
|
|
219
|
+
it('iterates a numeric range', async () => {
|
|
220
|
+
expect(await renderEn('{{#range 0 3}}{{this}},{{/range}}', {})).toBe('0,1,2,');
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
describe('escape helper (intentional deviation)', () => {
|
|
224
|
+
it('does real HTML-escaping, not the legacy bytes-repr quirk', async () => {
|
|
225
|
+
expect(await renderEn('{{escape s}}', {
|
|
226
|
+
s: '<b>&'
|
|
227
|
+
})).toBe('<b>&');
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
describe('date/time helpers', () => {
|
|
231
|
+
const dt = new Date(Date.UTC(2024, 11, 25, 15, 30));
|
|
232
|
+
it('date', async () => {
|
|
233
|
+
expect(await renderEn('{{date d}}', {
|
|
234
|
+
d: dt
|
|
235
|
+
})).toBe('Dec 25, 2024');
|
|
236
|
+
});
|
|
237
|
+
it('shortdate', async () => {
|
|
238
|
+
expect(await renderEn('{{shortdate d}}', {
|
|
239
|
+
d: dt
|
|
240
|
+
})).toBe('12/25/24');
|
|
241
|
+
});
|
|
242
|
+
it('longdate', async () => {
|
|
243
|
+
expect(await renderEn('{{longdate d}}', {
|
|
244
|
+
d: dt
|
|
245
|
+
})).toBe('December 25, 2024');
|
|
246
|
+
});
|
|
247
|
+
it('fulldate', async () => {
|
|
248
|
+
expect(await renderEn('{{fulldate d}}', {
|
|
249
|
+
d: dt
|
|
250
|
+
})).toBe('Wednesday, December 25, 2024');
|
|
251
|
+
});
|
|
252
|
+
it('time (always Berlin local time)', async () => {
|
|
253
|
+
// Node/ICU version dependent: this may render with a regular space
|
|
254
|
+
// or a narrow no-break space (U+202F) before AM/PM depending on runtime
|
|
255
|
+
// before the AM/PM marker, not a regular space
|
|
256
|
+
expect(await renderEn('{{time d}}', {
|
|
257
|
+
d: dt
|
|
258
|
+
})).toBe('4:30 PM');
|
|
259
|
+
});
|
|
260
|
+
it('longdate uses the render language, not the data', async () => {
|
|
261
|
+
expect(await renderDe('{{longdate d}}', {
|
|
262
|
+
d: dt
|
|
263
|
+
})).toBe('25. Dezember 2024');
|
|
264
|
+
});
|
|
265
|
+
it('longdateUtc converts to Berlin timezone, which can cross a day boundary', async () => {
|
|
266
|
+
const lateUtc = new Date(Date.UTC(2024, 11, 25, 23, 30));
|
|
267
|
+
expect(await renderEn('{{longdateUtc d}}', {
|
|
268
|
+
d: lateUtc
|
|
269
|
+
})).toBe('December 26, 2024');
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
describe('before/after/between helpers', () => {
|
|
273
|
+
it('before is true for a future timestamp', async () => {
|
|
274
|
+
expect(await renderEn('{{#before d}}FUTURE{{else}}PAST{{/before}}', {
|
|
275
|
+
d: '31.12.2999 00:00'
|
|
276
|
+
})).toBe('FUTURE');
|
|
277
|
+
});
|
|
278
|
+
it('before is false for a past timestamp', async () => {
|
|
279
|
+
expect(await renderEn('{{#before d}}FUTURE{{else}}PAST{{/before}}', {
|
|
280
|
+
d: '01.01.2000 00:00'
|
|
281
|
+
})).toBe('PAST');
|
|
282
|
+
});
|
|
283
|
+
it('between is true when now is inside the range', async () => {
|
|
284
|
+
const template = '{{#between a b}}INSIDE{{else}}OUTSIDE{{/between}}';
|
|
285
|
+
expect(await renderEn(template, {
|
|
286
|
+
a: '01.01.2000 00:00',
|
|
287
|
+
b: '31.12.2999 00:00'
|
|
288
|
+
})).toBe('INSIDE');
|
|
289
|
+
});
|
|
290
|
+
it('a truthy forced flag always takes the fn branch', async () => {
|
|
291
|
+
expect(await renderEn('{{#before d true}}FUTURE{{else}}PAST{{/before}}', {
|
|
292
|
+
d: '01.01.2000 00:00'
|
|
293
|
+
})).toBe('FUTURE');
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
describe('partial resolution', () => {
|
|
297
|
+
it('resolves a referenced partial through resolvePartial, recursively', async () => {
|
|
298
|
+
const html = await renderHandlebars('{{> footer}}', {}, {
|
|
299
|
+
...baseOptions,
|
|
300
|
+
lang: 'en',
|
|
301
|
+
languages: ['en'],
|
|
302
|
+
resolvePartial: async name => name === 'footer' ? 'FOOTER-{{x}}' : 'Template not found'
|
|
303
|
+
});
|
|
304
|
+
expect(html).toBe('FOOTER-');
|
|
305
|
+
});
|
|
306
|
+
it('an unresolvable partial renders as a literal placeholder, matching get_template()', async () => {
|
|
307
|
+
const html = await renderHandlebars('{{> does_not_exist}}', {}, {
|
|
308
|
+
...baseOptions,
|
|
309
|
+
lang: 'en',
|
|
310
|
+
languages: ['en'],
|
|
311
|
+
resolvePartial: async () => {
|
|
312
|
+
throw new Error('not found');
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
expect(html).toBe('Template not found');
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
describe('link helpers', () => {
|
|
319
|
+
it('unsubscribe_link embeds the attendee id and email name in a signed token', async () => {
|
|
320
|
+
const html = await renderEn('{{unsubscribe_link "a@b.com"}}', {});
|
|
321
|
+
expect(html.startsWith('https://test.frontend.example/unsubscribe?token=')).toBe(true);
|
|
322
|
+
});
|
|
323
|
+
it('link builds a tracked link when tracking is enabled', async () => {
|
|
324
|
+
const html = await renderEn('{{link "/home"}}', {});
|
|
325
|
+
expect(html.startsWith('https://test.frontend.example/link/')).toBe(true);
|
|
326
|
+
});
|
|
327
|
+
it('link returns the raw url when tracking is disabled', async () => {
|
|
328
|
+
const html = await renderHandlebars('{{link "/home"}}', {}, {
|
|
329
|
+
...baseOptions,
|
|
330
|
+
lang: 'en',
|
|
331
|
+
languages: ['en'],
|
|
332
|
+
linkContext: {
|
|
333
|
+
...baseOptions.linkContext,
|
|
334
|
+
trackingEnabled: false
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
expect(html).toBe('https://test.frontend.example/home');
|
|
338
|
+
});
|
|
339
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { TokenOptions } from './tokens';
|
|
2
|
+
/**
|
|
3
|
+
* Mirrors the link-building methods on FrontendConfig
|
|
4
|
+
* (common/models/frontend_config.py) on the backend, using the same
|
|
5
|
+
* shared FRONTEND_JWT_SECRET. These exist so a headless email's
|
|
6
|
+
* Handlebars-typed content can build tracking/unsubscribe/registration/
|
|
7
|
+
* wallet/QR links locally, without a backend round trip per link --
|
|
8
|
+
* tracking links in particular are template-dependent (an author can
|
|
9
|
+
* wrap an arbitrary one-off URL anywhere in a block), so the set of
|
|
10
|
+
* links needing signing can't be enumerated ahead of render time.
|
|
11
|
+
*/
|
|
12
|
+
export type LinkBuilderOptions = TokenOptions & {
|
|
13
|
+
domain: string;
|
|
14
|
+
};
|
|
15
|
+
export declare function makeTrackingLink(options: LinkBuilderOptions, args: {
|
|
16
|
+
attendeeId: string;
|
|
17
|
+
emailName: string;
|
|
18
|
+
mailingId?: string | null;
|
|
19
|
+
urlLink: string;
|
|
20
|
+
trackingEnabled: boolean;
|
|
21
|
+
}): string;
|
|
22
|
+
export declare function makeUnsubscribeLink(options: LinkBuilderOptions, args: {
|
|
23
|
+
attendeeId: string;
|
|
24
|
+
emailName: string;
|
|
25
|
+
emailAddress: string;
|
|
26
|
+
}): string;
|
|
27
|
+
export declare function makeRegistrationLink(options: LinkBuilderOptions, args: {
|
|
28
|
+
attendeeId: string;
|
|
29
|
+
sessionType?: string;
|
|
30
|
+
flags?: Record<string, unknown>;
|
|
31
|
+
expiresInSeconds?: number;
|
|
32
|
+
email?: {
|
|
33
|
+
name: string;
|
|
34
|
+
mailingId: string;
|
|
35
|
+
};
|
|
36
|
+
}): string;
|
|
37
|
+
export declare function makeWalletLink(options: LinkBuilderOptions, attendeeId: string): string;
|
|
38
|
+
export declare function makeGoogleWalletLink(options: LinkBuilderOptions, attendeeId: string): string;
|
|
39
|
+
export declare function makeQrCodeLink(options: LinkBuilderOptions, args: {
|
|
40
|
+
content: string;
|
|
41
|
+
size?: number;
|
|
42
|
+
background?: string;
|
|
43
|
+
body?: string;
|
|
44
|
+
corners?: string;
|
|
45
|
+
fancy?: 0 | 1;
|
|
46
|
+
}): string;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { encodeJWS } from './tokens';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Mirrors the link-building methods on FrontendConfig
|
|
5
|
+
* (common/models/frontend_config.py) on the backend, using the same
|
|
6
|
+
* shared FRONTEND_JWT_SECRET. These exist so a headless email's
|
|
7
|
+
* Handlebars-typed content can build tracking/unsubscribe/registration/
|
|
8
|
+
* wallet/QR links locally, without a backend round trip per link --
|
|
9
|
+
* tracking links in particular are template-dependent (an author can
|
|
10
|
+
* wrap an arbitrary one-off URL anywhere in a block), so the set of
|
|
11
|
+
* links needing signing can't be enumerated ahead of render time.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export function makeTrackingLink(options, args) {
|
|
15
|
+
const target = new URL(args.urlLink, `https://${options.domain}`).toString();
|
|
16
|
+
if (!args.trackingEnabled) return target;
|
|
17
|
+
const token = encodeJWS({
|
|
18
|
+
instance_name: options.instanceName,
|
|
19
|
+
attendee_id: args.attendeeId,
|
|
20
|
+
target,
|
|
21
|
+
email_name: args.emailName,
|
|
22
|
+
mailing_id: args.mailingId ?? null
|
|
23
|
+
}, options.jwtSecret);
|
|
24
|
+
return `https://${options.domain}/link/${token}`;
|
|
25
|
+
}
|
|
26
|
+
export function makeUnsubscribeLink(options, args) {
|
|
27
|
+
const token = encodeJWS({
|
|
28
|
+
instance_name: options.instanceName,
|
|
29
|
+
attendee_id: args.attendeeId,
|
|
30
|
+
email_address: args.emailAddress,
|
|
31
|
+
email_name: args.emailName
|
|
32
|
+
}, options.jwtSecret);
|
|
33
|
+
return `https://${options.domain}/unsubscribe?token=${token}`;
|
|
34
|
+
}
|
|
35
|
+
const REGISTRATION_TOKEN_DEFAULT_EXPIRY_SECONDS = 15 * 24 * 60 * 60;
|
|
36
|
+
export function makeRegistrationLink(options, args) {
|
|
37
|
+
const payload = {
|
|
38
|
+
attendee_id: args.attendeeId,
|
|
39
|
+
instance_name: options.instanceName,
|
|
40
|
+
session_type: args.sessionType ?? 'register',
|
|
41
|
+
flags: args.flags ?? {}
|
|
42
|
+
};
|
|
43
|
+
if (args.email) {
|
|
44
|
+
payload.email = {
|
|
45
|
+
name: args.email.name,
|
|
46
|
+
mailing_id: args.email.mailingId
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const token = encodeJWS(payload, options.jwtSecret, args.expiresInSeconds ?? REGISTRATION_TOKEN_DEFAULT_EXPIRY_SECONDS);
|
|
50
|
+
return `https://${options.domain}/register/${token}`;
|
|
51
|
+
}
|
|
52
|
+
export function makeWalletLink(options, attendeeId) {
|
|
53
|
+
return `https://${options.domain}/wallet/${attendeeId}`;
|
|
54
|
+
}
|
|
55
|
+
export function makeGoogleWalletLink(options, attendeeId) {
|
|
56
|
+
return `https://${options.domain}/google-wallet/${attendeeId}`;
|
|
57
|
+
}
|
|
58
|
+
export function makeQrCodeLink(options, args) {
|
|
59
|
+
const params = new URLSearchParams({
|
|
60
|
+
content: args.content,
|
|
61
|
+
size: String(args.size ?? 600),
|
|
62
|
+
background: args.background ?? 'white',
|
|
63
|
+
body: args.body ?? 'black',
|
|
64
|
+
corners: args.corners ?? 'black',
|
|
65
|
+
fancy: String(args.fancy ?? 0)
|
|
66
|
+
});
|
|
67
|
+
return `https://${options.domain}/qr/?${params.toString()}`;
|
|
68
|
+
}
|