@uptick-ads/react-native-web-sdk 0.2.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/LICENSE ADDED
@@ -0,0 +1,27 @@
1
+ # Uptick React Native Web SDK License Agreement
2
+
3
+ By using or integrating the Uptick React Native Web SDK, you agree to the following terms:
4
+
5
+ 1. **Grant of License**
6
+ Uptick grants you a non-exclusive, non-transferable, revocable license to use the Uptick React Native Web SDK solely for the purpose of integrating it into your applications and products.
7
+
8
+ 2. **Restrictions**
9
+ - You may not reverse engineer, decompile, disassemble, or otherwise attempt to discover the source code of the Uptick React Native Web SDK.
10
+ - You may not modify, adapt, or create derivative works from the Uptick React Native Web SDK.
11
+ - Redistribution of the Uptick React Native Web SDK is permitted only as part of your integrated application.
12
+
13
+ 3. **Intellectual Property**
14
+ All rights, title, and interest in and to the Uptick React Native Web SDK, including any updates, modifications, and enhancements, remain with Uptick.
15
+
16
+ 4. **Disclaimer of Warranties**
17
+ The Uptick React Native Web SDK is provided "as is," without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and noninfringement.
18
+
19
+ 5. **Limitation of Liability**
20
+ In no event shall Uptick be liable for any claim, damages, or other liability arising from the use of the Uptick React Native Web SDK.
21
+
22
+ 6. **Termination**
23
+ Uptick may terminate this license at any time if you violate its terms.
24
+
25
+ ---
26
+
27
+ © 2024 Uptick. All rights reserved.
package/README.md ADDED
@@ -0,0 +1,92 @@
1
+ # Uptick React Native Web SDK
2
+
3
+ Runs the Uptick web SDK inside a `react-native-webview`, so a React Native app can show offers without a native SDK.
4
+ One component, `UptickFlow`, with the same parameters as the [web integration](https://docs.uptick.com/publishers/web).
5
+
6
+ ## Requirements
7
+
8
+ - React Native 0.73 or newer
9
+ - `react-native-webview` 13.10 or newer (the component relies on `onOpenWindow`)
10
+ - An Uptick integration id and a placement configured for your account
11
+
12
+ ## Installation
13
+
14
+ ```
15
+ npm install @uptick-ads/react-native-web-sdk react-native-webview
16
+ ```
17
+
18
+ Expo apps: `npx expo install react-native-webview`, then install this package as above.
19
+
20
+ ## Usage
21
+
22
+ Mount `UptickFlow` where an inline offer should appear, in the flow of the screen. Pass the same parameters you would pass to the web SDK.
23
+
24
+ ```jsx
25
+ import { UptickFlow } from "@uptick-ads/react-native-web-sdk";
26
+
27
+ <UptickFlow
28
+ integrationId="AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE"
29
+ placement="order_confirmation"
30
+ first_name="Reggie"
31
+ total_price="$49.99"
32
+ shipping_price="$9.99"
33
+ order_id="12345"
34
+ customer_id="cust_12345"
35
+ callback={(event, data) => console.log(event, data)}
36
+ />
37
+ ```
38
+
39
+ Whether the offer renders inline or as a popup is configured by the Uptick offers team on your placement, not in your app.
40
+ When the placement is inline, the component sizes itself to the offer and starts at zero height, so nothing shows until an offer is ready.
41
+ When the placement is a popup, the component presents a modal over the whole screen with a scrim and a centred card, from wherever it is mounted. Tap outside, the card's close button, and the Android back button all dismiss it.
42
+ Because both presentations work from the same mount point, Uptick can switch a placement between inline and popup without a change to your app.
43
+
44
+ ## Props
45
+
46
+ | Prop | Default | Purpose |
47
+ | --- | --- | --- |
48
+ | `integrationId` | required | Your integration's public id |
49
+ | `placement` | `order_confirmation` | `checkout`, `order_confirmation` or `order_status` |
50
+ | `callback` | | `(event, data)`, the same events as the web SDK callback |
51
+ | `openUrl` | `Linking.openURL` | How accepted offers open; supply your own to use an in-app browser |
52
+ | `style` | | Style for the inline slot |
53
+ | `modal` | | `{ scrimColor, cardStyle, insets, maxCardHeight }` for the popup presentation |
54
+ | `webViewProps` | | Extra props passed through to `WebView` |
55
+ | `host` | `api.uptick.com` | API host, only for testing against other environments |
56
+
57
+ ### Flow parameters
58
+
59
+ Passed as flat props, named as in the web integration. All values are strings.
60
+
61
+ - **Site**: `site_id`
62
+ - **User**: `customer_id`, `first_name`, `locale`
63
+ - **Order**: `order_id`, `order_name`, `payment_method`, `currency`, `total_price`, `shipping_price`, `discount`, `zip`, `province`, `country_code`
64
+
65
+ They travel in the URL fragment, which is never sent to a server, so they do not appear in request logs. Props outside this list are not forwarded.
66
+
67
+ ### Events
68
+
69
+ `callback` receives the web SDK's lifecycle events with the same names and payloads: `init`, `flow_initialized`, `flow_delayed`, `offer_requested`, `offer_viewed`, `offer_accepted`, `offer_rejected`, `flow_complete`, plus `flow_hidden` when the user closes the offer.
70
+ The component adds `render_type` (`{ rendered: "inline" | "popup" }`) once the first offer renders, `popup_dismissed`, `webview_error`, `webview_http_error` and `open_url_failed`.
71
+
72
+ ## How click-out works
73
+
74
+ When a user accepts an offer the SDK opens the advertiser link with `window.open`. The component receives that through `onOpenWindow`, hands the URL to `openUrl`, and the page inside the WebView stays put, so the SDK moves on to the next offer while the user is in the browser. Do not set `setSupportMultipleWindows={false}` through `webViewProps`: on Android that makes the WebView start the navigation itself and leaves the offer frozen.
75
+
76
+ ## Demo
77
+
78
+ `uptick-ads/uptick-react-native-web` is an Expo app that exercises inline and popup placements against a local or hosted Uptick API.
79
+
80
+ ## License
81
+
82
+ See `LICENSE`.
83
+
84
+ ## Releasing
85
+
86
+ Bump `version` in `package.json` and the changelog, merge to `main`, then push a matching tag:
87
+
88
+ ```
89
+ git tag v0.2.1 && git push origin v0.2.1
90
+ ```
91
+
92
+ The Release workflow publishes to npm with provenance and creates the GitHub release.
package/index.d.ts ADDED
@@ -0,0 +1,46 @@
1
+ import type { ComponentProps } from "react";
2
+ import type { StyleProp, ViewStyle } from "react-native";
3
+ import type { WebView } from "react-native-webview";
4
+
5
+ /** Flow parameters, named exactly as in the web integration (docs.uptick.com/publishers/web). All values are strings. */
6
+ export interface UptickFlowParams {
7
+ site_id?: string;
8
+ customer_id?: string;
9
+ first_name?: string;
10
+ locale?: string;
11
+ order_id?: string;
12
+ order_name?: string;
13
+ payment_method?: string;
14
+ currency?: string;
15
+ total_price?: string;
16
+ shipping_price?: string;
17
+ discount?: string;
18
+ zip?: string;
19
+ province?: string;
20
+ country_code?: string;
21
+ }
22
+
23
+ export interface UptickModalOptions {
24
+ scrimColor?: string;
25
+ cardStyle?: StyleProp<ViewStyle>;
26
+ insets?: { top: number; bottom: number };
27
+ maxCardHeight?: number;
28
+ }
29
+
30
+ export interface UptickFlowProps extends UptickFlowParams {
31
+ integrationId: string;
32
+ placement?: string;
33
+ callback?: (event: string, data: unknown) => void;
34
+ openUrl?: (url: string) => void | Promise<unknown>;
35
+ style?: StyleProp<ViewStyle>;
36
+ modal?: UptickModalOptions;
37
+ webViewProps?: Partial<ComponentProps<typeof WebView>>;
38
+ host?: string;
39
+ }
40
+
41
+ export function UptickFlow(props: UptickFlowProps): JSX.Element;
42
+
43
+ export const FLOW_PARAMS: ReadonlyArray<keyof UptickFlowParams>;
44
+ export function pickFlowParams(props: Record<string, unknown>): UptickFlowParams;
45
+ export function buildEmbedUrl(options: { host: string; integrationId: string; placement?: string; params?: UptickFlowParams }): string;
46
+ export function embedPath(host: string, integrationId: string): string;
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@uptick-ads/react-native-web-sdk",
3
+ "version": "0.2.0",
4
+ "description": "React Native component that runs the Uptick web SDK inside a WebView, inline or as a popup, with the web integration's parameters",
5
+ "keywords": [
6
+ "uptick",
7
+ "react-native",
8
+ "webview",
9
+ "offers",
10
+ "post-purchase"
11
+ ],
12
+ "homepage": "https://docs.uptick.com/publisher_sdks/react-native",
13
+ "bugs": {
14
+ "url": "https://github.com/uptick-ads/react-native-web-sdk/issues"
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/uptick-ads/react-native-web-sdk.git"
19
+ },
20
+ "license": "UNLICENSED",
21
+ "author": "Uptick",
22
+ "main": "src/index.js",
23
+ "types": "index.d.ts",
24
+ "files": [
25
+ "src",
26
+ "index.d.ts",
27
+ "README.md",
28
+ "LICENSE"
29
+ ],
30
+ "engines": {
31
+ "node": ">=18"
32
+ },
33
+ "peerDependencies": {
34
+ "react": ">=18",
35
+ "react-native": ">=0.73",
36
+ "react-native-webview": ">=13.10"
37
+ },
38
+ "publishConfig": {
39
+ "access": "public",
40
+ "provenance": true
41
+ },
42
+ "scripts": {
43
+ "check": "node --check src/index.js && node --check src/UptickFlow.js && node --check src/embedUrl.js && npm pack --dry-run",
44
+ "prepublishOnly": "npm run check"
45
+ },
46
+ "private": false
47
+ }
@@ -0,0 +1,166 @@
1
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+ import { Linking, Modal, Pressable, StyleSheet, View } from "react-native";
3
+ import { WebView } from "react-native-webview";
4
+ import { buildEmbedUrl, embedPath, pickFlowParams } from "./embedUrl";
5
+
6
+ const DEFAULT_HOST = "api.uptick.com";
7
+ const MAX_HEIGHT = 1400;
8
+
9
+ // Messages from the host page that the component acts on itself; every message is also forwarded to `callback`.
10
+ const SHOW_EVENTS = ["offer_viewed"];
11
+ const HIDE_EVENTS = ["flow_complete", "flow_hidden"];
12
+
13
+ const FILL = { position: "absolute", top: 0, left: 0, right: 0, bottom: 0 };
14
+
15
+ // Renders an Uptick offer flow. Mount it where an inline offer should appear; when the placement is configured as a popup on the Uptick side it presents a modal over the whole screen instead, so switching a placement between inline and popup needs no app change.
16
+ export function UptickFlow({
17
+ integrationId,
18
+ placement = "order_confirmation",
19
+ callback,
20
+ host = DEFAULT_HOST,
21
+ openUrl,
22
+ style,
23
+ modal = {},
24
+ webViewProps,
25
+ ...flowProps
26
+ }) {
27
+ const [height, setHeight] = useState(0);
28
+ const [visible, setVisible] = useState(false);
29
+ // `null` until the page reports the rendered presentation; the placement's template decides it.
30
+ const [renderType, setRenderType] = useState(null);
31
+ const emit = useRef(callback);
32
+ emit.current = callback;
33
+
34
+ const params = pickFlowParams(flowProps);
35
+ const paramsKey = JSON.stringify(params);
36
+ const source = useMemo(
37
+ () => ({ uri: buildEmbedUrl({ host, integrationId, placement, params }) }),
38
+ // Callers pass fresh prop values each render; key on the serialised params so the page does not reload on every parent render.
39
+ // eslint-disable-next-line react-hooks/exhaustive-deps
40
+ [host, integrationId, placement, paramsKey]
41
+ );
42
+ const ownPath = embedPath(host, integrationId);
43
+
44
+ const leave = useCallback(
45
+ (url) => {
46
+ // Linking.openURL must be called on Linking; a detached reference loses `this` and throws inside React Native.
47
+ const opener = openUrl || ((target) => Linking.openURL(target));
48
+ const report = (error) => emit.current && emit.current("open_url_failed", { url, message: String(error) });
49
+ try {
50
+ Promise.resolve(opener(url)).catch(report);
51
+ } catch (error) {
52
+ report(error);
53
+ }
54
+ },
55
+ [openUrl]
56
+ );
57
+
58
+ const onMessage = useCallback((event) => {
59
+ let message;
60
+ try {
61
+ message = JSON.parse(event.nativeEvent.data);
62
+ } catch (error) {
63
+ return;
64
+ }
65
+ const { name, data } = message;
66
+ if (name === "height") {
67
+ setHeight(Math.min(Math.max(Number(data && data.height) || 0, 0), MAX_HEIGHT));
68
+ return;
69
+ }
70
+ if (name === "render_type" && data && data.rendered) setRenderType(data.rendered);
71
+ if (SHOW_EVENTS.includes(name)) setVisible(true);
72
+ if (HIDE_EVENTS.includes(name)) setVisible(false);
73
+ if (emit.current) emit.current(name, data);
74
+ }, []);
75
+
76
+ // Any navigation away from the host page is a click-out: hand it to the system browser and keep the page where it is.
77
+ const onShouldStartLoadWithRequest = useCallback(
78
+ (request) => {
79
+ const url = request.url || "";
80
+ if (url.startsWith(ownPath) || url.startsWith("about:")) return true;
81
+ leave(url);
82
+ return false;
83
+ },
84
+ [ownPath, leave]
85
+ );
86
+
87
+ // The SDK opens accept links with window.open. Receiving them here keeps the page from navigating, so the SDK advances to the next offer while the user is in the browser.
88
+ // This matters on Android: turning off multiple windows instead makes the WebView start the navigation, the page sees pagehide, and the SDK freezes the current offer.
89
+ const onOpenWindow = useCallback((event) => leave(event.nativeEvent.targetUrl), [leave]);
90
+
91
+ const dismiss = useCallback(() => {
92
+ setVisible(false);
93
+ if (emit.current) emit.current("popup_dismissed", null);
94
+ }, []);
95
+
96
+ useEffect(() => {
97
+ setRenderType(null);
98
+ setHeight(0);
99
+ setVisible(false);
100
+ }, [source]);
101
+
102
+ const isPopup = renderType === "popup";
103
+ const contentHeight = Math.max(height, 1);
104
+ const {
105
+ scrimColor = "rgba(0, 0, 0, 0.45)",
106
+ cardStyle,
107
+ insets = { top: 24, bottom: 24 },
108
+ maxCardHeight = 620,
109
+ } = modal;
110
+
111
+ const webView = (
112
+ <WebView
113
+ source={source}
114
+ originWhitelist={["*"]}
115
+ javaScriptEnabled
116
+ domStorageEnabled
117
+ setSupportMultipleWindows
118
+ onMessage={onMessage}
119
+ onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
120
+ onOpenWindow={onOpenWindow}
121
+ onError={(event) => emit.current && emit.current("webview_error", { description: event.nativeEvent.description })}
122
+ onHttpError={(event) => emit.current && emit.current("webview_http_error", { status: event.nativeEvent.statusCode, url: event.nativeEvent.url })}
123
+ scrollEnabled={false}
124
+ nestedScrollEnabled
125
+ opaque={false}
126
+ {...webViewProps}
127
+ style={[styles.transparent, { height: isPopup ? Math.min(contentHeight, maxCardHeight) : contentHeight }, webViewProps && webViewProps.style]}
128
+ />
129
+ );
130
+
131
+ if (!isPopup) {
132
+ // Inline, and also the state before the page has reported anything: a collapsed slot in the host's layout.
133
+ // The WebView's own container is flex: 1, so the slot must carry an explicit height or it collapses to zero.
134
+ return <View style={[styles.transparent, { height: renderType === "inline" ? contentHeight : 1, overflow: "hidden" }, style]}>{webView}</View>;
135
+ }
136
+
137
+ // Popup: the page has already loaded inside the inline slot, so keep that WebView alive and move it into a system modal.
138
+ // React Native's Modal presents above the whole window regardless of where this component sits in the tree.
139
+ return (
140
+ <View style={[styles.transparent, { height: 0 }, style]}>
141
+ <Modal visible transparent animationType="fade" statusBarTranslucent onRequestClose={dismiss}>
142
+ <View style={FILL} pointerEvents={visible ? "auto" : "none"}>
143
+ <Pressable accessibilityRole="button" accessibilityLabel="Dismiss" style={[FILL, { backgroundColor: scrimColor, opacity: visible ? 1 : 0 }]} onPress={dismiss} />
144
+ <View pointerEvents="box-none" style={[FILL, styles.cardWrap, { paddingTop: insets.top, paddingBottom: insets.bottom, opacity: visible ? 1 : 0 }]}>
145
+ <View style={[styles.card, { height: Math.min(contentHeight, maxCardHeight) }, cardStyle]}>{webView}</View>
146
+ </View>
147
+ </View>
148
+ </Modal>
149
+ </View>
150
+ );
151
+ }
152
+
153
+ const styles = StyleSheet.create({
154
+ transparent: { backgroundColor: "transparent" },
155
+ cardWrap: { justifyContent: "center", paddingHorizontal: 16 },
156
+ card: {
157
+ backgroundColor: "#fff",
158
+ borderRadius: 16,
159
+ overflow: "hidden",
160
+ shadowColor: "#000",
161
+ shadowOpacity: 0.25,
162
+ shadowRadius: 20,
163
+ shadowOffset: { width: 0, height: 10 },
164
+ elevation: 12,
165
+ },
166
+ });
@@ -0,0 +1,50 @@
1
+ // Builds the URL of the host page served by the Uptick API for WebView embeds.
2
+ // Flow parameters travel in the URL fragment, which browsers never send to the server, so customer and order details stay out of request logs and CDN caches.
3
+
4
+ // The web SDK's parameter list (see docs.uptick.com/publishers/web); anything else passed to the component is ignored rather than forwarded.
5
+ export const FLOW_PARAMS = [
6
+ // Site
7
+ "site_id",
8
+ // User
9
+ "customer_id",
10
+ "first_name",
11
+ "locale",
12
+ // Order
13
+ "order_id",
14
+ "order_name",
15
+ "payment_method",
16
+ "currency",
17
+ "total_price",
18
+ "shipping_price",
19
+ "discount",
20
+ "zip",
21
+ "province",
22
+ "country_code",
23
+ ];
24
+
25
+ function encodeQuery(params) {
26
+ return Object.keys(params)
27
+ .filter((key) => params[key] !== undefined && params[key] !== null && params[key] !== "")
28
+ .map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(String(params[key]))}`)
29
+ .join("&");
30
+ }
31
+
32
+ export function pickFlowParams(props) {
33
+ const out = {};
34
+ FLOW_PARAMS.forEach((key) => {
35
+ if (props[key] !== undefined && props[key] !== null) out[key] = props[key];
36
+ });
37
+ return out;
38
+ }
39
+
40
+ export function embedPath(host, integrationId) {
41
+ return `https://${host}/v1/places/${encodeURIComponent(integrationId)}/embed`;
42
+ }
43
+
44
+ export function buildEmbedUrl({ host, integrationId, placement, params }) {
45
+ if (!integrationId) throw new Error("UptickFlow: integrationId is required");
46
+ // The page renders whatever presentation the placement's template defines; `native` only asks it to leave the popup chrome to the app.
47
+ const search = encodeQuery({ placement, variant: "native" });
48
+ const fragment = encodeQuery(params || {});
49
+ return `${embedPath(host, integrationId)}?${search}${fragment ? `#${fragment}` : ""}`;
50
+ }
package/src/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { UptickFlow } from "./UptickFlow";
2
+ export { buildEmbedUrl, embedPath, pickFlowParams, FLOW_PARAMS } from "./embedUrl";