@cxpinsight/react-native 0.18.4 → 0.19.1

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.
@@ -239,7 +239,7 @@ function swipeRelease(g) {
239
239
  function surfaceScrolls(interactionKind) {
240
240
  return String(interactionKind || '').toLowerCase() !== 'notify';
241
241
  }
242
- function SurveyModal({ survey, visible, onSubmit, onDismiss }) {
242
+ function SurveyModal({ survey, visible, onSubmit, onDismiss, reply = null }) {
243
243
  const [currentPage, setCurrentPage] = (0, react_2.useState)(0);
244
244
  const [responses, setResponses] = (0, react_2.useState)({});
245
245
  const [showThankYou, setShowThankYou] = (0, react_2.useState)(false);
@@ -609,7 +609,7 @@ function SurveyModal({ survey, visible, onSubmit, onDismiss }) {
609
609
  // server rejection (survey deactivated, cap hit) surfaces a retry
610
610
  // message instead of a false thank-you.
611
611
  const submitAndReset = async () => {
612
- const ok = await onSubmit(buildSubmitPayload());
612
+ const ok = await onSubmit(buildSubmitPayload(), { via: viaRef.current });
613
613
  if (ok === false) {
614
614
  setSubmitError('We couldn’t submit your response. Please try again.');
615
615
  return; // keep the survey open for a retry
@@ -621,6 +621,15 @@ function SurveyModal({ survey, visible, onSubmit, onDismiss }) {
621
621
  if (String(survey.interactionType || '')
622
622
  .toLowerCase() === 'notify')
623
623
  return;
624
+ // AND WHEN THE AUTHOR TURNED THE ENDING OFF.
625
+ //
626
+ // showsConfirmation gated the OUTCOME panel below and not this state, so
627
+ // switching the ending off did not remove the ending — it fell through to
628
+ // the generic "Thank you" instead, which is a worse result than leaving it
629
+ // alone: the author lost the reward panel and gained a line they never
630
+ // wrote. Skipped here, so nothing draws and the 2.5s window never opens.
631
+ if (!(0, survey_spec_1.showsConfirmation)(survey))
632
+ return;
624
633
  setShowThankYou(true);
625
634
  const duration = survey.settings?.thankYouDuration ?? 2500;
626
635
  setTimeout(() => {
@@ -750,7 +759,19 @@ function SurveyModal({ survey, visible, onSubmit, onDismiss }) {
750
759
  * the interaction at all, because unlike web the card here was never
751
760
  * pressable in the first place. Same defect, one step worse.
752
761
  */
753
- const pressImplied = (0, react_2.useCallback)((key, a) => {
762
+ // WHICH SURFACE WAS PRESSED — 'button' or 'card'.
763
+ //
764
+ // Both routes end in pressImplied, so by the time the answer is built the
765
+ // two are the same choice and nothing downstream can tell them apart. Web
766
+ // has carried this since bf0c4b55 (metadata.via); mobile gained whole-card
767
+ // press without it, so on the platform where nearly all the traffic is, the
768
+ // builder's "Clicked the card" outcome could never be populated.
769
+ //
770
+ // A ref, not state: it is read during the submit that the press itself
771
+ // triggers, and a re-render in between would be a wasted frame.
772
+ const viaRef = (0, react_2.useRef)('button');
773
+ const pressImplied = (0, react_2.useCallback)((key, a, via = 'button') => {
774
+ viaRef.current = via;
754
775
  // "Remind me later" dismisses instead of answering, so the cadence records
755
776
  // a SKIP and brings the interaction back after the author's wait.
756
777
  // Answering would record a decision this person explicitly declined to
@@ -858,22 +879,16 @@ function SurveyModal({ survey, visible, onSubmit, onDismiss }) {
858
879
  // MultipleChoiceQuestion accepts. hasOther appends a synthetic
859
880
  // "Other" entry — full free-text follow-up UX lands in Phase 4.5.
860
881
  function normalizeChoices(question) {
861
- const out = [];
862
- if (Array.isArray(question.options)) {
863
- for (const o of question.options) {
864
- if (!o)
865
- continue;
866
- out.push({ value: String(o.value ?? ''), text: String(o.label ?? o.text ?? o.value ?? '') });
867
- }
868
- }
869
- else if (Array.isArray(question.choices)) {
870
- for (const c of question.choices) {
871
- if (typeof c === 'string')
872
- out.push({ value: c, text: c });
873
- else if (c)
874
- out.push({ value: String(c.value ?? ''), text: String(c.label ?? c.text ?? c.value ?? '') });
875
- }
876
- }
882
+ // WHICHEVER FIELD HAS SOMETHING IN IT — see questionChoices.
883
+ //
884
+ // This asked `Array.isArray(options)` first and took that branch. The
885
+ // server's schema declares `options`, so Mongoose defaults it to `[]` on
886
+ // every question: a question authored with `choices` arrived carrying an
887
+ // empty `options` beside it, this took the empty branch, and the phone
888
+ // drew a choice question with no choices at all. The web preview read
889
+ // `choices` first and was fine, so it looked like a mobile rendering bug
890
+ // and was a field-precedence one.
891
+ const out = (0, survey_spec_1.questionChoices)(question);
877
892
  if (question.hasOther) {
878
893
  out.push({ value: '__other__', text: question.otherLabel || 'Other' });
879
894
  }
@@ -1074,7 +1089,7 @@ function SurveyModal({ survey, visible, onSubmit, onDismiss }) {
1074
1089
  // The press is both the answer and the Next — the footer is
1075
1090
  // hidden on this step. Its body is pressImplied, so the CARD
1076
1091
  // can do exactly the same thing when this button is not drawn.
1077
- onPress: () => pressImplied(key, a), accessibilityRole: "button", accessibilityState: { selected: val === a.value }, style: [
1092
+ onPress: () => pressImplied(key, a, 'button'), accessibilityRole: "button", accessibilityState: { selected: val === a.value }, style: [
1078
1093
  styles.impliedBtn,
1079
1094
  // BESIDE THE TEXT, THE BUTTON IS AS WIDE AS ITS LABEL.
1080
1095
  //
@@ -1371,7 +1386,6 @@ function SurveyModal({ survey, visible, onSubmit, onDismiss }) {
1371
1386
  const fs = baseScale !== 1;
1372
1387
  const qTitleFS = fs ? { fontSize: scaled(15) } : undefined;
1373
1388
  const qDescFS = fs ? { fontSize: scaled(13) } : undefined;
1374
- const thankYouFS = fs ? { fontSize: scaled(18) } : undefined;
1375
1389
  const buttonFS = fs ? { fontSize: scaled(15) } : undefined;
1376
1390
  // Layout tokens (spec §24). String presets map to concrete pixel
1377
1391
  // values; a number passes through verbatim so authors can set
@@ -1540,6 +1554,16 @@ function SurveyModal({ survey, visible, onSubmit, onDismiss }) {
1540
1554
  JSON.stringify(surveyPaletteOverride), JSON.stringify(initPaletteOverride),
1541
1555
  JSON.stringify(survey.branding?.style)]);
1542
1556
  const styles = (0, react_2.useMemo)(() => makeStyles(contract), [contract]);
1557
+ // A picture band outside the header keeps the CARD'S padding. padBand exists only when a density
1558
+ // is set; without one the band had no horizontal padding at all and ran edge to edge, like
1559
+ // "Half the card", while the header and body below it kept theirs (styles.body carries the
1560
+ // contract's padding either way).
1561
+ const bandPad = (0, react_2.useMemo)(() => {
1562
+ if (padBand)
1563
+ return padBand;
1564
+ const b = react_native_1.StyleSheet.flatten(styles.body) || {};
1565
+ return { paddingLeft: b.paddingLeft, paddingRight: b.paddingRight };
1566
+ }, [padBand, styles]);
1543
1567
  const themeCard = isLight ? { backgroundColor: palette.card, borderColor: palette.cardBorder } : undefined;
1544
1568
  const themeTextPrimary = isLight ? { color: palette.textPrimary } : undefined;
1545
1569
  const themeTextSecondary = isLight ? { color: palette.textSecondary } : undefined;
@@ -1750,10 +1774,12 @@ function SurveyModal({ survey, visible, onSubmit, onDismiss }) {
1750
1774
  * The alternative was a Fade control that worked in the preview and did
1751
1775
  * nothing here, which is the thing this codebase keeps having to undo.
1752
1776
  */
1753
- const fadeSteps = (towards) => {
1754
- if (!imgD.fade)
1777
+ // `fade` is a parameter now, because there are two pictures asking: the
1778
+ // card's and the confirmation's, each with its own slider.
1779
+ const fadeSteps = (towards, fade = imgD.fade) => {
1780
+ if (!fade)
1755
1781
  return null;
1756
- const span = Math.max(2, Math.min(96, imgD.fade)) / 100;
1782
+ const span = Math.max(2, Math.min(96, fade)) / 100;
1757
1783
  const vertical = towards === 'top' || towards === 'bottom';
1758
1784
  // A REAL GRADIENT WHERE THE APP HAS ONE. react-native-svg is an optional
1759
1785
  // peer, already loaded this way by the signature question, and it draws
@@ -1879,6 +1905,96 @@ function SurveyModal({ survey, visible, onSubmit, onDismiss }) {
1879
1905
  const halfPadR = contract.cardPaddingRight ?? contract.cardPadding;
1880
1906
  const halfOuter = imgD.side === 'right' ? halfPadR : halfPadL;
1881
1907
  const halfWidth = Math.round(((cardWidth - halfPadL - halfPadR) * imgD.split) / 100) + halfOuter;
1908
+ /**
1909
+ * THE ENDING'S OWN PICTURE, which is not the card's.
1910
+ *
1911
+ * An offer's card sells the thing; its ending confirms it, and those want
1912
+ * different images far more often than the same one. It stands IN FOR the
1913
+ * ✓ glyph rather than sitting above it — a tick over a photograph is two
1914
+ * things saying "done", and the author chose the photograph.
1915
+ */
1916
+ // The card's height, as it was just before the ending replaced it — a floor
1917
+ // for the ending, never a cap, so a taller ending can still grow.
1918
+ const cardHeightRef = (0, react_2.useRef)(0);
1919
+ // A FLOOR AND A FILL, MINUS THE CARD'S OWN PADDING.
1920
+ //
1921
+ // The floor alone left the panel at its natural height inside a taller card,
1922
+ // so a picture told to stretch beside the words took the confirmation's few
1923
+ // lines rather than the card's height. And the floor was the CARD's height
1924
+ // — the outer view, padding included — applied to a panel that sits inside
1925
+ // that padding, which asks the card to grow by twice its own inset every
1926
+ // time the ending appears. The web measures the CONTENT; this is the same
1927
+ // number, arrived at by subtraction because the card is what onLayout sees.
1928
+ const endPadTop = contract.cardPaddingTop ?? contract.cardPadding ?? 0;
1929
+ const endPadBottom = contract.cardPaddingBottom ?? contract.cardPadding ?? 0;
1930
+ const endingHold = cardHeightRef.current > 0
1931
+ ? {
1932
+ minHeight: Math.max(0, cardHeightRef.current - endPadTop - endPadBottom),
1933
+ flexGrow: 1,
1934
+ }
1935
+ : null;
1936
+ // THE CONFIRMATION'S OWN DESIGN, inheriting the card's until it has one —
1937
+ // see confirmationDesign in the spec. It used to read `contract.imageHeight`
1938
+ // and `contract.imageRadius` directly, which tied the icon on this screen to
1939
+ // the photograph on the card: shrinking one shrank the other.
1940
+ // WHERE THE CARD'S WORDS SIT. `titleAlign` was already a survey field; the
1941
+ // other two are new and follow it, because a centred headline over a
1942
+ // left-ragged paragraph is the commonest way a card looks accidental.
1943
+ const cardTitleAlign = (0, survey_spec_1.alignOf)(survey.titleAlign, 'left');
1944
+ const cardDescAlign = (0, survey_spec_1.alignOf)(survey.descriptionAlign, cardTitleAlign);
1945
+ const cardQuestionAlign = (0, survey_spec_1.alignOf)(survey.questionAlign, 'left');
1946
+ const confirmD = (0, survey_spec_1.confirmationDesign)(survey, 'mobile');
1947
+ const endingImgSrc = confirmD.image.src;
1948
+ const endingAlign = confirmD.heading.align === 'start' ? 'flex-start'
1949
+ : confirmD.heading.align === 'end' ? 'flex-end' : 'center';
1950
+ const endingTextAlign = confirmD.heading.align === 'start' ? 'left'
1951
+ : confirmD.heading.align === 'end' ? 'right' : 'center';
1952
+ // LINE HEIGHT MOVES WITH THE SIZE. The stylesheet pins 26 and 20, so a
1953
+ // heading taken to 34 would have been clipped by its own leading.
1954
+ // scaled() FIRST, so the accessibility font scale still multiplies an
1955
+ // authored size — it used to scale a hard-coded 18 and the authored number
1956
+ // would have replaced it outright.
1957
+ const endingHeadSize = scaled(confirmD.heading.size);
1958
+ const endingSubSize = scaled(confirmD.body.size);
1959
+ const endingBodyAlign = confirmD.body.align === 'start' ? 'left'
1960
+ : confirmD.body.align === 'end' ? 'right' : 'center';
1961
+ const endingHeadFS = {
1962
+ fontSize: endingHeadSize,
1963
+ lineHeight: Math.round(endingHeadSize * 1.45),
1964
+ textAlign: endingTextAlign,
1965
+ };
1966
+ const endingSubFS = {
1967
+ fontSize: endingSubSize,
1968
+ lineHeight: Math.round(endingSubSize * confirmD.body.lineHeight),
1969
+ textAlign: endingBodyAlign,
1970
+ };
1971
+ // THE CARD'S WHOLE IMAGE VOCABULARY, on the second screen. It was a band and
1972
+ // nothing else — no Fade, no Dim, no Alignment, no Layout — because the
1973
+ // screen was built as a stack and the design was a hand-picked five fields
1974
+ // of the card's. confirmationDesign now answers with a full ImageDesign, so
1975
+ // there is one set of layouts and both screens draw from it.
1976
+ const cI = confirmD.image;
1977
+ const endingLayout = endingImgSrc ? cI.layout : 'none';
1978
+ const endingSelf = cI.align === 'start' ? 'flex-start'
1979
+ : cI.align === 'end' ? 'flex-end' : 'center';
1980
+ const endingScrim = cI.scrim ? ((0, jsx_runtime_1.jsx)(react_native_1.View, { pointerEvents: "none", style: { position: 'absolute', top: 0, right: 0, bottom: 0, left: 0,
1981
+ backgroundColor: contract.card, opacity: cI.scrim / 100 } })) : null;
1982
+ const endingPicture = (fill) => ((0, jsx_runtime_1.jsx)(react_native_1.Image, { source: { uri: endingImgSrc }, style: [{ width: '100%', height: '100%' }, fill], resizeMode: cI.fit, accessibilityIgnoresInvertColors: true }));
1983
+ /** The band — the picture as a strip, at its own width and alignment. */
1984
+ const endingImage = (endingLayout === 'above' || endingLayout === 'below') ? ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: {
1985
+ position: 'relative', overflow: 'hidden',
1986
+ width: `${cI.width}%`, height: cI.height, borderRadius: cI.radius,
1987
+ alignSelf: endingSelf, marginBottom: endingLayout === 'above' ? 16 : 0,
1988
+ marginTop: endingLayout === 'below' ? 16 : 0,
1989
+ }, children: [endingPicture({}), fadeSteps(endingLayout === 'below' ? 'top' : 'bottom', cI.fade), endingScrim] })) : null;
1990
+ // THE SECOND PICTURE, between the card's and the headline. Square, so one
1991
+ // number sizes it — and it replaces the ✓ rather than sitting under it.
1992
+ const endingIcon = confirmD.icon.src ? ((0, jsx_runtime_1.jsx)(react_native_1.Image, { source: { uri: confirmD.icon.src }, style: {
1993
+ width: confirmD.icon.size,
1994
+ height: confirmD.icon.size,
1995
+ borderRadius: confirmD.icon.radius,
1996
+ marginBottom: 16,
1997
+ }, resizeMode: confirmD.icon.fit, accessibilityIgnoresInvertColors: true })) : null;
1882
1998
  const cardImageHalf = imgLayout === 'half' ? ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: {
1883
1999
  position: 'relative',
1884
2000
  overflow: 'hidden',
@@ -2227,7 +2343,7 @@ function SurveyModal({ survey, visible, onSubmit, onDismiss }) {
2227
2343
  // uses, which is what keeps a deliberate flick from also counting as a tap.
2228
2344
  if (!st.moved) {
2229
2345
  if (cardTakesPress && cardImpliedList.length) {
2230
- pressImplied(cardImpliedKey, cardImpliedList[0]);
2346
+ pressImplied(cardImpliedKey, cardImpliedList[0], 'card');
2231
2347
  }
2232
2348
  return;
2233
2349
  }
@@ -2273,11 +2389,14 @@ function SurveyModal({ survey, visible, onSubmit, onDismiss }) {
2273
2389
  const last = qs[qs.length - 1];
2274
2390
  const id = last?.name ?? last?.id ?? '';
2275
2391
  const action = id ? (responses[id] ?? null) : null;
2276
- return (0, survey_spec_1.outcomeFor)({
2392
+ // The SCREEN, not the outcome the host hears: an accepted offer with no
2393
+ // code is 'fulfil' to the host and the author's confirmation here — the
2394
+ // web renderer asks the same question.
2395
+ return (0, survey_spec_1.endingScreenFor)((0, survey_spec_1.outcomeFor)({
2277
2396
  interactionType: sv.interactionType, action,
2278
2397
  affirmative: String(action) === String(acts[0].value),
2279
2398
  reward: sv.reward, compliance: sv.compliance,
2280
- });
2399
+ }), sv);
2281
2400
  }, [survey, responses]);
2282
2401
  const dismissMs = (0, survey_spec_1.autoDismissMs)(display, survey.delivery);
2283
2402
  const engagedRef = (0, react_2.useRef)(false);
@@ -2511,9 +2630,21 @@ function SurveyModal({ survey, visible, onSubmit, onDismiss }) {
2511
2630
  // Announced politely: urgency a sighted respondent gets from watching the
2512
2631
  // number should not be information only they receive.
2513
2632
  accessibilityLiveRegion: "polite", children: ["Ends in ", countdown.text] })) : null;
2514
- const progressEl = totalPages > 1 ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [effShowPageNumbers && ((0, jsx_runtime_1.jsxs)(react_native_1.Text, { style: styles.progress, children: [currentPage + 1, " / ", totalPages] })), (0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.progressBarTrack, accessible: true, accessibilityRole: "progressbar", accessibilityLabel: `Page ${currentPage + 1} of ${totalPages}`, accessibilityValue: { min: 1, max: totalPages, now: currentPage + 1 }, accessibilityLiveRegion: "polite", children: (0, jsx_runtime_1.jsx)(react_native_1.View, { style: [
2633
+ // Across pages the bar counts pages. On a single page it counts answered
2634
+ // questions, as web and link do, so the setting never silently does nothing.
2635
+ const _countable = currentQuestions.filter((q) => {
2636
+ const t = String(q.type || '').toLowerCase();
2637
+ return t !== 'html' && t !== 'pagebreak' && t !== 'content' && t !== 'implied';
2638
+ });
2639
+ const _answeredN = _countable.filter((q) => {
2640
+ const v = responses[q.id || q.name];
2641
+ return v != null && v !== '' && !(Array.isArray(v) && v.length === 0);
2642
+ }).length;
2643
+ const progressNow = totalPages > 1 ? currentPage + 1 : _answeredN;
2644
+ const progressMax = totalPages > 1 ? totalPages : _countable.length;
2645
+ const progressEl = (totalPages > 1 || _countable.length > 1) ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [effShowPageNumbers && totalPages > 1 && ((0, jsx_runtime_1.jsxs)(react_native_1.Text, { style: styles.progress, children: [currentPage + 1, " / ", totalPages] })), (0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.progressBarTrack, accessible: true, accessibilityRole: "progressbar", accessibilityLabel: totalPages > 1 ? `Page ${currentPage + 1} of ${totalPages}` : `${progressNow} of ${progressMax} answered`, accessibilityValue: { min: 0, max: progressMax, now: progressNow }, accessibilityLiveRegion: "polite", children: (0, jsx_runtime_1.jsx)(react_native_1.View, { style: [
2515
2646
  styles.progressBarFill,
2516
- { width: `${((currentPage + 1) / totalPages) * 100}%`, backgroundColor: primaryColor },
2647
+ { width: `${(progressNow / progressMax) * 100}%`, backgroundColor: primaryColor },
2517
2648
  ] }) })] })) : null;
2518
2649
  return ((0, jsx_runtime_1.jsx)(react_native_1.Modal, { visible: visible, transparent: true, animationType: modalAnimation,
2519
2650
  // Android's back gesture. Wrapped rather than passed: the handler now
@@ -2527,7 +2658,28 @@ function SurveyModal({ survey, visible, onSubmit, onDismiss }) {
2527
2658
  // The OVERLAY, not the card — a screen-shaped image with the card in
2528
2659
  // its real position, rather than a card-shaped one the replay player
2529
2660
  // would stretch across the whole device frame.
2530
- ref: (v) => { cardViewRef.current = v; }, pointerEvents: isPopupOverlay ? 'box-none' : 'auto', style: [
2661
+ ref: (v) => { cardViewRef.current = v; },
2662
+ // WITHOUT THIS THE VIEW MAY NOT EXIST NATIVELY, AND THE CAPTURE FAILS.
2663
+ //
2664
+ // React Native collapses views that have no native side effects. For a
2665
+ // POPUP this overlay is exactly that: backgroundColor transparent and
2666
+ // pointerEvents box-none, a container that only positions its child. So
2667
+ // it was optimised away, findNodeHandle had nothing to resolve,
2668
+ // captureRef rejected, captureView returned null, and the recorder fell
2669
+ // back to the host captureScreen — which on iOS returns the TOPMOST
2670
+ // UIWindow.
2671
+ //
2672
+ // That is why every offer and notification frame came back as whatever
2673
+ // native alert happened to be up, floating on flat grey, with the card
2674
+ // nowhere in it. Measured: three survey_shown frames at 73,548 / 73,512
2675
+ // / 73,528 bytes — the same wrong image each time — in a session where
2676
+ // the respondent demonstrably swiped one card away, closed another and
2677
+ // completed a third.
2678
+ //
2679
+ // It fails for popups specifically BECAUSE popups are the transparent
2680
+ // ones; a modal paints a scrim and survives collapsing, which is why
2681
+ // this looked like "offers are broken" rather than "capture is".
2682
+ collapsable: false, pointerEvents: isPopupOverlay ? 'box-none' : 'auto', style: [
2531
2683
  styles.overlay,
2532
2684
  // Transparent for a popup — the app behind it is not being taken over.
2533
2685
  isPopupOverlay && { backgroundColor: 'transparent' },
@@ -2552,10 +2704,19 @@ function SurveyModal({ survey, visible, onSubmit, onDismiss }) {
2552
2704
  // Feeds the authored-coordinate maths above. Only tracked when a
2553
2705
  // coordinate is actually in play, and only on a real change, so the
2554
2706
  // common case costs nothing and this cannot loop.
2555
- onLayout: hasPopupY ? (e) => {
2707
+ // MEASURED ALWAYS, not only for a coordinate. The second reader is
2708
+ // the confirmation: it replaces the card's whole content, and the
2709
+ // two screens hold different amounts — so the card shrank under a
2710
+ // respondent who had just pressed a button on it, which reads as a
2711
+ // different card arriving rather than the same one answering. The
2712
+ // last height measured BEFORE the ending becomes its floor.
2713
+ onLayout: (e) => {
2556
2714
  const h = Math.round(e.nativeEvent.layout.height);
2557
- setMeasuredCardH((prev) => (Math.abs(prev - h) > 1 ? h : prev));
2558
- } : undefined, style: [
2715
+ if (!showThankYou && h > 0)
2716
+ cardHeightRef.current = h;
2717
+ if (hasPopupY)
2718
+ setMeasuredCardH((prev) => (Math.abs(prev - h) > 1 ? h : prev));
2719
+ }, style: [
2559
2720
  styles.card,
2560
2721
  themeCard,
2561
2722
  enterStyle,
@@ -2575,34 +2736,96 @@ function SurveyModal({ survey, visible, onSubmit, onDismiss }) {
2575
2736
  ], children: [cardImageBack, closeShown && ((0, jsx_runtime_1.jsx)(react_native_1.TouchableOpacity, { ...swipeExempt, style: [
2576
2737
  styles.closeButton,
2577
2738
  isRtl ? { left: contract.closeInset } : { right: contract.closeInset },
2578
- ], onPress: () => handleDismiss('close'), activeOpacity: 0.7, children: (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.closeGlyph, children: [(0, jsx_runtime_1.jsx)(react_native_1.View, { style: [styles.closeBar, { transform: [{ rotate: '45deg' }] }] }), (0, jsx_runtime_1.jsx)(react_native_1.View, { style: [styles.closeBar, { transform: [{ rotate: '-45deg' }] }] })] }) })), showThankYou && endingKind !== 'thanks' && (0, survey_spec_1.showsConfirmation)(survey) ? (() => {
2579
- const rw = (0, survey_spec_1.rewardToShow)(survey);
2739
+ ], onPress: () => handleDismiss('close'), activeOpacity: 0.7, children: (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.closeGlyph, children: [(0, jsx_runtime_1.jsx)(react_native_1.View, { style: [styles.closeBar, { transform: [{ rotate: '45deg' }] }] }), (0, jsx_runtime_1.jsx)(react_native_1.View, { style: [styles.closeBar, { transform: [{ rotate: '-45deg' }] }] })] }) })), showThankYou && endingKind !== 'thanks' && endingKind !== 'fulfil' && (0, survey_spec_1.showsConfirmation)(survey) ? (() => {
2580
2740
  const comp = survey.compliance;
2581
- if (endingKind === 'fulfil' && rw) {
2582
- const exp = rw.expiresAt ? new Date(rw.expiresAt) : null;
2583
- 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: (0, survey_spec_1.confirmationText)(survey, 'It’s yours') }), (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()] }))] }));
2584
- }
2585
2741
  if (endingKind === 'hold') {
2586
- 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
2742
+ return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [styles.thankYou, endingHold, { alignItems: endingAlign }], accessibilityLiveRegion: "polite", children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [styles.thankYouTitle, fontStyle, themeTextPrimary, endingHeadFS], children: comp?.reviewRequired ? 'Under review' : 'Not completed' }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [styles.thankYouSub, fontStyle, themeTextSecondary, endingSubFS], children: comp?.blockedMessage
2587
2743
  ?? (comp?.reviewRequired
2588
2744
  ? 'We’re checking your details. You’ll be able to continue once that’s done.'
2589
2745
  : 'You need to accept this before you can continue.') })] }));
2590
2746
  }
2591
2747
  if (endingKind === 'unblock') {
2592
- 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." })] }));
2748
+ return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [styles.thankYou, endingHold, { alignItems: endingAlign }], children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [styles.thankYouTitle, fontStyle, themeTextPrimary, endingHeadFS], children: "Recorded" }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [styles.thankYouSub, fontStyle, themeTextSecondary, endingSubFS], children: "You can carry on." })] }));
2593
2749
  }
2594
- 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.' }) }));
2750
+ return ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: [styles.thankYou, endingHold, { alignItems: endingAlign }], children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [styles.thankYouSub, fontStyle, themeTextSecondary, endingSubFS], children: endingKind === 'navigate' ? 'Taking you there…' : 'Closed.' }) }));
2751
+ })() : (showThankYou && (reply === 'pending' || reply === 'failed' || reply === 'later')) ? (() => {
2752
+ // WAITING FOR THE APP, or what came of it — the author's words for
2753
+ // each state (appReply), in the confirmation's own header style.
2754
+ const ar = (0, survey_spec_1.appReply)(survey);
2755
+ const words = reply === 'pending' ? ar.pending : reply === 'failed' ? ar.failed : ar.later;
2756
+ return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [styles.thankYou, endingHold, { alignItems: endingAlign }], accessibilityLiveRegion: reply === 'failed' ? 'assertive' : 'polite', children: [reply === 'pending' && (0, jsx_runtime_1.jsx)(react_native_1.ActivityIndicator, { color: primaryColor, style: { marginBottom: 12 } }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [styles.thankYouTitle, dirText, fontStyle, themeTextPrimary, endingHeadFS], children: (0, interpolate_1.interpolatePlain)(words, evalCtx) })] }));
2595
2757
  })() : showThankYou ? (() => {
2596
2758
  // The thank-you message can be rich HTML from the web builder.
2597
2759
  // Pull out an embedded <img> (often a data: URI icon) and render
2598
2760
  // it as a real Image — stripHtml would otherwise drop it — then
2599
2761
  // show the remaining text. When an image is present it stands in
2600
2762
  // for the default ✓ glyph.
2763
+ // THE CONFIRMATION'S COMPONENTS, under its headline and body — the
2764
+ // schema's list, derived for anything made before v1.2 (a code
2765
+ // offer gets its code). Same rule the web renderer asks.
2766
+ const endParts = (0, survey_spec_1.confirmationQuestions)(survey);
2767
+ const endReward = (0, survey_spec_1.rewardToShow)(survey);
2768
+ const hasCode = !!endReward && endReward.type === 'code'
2769
+ && endParts.some((q) => String(q.type).toLowerCase() === 'reward');
2601
2770
  const rawThankYou = (0, interpolate_1.interpolatePlain)((0, translationLookup_1.surveyText)(survey, currentLanguage, 'thankYouMessage', survey.settings?.thankYouMessage)
2602
- ?? 'Thank you for your feedback!', evalCtx);
2771
+ ?? (0, survey_spec_1.confirmationDefaults)(survey, hasCode).title, evalCtx);
2772
+ const partViews = endParts.map((q) => {
2773
+ const t = String(q.type).toLowerCase();
2774
+ if (t === 'reward') {
2775
+ if (!hasCode || !endReward)
2776
+ return null;
2777
+ const exp = endReward.expiresAt ? new Date(endReward.expiresAt) : null;
2778
+ return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: { marginTop: 14, alignItems: endingAlign }, children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { selectable: true, style: [styles.rewardCode, fontStyle, themeTextPrimary], children: endReward.value }), !!endReward.whereValid && ((0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [styles.thankYouSub, dirText, fontStyle, themeTextSecondary, endingSubFS], children: endReward.whereValid })), !!exp && ((0, jsx_runtime_1.jsxs)(react_native_1.Text, { style: [styles.thankYouSub, dirText, fontStyle, themeTextSecondary, endingSubFS], children: ["Expires ", exp.toLocaleString()] }))] }, q.id));
2779
+ }
2780
+ const words = stripHtml((0, interpolate_1.interpolatePlain)(String(q.html ?? q.text ?? ''), evalCtx));
2781
+ return words ? ((0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [styles.thankYouSub, dirText, fontStyle, themeTextPrimary, endingSubFS, { marginTop: 12 }], children: words }, q.id)) : null;
2782
+ });
2603
2783
  const imgSrc = firstImageSrc(rawThankYou);
2604
2784
  const thankYouText = stripHtml(rawThankYou);
2605
- return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.thankYou, children: [imgSrc ? ((0, jsx_runtime_1.jsx)(react_native_1.Image, { source: { uri: imgSrc }, style: styles.thankYouImage, resizeMode: "contain" })) : ((0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [styles.thankYouEmoji, { color: primaryColor }], children: "\u2713" })), thankYouText ? ((0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [styles.thankYouTitle, dirText, fontStyle, themeTextPrimary, thankYouFS], children: thankYouText })) : null] }));
2785
+ // THE WORDS, AS ONE BLOCK — so a layout can put them beside the
2786
+ // picture, after it, or on top of it without three copies. The ✓
2787
+ // stands in only when NEITHER picture is set; it used to stand in
2788
+ // for the first alone, so a badge added to a card that already had
2789
+ // a photograph drew the photograph and swallowed the badge.
2790
+ const endWords = ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [endingIcon || ((endingImgSrc || imgSrc || hasCode) ? null : ((0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [styles.thankYouEmoji, { color: primaryColor }], children: "\u2713" }))), thankYouText ? ((0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [styles.thankYouTitle, dirText, fontStyle, themeTextPrimary, endingHeadFS], children: thankYouText })) : null, (() => {
2791
+ const sub = (0, survey_spec_1.confirmationSubtext)(survey, (0, survey_spec_1.confirmationDefaults)(survey, hasCode).body);
2792
+ return sub ? ((0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [styles.thankYouSub, dirText, fontStyle, themeTextSecondary, endingSubFS], children: (0, interpolate_1.interpolatePlain)(sub, evalCtx) })) : null;
2793
+ })(), partViews] }));
2794
+ // THE CARD'S OWN PADDING, undone on the picture's side only. The
2795
+ // panel's 48 used to sit INSIDE the card's, so the same picture at
2796
+ // the same Height drew smaller here than on the card.
2797
+ const endPad = 0;
2798
+ if (endingLayout === 'beside') {
2799
+ return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [styles.thankYou, endingHold, {
2800
+ alignItems: 'stretch', flexDirection: 'row', gap: 12,
2801
+ }], children: [(0, jsx_runtime_1.jsxs)(react_native_1.View, { style: {
2802
+ position: 'relative', overflow: 'hidden', alignSelf: endingSelf,
2803
+ width: cI.size, height: cI.size, borderRadius: cI.radius,
2804
+ }, children: [endingPicture({}), endingScrim] }), (0, jsx_runtime_1.jsx)(react_native_1.View, { style: { flex: 1, minWidth: 0, alignItems: endingAlign, justifyContent: 'center' }, children: endWords })] }));
2805
+ }
2806
+ if (endingLayout === 'half') {
2807
+ const vert = cI.side === 'top';
2808
+ // Negative margins undo the panel's own padding on the picture's
2809
+ // side only — the words keep theirs, which is what "bleeds to
2810
+ // the edges" means on the card too.
2811
+ return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [styles.thankYou, endingHold, {
2812
+ alignItems: 'stretch',
2813
+ flexDirection: vert ? 'column' : (cI.side === 'right' ? 'row-reverse' : 'row'),
2814
+ gap: vert ? 0 : 14,
2815
+ }], children: [(0, jsx_runtime_1.jsxs)(react_native_1.View, { style: {
2816
+ position: 'relative', overflow: 'hidden',
2817
+ ...(vert
2818
+ ? { marginTop: -endPad, marginLeft: -endPad, marginRight: -endPad,
2819
+ marginBottom: 16, height: cI.height }
2820
+ : { marginTop: -endPad, marginBottom: -endPad,
2821
+ [cI.side === 'right' ? 'marginRight' : 'marginLeft']: -endPad,
2822
+ width: `${cI.split}%` }),
2823
+ }, children: [endingPicture({}), fadeSteps(vert ? 'bottom' : (cI.side === 'right' ? 'left' : 'right'), cI.fade), endingScrim] }), (0, jsx_runtime_1.jsx)(react_native_1.View, { style: { flex: 1, minWidth: 0, alignItems: endingAlign, justifyContent: 'center' }, children: endWords })] }));
2824
+ }
2825
+ if (endingLayout === 'background') {
2826
+ return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [styles.thankYou, endingHold, { alignItems: endingAlign }], children: [(0, jsx_runtime_1.jsxs)(react_native_1.View, { pointerEvents: "none", style: { position: 'absolute', top: 0, right: 0, bottom: 0, left: 0, overflow: 'hidden' }, children: [endingPicture({}), endingScrim] }), endWords] }));
2827
+ }
2828
+ return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [styles.thankYou, endingHold, { alignItems: endingAlign }], children: [endingLayout === 'above' ? endingImage : null, (!endingImgSrc && imgSrc) ? ((0, jsx_runtime_1.jsx)(react_native_1.Image, { source: { uri: imgSrc }, style: styles.thankYouImage, resizeMode: "contain" })) : null, endWords, endingLayout === 'below' ? endingImage : null] }));
2606
2829
  })() : ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: halfSide
2607
2830
  ? { flexDirection: imgD.side === 'right' ? 'row-reverse' : 'row',
2608
2831
  alignItems: 'stretch' }
@@ -2625,7 +2848,7 @@ function SurveyModal({ survey, visible, onSubmit, onDismiss }) {
2625
2848
  ? { marginRight: 12 - halfPadR }
2626
2849
  : { marginLeft: 12 - halfPadL }),
2627
2850
  }
2628
- : undefined, children: [langSwitcher, imgLayout === 'above' ? (0, jsx_runtime_1.jsx)(react_native_1.View, { style: padBand, children: cardImageBand }) : null, (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [
2851
+ : undefined, children: [langSwitcher, imgLayout === 'above' && (besideImage || notifyBeside) ? (0, jsx_runtime_1.jsx)(react_native_1.View, { style: bandPad, children: cardImageBand }) : null, (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [
2629
2852
  styles.header,
2630
2853
  padHeader,
2631
2854
  // No ✕ reservation here in either direction — the title carries
@@ -2644,7 +2867,7 @@ function SurveyModal({ survey, visible, onSubmit, onDismiss }) {
2644
2867
  : survey.branding?.logoAlign === 'right'
2645
2868
  ? { justifyContent: 'flex-end' }
2646
2869
  : null),
2647
- ], children: [survey.branding?.showLogo !== false && survey.branding?.logo ? ((0, jsx_runtime_1.jsx)(react_native_1.Image, { source: { uri: survey.branding.logo }, style: styles.brandLogo, resizeMode: "contain", accessible: !!survey.branding.brandName, accessibilityLabel: survey.branding.brandName })) : null, survey.branding?.brandName ? ((0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [styles.brandName, dirText, fontStyle, themeTextSecondary], children: survey.branding.brandName })) : null] })), besideImage ? ((0, jsx_runtime_1.jsx)(react_native_1.Image, { source: { uri: cardImageQ?.imageUrl }, accessibilityLabel: cardImageQ?.alt || undefined, accessible: !!cardImageQ?.alt, style: {
2870
+ ], children: [survey.branding?.showLogo !== false && survey.branding?.logo ? ((0, jsx_runtime_1.jsx)(react_native_1.Image, { source: { uri: survey.branding.logo }, style: styles.brandLogo, resizeMode: "contain", accessible: !!survey.branding.brandName, accessibilityLabel: survey.branding.brandName })) : null, survey.branding?.brandName ? ((0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [styles.brandName, dirText, fontStyle, themeTextSecondary], children: survey.branding.brandName })) : null] })), imgLayout === 'above' && !(besideImage || notifyBeside) ? cardImageBand : null, besideImage ? ((0, jsx_runtime_1.jsx)(react_native_1.Image, { source: { uri: cardImageQ?.imageUrl }, accessibilityLabel: cardImageQ?.alt || undefined, accessible: !!cardImageQ?.alt, style: {
2648
2871
  width: contract.imageSize,
2649
2872
  height: contract.imageSize,
2650
2873
  borderRadius: contract.imageRadius,
@@ -2656,9 +2879,7 @@ function SurveyModal({ survey, visible, onSubmit, onDismiss }) {
2656
2879
  : imgD.align === 'end' ? 'flex-end' : 'center',
2657
2880
  }, resizeMode: imgD.fit })) : null, (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: (besideImage || notifyBeside) ? { flex: 1, minWidth: 0 } : undefined, children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [styles.title, dirText, fontStyle, themeTextPrimary, titleFS, notifyTitleFS,
2658
2881
  closeReserve,
2659
- survey.titleAlign
2660
- ? { textAlign: survey.titleAlign }
2661
- : null], children: (0, interpolate_1.interpolatePlain)(
2882
+ { textAlign: cardTitleAlign }], children: (0, interpolate_1.interpolatePlain)(
2662
2883
  // Title is plain text; strip any stray rich-text markup +
2663
2884
  // decode &nbsp; so it never shows literal HTML on native.
2664
2885
  String((0, translationLookup_1.surveyText)(survey, currentLanguage, 'title', survey.title) || '')
@@ -2672,6 +2893,7 @@ function SurveyModal({ survey, visible, onSubmit, onDismiss }) {
2672
2893
  const descRaw = (0, translationLookup_1.surveyText)(survey, currentLanguage, 'description', survey.description);
2673
2894
  const desc = String(descRaw || '').replace(/<[^>]*>/g, '').replace(/&nbsp;/gi, ' ').trim();
2674
2895
  return desc ? ((0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [styles.description, dirText, fontStyle, themeTextSecondary, descFS, notifyDescFS,
2896
+ { textAlign: cardDescAlign },
2675
2897
  // Takes the width the button leaves, and no more.
2676
2898
  notifyBeside && { flex: 1, minWidth: 0, marginTop: 2 }], children: (0, interpolate_1.interpolatePlain)(desc, evalCtx) })) : null;
2677
2899
  })(), notifyActionQ ? ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: { flexShrink: 0, marginLeft: 12 }, children: renderQuestion(notifyActionQ) })) : null] })] }), deadlineEl, (() => {
@@ -2681,7 +2903,7 @@ function SurveyModal({ survey, visible, onSubmit, onDismiss }) {
2681
2903
  { fontSize: Math.round((_num(_authoredStyle.fontSizeDescription) ?? 14) * 0.85),
2682
2904
  marginTop: 6 },
2683
2905
  ], children: (0, interpolate_1.interpolatePlain)(terms, evalCtx) })) : null;
2684
- })(), showProgressTop && progressEl] }), imgLayout === 'below' ? (0, jsx_runtime_1.jsx)(react_native_1.View, { style: padBand, children: cardImageBand }) : null, (0, jsx_runtime_1.jsx)(BodyContainer, { style: [styles.body, padBody, fsContent], showsVerticalScrollIndicator: false, children: currentQuestions.filter((q) => {
2906
+ })(), showProgressTop && progressEl] }), imgLayout === 'below' ? (0, jsx_runtime_1.jsx)(react_native_1.View, { style: bandPad, children: cardImageBand }) : null, (0, jsx_runtime_1.jsx)(BodyContainer, { style: [styles.body, padBody, fsContent], showsVerticalScrollIndicator: false, children: currentQuestions.filter((q) => {
2685
2907
  // The card composes the picture itself — beside the text, in
2686
2908
  // a band, or behind everything. Leaving it here too would
2687
2909
  // render it twice, the second time full-bleed.
@@ -2727,7 +2949,7 @@ function SurveyModal({ survey, visible, onSubmit, onDismiss }) {
2727
2949
  // have. Web spends 12px above the action and nothing
2728
2950
  // below it (`.cxp-notify-action-below`); this matches.
2729
2951
  isNotifyBanner && { marginTop: 12, marginBottom: 0 },
2730
- ], children: [!(0, survey_spec_1.showsQuestionLabel)(q) ? null : ((0, jsx_runtime_1.jsxs)(react_native_1.Text, { style: [styles.questionTitle, dirText, fontStyle, themeTextPrimary, qTitleFS],
2952
+ ], children: [!(0, survey_spec_1.showsQuestionLabel)(q) ? null : ((0, jsx_runtime_1.jsxs)(react_native_1.Text, { style: [styles.questionTitle, dirText, fontStyle, themeTextPrimary, qTitleFS, { textAlign: cardQuestionAlign }],
2731
2953
  // The asterisk below is decoration: a screen reader
2732
2954
  // reads it as "star", or skips it, so a required
2733
2955
  // question sounded exactly like an optional one until
@@ -2736,7 +2958,7 @@ function SurveyModal({ survey, visible, onSubmit, onDismiss }) {
2736
2958
  if (isImplied)
2737
2959
  return null;
2738
2960
  const qDesc = (0, translationLookup_1.questionText)(survey, currentLanguage, rowKey, 'description', q.description);
2739
- return qDesc ? ((0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [styles.questionDesc, dirText, fontStyle, themeTextMuted, qDescFS], children: (0, interpolate_1.interpolatePlain)(qDesc, evalCtx) })) : null;
2961
+ return qDesc ? ((0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [styles.questionDesc, dirText, fontStyle, themeTextMuted, qDescFS, { textAlign: cardQuestionAlign }], children: (0, interpolate_1.interpolatePlain)(qDesc, evalCtx) })) : null;
2740
2962
  })(), renderQuestion(q), errors[rowKey] ? ((0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [styles.errorText, dirText, fontStyle],
2741
2963
  // Announced when it appears, and reachable afterwards:
2742
2964
  // a message that is only spoken once leaves someone who
@@ -2766,7 +2988,7 @@ function SurveyModal({ survey, visible, onSubmit, onDismiss }) {
2766
2988
  // Right-align parity with web: Back hugs the left edge while
2767
2989
  // Next/Submit sits on the right (web uses margin-right:auto).
2768
2990
  buttonAlign === 'right' && { marginRight: 'auto' },
2769
- ], 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 : (() => {
2991
+ ], 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 || !(0, survey_spec_1.drawsOwnSubmit)(survey.interactionType) ? null : (() => {
2770
2992
  // Reflect the actual outcome of pressing Next: if a
2771
2993
  // branch resolves to __end__ we're submitting even
2772
2994
  // when we're not on the last page. Cheap to evaluate
@@ -3139,7 +3361,12 @@ const makeStyles = (c) => react_native_1.StyleSheet.create({
3139
3361
  fontWeight: '700',
3140
3362
  },
3141
3363
  thankYou: {
3142
- padding: 48,
3364
+ // NO PADDING OF ITS OWN. The card already pads its content; a second inset
3365
+ // here made the confirmation's box narrower and shorter than the card's,
3366
+ // so the same picture at the same Width and Height drew smaller on the
3367
+ // second screen with no control to explain it. The components space
3368
+ // themselves with their own margins, exactly as they do on the card.
3369
+ paddingVertical: 8,
3143
3370
  alignItems: 'center',
3144
3371
  justifyContent: 'center',
3145
3372
  },
@@ -3149,8 +3376,9 @@ const makeStyles = (c) => react_native_1.StyleSheet.create({
3149
3376
  marginBottom: 16,
3150
3377
  },
3151
3378
  thankYouImage: {
3152
- width: 72,
3153
- height: 72,
3379
+ // FULL WIDTH, and the height comes from the contract at the render site.
3380
+ // 72x72 was a glyph's footprint, not a picture's.
3381
+ width: '100%',
3154
3382
  marginBottom: 16,
3155
3383
  },
3156
3384
  thankYouTitle: {
@@ -3161,6 +3389,15 @@ const makeStyles = (c) => react_native_1.StyleSheet.create({
3161
3389
  lineHeight: 26,
3162
3390
  },
3163
3391
  thankYouSub: {
3392
+ // A COLOUR OF ITS OWN, because the override beside it is LIGHT-ONLY.
3393
+ //
3394
+ // themeTextSecondary is `isLight ? {...} : undefined`, on the assumption
3395
+ // that every style it decorates already carries the DARK colour. This one
3396
+ // carried none, so on a dark card it fell through to React Native's
3397
+ // default Text colour — black — and the confirmation's sub-line was dark
3398
+ // text on a dark background. The title above it was fine because it had
3399
+ // `c.headingColor`; that difference is the whole bug.
3400
+ color: c.textSecondary,
3164
3401
  fontSize: 14,
3165
3402
  textAlign: 'center',
3166
3403
  lineHeight: 20,
@@ -3170,6 +3407,8 @@ const makeStyles = (c) => react_native_1.StyleSheet.create({
3170
3407
  // button here because RN's clipboard is an optional peer, and a code you can
3171
3408
  // select is better than a button that may not exist.
3172
3409
  rewardCode: {
3410
+ // Same fault, and worse: this is the discount code itself.
3411
+ color: c.textPrimary,
3173
3412
  fontSize: 22,
3174
3413
  fontWeight: '700',
3175
3414
  letterSpacing: 1.5,