@thepayulink/checkout-react-native 1.0.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 ADDED
@@ -0,0 +1,96 @@
1
+ # @thepayulink/checkout-react-native
2
+
3
+ Elite Yatra / PayuLink UPI checkout for **React Native**. Opens the same secure hosted checkout used on the web inside a native full-screen modal (WebView), and bridges the payment result back to your app.
4
+
5
+ > Why a WebView? UPI collection, the QR, PayuLink signing and webhook verification all live on the Elite Yatra server. The app never touches secrets — it just shows the checkout and receives the confirmed result.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @thepayulink/checkout-react-native react-native-webview
11
+ cd ios && pod install # iOS only
12
+ ```
13
+
14
+ ## Secure usage (recommended)
15
+
16
+ Create the order on **your backend** with the secret key, then pass the `orderId` to the app. The amount is fixed on the server and cannot be tampered with.
17
+
18
+ ```jsx
19
+ import React, { useState } from 'react';
20
+ import { Button } from 'react-native';
21
+ import PayulinkCheckout from '@thepayulink/checkout-react-native';
22
+
23
+ export default function PayScreen() {
24
+ const [order, setOrder] = useState(null);
25
+
26
+ async function startPayment() {
27
+ // Call YOUR backend, which calls POST /api/orders with the secret key.
28
+ const { orderId } = await fetch('https://your-backend/create-order', {
29
+ method: 'POST',
30
+ headers: { 'Content-Type': 'application/json' },
31
+ body: JSON.stringify({ packageId: 'kailash-heli', travellers: 2 }),
32
+ }).then((r) => r.json());
33
+ setOrder(orderId);
34
+ }
35
+
36
+ return (
37
+ <>
38
+ <Button title="Book ₹19,900" onPress={startPayment} />
39
+ {order && (
40
+ <PayulinkCheckout
41
+ visible
42
+ apiBase="https://eliteyatra.vip"
43
+ orderId={order}
44
+ publishableKey="pl_pk_live_xxxxx"
45
+ prefill={{ name: 'Asha', phone: '9876543210', email: 'asha@example.com' }}
46
+ onSuccess={(r) => { setOrder(null); console.log('Paid!', r.order_id, r.utr); }}
47
+ onDismiss={() => setOrder(null)}
48
+ onError={(e) => { setOrder(null); console.warn(e.message); }}
49
+ />
50
+ )}
51
+ </>
52
+ );
53
+ }
54
+ ```
55
+
56
+ ## Convenience usage (amount from the app)
57
+
58
+ Only if `PAYULINK_ALLOW_CLIENT_AMOUNT=1` on the server. Less secure — the amount is client-controlled.
59
+
60
+ ```jsx
61
+ <PayulinkCheckout
62
+ visible={show}
63
+ apiBase="https://eliteyatra.vip"
64
+ amount={19900}
65
+ item="Kailash Mansarovar Yatra"
66
+ onSuccess={(r) => setShow(false)}
67
+ onDismiss={() => setShow(false)}
68
+ />
69
+ ```
70
+
71
+ ## Props
72
+
73
+ | Prop | Type | Description |
74
+ | --- | --- | --- |
75
+ | `visible` | boolean | Show/hide the checkout modal. |
76
+ | `apiBase` | string | Pay server URL. Default `https://eliteyatra.vip`. |
77
+ | `orderId` | string | Secure: backend-created order id. |
78
+ | `amount` | number | Convenience: rupees (ignored if `orderId` set). |
79
+ | `item` | string | Description (convenience flow). |
80
+ | `publishableKey` | string | `pl_pk_…`, safe to embed. |
81
+ | `prefill` | object | `{ name, phone, email }`. |
82
+ | `onSuccess` | function | `({ order_id, utr, amount })`. Fired once, server-confirmed. |
83
+ | `onDismiss` | function | User closed / hardware back. |
84
+ | `onError` | function | Load or order error. |
85
+
86
+ ## Important: confirm server-side
87
+
88
+ `onSuccess` is triggered by the webhook-verified status streamed from the server — reliable for UX. For anything money-critical (unlocking a booking, sending tickets), still verify on **your backend** by reading the order status or handling the PayuLink webhook. Never trust the client alone.
89
+
90
+ ## Expo
91
+
92
+ Works with Expo (Dev Client / prebuild) since it needs `react-native-webview`. It does **not** work in Expo Go if your Expo SDK's Go build excludes the WebView native module — use a development build.
93
+
94
+ ## License
95
+
96
+ UNLICENSED — internal to Elite Yatra.
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@thepayulink/checkout-react-native",
3
+ "version": "1.0.0",
4
+ "description": "Elite Yatra / PayuLink UPI checkout for React Native — opens the secure hosted checkout in a native modal.",
5
+ "main": "src/index.js",
6
+ "types": "src/index.d.ts",
7
+ "files": [
8
+ "src",
9
+ "README.md"
10
+ ],
11
+ "keywords": [
12
+ "eliteyatra",
13
+ "payulink",
14
+ "react-native",
15
+ "payment",
16
+ "upi",
17
+ "checkout"
18
+ ],
19
+ "peerDependencies": {
20
+ "react": ">=17.0.0",
21
+ "react-native": ">=0.64.0",
22
+ "react-native-webview": ">=11.0.0"
23
+ },
24
+ "author": "Elite Yatra",
25
+ "license": "MIT"
26
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,41 @@
1
+ import * as React from 'react';
2
+ import { StyleProp, ViewStyle } from 'react-native';
3
+
4
+ export interface PayulinkPrefill {
5
+ name?: string;
6
+ phone?: string;
7
+ contact?: string;
8
+ mobile?: string;
9
+ email?: string;
10
+ }
11
+
12
+ export interface PayulinkSuccess {
13
+ order_id: string;
14
+ utr: string | null;
15
+ amount?: number;
16
+ }
17
+
18
+ export interface PayulinkCheckoutProps {
19
+ visible?: boolean;
20
+ /** Elite Yatra pay server base URL. Default: https://eliteyatra.vip */
21
+ apiBase?: string;
22
+ /** SECURE flow: order id created by your backend via POST /api/orders. */
23
+ orderId?: string;
24
+ /** Convenience flow: amount in rupees (ignored when orderId is set). */
25
+ amount?: number;
26
+ item?: string;
27
+ /** Publishable key (pl_pk_…). Safe to embed. */
28
+ publishableKey?: string;
29
+ prefill?: PayulinkPrefill;
30
+ onSuccess?: (result: PayulinkSuccess) => void;
31
+ onDismiss?: () => void;
32
+ onError?: (error: Error) => void;
33
+ style?: StyleProp<ViewStyle>;
34
+ }
35
+
36
+ export default function PayulinkCheckout(props: PayulinkCheckoutProps): React.JSX.Element;
37
+
38
+ export function createCheckout(options: Omit<PayulinkCheckoutProps, 'visible'>): {
39
+ Component: (extra?: Partial<PayulinkCheckoutProps>) => React.JSX.Element;
40
+ promise: Promise<PayulinkSuccess>;
41
+ };
package/src/index.js ADDED
@@ -0,0 +1,146 @@
1
+ // @thepayulink/checkout-react-native
2
+ // Renders the Elite Yatra hosted checkout inside a native full-screen Modal +
3
+ // WebView, and bridges success/dismiss events back to JS via postMessage.
4
+ import React from 'react';
5
+ import { Modal, View, ActivityIndicator, StyleSheet, BackHandler, Platform } from 'react-native';
6
+ import { WebView } from 'react-native-webview';
7
+
8
+ const DEFAULT_API_BASE = 'https://eliteyatra.vip';
9
+
10
+ function buildUrl(apiBase, params) {
11
+ const base = String(apiBase || DEFAULT_API_BASE).replace(/\/$/, '');
12
+ const q = Object.entries(params)
13
+ .filter(([, v]) => v != null && v !== '')
14
+ .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
15
+ .join('&');
16
+ return `${base}/pay${q ? `?${q}` : ''}`;
17
+ }
18
+
19
+ /**
20
+ * <PayulinkCheckout /> — controlled component.
21
+ *
22
+ * <PayulinkCheckout
23
+ * visible={show}
24
+ * apiBase="https://eliteyatra.vip"
25
+ * orderId={orderId} // SECURE: created by your backend (recommended)
26
+ * // or: amount={19900} item="Tour" // convenience flow
27
+ * publishableKey="pl_pk_live_…"
28
+ * prefill={{ name, phone, email }}
29
+ * onSuccess={(r) => {}} // { order_id, utr, amount }
30
+ * onDismiss={() => setShow(false)}
31
+ * onError={(e) => {}}
32
+ * />
33
+ */
34
+ export default function PayulinkCheckout(props) {
35
+ const {
36
+ visible = true,
37
+ apiBase = DEFAULT_API_BASE,
38
+ orderId,
39
+ amount,
40
+ item,
41
+ publishableKey,
42
+ prefill = {},
43
+ onSuccess,
44
+ onDismiss,
45
+ onError,
46
+ style,
47
+ } = props;
48
+
49
+ const uri = React.useMemo(
50
+ () => buildUrl(apiBase, {
51
+ order_id: orderId,
52
+ key: publishableKey,
53
+ amount: orderId ? undefined : amount,
54
+ item: orderId ? undefined : item,
55
+ name: prefill.name,
56
+ phone: prefill.phone || prefill.contact || prefill.mobile,
57
+ email: prefill.email,
58
+ back: '', // no in-page navigation; the app owns closing
59
+ }),
60
+ [apiBase, orderId, amount, item, publishableKey, prefill.name, prefill.phone, prefill.contact, prefill.mobile, prefill.email]
61
+ );
62
+
63
+ const settled = React.useRef(false);
64
+ React.useEffect(() => { if (visible) settled.current = false; }, [visible]);
65
+
66
+ // Android hardware back closes the sheet.
67
+ React.useEffect(() => {
68
+ if (!visible || Platform.OS !== 'android') return;
69
+ const sub = BackHandler.addEventListener('hardwareBackPress', () => {
70
+ onDismiss && onDismiss();
71
+ return true;
72
+ });
73
+ return () => sub.remove();
74
+ }, [visible, onDismiss]);
75
+
76
+ const handleMessage = (event) => {
77
+ let msg;
78
+ try { msg = JSON.parse(event.nativeEvent.data); } catch { return; }
79
+ switch (msg && msg.type) {
80
+ case 'checkout.success':
81
+ if (settled.current) return;
82
+ settled.current = true;
83
+ onSuccess && onSuccess({ order_id: msg.order_id, utr: msg.utr || null, amount: msg.amount });
84
+ break;
85
+ case 'checkout.dismiss':
86
+ onDismiss && onDismiss();
87
+ break;
88
+ case 'checkout.error':
89
+ onError && onError(new Error(msg.error || 'Checkout error'));
90
+ break;
91
+ default:
92
+ break;
93
+ }
94
+ };
95
+
96
+ return (
97
+ <Modal visible={visible} animationType="slide" transparent={false} onRequestClose={() => onDismiss && onDismiss()}>
98
+ <View style={[styles.fill, style]}>
99
+ <WebView
100
+ source={{ uri }}
101
+ onMessage={handleMessage}
102
+ startInLoadingState
103
+ renderLoading={() => (
104
+ <View style={styles.loading}><ActivityIndicator size="large" /></View>
105
+ )}
106
+ onError={(e) => onError && onError(new Error(e.nativeEvent.description || 'WebView error'))}
107
+ originWhitelist={['*']}
108
+ javaScriptEnabled
109
+ domStorageEnabled
110
+ // Let UPI apps open from upi:// intent links.
111
+ onShouldStartLoadWithRequest={(r) => {
112
+ if (/^(upi|tez|phonepe|paytmmp|gpay|intent):/i.test(r.url)) return true;
113
+ return true;
114
+ }}
115
+ />
116
+ </View>
117
+ </Modal>
118
+ );
119
+ }
120
+
121
+ /**
122
+ * Imperative helper for apps that prefer a promise over a component.
123
+ * Returns { Component, promise }. Render <Component/> once; the promise
124
+ * resolves on success and rejects on dismiss/error.
125
+ *
126
+ * Most apps should just use the <PayulinkCheckout/> component above.
127
+ */
128
+ export function createCheckout(options) {
129
+ let resolve, reject;
130
+ const promise = new Promise((res, rej) => { resolve = res; reject = rej; });
131
+ const Component = (extra) => (
132
+ <PayulinkCheckout
133
+ {...options}
134
+ {...extra}
135
+ onSuccess={(r) => resolve(r)}
136
+ onDismiss={() => reject(new Error('dismissed'))}
137
+ onError={(e) => reject(e)}
138
+ />
139
+ );
140
+ return { Component, promise };
141
+ }
142
+
143
+ const styles = StyleSheet.create({
144
+ fill: { flex: 1, backgroundColor: '#0d1117' },
145
+ loading: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, alignItems: 'center', justifyContent: 'center' },
146
+ });