@cxpinsight/react-native 0.17.4 → 0.17.7
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/lib/CXPinsight.d.ts.map +1 -1
- package/lib/CXPinsight.js +21 -1
- package/lib/CXPinsight.js.map +1 -1
- package/lib/CXPinsightProvider.d.ts.map +1 -1
- package/lib/CXPinsightProvider.js +23 -0
- package/lib/CXPinsightProvider.js.map +1 -1
- package/lib/core/autoTapCapture.d.ts.map +1 -1
- package/lib/core/autoTapCapture.js +4 -37
- package/lib/core/autoTapCapture.js.map +1 -1
- package/lib/core/mobileRecorder.d.ts +15 -0
- package/lib/core/mobileRecorder.d.ts.map +1 -1
- package/lib/core/mobileRecorder.js +18 -0
- package/lib/core/mobileRecorder.js.map +1 -1
- package/lib/core/triggerEngine.d.ts +13 -0
- package/lib/core/triggerEngine.d.ts.map +1 -1
- package/lib/core/triggerEngine.js +32 -1
- package/lib/core/triggerEngine.js.map +1 -1
- package/lib/instrumented/interactionObserver.d.ts.map +1 -1
- package/lib/instrumented/interactionObserver.js +43 -0
- package/lib/instrumented/interactionObserver.js.map +1 -1
- package/lib/survey/SurveyModal.d.ts +31 -0
- package/lib/survey/SurveyModal.d.ts.map +1 -1
- package/lib/survey/SurveyModal.js +244 -36
- package/lib/survey/SurveyModal.js.map +1 -1
- package/lib/survey/styleContract.d.ts +5 -0
- package/lib/survey/styleContract.d.ts.map +1 -1
- package/lib/survey/styleContract.js +2 -0
- package/lib/survey/styleContract.js.map +1 -1
- package/lib/types.d.ts +26 -0
- package/lib/types.d.ts.map +1 -1
- package/lib/types.js.map +1 -1
- package/package.json +2 -2
|
@@ -33,6 +33,9 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.stylePlatformSlice = stylePlatformSlice;
|
|
37
|
+
exports.topInsetForHeight = topInsetForHeight;
|
|
38
|
+
exports.resolvePopupTop = resolvePopupTop;
|
|
36
39
|
exports.SurveyModal = SurveyModal;
|
|
37
40
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
38
41
|
const react_1 = __importStar(require("react"));
|
|
@@ -94,6 +97,54 @@ function stripHtml(input) {
|
|
|
94
97
|
.replace(/\n{3,}/g, '\n\n')
|
|
95
98
|
.trim();
|
|
96
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* `branding.style` flattened for THIS platform: shared keys first, the `mobile`
|
|
102
|
+
* slice on top. Mirrors how `settings.display` is sliced — the two are written
|
|
103
|
+
* by the same studio controls and have to be read the same way. `web` is
|
|
104
|
+
* dropped rather than merged; it describes a different form factor.
|
|
105
|
+
*/
|
|
106
|
+
function stylePlatformSlice(style) {
|
|
107
|
+
if (!style || typeof style !== 'object')
|
|
108
|
+
return {};
|
|
109
|
+
const out = {};
|
|
110
|
+
for (const [k, v] of Object.entries(style)) {
|
|
111
|
+
if (k === 'web' || k === 'mobile' || k === 'link')
|
|
112
|
+
continue;
|
|
113
|
+
out[k] = v;
|
|
114
|
+
}
|
|
115
|
+
const m = style.mobile;
|
|
116
|
+
if (m && typeof m === 'object')
|
|
117
|
+
Object.assign(out, m);
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* iOS top safe-area inset, derived from the screen height.
|
|
122
|
+
*
|
|
123
|
+
* This package declares no dependencies, so react-native-safe-area-context is
|
|
124
|
+
* not available and the height is the only signal. 852/932pt are the Dynamic
|
|
125
|
+
* Island sizes (59pt), 812-896 the notch generation (44pt), the rest pre-notch.
|
|
126
|
+
*/
|
|
127
|
+
function topInsetForHeight(screenH) {
|
|
128
|
+
return screenH >= 852 ? 59 : screenH >= 812 ? 44 : 20;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Resolve an authored `y` (percent of screen, marking the card's CENTRE — the
|
|
132
|
+
* web SDK's `top:y%` + `translate(-50%,-50%)`) to a paddingTop in points, with
|
|
133
|
+
* the whole card kept inside the safe area.
|
|
134
|
+
*
|
|
135
|
+
* Returns null when there is no authored coordinate, so the caller falls back
|
|
136
|
+
* to flex placement.
|
|
137
|
+
*/
|
|
138
|
+
function resolvePopupTop(opts) {
|
|
139
|
+
const { y, screenH, cardH, topInset, bottomInset, gutter } = opts;
|
|
140
|
+
if (!Number.isFinite(y))
|
|
141
|
+
return null;
|
|
142
|
+
const minTop = topInset + gutter;
|
|
143
|
+
const maxTop = screenH - bottomInset - gutter - cardH;
|
|
144
|
+
const wanted = (y / 100) * screenH - cardH / 2;
|
|
145
|
+
// A card taller than the safe band pins to the top rather than going negative.
|
|
146
|
+
return Math.max(minTop, maxTop > minTop ? Math.min(wanted, maxTop) : minTop);
|
|
147
|
+
}
|
|
97
148
|
function SurveyModal({ survey, visible, onSubmit, onDismiss }) {
|
|
98
149
|
const [currentPage, setCurrentPage] = (0, react_1.useState)(0);
|
|
99
150
|
const [responses, setResponses] = (0, react_1.useState)({});
|
|
@@ -1249,7 +1300,12 @@ function SurveyModal({ survey, visible, onSubmit, onDismiss }) {
|
|
|
1249
1300
|
// radius set once applies in both themes, exactly as it does on web.
|
|
1250
1301
|
surveyOverride: {
|
|
1251
1302
|
...(0, styleContract_1.fromAuthoredPalette)(surveyPaletteOverride),
|
|
1252
|
-
|
|
1303
|
+
// PER PLATFORM, like `display` below. branding.style carries web/mobile
|
|
1304
|
+
// slices — the studio writes a size dragged on the phone frame into
|
|
1305
|
+
// style.mobile — and this spread was flat, so mobile silently used the
|
|
1306
|
+
// SHARED values. A notification authored with style.mobile.imageSize 53
|
|
1307
|
+
// and cardPadding 17 rendered at the root's 101 and 36.
|
|
1308
|
+
...stylePlatformSlice(survey.branding?.style),
|
|
1253
1309
|
},
|
|
1254
1310
|
initOverride: initPaletteOverride,
|
|
1255
1311
|
}),
|
|
@@ -1327,6 +1383,36 @@ function SurveyModal({ survey, visible, onSubmit, onDismiss }) {
|
|
|
1327
1383
|
// 420px of horizontal room a handset does not have, and the sheet is the
|
|
1328
1384
|
// native equivalent. A drawer with nothing set anchors to the BOTTOM (the
|
|
1329
1385
|
// platform convention); anything else centres, as before.
|
|
1386
|
+
// A NOTIFICATION IS NOT A FORM.
|
|
1387
|
+
//
|
|
1388
|
+
// Web composes one: `.cxp-notify-row` puts a small thumbnail beside the title
|
|
1389
|
+
// and description, hides the implied question that carries the action, and
|
|
1390
|
+
// puts that action full-width underneath. Mobile had none of it — `notify`
|
|
1391
|
+
// appeared nowhere in this file — so the same interaction rendered as a plain
|
|
1392
|
+
// question list: the avatar as a full-bleed image question, and the implied
|
|
1393
|
+
// question's `text` as a visible prompt above it (the reported stray "tte").
|
|
1394
|
+
const interactionKind = String(survey.interactionType || '').toLowerCase();
|
|
1395
|
+
// BOTH shapes: the wire payload carries `questions`, and the provider sets
|
|
1396
|
+
// `rawQuestions` only when it re-paginates. Reading one alone missed the
|
|
1397
|
+
// notification entirely — the composition below silently never engaged.
|
|
1398
|
+
const allQuestions = (survey.rawQuestions
|
|
1399
|
+
|| survey.questions
|
|
1400
|
+
|| []);
|
|
1401
|
+
const notifyImageQ = allQuestions.find((q) => String(q?.id ?? '') === 'notify_image' && String(q?.type ?? '').toLowerCase() === 'image');
|
|
1402
|
+
// Keyed off the composed question, not the type alone: an Ask that happens to
|
|
1403
|
+
// carry an image question must keep rendering it as a question.
|
|
1404
|
+
const isNotifyCard = interactionKind === 'notify' && !!notifyImageQ?.imageUrl;
|
|
1405
|
+
// A NOTIFICATION'S TYPE SITS ONE SIZE DOWN from a survey's — web says so
|
|
1406
|
+
// explicitly (notifyFsHeading/notifyFsDesc, defaulting to 15/13), and mobile
|
|
1407
|
+
// had no equivalent: it used the survey title and a hardcoded 14 description,
|
|
1408
|
+
// so an unstyled notification rendered at full survey type here and banner
|
|
1409
|
+
// type there. An authored size still wins, on both.
|
|
1410
|
+
const _authoredStyle = stylePlatformSlice(survey.branding?.style);
|
|
1411
|
+
const _num = (v) => (typeof v === 'number' && v > 0 ? v : undefined);
|
|
1412
|
+
const notifyTitleFS = isNotifyCard
|
|
1413
|
+
? { fontSize: _num(_authoredStyle.fontSizeHeading) ?? 15 } : null;
|
|
1414
|
+
const notifyDescFS = isNotifyCard
|
|
1415
|
+
? { fontSize: _num(_authoredStyle.fontSizeDescription) ?? 13, lineHeight: 18 } : null;
|
|
1330
1416
|
const rawPos = display?.position;
|
|
1331
1417
|
const placement = rawPos === 'top' ? 'top'
|
|
1332
1418
|
: rawPos === 'bottom' ? 'bottom'
|
|
@@ -1334,12 +1420,32 @@ function SurveyModal({ survey, visible, onSubmit, onDismiss }) {
|
|
|
1334
1420
|
: rawPos === 'center' ? 'center'
|
|
1335
1421
|
: isDrawer ? 'bottom' : 'center';
|
|
1336
1422
|
const isSheet = isDrawer && (placement === 'top' || placement === 'bottom');
|
|
1337
|
-
// Popup coordinate.
|
|
1338
|
-
// —
|
|
1339
|
-
//
|
|
1340
|
-
//
|
|
1423
|
+
// Popup coordinate. `y` is a PERCENTAGE OF THE SCREEN and marks the CARD'S
|
|
1424
|
+
// CENTRE — the web SDK's `top:y%` + `translate(-50%,-50%)`. This used to be
|
|
1425
|
+
// bucketed into three placements (<33 top, >67 bottom, else centre) on the
|
|
1426
|
+
// belief that RN could not express it: translateY takes points, not
|
|
1427
|
+
// percentages, and percentage margins resolve against the parent WIDTH.
|
|
1428
|
+
//
|
|
1429
|
+
// Both are true and neither is binding. The screen dimensions are known here,
|
|
1430
|
+
// so the percentage resolves to POINTS and rides in as paddingTop on a
|
|
1431
|
+
// flex-start overlay — no absolute positioning, so KeyboardAvoidingView keeps
|
|
1432
|
+
// working. The card's own height comes from onLayout; until that first pass
|
|
1433
|
+
// the authored height (or a reasonable default) stands in, and the safe-area
|
|
1434
|
+
// clamp below usually decides the result anyway.
|
|
1435
|
+
//
|
|
1436
|
+
// The bucketing was a real mismatch, not a rounding one: an author who
|
|
1437
|
+
// dragged the card to y=17 in the studio got it flush against the top of the
|
|
1438
|
+
// device — 20dp instead of the ~145dp they placed it at on an 852dp screen.
|
|
1439
|
+
//
|
|
1440
|
+
// x stays ignored: the studio disables horizontal dragging on the mobile
|
|
1441
|
+
// frame (`moveX: platformForFrame(frame) !== 'mobile'`), so a phone centres.
|
|
1341
1442
|
const popupY = !isDrawer && !isFullscreen && display?.y != null ? parseFloat(String(display.y)) : NaN;
|
|
1342
|
-
const
|
|
1443
|
+
const hasPopupY = !isNaN(popupY);
|
|
1444
|
+
const effPlacement = hasPopupY ? 'top' : placement;
|
|
1445
|
+
// Measured card height; the authored height is the first-frame stand-in.
|
|
1446
|
+
const [measuredCardH, setMeasuredCardH] = (0, react_1.useState)(0);
|
|
1447
|
+
const authoredH = Number.parseFloat(String(display?.height ?? ''));
|
|
1448
|
+
const estCardH = measuredCardH || (Number.isFinite(authoredH) && authoredH > 0 ? authoredH : 220);
|
|
1343
1449
|
const overlayPlacement = {
|
|
1344
1450
|
justifyContent: effPlacement === 'top' ? 'flex-start'
|
|
1345
1451
|
: effPlacement === 'bottom' ? 'flex-end'
|
|
@@ -1353,7 +1459,15 @@ function SurveyModal({ survey, visible, onSubmit, onDismiss }) {
|
|
|
1353
1459
|
// a bottom sheet and a centred popup both rise, and fade has no direction.
|
|
1354
1460
|
// So the Modal animation is handed over ONLY for a top sheet, and the card
|
|
1355
1461
|
// animates itself down from above instead.
|
|
1356
|
-
|
|
1462
|
+
//
|
|
1463
|
+
// A POPUP resolved to the top has exactly the same problem and was not
|
|
1464
|
+
// covered: the gate asked for `isSheet`, which a popup never is. So a
|
|
1465
|
+
// notification authored near the top of the screen flew up from the BOTTOM
|
|
1466
|
+
// bezel, crossed the whole display, and settled backwards into place — the
|
|
1467
|
+
// reported "the animation is wrong". Keyed off the RESOLVED placement, which
|
|
1468
|
+
// is what the card actually uses.
|
|
1469
|
+
const needsTopSlide = animationType === 'slide'
|
|
1470
|
+
&& ((isSheet && placement === 'top') || (!isDrawer && !isFullscreen && effPlacement === 'top'));
|
|
1357
1471
|
const modalAnimation = needsTopSlide ? 'none' : animationType;
|
|
1358
1472
|
const enterAnim = (0, react_1.useRef)(new react_native_1.Animated.Value(1)).current;
|
|
1359
1473
|
(0, react_1.useEffect)(() => {
|
|
@@ -1388,6 +1502,21 @@ function SurveyModal({ survey, visible, onSubmit, onDismiss }) {
|
|
|
1388
1502
|
onStartShouldSetPanResponder: () => false,
|
|
1389
1503
|
// Claim the gesture only once it is clearly a drag, so a tap on the action
|
|
1390
1504
|
// button still presses the button.
|
|
1505
|
+
//
|
|
1506
|
+
// CAPTURE, not bubble. The card's content sits in a ScrollView, and a
|
|
1507
|
+
// ScrollView claims a vertical drag first — so the flick that is supposed
|
|
1508
|
+
// to dismiss a notification was being consumed as a scroll of content that
|
|
1509
|
+
// does not even overflow, and the banner could not be swiped away at all.
|
|
1510
|
+
// The 8px threshold still protects the button: a tap never reaches here.
|
|
1511
|
+
onMoveShouldSetPanResponderCapture: (_e, g) => {
|
|
1512
|
+
const claim = swipeable && (Math.abs(g.dx) > 8 || Math.abs(g.dy) > 8);
|
|
1513
|
+
// TEMP-DIAG
|
|
1514
|
+
if (__DEV__ && (Math.abs(g.dx) > 8 || Math.abs(g.dy) > 8)) {
|
|
1515
|
+
// eslint-disable-next-line no-console
|
|
1516
|
+
console.log('[CXP-SWIPE]', JSON.stringify({ swipeable, dx: Math.round(g.dx), dy: Math.round(g.dy), claim }));
|
|
1517
|
+
}
|
|
1518
|
+
return claim;
|
|
1519
|
+
},
|
|
1391
1520
|
onMoveShouldSetPanResponder: (_e, g) => swipeable && (Math.abs(g.dx) > 8 || Math.abs(g.dy) > 8),
|
|
1392
1521
|
onPanResponderMove: (_e, g) => {
|
|
1393
1522
|
const horizontal = Math.abs(g.dx) > Math.abs(g.dy);
|
|
@@ -1442,10 +1571,16 @@ function SurveyModal({ survey, visible, onSubmit, onDismiss }) {
|
|
|
1442
1571
|
}, dismissMs);
|
|
1443
1572
|
return () => clearTimeout(t);
|
|
1444
1573
|
}, [visible, dismissMs]);
|
|
1574
|
+
// COMPOSED, not stacked. The swipe transform used to be a separate entry in
|
|
1575
|
+
// the card's style array, applied after this one — and a later `transform`
|
|
1576
|
+
// REPLACES an earlier one rather than merging. That was harmless while only
|
|
1577
|
+
// sheets animated themselves, but a notification is both top-placed and
|
|
1578
|
+
// swipeable, so the entrance translate would have been silently dropped.
|
|
1445
1579
|
const enterStyle = needsTopSlide
|
|
1446
1580
|
? {
|
|
1447
1581
|
opacity: enterAnim,
|
|
1448
|
-
transform: [
|
|
1582
|
+
transform: [
|
|
1583
|
+
{
|
|
1449
1584
|
translateY: enterAnim.interpolate({
|
|
1450
1585
|
inputRange: [0, 1],
|
|
1451
1586
|
// Its own height is not known before layout, so it travels a screen
|
|
@@ -1453,7 +1588,9 @@ function SurveyModal({ survey, visible, onSubmit, onDismiss }) {
|
|
|
1453
1588
|
// enough that the easing still lands crisply.
|
|
1454
1589
|
outputRange: [-SCREEN_HEIGHT / 3, 0],
|
|
1455
1590
|
}),
|
|
1456
|
-
}
|
|
1591
|
+
},
|
|
1592
|
+
...(swipeable ? swipeXY.getTranslateTransform() : []),
|
|
1593
|
+
],
|
|
1457
1594
|
}
|
|
1458
1595
|
: null;
|
|
1459
1596
|
const sheetRadius = cornerRadius !== undefined ? cornerRadius : 16;
|
|
@@ -1468,13 +1605,28 @@ function SurveyModal({ survey, visible, onSubmit, onDismiss }) {
|
|
|
1468
1605
|
// Safe areas. Deliberately built from react-native primitives rather than
|
|
1469
1606
|
// react-native-safe-area-context: this package declares no dependencies, and
|
|
1470
1607
|
// adding one would force every host app to install it.
|
|
1471
|
-
|
|
1608
|
+
//
|
|
1609
|
+
// 44 was the notched-iPhone value and was applied to every iOS device, so on a
|
|
1610
|
+
// Dynamic Island phone (59pt) the card rode up under the island. Derived from
|
|
1611
|
+
// the screen height instead — the only signal available without the dependency:
|
|
1612
|
+
// 852pt/932pt are the Island sizes, 812-896 the notch generation, the rest pre-notch.
|
|
1613
|
+
const topInset = react_native_1.Platform.OS === 'android'
|
|
1614
|
+
? (react_native_1.StatusBar.currentHeight || 0)
|
|
1615
|
+
: topInsetForHeight(SCREEN_HEIGHT);
|
|
1472
1616
|
const bottomInset = react_native_1.Platform.OS === 'ios' ? 34 : 0;
|
|
1473
1617
|
const safePad = isFullscreen
|
|
1474
1618
|
? { paddingTop: topInset, paddingBottom: bottomInset }
|
|
1475
1619
|
: isSheet && placement === 'bottom' ? { paddingBottom: bottomInset }
|
|
1476
1620
|
: isSheet && placement === 'top' ? { paddingTop: topInset }
|
|
1477
1621
|
: null;
|
|
1622
|
+
// Resolve the authored `y` to a top offset in points, keeping the whole card
|
|
1623
|
+
// inside the safe area. GUTTER is the overlay's own padding, so the clamp is
|
|
1624
|
+
// expressed against the same edges the card would otherwise rest on.
|
|
1625
|
+
const GUTTER = 20;
|
|
1626
|
+
const popupTopPx = hasPopupY
|
|
1627
|
+
? resolvePopupTop({ y: popupY, screenH: SCREEN_HEIGHT, cardH: estCardH, topInset, bottomInset, gutter: GUTTER })
|
|
1628
|
+
: null;
|
|
1629
|
+
const popupTop = popupTopPx == null ? null : { paddingTop: popupTopPx };
|
|
1478
1630
|
// Action-button layout. UNSET keeps the native full-width split (default);
|
|
1479
1631
|
// 'full' is explicit full-width; right/center/left shrink to content + align.
|
|
1480
1632
|
const buttonAlign = display?.buttonAlign;
|
|
@@ -1532,8 +1684,17 @@ function SurveyModal({ survey, visible, onSubmit, onDismiss }) {
|
|
|
1532
1684
|
if (pct)
|
|
1533
1685
|
return { width: `${pct[1]}%` };
|
|
1534
1686
|
const px = /^(\d+(?:\.\d+)?)px$/.exec(String(w));
|
|
1687
|
+
// The authored width is a CAP, not a target — web renders the popup as
|
|
1688
|
+
// `width:90vw; max-width:<authored>`, so the card is 90% of the viewport
|
|
1689
|
+
// until the author's number is the smaller of the two.
|
|
1690
|
+
//
|
|
1691
|
+
// I briefly clamped this to the full SCREEN instead, reading "390px on a
|
|
1692
|
+
// 393pt frame" as a request for full bleed. It is not: the preview showed
|
|
1693
|
+
// 351px (90vw of 390) and the device then drew 390, so the SAME 53pt
|
|
1694
|
+
// thumbnail sat in a wider card and read as too small. Matching web's 90vw
|
|
1695
|
+
// is what keeps the proportions identical on both.
|
|
1535
1696
|
if (px)
|
|
1536
|
-
return { width: Math.min(SCREEN_WIDTH
|
|
1697
|
+
return { width: Math.min(SCREEN_WIDTH * CARD_VIEWPORT_FRACTION, Number(px[1])), maxWidth: Number(px[1]) };
|
|
1537
1698
|
return null;
|
|
1538
1699
|
})();
|
|
1539
1700
|
// Fullscreen ONLY: the card fills (survey bg), and these constrain the CONTENT
|
|
@@ -1627,15 +1788,25 @@ function SurveyModal({ survey, visible, onSubmit, onDismiss }) {
|
|
|
1627
1788
|
return ((0, jsx_runtime_1.jsx)(react_native_1.Modal, { visible: visible, transparent: true, animationType: modalAnimation, onRequestClose: handleDismiss, children: (0, jsx_runtime_1.jsxs)(react_native_1.KeyboardAvoidingView, { style: [
|
|
1628
1789
|
styles.overlay,
|
|
1629
1790
|
overlayPlacement,
|
|
1791
|
+
// The authored coordinate, resolved to points. Before the sheet and
|
|
1792
|
+
// fullscreen overrides so those keep winning.
|
|
1793
|
+
popupTop,
|
|
1630
1794
|
// A sheet sits flush against its edge, so the overlay loses its gutter.
|
|
1631
1795
|
isSheet && { paddingHorizontal: 0, paddingTop: 0, paddingBottom: 0 },
|
|
1632
1796
|
isFullscreen && styles.overlayFullscreen,
|
|
1633
|
-
], behavior: react_native_1.Platform.OS === 'ios' ? 'padding' : 'height', children: [(0, jsx_runtime_1.jsxs)(react_native_1.Animated.View, { onTouchStart: () => { engagedRef.current = true; },
|
|
1797
|
+
], behavior: react_native_1.Platform.OS === 'ios' ? 'padding' : 'height', children: [(0, jsx_runtime_1.jsxs)(react_native_1.Animated.View, { onTouchStart: () => { engagedRef.current = true; },
|
|
1798
|
+
// Feeds the authored-coordinate maths above. Only tracked when a
|
|
1799
|
+
// coordinate is actually in play, and only on a real change, so the
|
|
1800
|
+
// common case costs nothing and this cannot loop.
|
|
1801
|
+
onLayout: hasPopupY ? (e) => {
|
|
1802
|
+
const h = Math.round(e.nativeEvent.layout.height);
|
|
1803
|
+
setMeasuredCardH((prev) => (Math.abs(prev - h) > 1 ? h : prev));
|
|
1804
|
+
} : undefined, ...(swipeable ? swipeResponder.panHandlers : {}), style: [
|
|
1634
1805
|
styles.card,
|
|
1635
1806
|
themeCard,
|
|
1636
1807
|
enterStyle,
|
|
1637
|
-
swipeable && { transform: swipeXY.getTranslateTransform() },
|
|
1638
|
-
maxWidth !== undefined && !isFullscreen && { width: Math.min(SCREEN_WIDTH
|
|
1808
|
+
swipeable && !needsTopSlide && { transform: swipeXY.getTranslateTransform() },
|
|
1809
|
+
maxWidth !== undefined && !isFullscreen && { width: Math.min(SCREEN_WIDTH * CARD_VIEWPORT_FRACTION, maxWidth), maxWidth },
|
|
1639
1810
|
cornerRadius !== undefined && !isFullscreen && { borderRadius: cornerRadius },
|
|
1640
1811
|
// cardFullscreen fills; an explicit content height/width below OVERRIDES
|
|
1641
1812
|
// the fill so a fullscreen survey can be contained to that size.
|
|
@@ -1695,6 +1866,8 @@ function SurveyModal({ survey, visible, onSubmit, onDismiss }) {
|
|
|
1695
1866
|
styles.header,
|
|
1696
1867
|
padHeader,
|
|
1697
1868
|
isRtl && { paddingLeft: 48, paddingRight: 24 },
|
|
1869
|
+
// Thumbnail beside the text, as `.cxp-notify-row` does on web.
|
|
1870
|
+
isNotifyCard && { flexDirection: 'row', alignItems: 'center' },
|
|
1698
1871
|
], children: [((survey.branding?.showLogo !== false && survey.branding?.logo)
|
|
1699
1872
|
|| survey.branding?.brandName) && ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [
|
|
1700
1873
|
styles.brandRow,
|
|
@@ -1704,20 +1877,32 @@ function SurveyModal({ survey, visible, onSubmit, onDismiss }) {
|
|
|
1704
1877
|
: survey.branding?.logoAlign === 'right'
|
|
1705
1878
|
? { justifyContent: 'flex-end' }
|
|
1706
1879
|
: null),
|
|
1707
|
-
], 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] })), (0, jsx_runtime_1.jsx)(react_native_1.
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1880
|
+
], 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] })), isNotifyCard ? ((0, jsx_runtime_1.jsx)(react_native_1.Image, { source: { uri: notifyImageQ?.imageUrl }, accessibilityLabel: notifyImageQ?.alt || undefined, accessible: !!notifyImageQ?.alt, style: {
|
|
1881
|
+
width: contract.imageSize,
|
|
1882
|
+
height: contract.imageSize,
|
|
1883
|
+
borderRadius: contract.imageRadius,
|
|
1884
|
+
marginRight: 12,
|
|
1885
|
+
flexShrink: 0,
|
|
1886
|
+
}, resizeMode: "cover" })) : null, (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: isNotifyCard ? { flex: 1, minWidth: 0 } : undefined, children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [styles.title, dirText, fontStyle, themeTextPrimary, titleFS, notifyTitleFS,
|
|
1887
|
+
survey.titleAlign
|
|
1888
|
+
? { textAlign: survey.titleAlign }
|
|
1889
|
+
: null], children: (0, interpolate_1.interpolatePlain)(
|
|
1890
|
+
// Title is plain text; strip any stray rich-text markup +
|
|
1891
|
+
// decode so it never shows literal HTML on native.
|
|
1892
|
+
String((0, translationLookup_1.surveyText)(survey, currentLanguage, 'title', survey.title) || '')
|
|
1893
|
+
.replace(/<[^>]*>/g, '').replace(/ /gi, ' '), evalCtx) }), (() => {
|
|
1894
|
+
// Description is authored as rich HTML; native Text can't render
|
|
1895
|
+
// markup, so strip tags + decode to plain text.
|
|
1896
|
+
const descRaw = (0, translationLookup_1.surveyText)(survey, currentLanguage, 'description', survey.description);
|
|
1897
|
+
const desc = String(descRaw || '').replace(/<[^>]*>/g, '').replace(/ /gi, ' ').trim();
|
|
1898
|
+
return desc ? ((0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [styles.description, dirText, fontStyle, themeTextSecondary, descFS, notifyDescFS], children: (0, interpolate_1.interpolatePlain)(desc, evalCtx) })) : null;
|
|
1899
|
+
})()] }), deadlineEl, showProgressTop && progressEl] }), (0, jsx_runtime_1.jsx)(react_native_1.ScrollView, { style: [styles.body, padBody, fsContent], showsVerticalScrollIndicator: false, children: currentQuestions.filter((q) => {
|
|
1900
|
+
// The notification's thumbnail is drawn in the header above;
|
|
1901
|
+
// leaving it here too would render it twice, once full-bleed.
|
|
1902
|
+
if (isNotifyCard && String(q?.id ?? '') === 'notify_image')
|
|
1903
|
+
return false;
|
|
1904
|
+
return true;
|
|
1905
|
+
}).map((q) => {
|
|
1721
1906
|
// v1.1 uses `id`+`required`; legacy uses `name`+`isRequired`.
|
|
1722
1907
|
// The provider normalizes both onto the same object so
|
|
1723
1908
|
// either lookup works.
|
|
@@ -1727,13 +1912,23 @@ function SurveyModal({ survey, visible, onSubmit, onDismiss }) {
|
|
|
1727
1912
|
// marked with nothing at all.
|
|
1728
1913
|
const isRequired = !!forValidation(q).required
|
|
1729
1914
|
|| q.required || q.isRequired;
|
|
1915
|
+
// An implied question has no prompt: its ACTIONS are the UI,
|
|
1916
|
+
// and its `text` is bookkeeping the author never meant to
|
|
1917
|
+
// show. Web renders the whole block display:none and lifts
|
|
1918
|
+
// the buttons into the nav bar; here the buttons render in
|
|
1919
|
+
// place, so only the label and description are suppressed.
|
|
1920
|
+
// Without this the stray authoring text ("tte") appeared as a
|
|
1921
|
+
// question above the notification's own button.
|
|
1922
|
+
const isImplied = String(q?.type ?? '').toLowerCase() === 'implied';
|
|
1730
1923
|
const qLabel = (0, interpolate_1.interpolatePlain)((0, translationLookup_1.questionText)(survey, currentLanguage, rowKey, 'text', q.title), evalCtx);
|
|
1731
|
-
return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { ref: (el) => { questionRefs.current[rowKey] = el; }, style: [styles.questionBlock, gapQBlock], children: [(0, jsx_runtime_1.jsxs)(react_native_1.Text, { style: [styles.questionTitle, dirText, fontStyle, themeTextPrimary, qTitleFS],
|
|
1924
|
+
return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { ref: (el) => { questionRefs.current[rowKey] = el; }, style: [styles.questionBlock, gapQBlock], children: [isImplied ? null : ((0, jsx_runtime_1.jsxs)(react_native_1.Text, { style: [styles.questionTitle, dirText, fontStyle, themeTextPrimary, qTitleFS],
|
|
1732
1925
|
// The asterisk below is decoration: a screen reader
|
|
1733
1926
|
// reads it as "star", or skips it, so a required
|
|
1734
1927
|
// question sounded exactly like an optional one until
|
|
1735
1928
|
// submitting failed. The label says the word.
|
|
1736
|
-
accessibilityLabel: isRequired ? `${qLabel}, required` : qLabel, children: [questionNumberById[rowKey] ? `Q${questionNumberById[rowKey]}. ` : '', (0, interpolate_1.interpolatePlain)((0, translationLookup_1.questionText)(survey, currentLanguage, rowKey, 'text', q.title), evalCtx), isRequired && (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.required, children: " *" })] }), (() => {
|
|
1929
|
+
accessibilityLabel: isRequired ? `${qLabel}, required` : qLabel, children: [questionNumberById[rowKey] ? `Q${questionNumberById[rowKey]}. ` : '', (0, interpolate_1.interpolatePlain)((0, translationLookup_1.questionText)(survey, currentLanguage, rowKey, 'text', q.title), evalCtx), isRequired && (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.required, children: " *" })] })), (() => {
|
|
1930
|
+
if (isImplied)
|
|
1931
|
+
return null;
|
|
1737
1932
|
const qDesc = (0, translationLookup_1.questionText)(survey, currentLanguage, rowKey, 'description', q.description);
|
|
1738
1933
|
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;
|
|
1739
1934
|
})(), renderQuestion(q), errors[rowKey] ? ((0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [styles.errorText, dirText, fontStyle],
|
|
@@ -1792,6 +1987,8 @@ function SurveyModal({ survey, visible, onSubmit, onDismiss }) {
|
|
|
1792
1987
|
})()] })] }))] }), (0, jsx_runtime_1.jsx)(handsFreeVoice_1.VoiceOverlay, { visible: voiceActive, phase: voicePhase, transcript: voiceTranscript, onStop: () => (0, handsFreeVoice_1.stopVoice)() })] }) }));
|
|
1793
1988
|
}
|
|
1794
1989
|
const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = react_native_1.Dimensions.get('window');
|
|
1990
|
+
/** Web draws a popup at `width:90vw`; the card matches so both read the same. */
|
|
1991
|
+
const CARD_VIEWPORT_FRACTION = 0.9;
|
|
1795
1992
|
/**
|
|
1796
1993
|
* Contract-driven styles. Was a static StyleSheet with 31 hardcoded colours;
|
|
1797
1994
|
* every one now resolves from the style contract, so a branded survey brands the
|
|
@@ -1884,7 +2081,12 @@ const makeStyles = (c) => react_native_1.StyleSheet.create({
|
|
|
1884
2081
|
title: {
|
|
1885
2082
|
color: c.headingColor,
|
|
1886
2083
|
fontSize: c.fontSizeHeading,
|
|
1887
|
-
|
|
2084
|
+
// 600, matching web's `.cxp-widget-title`. At 700 the same string is about
|
|
2085
|
+
// 5% wider, which is enough to tip a notification's headline from one line
|
|
2086
|
+
// to two — the device wrapped "Testing the notification" where the studio
|
|
2087
|
+
// preview kept it on one, and that single difference is what made the two
|
|
2088
|
+
// cards look unalike.
|
|
2089
|
+
fontWeight: '600',
|
|
1888
2090
|
},
|
|
1889
2091
|
description: {
|
|
1890
2092
|
color: c.descriptionColor,
|
|
@@ -1984,13 +2186,19 @@ const makeStyles = (c) => react_native_1.StyleSheet.create({
|
|
|
1984
2186
|
impliedActions: { flexDirection: 'row', flexWrap: 'wrap', gap: 10, marginTop: 4 },
|
|
1985
2187
|
impliedBtn: {
|
|
1986
2188
|
flexGrow: 1, flexBasis: 140, paddingVertical: 13, paddingHorizontal: 18,
|
|
1987
|
-
|
|
2189
|
+
// From the contract, like every other button. A literal 8 here meant an
|
|
2190
|
+
// authored button radius applied to Next and Back but not to the action a
|
|
2191
|
+
// notification actually offers.
|
|
2192
|
+
borderRadius: c.buttonRadius, alignItems: 'center', borderWidth: 1, borderColor: 'transparent',
|
|
1988
2193
|
},
|
|
1989
|
-
|
|
1990
|
-
|
|
2194
|
+
// The brand accent, not a hardcoded blue. `#2b6cb0` ignored primaryColor
|
|
2195
|
+
// entirely, so a notification's only action was off-brand on every survey —
|
|
2196
|
+
// the same button renders in the authored accent on web.
|
|
2197
|
+
impliedPrimary: { backgroundColor: c.primaryBg },
|
|
2198
|
+
impliedSecondary: { backgroundColor: 'transparent', borderColor: c.inputBorder },
|
|
1991
2199
|
impliedChosen: { opacity: 0.75 },
|
|
1992
|
-
impliedPrimaryLabel: { color:
|
|
1993
|
-
impliedSecondaryLabel: { color:
|
|
2200
|
+
impliedPrimaryLabel: { color: c.primaryText, fontWeight: '600', fontSize: 15 },
|
|
2201
|
+
impliedSecondaryLabel: { color: c.textPrimary, fontWeight: '600', fontSize: 15 },
|
|
1994
2202
|
desktopHint: {
|
|
1995
2203
|
color: c.hintText,
|
|
1996
2204
|
fontSize: 13,
|