tf-checkout-react-native 2.0.0-beta.7 → 2.0.0-beta.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tf-checkout-react-native",
3
- "version": "2.0.0-beta.7",
3
+ "version": "2.0.0-beta.9",
4
4
  "description": "TheTicketFairy ReactNative Checkout library",
5
5
  "homepage": "https://github.com/theticketfairy/tf-shared-libraries#readme",
6
6
  "author": "The Ticket Fairy",
@@ -37,7 +37,7 @@
37
37
  "!**/.*"
38
38
  ],
39
39
  "scripts": {
40
- "test": "jest --passWithNoTests",
40
+ "test": "node scripts/verify-discussion-support.cjs && jest --passWithNoTests",
41
41
  "typecheck": "tsc -p tsconfig.build.json",
42
42
  "lint": "eslint \"**/*.{js,ts,tsx}\"",
43
43
  "lint:fix": "eslint \"**/*.{js,ts,tsx}\" --fix",
@@ -0,0 +1,131 @@
1
+ /* Executes the real native support Pressable; no native OS/mail account required. */
2
+ /* eslint-disable @typescript-eslint/no-require-imports -- Node CommonJS verifier, matching the package's script entry. */
3
+ const assert = require('node:assert/strict')
4
+ const fs = require('node:fs')
5
+ const path = require('node:path')
6
+ const vm = require('node:vm')
7
+ const ts = require('typescript')
8
+ const React = require('react')
9
+ const opened = []
10
+ let rejectOpen = false
11
+ const source = fs.readFileSync(
12
+ path.join(__dirname, '../src/sections/EventDiscussionView.tsx'),
13
+ 'utf8'
14
+ )
15
+ const compiled = ts.transpileModule(source, {
16
+ compilerOptions: {
17
+ module: ts.ModuleKind.CommonJS,
18
+ target: ts.ScriptTarget.ES2022,
19
+ jsx: ts.JsxEmit.React,
20
+ esModuleInterop: true,
21
+ },
22
+ }).outputText
23
+ const moduleExports = {}
24
+ vm.runInNewContext(compiled, {
25
+ exports: moduleExports,
26
+ require: (name) => {
27
+ if (name === 'react')
28
+ return {
29
+ ...React,
30
+ useRef: (value) => ({ current: value }),
31
+ useState: (value) => [value, () => {}],
32
+ }
33
+ if (name === 'react-native')
34
+ return {
35
+ ...Object.fromEntries(
36
+ [
37
+ 'ActivityIndicator',
38
+ 'FlatList',
39
+ 'Image',
40
+ 'KeyboardAvoidingView',
41
+ 'Modal',
42
+ 'Pressable',
43
+ 'Text',
44
+ 'TextInput',
45
+ 'View',
46
+ ].map((name) => [name, name])
47
+ ),
48
+ Platform: { OS: 'ios' },
49
+ StyleSheet: { create: (styles) => styles },
50
+ Linking: {
51
+ openURL: (url) => {
52
+ opened.push(url)
53
+ return rejectOpen
54
+ ? Promise.reject(Error('No mail client'))
55
+ : Promise.resolve()
56
+ },
57
+ },
58
+ }
59
+ if (name === 'tf-checkout-shared') return { EVENT_COMMENT_IMAGE_MAX_COUNT: 5 }
60
+ if (name === '../ui/CheckoutProvider') return { useTheme: () => ({}) }
61
+ if (name === '../ui/useAccountT') return { useAccountT: () => (value) => value }
62
+ if (name === './discussion/CommentImages')
63
+ return { CommentImages: 'CommentImages', CommentImageDrafts: 'CommentImageDrafts' }
64
+ if (name === './discussion/SwipeReply') return { SwipeReply: 'SwipeReply' }
65
+ throw Error('Unexpected dependency: ' + name)
66
+ },
67
+ })
68
+ const links = (node) =>
69
+ !node || typeof node !== 'object'
70
+ ? []
71
+ : [
72
+ ...(node.type === 'Pressable' && node.props.accessibilityRole === 'link'
73
+ ? [node]
74
+ : []),
75
+ ...React.Children.toArray(node.props?.children).flatMap(links),
76
+ ]
77
+ const render = (url) => {
78
+ const element = moduleExports.EventDiscussionView({
79
+ eventId: '42',
80
+ discussion: {
81
+ scopeKey: 'synthetic-actor-7',
82
+ comments: [],
83
+ blockedAuthors: [],
84
+ imagesBase64: [],
85
+ body: '',
86
+ canPost: false,
87
+ supportUrl: url,
88
+ },
89
+ })
90
+ const tree = element.type(element.props)
91
+ const support = links(tree)
92
+ assert.equal(support.length, 1)
93
+ return support[0]
94
+ }
95
+ ;(async () => {
96
+ // This exact URI comes from EventDiscussionService::page, including its subject.
97
+ const canonical = 'mailto:support@theticketfairy.com?subject=Event%20comment%20report'
98
+ for (const url of [canonical, 'https://www.ticketfairy.com/support']) {
99
+ opened.length = 0
100
+ const support = render(url)
101
+ assert.deepEqual(opened, [], 'rendering must never open or send anything')
102
+ support.props.onPress()
103
+ await new Promise((resolve) => setImmediate(resolve))
104
+ assert.deepEqual(opened, [url], 'the user press must reach the native Linking port')
105
+ }
106
+ for (const url of [
107
+ 'javascript:alert(1)',
108
+ 'file:///private',
109
+ 'http://example.invalid',
110
+ 'tel:123',
111
+ ]) {
112
+ opened.length = 0
113
+ render(url).props.onPress()
114
+ assert.deepEqual(opened, [], 'unsupported schemes must not reach the OS')
115
+ }
116
+ rejectOpen = true
117
+ opened.length = 0
118
+ render(canonical).props.onPress()
119
+ await new Promise((resolve) => setImmediate(resolve))
120
+ assert.deepEqual(
121
+ opened,
122
+ [canonical],
123
+ 'an unavailable mail client must be handled quietly'
124
+ )
125
+ process.stdout.write(
126
+ 'Native discussion support PASS: user press, canonical mailto, HTTPS, unsafe schemes and unavailable client.\n'
127
+ )
128
+ })().catch((error) => {
129
+ console.error(error)
130
+ process.exitCode = 1
131
+ })
@@ -144,11 +144,13 @@ const EventDiscussionBody = ({
144
144
  <View style={s.authorRow}>
145
145
  <Text style={[muted, s.author]}>
146
146
  {comment.author.name}
147
- {own ? ' (you)' : ''}
147
+ {own ? t(' (you)') : ''}
148
148
  </Text>
149
149
  <Pressable
150
150
  accessibilityRole="button"
151
- accessibilityLabel={`Actions for comment by ${comment.author.name}`}
151
+ accessibilityLabel={t('Actions for comment by {{name}}', {
152
+ name: comment.author.name,
153
+ })}
152
154
  hitSlop={10}
153
155
  onPress={() => setActions(comment)}
154
156
  >
@@ -177,27 +179,27 @@ const EventDiscussionBody = ({
177
179
  renderCloseIcon={renderImageCloseIcon}
178
180
  />
179
181
  <Text selectable style={[text, s.body]}>
180
- {comment.status === 'deleted' ? 'Comment removed' : comment.body}
182
+ {comment.status === 'deleted' ? t('Comment removed') : comment.body}
181
183
  </Text>
182
184
  {comment.status === 'pending' && (
183
185
  <Text accessibilityLiveRegion="polite" style={[muted, s.pending]}>
184
- Awaiting review
186
+ {t('Awaiting review')}
185
187
  </Text>
186
188
  )}
187
189
  {comment.status === 'hidden' && (
188
- <Text style={[muted, s.pending]}>Hidden</Text>
190
+ <Text style={[muted, s.pending]}>{t('Hidden')}</Text>
189
191
  )}
190
192
  <View style={s.meta}>
191
193
  <Text style={[muted, s.date]}>
192
194
  {new Date(comment.createdAt).toLocaleString()}
193
- {comment.editedAt ? ' · Edited' : ''}
195
+ {comment.editedAt ? t(' · edited') : ''}
194
196
  </Text>
195
197
  {(['up', 'down'] as const).map((reaction) => (
196
198
  <Pressable
197
199
  key={reaction}
198
200
  accessibilityRole="button"
199
201
  accessibilityLabel={
200
- reaction === 'up' ? 'Like comment' : 'Dislike comment'
202
+ reaction === 'up' ? t('Like comment') : t('Dislike comment')
201
203
  }
202
204
  accessibilityState={{
203
205
  selected: comment.reactions.mine === reaction,
@@ -243,8 +245,12 @@ const EventDiscussionBody = ({
243
245
  onPress={() => d.replyTo(comment)}
244
246
  >
245
247
  <Text style={[muted, s.pending]}>
246
- {comment.replyCount} {comment.replyCount === 1 ? 'reply' : 'replies'} ·
247
- Reply
248
+ {t(
249
+ comment.replyCount === 1
250
+ ? '{{count}} reply · Reply'
251
+ : '{{count}} replies · Reply',
252
+ { count: comment.replyCount }
253
+ )}
248
254
  </Text>
249
255
  </Pressable>
250
256
  )}
@@ -253,16 +259,20 @@ const EventDiscussionBody = ({
253
259
  </SwipeReply>
254
260
  )
255
261
  }
256
- const actionButton = (label: string, onPress: () => void) => (
262
+ const actionButton = (
263
+ label: string,
264
+ onPress: () => void,
265
+ vars?: Record<string, string | number>
266
+ ) => (
257
267
  <Pressable key={label} accessibilityRole="button" onPress={onPress} style={s.action}>
258
- <Text style={text}>{t(label)}</Text>
268
+ <Text style={text}>{t(label, vars)}</Text>
259
269
  </Pressable>
260
270
  )
261
271
  const sheet = (visible: boolean, close: () => void, children: React.ReactNode) => (
262
272
  <Modal transparent animationType="slide" visible={visible} onRequestClose={close}>
263
273
  <View style={s.overlay}>
264
274
  <Pressable
265
- accessibilityLabel="Close menu"
275
+ accessibilityLabel={t('Close menu')}
266
276
  style={StyleSheet.absoluteFill}
267
277
  onPress={close}
268
278
  />
@@ -293,7 +303,7 @@ const EventDiscussionBody = ({
293
303
  onPress={() => setBlocksVisible(true)}
294
304
  style={s.blocked}
295
305
  >
296
- <Text style={muted}>Blocked people</Text>
306
+ <Text style={muted}>{t('Blocked people')}</Text>
297
307
  </Pressable>
298
308
  )}
299
309
  {d.isLoading ? (
@@ -317,7 +327,7 @@ const EventDiscussionBody = ({
317
327
  d.hasMore ? actionButton('Earlier comments', d.loadMore) : null
318
328
  }
319
329
  ListEmptyComponent={
320
- <Text style={[muted, s.empty]}>Be the first to leave a comment.</Text>
330
+ <Text style={[muted, s.empty]}>{t('Be the first to leave a comment.')}</Text>
321
331
  }
322
332
  refreshing={d.isLoading}
323
333
  onRefresh={() => void d.refresh()}
@@ -329,7 +339,7 @@ const EventDiscussionBody = ({
329
339
  {t(d.error || pickerError)}
330
340
  </Text>
331
341
  <Pressable accessibilityRole="button" onPress={() => void d.refresh()}>
332
- <Text style={muted}>Try again</Text>
342
+ <Text style={muted}>{t('Try again')}</Text>
333
343
  </Pressable>
334
344
  </View>
335
345
  )}
@@ -342,11 +352,11 @@ const EventDiscussionBody = ({
342
352
  <View style={[s.draftContext, { borderColor: theme.border }]}>
343
353
  <Text style={muted} numberOfLines={1}>
344
354
  {d.editing
345
- ? 'Edit your comment'
355
+ ? t('Edit your comment')
346
356
  : t('Reply to {{name}}', { name: d.replyingTo?.author.name ?? '' })}
347
357
  </Text>
348
358
  <Pressable accessibilityRole="button" onPress={d.cancelDraft}>
349
- <Text style={muted}>Cancel</Text>
359
+ <Text style={muted}>{t('Cancel')}</Text>
350
360
  </Pressable>
351
361
  </View>
352
362
  )}
@@ -379,8 +389,8 @@ const EventDiscussionBody = ({
379
389
  </Pressable>
380
390
  )}
381
391
  <TextInput
382
- accessibilityLabel="Your comment"
383
- placeholder="Enter your comment…"
392
+ accessibilityLabel={t('Your comment')}
393
+ placeholder={t('Enter your comment…')}
384
394
  placeholderTextColor={theme.muted}
385
395
  multiline
386
396
  maxLength={4000}
@@ -391,7 +401,7 @@ const EventDiscussionBody = ({
391
401
  />
392
402
  <Pressable
393
403
  accessibilityRole="button"
394
- accessibilityLabel={d.editing ? 'Save comment' : 'Post comment'}
404
+ accessibilityLabel={d.editing ? t('Save comment') : t('Post comment')}
395
405
  disabled={
396
406
  d.isSubmitting ||
397
407
  pickerBusy ||
@@ -403,7 +413,9 @@ const EventDiscussionBody = ({
403
413
  {d.isSubmitting ? (
404
414
  <ActivityIndicator color={theme.accent} />
405
415
  ) : (
406
- <Text style={{ color: theme.accent }}>{d.editing ? 'Save' : 'Send'}</Text>
416
+ <Text style={{ color: theme.accent }}>
417
+ {d.editing ? t('Save') : t('Send')}
418
+ </Text>
407
419
  )}
408
420
  </Pressable>
409
421
  </View>
@@ -416,18 +428,18 @@ const EventDiscussionBody = ({
416
428
  { borderColor: theme.border, backgroundColor: theme.surface },
417
429
  ]}
418
430
  >
419
- <Text style={text}>Sign in to leave a comment</Text>
431
+ <Text style={text}>{t('Sign in to leave a comment')}</Text>
420
432
  </Pressable>
421
433
  )}
422
434
  <Pressable
423
435
  accessibilityRole="link"
424
436
  style={s.support}
425
437
  onPress={() => {
426
- if (/^https:\/\//.test(d.supportUrl))
438
+ if (/^(https:\/\/|mailto:)/i.test(d.supportUrl))
427
439
  void Linking.openURL(d.supportUrl).catch(() => undefined)
428
440
  }}
429
441
  >
430
- <Text style={[muted, s.date]}>Need help? Contact support</Text>
442
+ <Text style={[muted, s.date]}>{t('Need help? Contact support')}</Text>
431
443
  </Pressable>
432
444
  {sheet(
433
445
  Boolean(actions),
@@ -442,12 +454,12 @@ const EventDiscussionBody = ({
442
454
  actionButton('Delete', () =>
443
455
  closeActions((comment) =>
444
456
  Alert.alert(
445
- 'Delete comment?',
446
- 'Your comment will be removed from this conversation.',
457
+ t('Delete your comment?'),
458
+ t('Your comment will be removed from this conversation.'),
447
459
  [
448
- { text: 'Cancel', style: 'cancel' },
460
+ { text: t('Cancel'), style: 'cancel' },
449
461
  {
450
- text: 'Delete',
462
+ text: t('Delete'),
451
463
  style: 'destructive',
452
464
  onPress: () => void d.remove(comment),
453
465
  },
@@ -462,12 +474,14 @@ const EventDiscussionBody = ({
462
474
  actionButton('Block this person', () =>
463
475
  closeActions((comment) =>
464
476
  Alert.alert(
465
- `Block ${comment.author.name}?`,
466
- 'You won’t see their comments. You can unblock them from Blocked people.',
477
+ t('Block {{name}}?', { name: comment.author.name }),
478
+ t(
479
+ 'You won’t see their comments. You can unblock them from Blocked people.'
480
+ ),
467
481
  [
468
- { text: 'Cancel', style: 'cancel' },
482
+ { text: t('Cancel'), style: 'cancel' },
469
483
  {
470
- text: 'Block',
484
+ text: t('Block'),
471
485
  onPress: () => void d.block(comment.author.id, true),
472
486
  },
473
487
  ]
@@ -493,7 +507,7 @@ const EventDiscussionBody = ({
493
507
  Boolean(reporting),
494
508
  () => setReporting(null),
495
509
  <>
496
- <Text style={text}>Report comment</Text>
510
+ <Text style={text}>{t('Report comment')}</Text>
497
511
  {(
498
512
  [
499
513
  ['spam', 'Spam'],
@@ -514,11 +528,13 @@ const EventDiscussionBody = ({
514
528
  () => setModerating(null),
515
529
  <>
516
530
  <Text style={text}>
517
- {moderating?.status === 'published' ? 'Approve comment' : 'Hide comment'}
531
+ {moderating?.status === 'published'
532
+ ? t('Approve comment')
533
+ : t('Hide comment')}
518
534
  </Text>
519
535
  <TextInput
520
- accessibilityLabel="Moderation reason"
521
- placeholder="Reason"
536
+ accessibilityLabel={t('Moderation reason')}
537
+ placeholder={t('Reason')}
522
538
  placeholderTextColor={theme.muted}
523
539
  value={reason}
524
540
  onChangeText={setReason}
@@ -536,12 +552,14 @@ const EventDiscussionBody = ({
536
552
  blocksVisible,
537
553
  () => setBlocksVisible(false),
538
554
  <>
539
- <Text style={text}>Blocked people</Text>
555
+ <Text style={text}>{t('Blocked people')}</Text>
540
556
  <FlatList
541
557
  data={d.blockedAuthors}
542
558
  keyExtractor={(person) => person.id}
543
559
  renderItem={({ item: person }) =>
544
- actionButton(`Unblock ${person.name}`, () => void d.block(person.id, false))
560
+ actionButton('Unblock {{name}}', () => void d.block(person.id, false), {
561
+ name: person.name,
562
+ })
545
563
  }
546
564
  />
547
565
  </>