@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 +96 -0
- package/package.json +26 -0
- package/src/index.d.ts +41 -0
- package/src/index.js +146 -0
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
|
+
});
|