@cxpinsight/react-native 0.8.0 → 0.10.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.
@@ -51,6 +51,7 @@ const MatrixDropdownQuestion_1 = require("./questions/MatrixDropdownQuestion");
51
51
  const MatrixDynamicQuestion_1 = require("./questions/MatrixDynamicQuestion");
52
52
  const PanelDynamicQuestion_1 = require("./questions/PanelDynamicQuestion");
53
53
  const DateQuestion_1 = require("./questions/DateQuestion");
54
+ const survey_spec_1 = require("@cxpinsight/survey-spec");
54
55
  const SignatureQuestion_1 = require("./questions/SignatureQuestion");
55
56
  const FileUploadQuestion_1 = require("./questions/FileUploadQuestion");
56
57
  const MultiTextQuestion_1 = require("./questions/MultiTextQuestion");
@@ -241,13 +242,22 @@ function SurveyModal({ survey, visible, onSubmit, onDismiss }) {
241
242
  // defaults to "visible". Fail-open on parse errors — see
242
243
  // expressionEval.ts.
243
244
  const isVisible = (q) => {
244
- if (q.showWhen && !(0, expressionEval_1.evaluateCondition)(q.showWhen, evalCtx))
245
+ // Prefer the tree the server compiled; fall back to parsing the string.
246
+ // Guarded inside evaluateConditionCompiled, because an app on
247
+ // survey-spec@0.4.0 has no tree walker at all.
248
+ const qc = q;
249
+ if (q.showWhen && !(0, expressionEval_1.evaluateConditionCompiled)(q.showWhen, qc.showWhenCompiled, evalCtx))
245
250
  return false;
246
- if (q.hideWhen && (0, expressionEval_1.evaluateCondition)(q.hideWhen, evalCtx))
251
+ if (q.hideWhen && (0, expressionEval_1.evaluateConditionCompiled)(q.hideWhen, qc.hideWhenCompiled, evalCtx))
247
252
  return false;
248
253
  return true;
249
254
  };
250
255
  const currentQuestions = (pages[currentPage]?.elements ?? []).filter(isVisible);
256
+ // A step ending in an implied question has no Next/Submit — its action
257
+ // buttons are the answer, and pressing one commits. Shared with the web SDK
258
+ // and the React renderer so a Notify/Offer/Comply template behaves the same
259
+ // on a phone as it does on a website. Suppresses the footer below.
260
+ const impliedNow = (0, survey_spec_1.impliedStepActions)(currentQuestions);
251
261
  const updateResponse = (0, react_1.useCallback)((name, value) => {
252
262
  setResponses((prev) => ({ ...prev, [name]: value }));
253
263
  // Live-clear the error entry the moment the new answer passes
@@ -275,7 +285,8 @@ function SurveyModal({ survey, visible, onSubmit, onDismiss }) {
275
285
  if (!Array.isArray(q.branches) || q.branches.length === 0)
276
286
  continue;
277
287
  for (const b of q.branches) {
278
- if (b.when && !(0, expressionEval_1.evaluateCondition)(b.when, evalCtx))
288
+ const bc = b;
289
+ if (b.when && !(0, expressionEval_1.evaluateConditionCompiled)(b.when, bc.whenCompiled, evalCtx))
279
290
  continue;
280
291
  if (b?.target === 'flow') {
281
292
  if (!b.flowId)
@@ -382,6 +393,9 @@ function SurveyModal({ survey, visible, onSubmit, onDismiss }) {
382
393
  setForkStack([]);
383
394
  }, duration);
384
395
  };
396
+ // Kept fresh on every render so an implied action's deferred press runs the
397
+ // current closure rather than the one captured when the button mounted.
398
+ const handleNextRef = react_1.default.useRef(() => { });
385
399
  const handleNext = () => {
386
400
  const pageErrors = validateCurrentPage();
387
401
  if (Object.keys(pageErrors).length > 0) {
@@ -437,6 +451,7 @@ function SurveyModal({ survey, visible, onSubmit, onDismiss }) {
437
451
  setHistory((h) => [...h, { pageIndex: currentPage, flow: currentFlow }]);
438
452
  setCurrentPage(linearNext);
439
453
  };
454
+ handleNextRef.current = handleNext;
440
455
  const handleBack = () => {
441
456
  // Non-linear navigation: pop the actual history stack, not just
442
457
  // currentPage - 1 (which would skip over a jump). Any errors
@@ -462,6 +477,9 @@ function SurveyModal({ survey, visible, onSubmit, onDismiss }) {
462
477
  setCurrentPage((p) => p - 1);
463
478
  }
464
479
  };
480
+ // Kept fresh so the memoized pan responder never dismisses through a stale
481
+ // closure — the same reason handleNextRef exists.
482
+ const handleDismissRef = (0, react_1.useRef)(() => { });
465
483
  const handleDismiss = () => {
466
484
  setCurrentPage(0);
467
485
  setResponses({});
@@ -473,6 +491,7 @@ function SurveyModal({ survey, visible, onSubmit, onDismiss }) {
473
491
  setForkStack([]);
474
492
  onDismiss();
475
493
  };
494
+ handleDismissRef.current = handleDismiss;
476
495
  // Normalize the v1.1 options[] {value,label} and the legacy
477
496
  // choices[] (string | {value,text}) into a single shape that
478
497
  // MultipleChoiceQuestion accepts. hasOther appends a synthetic
@@ -499,7 +518,30 @@ function SurveyModal({ survey, visible, onSubmit, onDismiss }) {
499
518
  }
500
519
  return out;
501
520
  }
502
- function renderQuestion(question) {
521
+ /**
522
+ * Compiled props, under the names the existing renderers already read.
523
+ *
524
+ * The renderers were written against the author's question shape
525
+ * (minValue/maxValue, options); the server's compiler emits the contract's
526
+ * names (min/max). Translating onto a COPY avoids touching thirty-nine
527
+ * renderers, and author fields win — the compiler derives csat's 1..5 because
528
+ * the author stated none, and must never override a range they did state.
529
+ */
530
+ function applyRenderProps(q) {
531
+ const r = q.render;
532
+ if (!r || !r.props)
533
+ return q;
534
+ const out = { ...q };
535
+ const MAP = { min: 'minValue', max: 'maxValue' };
536
+ for (const [k, v] of Object.entries(r.props)) {
537
+ const target = MAP[k] || k;
538
+ if (out[target] === undefined || out[target] === null)
539
+ out[target] = v;
540
+ }
541
+ return out;
542
+ }
543
+ function renderQuestion(rawQuestion) {
544
+ const question = applyRenderProps(rawQuestion);
503
545
  const key = question.id || question.name;
504
546
  const val = responses[key];
505
547
  // Shown when a question type needs an optional peer the host app did not
@@ -644,6 +686,51 @@ function SurveyModal({ survey, visible, onSubmit, onDismiss }) {
644
686
  // they render for real; when it did not, they fall back to the hint that
645
687
  // used to be the ONLY behaviour. Nothing is forced into an app's bundle
646
688
  // for a question type it never asks.
689
+ // A Notify, Offer or Comply interaction asks one question and the answer
690
+ // is which action was taken. The choices ARE the buttons, and their
691
+ // labels come from the shared spec so "Activate discount" reads the same
692
+ // here as on a website and on a link.
693
+ 'implied': () => {
694
+ // Same resolver the thank-you text uses, so `myapp://cart/{{id}}`
695
+ // opens the customer's cart rather than a literal deep link nothing
696
+ // can handle.
697
+ const actions = (0, survey_spec_1.impliedActions)(question, (t) => (0, interpolate_1.interpolatePlain)(t, evalCtx));
698
+ const list = actions.length ? actions : [{ value: 'acknowledged', label: 'Continue' }];
699
+ // Consent choices render identically — see impliedEqualProminence.
700
+ const equal = (0, survey_spec_1.impliedEqualProminence)([question]);
701
+ return ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.impliedActions, children: list.map((a, i) => ((0, jsx_runtime_1.jsx)(react_native_1.TouchableOpacity, {
702
+ // The press is both the answer and the Next — the footer is
703
+ // hidden on this step. Deferred a tick so setResponses has
704
+ // flushed before handleNext validates what it just replaced.
705
+ onPress: () => {
706
+ // "Remind me later" dismisses instead of answering, so the
707
+ // cadence records a SKIP and brings the interaction back
708
+ // after the author's wait. Answering would record a decision
709
+ // this person explicitly declined to make and retire it
710
+ // against maxSubmissions. It does not follow the action's
711
+ // link either — postponing is not a request to go anywhere.
712
+ if (a.defer) {
713
+ handleDismissRef.current();
714
+ return;
715
+ }
716
+ updateResponse(key, a.value);
717
+ setTimeout(() => handleNextRef.current(), 0);
718
+ // Follow the action's deep link. `myapp://cart` is the point
719
+ // of this on a phone; Linking refuses a scheme the OS has no
720
+ // handler for, so a bad target fails quietly rather than
721
+ // crashing the survey. The href already passed
722
+ // safeActionHref in the shared spec.
723
+ if (a.href) {
724
+ react_native_1.Linking.openURL(a.href).catch(() => { });
725
+ }
726
+ }, accessibilityRole: "button", accessibilityState: { selected: val === a.value }, style: [
727
+ styles.impliedBtn,
728
+ // The first action is the affirmative one. An author who
729
+ // lists "Decline" first gets it emphasised — their call.
730
+ (i === 0 && !equal) ? styles.impliedPrimary : styles.impliedSecondary,
731
+ val === a.value && styles.impliedChosen,
732
+ ], children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: (i === 0 && !equal) ? styles.impliedPrimaryLabel : styles.impliedSecondaryLabel, children: a.label }) }, a.value || String(i)))) }));
733
+ },
647
734
  'signature': () => {
648
735
  if (!(0, SignatureQuestion_1.isSignatureAvailable)())
649
736
  return renderDesktopHint();
@@ -661,6 +748,37 @@ function SurveyModal({ survey, visible, onSubmit, onDismiss }) {
661
748
  // still runs. Server-side validation will reject any junk.
662
749
  return ((0, jsx_runtime_1.jsx)(TextQuestion_1.TextQuestion, { value: val ?? '', onChange: (v) => updateResponse(key, v), placeholder: question.placeholder, contract: contract }));
663
750
  };
751
+ // ── Primitive dispatch ────────────────────────────────────────────
752
+ //
753
+ // The server compiles a question type to one of eight primitives plus a
754
+ // presentation hint. Dispatching on THAT means a question type this build
755
+ // has never heard of still renders — which matters more here than on the
756
+ // web, where a new type arrives with the next SDK fetch. On mobile it would
757
+ // otherwise need an App Store release and every respondent updating.
758
+ //
759
+ // The markup is the existing markup: this maps a primitive onto the
760
+ // renderer that already draws it. Anything without an entry — slider, and
761
+ // the grid/group/display/capture families — falls through to the type
762
+ // table exactly as before.
763
+ const PRIMITIVES = {
764
+ 'choice_one:list': 'radio',
765
+ 'choice_one:dropdown': 'dropdown',
766
+ 'choice_one:toggle': 'binary',
767
+ 'choice_one:images': 'image_picker',
768
+ 'choice_one:scale': 'nps',
769
+ 'choice_one:stars': 'rating',
770
+ 'choice_one:actions': 'implied',
771
+ 'choice_many:list': 'checkbox',
772
+ 'choice_many:rank': 'ranking',
773
+ 'text:single': 'text',
774
+ 'text:multi': 'comment',
775
+ };
776
+ const render = question.render;
777
+ const primName = render && render.primitive
778
+ ? PRIMITIVES[`${render.primitive}:${render.presentation || ''}`]
779
+ : undefined;
780
+ if (primName && RENDERERS[primName])
781
+ return RENDERERS[primName]();
664
782
  const _fn = RENDERERS[String(question.type || '')] || renderUnsupported;
665
783
  return _fn();
666
784
  }
@@ -1138,6 +1256,75 @@ function SurveyModal({ survey, visible, onSubmit, onDismiss }) {
1138
1256
  useNativeDriver: true,
1139
1257
  }).start();
1140
1258
  }, [visible, needsTopSlide, enterAnim]);
1259
+ // ── Swipe to dismiss ────────────────────────────────────────────────
1260
+ //
1261
+ // A push notification goes away when you flick it. The mobile display slice
1262
+ // drops the ✕ for exactly that reason, so this is the affordance that
1263
+ // replaces it — without it the banner would simply be stuck on screen.
1264
+ //
1265
+ // canSwipeToDismiss requires a popup and vetoes `mustShow`, which is what
1266
+ // keeps a compliance takeover — showClose:false for the OPPOSITE reason —
1267
+ // from ever becoming flickable.
1268
+ const swipeable = (0, survey_spec_1.canSwipeToDismiss)(display, survey.delivery);
1269
+ const swipeXY = (0, react_1.useRef)(new react_native_1.Animated.ValueXY({ x: 0, y: 0 })).current;
1270
+ const swipeResponder = (0, react_1.useMemo)(() => react_native_1.PanResponder.create({
1271
+ onStartShouldSetPanResponder: () => false,
1272
+ // Claim the gesture only once it is clearly a drag, so a tap on the action
1273
+ // button still presses the button.
1274
+ onMoveShouldSetPanResponder: (_e, g) => swipeable && (Math.abs(g.dx) > 8 || Math.abs(g.dy) > 8),
1275
+ onPanResponderMove: (_e, g) => {
1276
+ const horizontal = Math.abs(g.dx) > Math.abs(g.dy);
1277
+ // A top banner travels up, never down — dragging it down is a pull.
1278
+ const dy = placement === 'top' ? Math.min(0, g.dy) : placement === 'bottom' ? Math.max(0, g.dy) : g.dy;
1279
+ swipeXY.setValue(horizontal ? { x: g.dx, y: 0 } : { x: 0, y: dy });
1280
+ },
1281
+ onPanResponderRelease: (_e, g) => {
1282
+ const horizontal = Math.abs(g.dx) > Math.abs(g.dy);
1283
+ const moved = horizontal ? g.dx : g.dy;
1284
+ const fast = Math.abs(horizontal ? g.vx : g.vy) > 0.5;
1285
+ if (Math.abs(moved) >= 56 || fast) {
1286
+ const to = moved < 0 ? -SCREEN_HEIGHT : SCREEN_HEIGHT;
1287
+ react_native_1.Animated.timing(swipeXY, {
1288
+ toValue: horizontal ? { x: moved < 0 ? -SCREEN_WIDTH : SCREEN_WIDTH, y: 0 } : { x: 0, y: to },
1289
+ duration: 180, useNativeDriver: true,
1290
+ }).start(() => { swipeXY.setValue({ x: 0, y: 0 }); handleDismissRef.current(); });
1291
+ return;
1292
+ }
1293
+ react_native_1.Animated.spring(swipeXY, { toValue: { x: 0, y: 0 }, useNativeDriver: true, bounciness: 6 }).start();
1294
+ },
1295
+ }), [swipeable, placement, swipeXY]);
1296
+ // Auto-dismiss. Same rule as web (autoDismissMs vetoes fullscreen and
1297
+ // mustShow, clamps to 2s). Cancelled for good once the respondent touches
1298
+ // anything — a timer that fires after someone has started answering would
1299
+ // pull the survey out from under them.
1300
+ // Which ending this interaction gets — see the render block below.
1301
+ const endingKind = (0, react_1.useMemo)(() => {
1302
+ const sv = survey;
1303
+ const qs = (survey.rawQuestions ?? []);
1304
+ const acts = (0, survey_spec_1.impliedStepActions)(qs) || [];
1305
+ if (!acts.length)
1306
+ return 'thanks';
1307
+ const last = qs[qs.length - 1];
1308
+ const id = last?.name ?? last?.id ?? '';
1309
+ const action = id ? (responses[id] ?? null) : null;
1310
+ return (0, survey_spec_1.outcomeFor)({
1311
+ interactionType: sv.interactionType, action,
1312
+ affirmative: String(action) === String(acts[0].value),
1313
+ reward: sv.reward, compliance: sv.compliance,
1314
+ });
1315
+ }, [survey, responses]);
1316
+ const dismissMs = (0, survey_spec_1.autoDismissMs)(display, survey.delivery);
1317
+ const engagedRef = (0, react_1.useRef)(false);
1318
+ (0, react_1.useEffect)(() => {
1319
+ if (!visible || !dismissMs)
1320
+ return;
1321
+ engagedRef.current = false;
1322
+ const t = setTimeout(() => {
1323
+ if (!engagedRef.current)
1324
+ handleDismissRef.current();
1325
+ }, dismissMs);
1326
+ return () => clearTimeout(t);
1327
+ }, [visible, dismissMs]);
1141
1328
  const enterStyle = needsTopSlide
1142
1329
  ? {
1143
1330
  opacity: enterAnim,
@@ -1293,10 +1480,11 @@ function SurveyModal({ survey, visible, onSubmit, onDismiss }) {
1293
1480
  // A sheet sits flush against its edge, so the overlay loses its gutter.
1294
1481
  isSheet && { paddingHorizontal: 0, paddingTop: 0, paddingBottom: 0 },
1295
1482
  isFullscreen && styles.overlayFullscreen,
1296
- ], behavior: react_native_1.Platform.OS === 'ios' ? 'padding' : 'height', children: [(0, jsx_runtime_1.jsxs)(react_native_1.Animated.View, { style: [
1483
+ ], behavior: react_native_1.Platform.OS === 'ios' ? 'padding' : 'height', children: [(0, jsx_runtime_1.jsxs)(react_native_1.Animated.View, { onTouchStart: () => { engagedRef.current = true; }, ...(swipeable ? swipeResponder.panHandlers : {}), style: [
1297
1484
  styles.card,
1298
1485
  themeCard,
1299
1486
  enterStyle,
1487
+ swipeable && { transform: swipeXY.getTranslateTransform() },
1300
1488
  maxWidth !== undefined && !isFullscreen && { width: Math.min(SCREEN_WIDTH - 40, maxWidth), maxWidth },
1301
1489
  cornerRadius !== undefined && !isFullscreen && { borderRadius: cornerRadius },
1302
1490
  // cardFullscreen fills; an explicit content height/width below OVERRIDES
@@ -1309,7 +1497,24 @@ function SurveyModal({ survey, visible, onSubmit, onDismiss }) {
1309
1497
  ], children: [display?.showClose !== false && ((0, jsx_runtime_1.jsx)(react_native_1.TouchableOpacity, { style: [
1310
1498
  styles.closeButton,
1311
1499
  isRtl ? { left: 12 } : { right: 12 },
1312
- ], onPress: handleDismiss, activeOpacity: 0.7, children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [styles.closeText, themeTextSecondary], children: "\u2715" }) })), showThankYou ? (() => {
1500
+ ], onPress: handleDismiss, activeOpacity: 0.7, children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [styles.closeText, themeTextSecondary], children: "\u2715" }) })), showThankYou && endingKind !== 'thanks' ? (() => {
1501
+ const rw = (0, survey_spec_1.rewardToShow)(survey);
1502
+ const comp = survey.compliance;
1503
+ if (endingKind === 'fulfil' && rw) {
1504
+ const exp = rw.expiresAt ? new Date(rw.expiresAt) : null;
1505
+ return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.thankYou, children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [styles.thankYouTitle, fontStyle, themeTextPrimary], children: "It\u2019s yours" }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [styles.thankYouSub, fontStyle, themeTextSecondary], children: "Use this at checkout." }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { selectable: true, style: [styles.rewardCode, fontStyle, themeTextPrimary], children: rw.value }), !!rw.whereValid && ((0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [styles.thankYouSub, fontStyle, themeTextSecondary], children: rw.whereValid })), !!exp && ((0, jsx_runtime_1.jsxs)(react_native_1.Text, { style: [styles.thankYouSub, fontStyle, themeTextSecondary], children: ["Expires ", exp.toLocaleString()] }))] }));
1506
+ }
1507
+ if (endingKind === 'hold') {
1508
+ return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.thankYou, accessibilityLiveRegion: "polite", children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [styles.thankYouTitle, fontStyle, themeTextPrimary], children: comp?.reviewRequired ? 'Under review' : 'Not completed' }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [styles.thankYouSub, fontStyle, themeTextSecondary], children: comp?.blockedMessage
1509
+ ?? (comp?.reviewRequired
1510
+ ? 'We’re checking your details. You’ll be able to continue once that’s done.'
1511
+ : 'You need to accept this before you can continue.') })] }));
1512
+ }
1513
+ if (endingKind === 'unblock') {
1514
+ return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.thankYou, children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [styles.thankYouTitle, fontStyle, themeTextPrimary], children: "Recorded" }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [styles.thankYouSub, fontStyle, themeTextSecondary], children: "You can carry on." })] }));
1515
+ }
1516
+ return ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.thankYou, children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [styles.thankYouSub, fontStyle, themeTextSecondary], children: endingKind === 'navigate' ? 'Taking you there…' : 'Closed.' }) }));
1517
+ })() : showThankYou ? (() => {
1313
1518
  // The thank-you message can be rich HTML from the web builder.
1314
1519
  // Pull out an embedded <img> (often a data: URI icon) and render
1315
1520
  // it as a real Image — stripHtml would otherwise drop it — then
@@ -1392,7 +1597,7 @@ function SurveyModal({ survey, visible, onSubmit, onDismiss }) {
1392
1597
  // Right-align parity with web: Back hugs the left edge while
1393
1598
  // Next/Submit sits on the right (web uses margin-right:auto).
1394
1599
  buttonAlign === 'right' && { marginRight: 'auto' },
1395
- ], onPress: handleBack, activeOpacity: 0.7, children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [styles.backButtonText, fontStyle, themeTextSecondary, buttonFS], children: "Back" }) })), (() => {
1600
+ ], onPress: handleBack, activeOpacity: 0.7, children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [styles.backButtonText, fontStyle, themeTextSecondary, buttonFS], children: "Back" }) })), impliedNow.length > 0 ? null : (() => {
1396
1601
  // Reflect the actual outcome of pressing Next: if a
1397
1602
  // branch resolves to __end__ we're submitting even
1398
1603
  // when we're not on the last page. Cheap to evaluate
@@ -1606,6 +1811,18 @@ const makeStyles = (c) => react_native_1.StyleSheet.create({
1606
1811
  // "Please complete on desktop" hint shown for question types that
1607
1812
  // require heavy native UX (matrix, panelDynamic, signature, etc.).
1608
1813
  // Phase 4.5 will bring native implementations for the priority ones.
1814
+ // Implied actions: the buttons ARE the answer, so they get real button
1815
+ // weight rather than looking like an option list.
1816
+ impliedActions: { flexDirection: 'row', flexWrap: 'wrap', gap: 10, marginTop: 4 },
1817
+ impliedBtn: {
1818
+ flexGrow: 1, flexBasis: 140, paddingVertical: 13, paddingHorizontal: 18,
1819
+ borderRadius: 8, alignItems: 'center', borderWidth: 1, borderColor: 'transparent',
1820
+ },
1821
+ impliedPrimary: { backgroundColor: '#2b6cb0' },
1822
+ impliedSecondary: { backgroundColor: 'transparent', borderColor: '#d4d4d4' },
1823
+ impliedChosen: { opacity: 0.75 },
1824
+ impliedPrimaryLabel: { color: '#fff', fontWeight: '600', fontSize: 15 },
1825
+ impliedSecondaryLabel: { color: '#1a1a1a', fontWeight: '600', fontSize: 15 },
1609
1826
  desktopHint: {
1610
1827
  color: c.hintText,
1611
1828
  fontSize: 13,
@@ -1691,5 +1908,22 @@ const makeStyles = (c) => react_native_1.StyleSheet.create({
1691
1908
  textAlign: 'center',
1692
1909
  lineHeight: 26,
1693
1910
  },
1911
+ thankYouSub: {
1912
+ fontSize: 14,
1913
+ textAlign: 'center',
1914
+ lineHeight: 20,
1915
+ marginTop: 6,
1916
+ },
1917
+ // Selectable so the code can be copied by long-press — there is no clipboard
1918
+ // button here because RN's clipboard is an optional peer, and a code you can
1919
+ // select is better than a button that may not exist.
1920
+ rewardCode: {
1921
+ fontSize: 22,
1922
+ fontWeight: '700',
1923
+ letterSpacing: 1.5,
1924
+ textAlign: 'center',
1925
+ marginTop: 14,
1926
+ marginBottom: 10,
1927
+ },
1694
1928
  });
1695
1929
  //# sourceMappingURL=SurveyModal.js.map