@moonpay/platform-sdk-react-native 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -61,6 +61,8 @@ export function App() {
61
61
 
62
62
  `MoonPayProvider` also accepts optional `apiBaseUrl` and `frameBaseUrl` props that point the SDK at a different MoonPay environment — useful if MoonPay gives you test endpoints during onboarding. Leave them unset to use production.
63
63
 
64
+ Pass an optional `theme={{ appearance: 'light' | 'dark' }}` to render every MoonPay frame in light or dark mode. Omit it to follow the customer's system preference. The connect flow can override it per call (`client.connect({ theme })` or `<MoonPayConnect theme={...} />`).
65
+
64
66
  **3. Check the connection and connect the customer:**
65
67
 
66
68
  ```tsx
package/dist/index.cjs CHANGED
@@ -158,6 +158,8 @@ var buyButtonSpec = {
158
158
  },
159
159
  mapMessage: (msg) => {
160
160
  switch (msg.kind) {
161
+ case "ready":
162
+ return { kind: "ready" };
161
163
  case "complete":
162
164
  return { kind: "complete", payload: msg.payload };
163
165
  case "challenge":
@@ -220,7 +222,9 @@ var connectSpec = {
220
222
  buildParams: (ctx, props, publicKey) => ({
221
223
  sessionToken: ctx.sessionToken,
222
224
  publicKey,
223
- ...props.theme?.appearance && { appearance: props.theme.appearance }
225
+ // Per-call override; `applyThemeParam` in the engine fills the client
226
+ // default for connect (and every other visible frame) when this is absent.
227
+ ...(0, import_platform_sdk_core.themeParams)(props.theme)
224
228
  }),
225
229
  onMessageEffect: applyCredentialsOnComplete,
226
230
  mapMessage: (msg) => {
@@ -521,11 +525,12 @@ function createFrameSession(spec, ctx, props, overrides = {}) {
521
525
  const generated = `${spec.channelPrefix}-${Date.now()}`;
522
526
  const channelId = spec.resolveChannelId ? spec.resolveChannelId(props, generated) : generated;
523
527
  const keyPair = spec.needsKeyPair ? (0, import_platform_sdk_core2.generateKeyPair)() : void 0;
524
- const url = spec.buildUrl ? spec.buildUrl(ctx, props, channelId, keyPair?.publicKey) : (0, import_platform_sdk_core2.buildFrameUrl)(
528
+ const builtUrl = spec.buildUrl ? spec.buildUrl(ctx, props, channelId, keyPair?.publicKey) : (0, import_platform_sdk_core2.buildFrameUrl)(
525
529
  spec.path,
526
530
  { ...spec.buildParams?.(ctx, props, keyPair?.publicKey), channelId },
527
531
  { frameBaseUrl: ctx.frameBaseUrl }
528
532
  );
533
+ const url = spec.hidden ? builtUrl : (0, import_platform_sdk_core2.applyThemeParam)(builtUrl, ctx.theme);
529
534
  const transport = (overrides.createTransport ?? (() => new WebViewTransport()))();
530
535
  let resolvedHandle = null;
531
536
  let rejectDisposed;
@@ -909,7 +914,34 @@ function createMountSession(specCtx, host) {
909
914
  // src/VisibleFrameSlotSheet.tsx
910
915
  var import_react2 = require("react");
911
916
  var import_react_native2 = require("react-native");
917
+
918
+ // src/sheet-presentation.ts
919
+ var NOOP = {
920
+ present: false,
921
+ liftSpinner: false,
922
+ dismissSilent: false,
923
+ close: false
924
+ };
925
+ function resolvePresentation(phase, signal) {
926
+ if (phase === "closed")
927
+ return { phase: "closed", ...NOOP };
928
+ switch (signal) {
929
+ case "ready":
930
+ return { ...NOOP, phase: "presented", present: phase === "hidden", liftSpinner: true };
931
+ case "grace":
932
+ if (phase === "presented")
933
+ return { phase: "presented", ...NOOP };
934
+ return { ...NOOP, phase: "presented", present: true };
935
+ case "terminal":
936
+ if (phase === "hidden")
937
+ return { ...NOOP, phase: "closed", dismissSilent: true };
938
+ return { ...NOOP, phase: "closed", close: true };
939
+ }
940
+ }
941
+
942
+ // src/VisibleFrameSlotSheet.tsx
912
943
  var import_jsx_runtime2 = require("react/jsx-runtime");
944
+ var PRESENT_GRACE_MS = 3e3;
913
945
  function animateSheet(value, toValue, onDone) {
914
946
  const animation = import_react_native2.Platform.OS === "android" ? import_react_native2.Animated.timing(value, {
915
947
  toValue,
@@ -936,6 +968,8 @@ function VisibleFrameSlotSheet({
936
968
  const sheetHeight = Math.round(height * 0.9);
937
969
  const translateY = (0, import_react2.useRef)(new import_react_native2.Animated.Value(sheetHeight)).current;
938
970
  const [frameReady, setFrameReady] = (0, import_react2.useState)(false);
971
+ const [presented, setPresented] = (0, import_react2.useState)(false);
972
+ const phaseRef = (0, import_react2.useRef)("hidden");
939
973
  const onCloseRef = (0, import_react2.useRef)(onClose);
940
974
  onCloseRef.current = onClose;
941
975
  const closingRef = (0, import_react2.useRef)(false);
@@ -943,29 +977,80 @@ function VisibleFrameSlotSheet({
943
977
  if (closingRef.current)
944
978
  return;
945
979
  closingRef.current = true;
980
+ phaseRef.current = "closed";
946
981
  animateSheet(translateY, sheetHeight, ({ finished }) => {
947
982
  if (finished)
948
983
  onCloseRef.current();
949
984
  });
950
985
  }, [translateY, sheetHeight]);
986
+ const panResponder = (0, import_react2.useMemo)(
987
+ () => import_react_native2.PanResponder.create({
988
+ // Claim the gesture only for a clearly downward drag — never on a tap
989
+ // (so the handle's onPress still fires) or a mostly-horizontal swipe.
990
+ onMoveShouldSetPanResponder: (_evt, { dy, dx }) => dy > 5 && dy > Math.abs(dx),
991
+ onPanResponderGrant: () => {
992
+ translateY.stopAnimation(() => {
993
+ translateY.extractOffset();
994
+ });
995
+ },
996
+ // Track the finger downward; clamp upward travel to the open position.
997
+ onPanResponderMove: (_evt, { dy }) => {
998
+ translateY.setValue(Math.max(0, dy));
999
+ },
1000
+ onPanResponderRelease: (_evt, { dy, vy }) => {
1001
+ translateY.flattenOffset();
1002
+ const draggedPastThreshold = dy > sheetHeight * 0.35;
1003
+ const flungDownFast = vy > 0.5;
1004
+ if (draggedPastThreshold || flungDownFast) {
1005
+ close();
1006
+ } else {
1007
+ animateSheet(translateY, 0);
1008
+ }
1009
+ },
1010
+ // Another responder stole the gesture: settle to a known state.
1011
+ onPanResponderTerminate: () => {
1012
+ translateY.flattenOffset();
1013
+ animateSheet(translateY, 0);
1014
+ }
1015
+ }),
1016
+ [translateY, sheetHeight, close]
1017
+ );
1018
+ const dispatch = (0, import_react2.useCallback)(
1019
+ (signal) => {
1020
+ const result = resolvePresentation(phaseRef.current, signal);
1021
+ phaseRef.current = result.phase;
1022
+ if (result.present) {
1023
+ setPresented(true);
1024
+ animateSheet(translateY, 0);
1025
+ }
1026
+ if (result.liftSpinner)
1027
+ setFrameReady(true);
1028
+ if (result.dismissSilent)
1029
+ onCloseRef.current();
1030
+ if (result.close)
1031
+ close();
1032
+ },
1033
+ [translateY, close]
1034
+ );
951
1035
  (0, import_react2.useEffect)(() => {
952
- animateSheet(translateY, 0);
953
- }, [translateY]);
1036
+ const timer = setTimeout(() => dispatch("grace"), PRESENT_GRACE_MS);
1037
+ return () => clearTimeout(timer);
1038
+ }, [dispatch]);
954
1039
  (0, import_react2.useEffect)(() => {
955
1040
  const unsubscribe = slot.transport.onMessage((msg) => {
956
1041
  if (msg.kind === "ready")
957
- setFrameReady(true);
1042
+ dispatch("ready");
958
1043
  else if (msg.kind === "complete" || msg.kind === "error")
959
- close();
1044
+ dispatch("terminal");
960
1045
  });
961
1046
  return unsubscribe;
962
- }, [slot.transport, close]);
1047
+ }, [slot.transport, dispatch]);
963
1048
  const backdropOpacity = translateY.interpolate({
964
1049
  inputRange: [0, sheetHeight],
965
1050
  outputRange: [1, 0],
966
1051
  extrapolate: "clamp"
967
1052
  });
968
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native2.Modal, { visible: true, transparent: true, animationType: "none", onRequestClose: close, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react_native2.View, { style: styles.root, children: [
1053
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native2.Modal, { visible: true, transparent: true, animationType: "none", onRequestClose: close, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react_native2.View, { style: styles.root, pointerEvents: presented ? "auto" : "none", children: [
969
1054
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native2.Animated.View, { style: [styles.backdrop, { opacity: backdropOpacity }], children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
970
1055
  import_react_native2.Pressable,
971
1056
  {
@@ -978,17 +1063,16 @@ function VisibleFrameSlotSheet({
978
1063
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react_native2.Animated.View, { style: [styles.sheet, { height: sheetHeight, transform: [{ translateY }] }], children: [
979
1064
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(MoonPayFrame, { transport: slot.transport }),
980
1065
  !frameReady && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native2.View, { style: styles.loading, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native2.ActivityIndicator, { size: "large", color: "#000" }) }),
981
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1066
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native2.View, { style: styles.handleArea, ...panResponder.panHandlers, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
982
1067
  import_react_native2.Pressable,
983
1068
  {
984
1069
  onPress: close,
985
1070
  accessibilityRole: "button",
986
1071
  accessibilityLabel: "Close",
987
- hitSlop: 8,
988
- style: styles.closeButton,
989
- children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native2.Text, { style: styles.closeButtonText, children: "\u2715" })
1072
+ hitSlop: 12,
1073
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native2.View, { style: styles.handleBar })
990
1074
  }
991
- )
1075
+ ) })
992
1076
  ] })
993
1077
  ] }) });
994
1078
  }
@@ -1018,25 +1102,25 @@ var styles = import_react_native2.StyleSheet.create({
1018
1102
  alignItems: "center",
1019
1103
  justifyContent: "center"
1020
1104
  },
1021
- // Floating X in the top-right corner, over the frame.
1022
- closeButton: {
1105
+ // Drag-to-dismiss strip pinned across the top of the sheet, floating over the
1106
+ // frame. Tall enough to be an easy grab target without eating much of the UI.
1107
+ handleArea: {
1023
1108
  position: "absolute",
1024
- top: 12,
1025
- right: 12,
1026
- width: 32,
1027
- height: 32,
1028
- borderRadius: 16,
1029
- backgroundColor: "rgba(0, 0, 0, 0.06)",
1109
+ top: 0,
1110
+ left: 0,
1111
+ right: 0,
1112
+ height: 28,
1030
1113
  alignItems: "center",
1031
1114
  justifyContent: "center",
1032
1115
  zIndex: 1,
1033
1116
  elevation: 2
1034
1117
  },
1035
- closeButtonText: {
1036
- fontSize: 17,
1037
- lineHeight: 20,
1038
- color: "#000",
1039
- fontWeight: "500"
1118
+ // The visible pill.
1119
+ handleBar: {
1120
+ width: 36,
1121
+ height: 5,
1122
+ borderRadius: 3,
1123
+ backgroundColor: "rgba(0, 0, 0, 0.2)"
1040
1124
  }
1041
1125
  });
1042
1126
 
@@ -1047,6 +1131,7 @@ function MoonPayProvider({
1047
1131
  sessionToken,
1048
1132
  apiBaseUrl,
1049
1133
  frameBaseUrl,
1134
+ theme,
1050
1135
  children
1051
1136
  }) {
1052
1137
  const [frameSlots, setFrameSlots] = (0, import_react3.useState)([]);
@@ -1066,13 +1151,13 @@ function MoonPayProvider({
1066
1151
  }
1067
1152
  const core = coreRef.current;
1068
1153
  const client = (0, import_react3.useMemo)(() => {
1069
- const specCtx = { sessionToken, core, frameBaseUrl };
1154
+ const specCtx = { sessionToken, core, frameBaseUrl, theme };
1070
1155
  return createRNClient({
1071
1156
  mountSession: createMountSession(specCtx, { addSlot, removeSlot }),
1072
1157
  core
1073
1158
  });
1074
- }, [sessionToken, frameBaseUrl, core, addSlot, removeSlot]);
1075
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(MoonPayContext.Provider, { value: { client, sessionToken, core, frameBaseUrl }, children: [
1159
+ }, [sessionToken, frameBaseUrl, core, addSlot, removeSlot, theme?.appearance]);
1160
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(MoonPayContext.Provider, { value: { client, sessionToken, core, frameBaseUrl, theme }, children: [
1076
1161
  children,
1077
1162
  frameSlots.filter((slot) => slot.hidden).map((slot) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MoonPayFrame, { transport: slot.transport, hidden: true }, slot.id)),
1078
1163
  frameSlots.filter((slot) => !slot.hidden).map((slot) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
@@ -1117,7 +1202,7 @@ function MoonPayFrameView({
1117
1202
  defaultStyle,
1118
1203
  onEvent
1119
1204
  }) {
1120
- const { sessionToken, core, frameBaseUrl } = useMoonPayContext();
1205
+ const { sessionToken, core, frameBaseUrl, theme } = useMoonPayContext();
1121
1206
  const [transport, setTransport] = (0, import_react4.useState)(null);
1122
1207
  const handleRef = (0, import_react4.useRef)(null);
1123
1208
  const onEventRef = (0, import_react4.useRef)(onEvent);
@@ -1143,7 +1228,11 @@ function MoonPayFrameView({
1143
1228
  }
1144
1229
  appliedRef.current = applied;
1145
1230
  try {
1146
- session = createFrameSession(spec, { sessionToken, core, frameBaseUrl }, propsRef.current);
1231
+ session = createFrameSession(
1232
+ spec,
1233
+ { sessionToken, core, frameBaseUrl, theme },
1234
+ propsRef.current
1235
+ );
1147
1236
  } catch (e) {
1148
1237
  onEventRef.current?.(
1149
1238
  spec.errorEvent(e instanceof Error ? e.message : "Failed to create frame")