@inno-isw/react-native-rtp 0.0.0-stage → 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 +20 -0
- package/README.md +385 -2
- package/lib/module/client.js +391 -0
- package/lib/module/client.js.map +1 -0
- package/lib/module/constants.js +49 -0
- package/lib/module/constants.js.map +1 -0
- package/lib/module/emitter.js +46 -0
- package/lib/module/emitter.js.map +1 -0
- package/lib/module/index.js +16 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/mock/makeRtpPayload.js +54 -0
- package/lib/module/mock/makeRtpPayload.js.map +1 -0
- package/lib/module/mock/mockTransport.js +89 -0
- package/lib/module/mock/mockTransport.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/push.js +96 -0
- package/lib/module/push.js.map +1 -0
- package/lib/module/react/RtpProvider.js +152 -0
- package/lib/module/react/RtpProvider.js.map +1 -0
- package/lib/module/types.js +4 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/validate.js +122 -0
- package/lib/module/validate.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/client.d.ts +20 -0
- package/lib/typescript/src/client.d.ts.map +1 -0
- package/lib/typescript/src/constants.d.ts +50 -0
- package/lib/typescript/src/constants.d.ts.map +1 -0
- package/lib/typescript/src/emitter.d.ts +15 -0
- package/lib/typescript/src/emitter.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +14 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/mock/makeRtpPayload.d.ts +17 -0
- package/lib/typescript/src/mock/makeRtpPayload.d.ts.map +1 -0
- package/lib/typescript/src/mock/mockTransport.d.ts +35 -0
- package/lib/typescript/src/mock/mockTransport.d.ts.map +1 -0
- package/lib/typescript/src/push.d.ts +41 -0
- package/lib/typescript/src/push.d.ts.map +1 -0
- package/lib/typescript/src/react/RtpProvider.d.ts +60 -0
- package/lib/typescript/src/react/RtpProvider.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +153 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/lib/typescript/src/validate.d.ts +33 -0
- package/lib/typescript/src/validate.d.ts.map +1 -0
- package/package.json +141 -4
- package/src/client.ts +428 -0
- package/src/constants.ts +54 -0
- package/src/emitter.ts +53 -0
- package/src/index.tsx +45 -0
- package/src/mock/makeRtpPayload.ts +69 -0
- package/src/mock/mockTransport.ts +119 -0
- package/src/push.ts +109 -0
- package/src/react/RtpProvider.tsx +235 -0
- package/src/types.ts +172 -0
- package/src/validate.ts +141 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { RtpHttpResponse, RtpTransport } from '../types';
|
|
2
|
+
|
|
3
|
+
export interface MockTransportOptions {
|
|
4
|
+
/** Simulated network latency in ms. */
|
|
5
|
+
latencyMs?: number;
|
|
6
|
+
/** Simulate a network failure on accept. */
|
|
7
|
+
failAccept?: boolean;
|
|
8
|
+
/** Simulate a network failure on decline. */
|
|
9
|
+
failDecline?: boolean;
|
|
10
|
+
/** Simulate a network failure on retrieve (details fetch). */
|
|
11
|
+
failRetrieve?: boolean;
|
|
12
|
+
/** Backend status to return for accept. */
|
|
13
|
+
acceptStatus?: 'ACCEPTED' | 'DECLINED' | 'FAILED' | 'PENDING' | 'EXPIRED';
|
|
14
|
+
/** Backend status to return for retrieve. Unknown values fall through to READY. */
|
|
15
|
+
retrieveStatus?: string;
|
|
16
|
+
/** Request details to return from retrieve (overlaid onto the held request). */
|
|
17
|
+
retrieveRequest?: Record<string, unknown>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface RecordedCall {
|
|
21
|
+
method: 'post' | 'get';
|
|
22
|
+
path: string;
|
|
23
|
+
body?: unknown;
|
|
24
|
+
headers?: Record<string, string>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface MockTransport {
|
|
28
|
+
transport: RtpTransport;
|
|
29
|
+
/** Recorded calls, for assertions in tests and the example app. */
|
|
30
|
+
calls: RecordedCall[];
|
|
31
|
+
/** Clear recorded calls. */
|
|
32
|
+
reset: () => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const extractId = (path: string): string => {
|
|
36
|
+
const parts = path.split('/');
|
|
37
|
+
return parts[parts.length - 2] ?? '';
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Deterministic in-memory transport. No real network calls are made.
|
|
42
|
+
*/
|
|
43
|
+
export function createMockTransport(
|
|
44
|
+
options: MockTransportOptions = {}
|
|
45
|
+
): MockTransport {
|
|
46
|
+
const {
|
|
47
|
+
latencyMs = 250,
|
|
48
|
+
failAccept = false,
|
|
49
|
+
failDecline = false,
|
|
50
|
+
failRetrieve = false,
|
|
51
|
+
acceptStatus = 'ACCEPTED',
|
|
52
|
+
retrieveStatus = 'READY',
|
|
53
|
+
retrieveRequest,
|
|
54
|
+
} = options;
|
|
55
|
+
|
|
56
|
+
const calls: RecordedCall[] = [];
|
|
57
|
+
const delay = () =>
|
|
58
|
+
new Promise<void>((resolve) => setTimeout(resolve, latencyMs));
|
|
59
|
+
|
|
60
|
+
const transport: RtpTransport = {
|
|
61
|
+
async post<T = any>(
|
|
62
|
+
path: string,
|
|
63
|
+
body?: unknown,
|
|
64
|
+
headers?: Record<string, string>
|
|
65
|
+
): Promise<RtpHttpResponse<T>> {
|
|
66
|
+
calls.push({ method: 'post', path, body, headers });
|
|
67
|
+
await delay();
|
|
68
|
+
|
|
69
|
+
if (path.endsWith('/accept')) {
|
|
70
|
+
if (failAccept) {
|
|
71
|
+
throw new Error('Mock network failure on accept');
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
status: 200,
|
|
75
|
+
data: { id: extractId(path), status: acceptStatus } as T,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (path.endsWith('/decline')) {
|
|
80
|
+
if (failDecline) {
|
|
81
|
+
throw new Error('Mock network failure on decline');
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
status: 200,
|
|
85
|
+
data: { id: extractId(path), status: 'DECLINED' } as T,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return { status: 200, data: {} as T };
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
async get<T = any>(
|
|
93
|
+
path: string,
|
|
94
|
+
headers?: Record<string, string>
|
|
95
|
+
): Promise<RtpHttpResponse<T>> {
|
|
96
|
+
calls.push({ method: 'get', path, headers });
|
|
97
|
+
await delay();
|
|
98
|
+
if (failRetrieve) {
|
|
99
|
+
throw new Error('Mock network failure on retrieve');
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
status: 200,
|
|
103
|
+
data: {
|
|
104
|
+
id: extractId(path),
|
|
105
|
+
status: retrieveStatus,
|
|
106
|
+
request: retrieveRequest,
|
|
107
|
+
} as T,
|
|
108
|
+
};
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
transport,
|
|
114
|
+
calls,
|
|
115
|
+
reset: () => {
|
|
116
|
+
calls.length = 0;
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
package/src/push.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { isRtpPayload } from './validate';
|
|
2
|
+
import type { RtpPayload } from './types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A generic push notification envelope.
|
|
6
|
+
*
|
|
7
|
+
* The SDK does not know or care what a push looks like — every host platform
|
|
8
|
+
* has a different shape, and this is the single seam where that difference is
|
|
9
|
+
* absorbed. A Kotlin/Swift integration implements the equivalent of
|
|
10
|
+
* `mapPushToRtp` against the same `RtpPayload` contract.
|
|
11
|
+
*/
|
|
12
|
+
export interface PushEnvelope {
|
|
13
|
+
id?: string;
|
|
14
|
+
title?: string;
|
|
15
|
+
message?: string;
|
|
16
|
+
foreground?: boolean;
|
|
17
|
+
/** RTCP `onRemoteNotification` style payload. */
|
|
18
|
+
data?: Record<string, unknown>;
|
|
19
|
+
/** RTCP `onNotificationTapped` flattens the body to the top level. */
|
|
20
|
+
body?: string;
|
|
21
|
+
[key: string]: unknown;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Extract an RTP payload from a push envelope, or return null if the push is
|
|
26
|
+
* not an RTP request.
|
|
27
|
+
*
|
|
28
|
+
* RTCP delivers two distinct shapes for the same notification:
|
|
29
|
+
*
|
|
30
|
+
* 1. `onRemoteNotification` (foreground/background):
|
|
31
|
+
* `{ data: { app_data: { type: 'RTP', ... } } }`
|
|
32
|
+
*
|
|
33
|
+
* 2. `onNotificationTapped`: scalars are stringified and the custom payload
|
|
34
|
+
* arrives as a JSON string:
|
|
35
|
+
* `{ data: { app_data: '{"type":"RTP",...}' } }`
|
|
36
|
+
*
|
|
37
|
+
* Both are normalized here so callers only ever deal with `RtpPayload`.
|
|
38
|
+
*
|
|
39
|
+
* `getData` is injectable so hosts that expose push data differently (e.g. a
|
|
40
|
+
* Map-like object on native) can plug in their accessor without changing the
|
|
41
|
+
* rest of the SDK.
|
|
42
|
+
*/
|
|
43
|
+
export function mapPushToRtp(
|
|
44
|
+
envelope: PushEnvelope | null | undefined,
|
|
45
|
+
getData?: (envelope: PushEnvelope) => Record<string, unknown> | undefined
|
|
46
|
+
): RtpPayload | null {
|
|
47
|
+
if (!envelope) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const data = (getData ? getData(envelope) : envelope.data) ?? {};
|
|
52
|
+
|
|
53
|
+
// Some providers nest the custom payload under `app_data` (RTCP/FCM).
|
|
54
|
+
const candidate = unwrapAppData(data);
|
|
55
|
+
|
|
56
|
+
if (!isRtpPayload(candidate)) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return normalizePayload(candidate);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* RTCP/FCM delivers the custom payload as a JSON string under `app_data`.
|
|
65
|
+
* The tapped shape sits one level deeper, under `data` / `body`.
|
|
66
|
+
*/
|
|
67
|
+
function unwrapAppData(data: Record<string, unknown>): Record<string, unknown> {
|
|
68
|
+
let source = data;
|
|
69
|
+
|
|
70
|
+
// Tapped shape: `{ time, app_id, message, read, push_id, app_data }`
|
|
71
|
+
// Received shape: `{ app_data, message, app_id, push_id, time, read }`
|
|
72
|
+
// In both cases `app_data` holds the custom payload.
|
|
73
|
+
const appData = source.app_data;
|
|
74
|
+
if (appData === undefined) {
|
|
75
|
+
return source;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let parsed: Record<string, unknown> = {};
|
|
79
|
+
if (typeof appData === 'string') {
|
|
80
|
+
try {
|
|
81
|
+
parsed = JSON.parse(appData) as Record<string, unknown>;
|
|
82
|
+
} catch {
|
|
83
|
+
return source;
|
|
84
|
+
}
|
|
85
|
+
} else if (appData && typeof appData === 'object') {
|
|
86
|
+
parsed = appData as Record<string, unknown>;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return { ...source, ...parsed };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Push transports stringify scalars. Coerce the fields the SDK cares about so
|
|
94
|
+
* the decoded payload always has stable types (this is what makes the payload
|
|
95
|
+
* portable across platforms).
|
|
96
|
+
*/
|
|
97
|
+
function normalizePayload(payload: Record<string, unknown>): RtpPayload {
|
|
98
|
+
const normalized: Record<string, unknown> = { ...payload };
|
|
99
|
+
|
|
100
|
+
// `amount` may arrive as "250000" on tapped notifications.
|
|
101
|
+
if (typeof normalized.amount === 'string') {
|
|
102
|
+
const parsed = Number(normalized.amount);
|
|
103
|
+
if (!Number.isNaN(parsed)) {
|
|
104
|
+
normalized.amount = parsed;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return normalized as unknown as RtpPayload;
|
|
109
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
createContext,
|
|
3
|
+
useCallback,
|
|
4
|
+
useContext,
|
|
5
|
+
useEffect,
|
|
6
|
+
useMemo,
|
|
7
|
+
useRef,
|
|
8
|
+
useState,
|
|
9
|
+
} from 'react';
|
|
10
|
+
import type {
|
|
11
|
+
RtpClient,
|
|
12
|
+
RtpCredential,
|
|
13
|
+
RtpReceiveOutcome,
|
|
14
|
+
RtpRequest,
|
|
15
|
+
RtpResult,
|
|
16
|
+
} from '../types';
|
|
17
|
+
import { RTP_STATUS } from '../constants';
|
|
18
|
+
import type { RtpStatus } from '../constants';
|
|
19
|
+
|
|
20
|
+
const STORAGE_KEY = 'rtp.pendingPayload';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A minimal persistence port for the provider. The host supplies its own
|
|
24
|
+
* storage (e.g. react-native-default-preference / AsyncStorage) so the SDK
|
|
25
|
+
* stays dependency-free. When omitted, persistence is in-memory only.
|
|
26
|
+
*/
|
|
27
|
+
export interface RtpProviderStorage {
|
|
28
|
+
getItem: (key: string) => Promise<string | null> | string | null;
|
|
29
|
+
setItem: (key: string, value: string) => Promise<void> | void;
|
|
30
|
+
removeItem: (key: string) => Promise<void> | void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface RtpProviderProps {
|
|
34
|
+
/** The client created via `createRtpClient`. */
|
|
35
|
+
client: RtpClient;
|
|
36
|
+
/** Whether the user is currently authenticated. Drives the auth gate. */
|
|
37
|
+
isAuthenticated: boolean;
|
|
38
|
+
/** Optional persistence used to survive cold starts / logged-out flows. */
|
|
39
|
+
storage?: RtpProviderStorage;
|
|
40
|
+
/**
|
|
41
|
+
* Called with the raw decoded payload before it is fed to the client.
|
|
42
|
+
* Use this to persist the payload yourself, log it, etc.
|
|
43
|
+
*/
|
|
44
|
+
onIncomingPayload?: (payload: unknown) => void;
|
|
45
|
+
children: React.ReactNode;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface RtpContextValue {
|
|
49
|
+
/** The active request, if any. */
|
|
50
|
+
request: RtpRequest | null;
|
|
51
|
+
/** The active request's normalized status. */
|
|
52
|
+
status: RtpStatus | null;
|
|
53
|
+
/** True when a request is ready for the user to act on. */
|
|
54
|
+
isReady: boolean;
|
|
55
|
+
/** True while authorization details are being fetched from the backend. */
|
|
56
|
+
isLoading: boolean;
|
|
57
|
+
/** Accept the active request with an opaque credential. */
|
|
58
|
+
accept: (credential?: RtpCredential) => Promise<RtpResult | undefined>;
|
|
59
|
+
/** Decline the active request. */
|
|
60
|
+
decline: (reason?: string) => Promise<RtpResult | undefined>;
|
|
61
|
+
/** Fetch/refresh authorization details for the active request. */
|
|
62
|
+
retrieve: (id?: string) => Promise<RtpResult | undefined>;
|
|
63
|
+
/** Dismiss the active request from the UI. */
|
|
64
|
+
dismiss: () => void;
|
|
65
|
+
/** Feed a raw decoded payload into the client (for host push adapters). */
|
|
66
|
+
receive: (payload: unknown) => Promise<RtpReceiveOutcome | undefined>;
|
|
67
|
+
/** Last terminal result, for rendering feedback. */
|
|
68
|
+
lastResult: RtpResult | null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const RtpContext = createContext<RtpContextValue | null>(null);
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Bridges the framework-agnostic RTP client into React. Owns no UI — hosts
|
|
75
|
+
* render their own bottom sheet / modal and react to `request` + `status`.
|
|
76
|
+
*/
|
|
77
|
+
export function RtpProvider({
|
|
78
|
+
client,
|
|
79
|
+
isAuthenticated,
|
|
80
|
+
storage,
|
|
81
|
+
onIncomingPayload,
|
|
82
|
+
children,
|
|
83
|
+
}: RtpProviderProps) {
|
|
84
|
+
const [request, setRequest] = useState<RtpRequest | null>(null);
|
|
85
|
+
const [status, setStatus] = useState<RtpStatus | null>(null);
|
|
86
|
+
const [lastResult, setLastResult] = useState<RtpResult | null>(null);
|
|
87
|
+
|
|
88
|
+
// Keep the latest request id so async resume/drain can target it.
|
|
89
|
+
const activeIdRef = useRef<string | null>(null);
|
|
90
|
+
|
|
91
|
+
const receive = useCallback(
|
|
92
|
+
async (payload: unknown) => {
|
|
93
|
+
onIncomingPayload?.(payload);
|
|
94
|
+
return client.receive(payload);
|
|
95
|
+
},
|
|
96
|
+
[client, onIncomingPayload]
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
// Subscribe to client events exactly once per client.
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
const offRequest = client.on('request', (req) => {
|
|
102
|
+
activeIdRef.current = req.id;
|
|
103
|
+
setRequest(req);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const offState = client.on('state', ({ id, status: next }) => {
|
|
107
|
+
if (activeIdRef.current && id !== activeIdRef.current) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
setStatus(next);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const offResult = client.on('result', (result) => {
|
|
114
|
+
setLastResult(result);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
return () => {
|
|
118
|
+
offRequest();
|
|
119
|
+
offState();
|
|
120
|
+
offResult();
|
|
121
|
+
};
|
|
122
|
+
}, [client]);
|
|
123
|
+
|
|
124
|
+
// Drain any payload persisted before React mounted (cold start / killed app).
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
let cancelled = false;
|
|
127
|
+
async function drain() {
|
|
128
|
+
if (!storage) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const raw = await storage.getItem(STORAGE_KEY);
|
|
132
|
+
if (!raw || cancelled) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
await storage.removeItem(STORAGE_KEY);
|
|
136
|
+
client.receive(JSON.parse(raw));
|
|
137
|
+
}
|
|
138
|
+
drain();
|
|
139
|
+
return () => {
|
|
140
|
+
cancelled = true;
|
|
141
|
+
};
|
|
142
|
+
}, [client, storage]);
|
|
143
|
+
|
|
144
|
+
// Auth gate: once authenticated, release any AWAITING_AUTH requests.
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
if (isAuthenticated) {
|
|
147
|
+
client.resumeAuth();
|
|
148
|
+
}
|
|
149
|
+
}, [client, isAuthenticated]);
|
|
150
|
+
|
|
151
|
+
const accept = useCallback(
|
|
152
|
+
async (credential?: RtpCredential) => {
|
|
153
|
+
const id = request?.id ?? activeIdRef.current;
|
|
154
|
+
if (!id) {
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
return client.accept({ id, credential });
|
|
158
|
+
},
|
|
159
|
+
[client, request]
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const decline = useCallback(
|
|
163
|
+
async (reason?: string) => {
|
|
164
|
+
const id = request?.id ?? activeIdRef.current;
|
|
165
|
+
if (!id) {
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
return client.decline({ id, reason });
|
|
169
|
+
},
|
|
170
|
+
[client, request]
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
const retrieve = useCallback(
|
|
174
|
+
async (id?: string) => {
|
|
175
|
+
const target = id ?? request?.id ?? activeIdRef.current;
|
|
176
|
+
if (!target) {
|
|
177
|
+
return undefined;
|
|
178
|
+
}
|
|
179
|
+
return client.retrieve(target);
|
|
180
|
+
},
|
|
181
|
+
[client, request]
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
const dismiss = useCallback(() => {
|
|
185
|
+
setRequest(null);
|
|
186
|
+
setStatus(null);
|
|
187
|
+
setLastResult(null);
|
|
188
|
+
activeIdRef.current = null;
|
|
189
|
+
}, []);
|
|
190
|
+
|
|
191
|
+
const isReady = status === RTP_STATUS.READY;
|
|
192
|
+
const isLoading = status === RTP_STATUS.RETRIEVING;
|
|
193
|
+
|
|
194
|
+
const value = useMemo<RtpContextValue>(
|
|
195
|
+
() => ({
|
|
196
|
+
request,
|
|
197
|
+
status,
|
|
198
|
+
isReady,
|
|
199
|
+
isLoading,
|
|
200
|
+
accept,
|
|
201
|
+
decline,
|
|
202
|
+
retrieve,
|
|
203
|
+
dismiss,
|
|
204
|
+
receive,
|
|
205
|
+
lastResult,
|
|
206
|
+
}),
|
|
207
|
+
[
|
|
208
|
+
request,
|
|
209
|
+
status,
|
|
210
|
+
isReady,
|
|
211
|
+
isLoading,
|
|
212
|
+
accept,
|
|
213
|
+
decline,
|
|
214
|
+
retrieve,
|
|
215
|
+
dismiss,
|
|
216
|
+
receive,
|
|
217
|
+
lastResult,
|
|
218
|
+
]
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
return <RtpContext.Provider value={value}>{children}</RtpContext.Provider>;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Access the RTP context. Throws when used outside of an `<RtpProvider>`.
|
|
226
|
+
*/
|
|
227
|
+
export function useRtp(): RtpContextValue {
|
|
228
|
+
const context = useContext(RtpContext);
|
|
229
|
+
if (!context) {
|
|
230
|
+
throw new Error(
|
|
231
|
+
'[react-native-rtp] useRtp must be used within RtpProvider'
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
return context;
|
|
235
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import type { RtpError, RtpStatus } from './constants';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The decoded RTP payload.
|
|
5
|
+
*
|
|
6
|
+
* IMPORTANT: this is the *decoded* payload, not a push notification envelope.
|
|
7
|
+
* Each host platform is responsible for mapping its own push shape (RTCP,
|
|
8
|
+
* raw FCM, APNs, etc.) into this structure via an adapter. This keeps the SDK
|
|
9
|
+
* portable across React Native, Kotlin and Swift.
|
|
10
|
+
*/
|
|
11
|
+
export interface RtpPayload {
|
|
12
|
+
/** Payload schema version. Unknown fields must be ignored, not rejected. */
|
|
13
|
+
version?: string;
|
|
14
|
+
/** Discriminator. Only `'RTP'` payloads are handled by this SDK. */
|
|
15
|
+
type: 'RTP';
|
|
16
|
+
/** Stable, unique id for the request (idempotency key). */
|
|
17
|
+
rtpId: string;
|
|
18
|
+
/** Amount in the currency's minor unit (e.g. kobo). Integer to avoid floats. */
|
|
19
|
+
amount: number;
|
|
20
|
+
/** ISO 4217 currency code. e.g. "NGN". */
|
|
21
|
+
currency: string;
|
|
22
|
+
/** Party being asked to pay. */
|
|
23
|
+
payer: RtpParty;
|
|
24
|
+
/** Party that will receive the money. */
|
|
25
|
+
payee: RtpParty;
|
|
26
|
+
/** Human-readable reason for the request. */
|
|
27
|
+
narration?: string;
|
|
28
|
+
/** Backend reference for the transaction. */
|
|
29
|
+
transactionRef?: string;
|
|
30
|
+
/** Merchant/issuer order number for the request (distinct from transactionRef). */
|
|
31
|
+
orderNumber?: string;
|
|
32
|
+
/** Secure transaction / deep-link reference the host can use to open the flow. */
|
|
33
|
+
deepLink?: string;
|
|
34
|
+
/** Instruction shown to the customer describing the action required. */
|
|
35
|
+
actionMessage?: string;
|
|
36
|
+
/** ISO 8601 timestamp after which the request can no longer be actioned. */
|
|
37
|
+
expiresAt: string;
|
|
38
|
+
/** Alias (phone/email) the request is addressed to, for identity matching. */
|
|
39
|
+
recipient?: string;
|
|
40
|
+
/** Arbitrary passthrough data. Ignored by the SDK. */
|
|
41
|
+
[key: string]: unknown;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** A party on either side of a request (payer or payee). */
|
|
45
|
+
export interface RtpParty {
|
|
46
|
+
name: string;
|
|
47
|
+
accountNumber?: string;
|
|
48
|
+
bank?: string;
|
|
49
|
+
/** Optional display URL for a logo/avatar. */
|
|
50
|
+
logoUrl?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** A normalized, validated request held by the SDK. */
|
|
54
|
+
export interface RtpRequest {
|
|
55
|
+
id: string;
|
|
56
|
+
amount: number;
|
|
57
|
+
currency: string;
|
|
58
|
+
payer: RtpParty;
|
|
59
|
+
payee: RtpParty;
|
|
60
|
+
narration?: string;
|
|
61
|
+
transactionRef?: string;
|
|
62
|
+
orderNumber?: string;
|
|
63
|
+
deepLink?: string;
|
|
64
|
+
actionMessage?: string;
|
|
65
|
+
expiresAt: string;
|
|
66
|
+
recipient?: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Credential produced by the host platform to authorise an accept.
|
|
71
|
+
* The SDK never inspects this — it is opaque and forwarded to the backend.
|
|
72
|
+
*
|
|
73
|
+
* For React Native this is typically `{ secureData, pinBlock, authData }`
|
|
74
|
+
* produced by the app's card/PIN service.
|
|
75
|
+
*/
|
|
76
|
+
export type RtpCredential = Record<string, unknown>;
|
|
77
|
+
|
|
78
|
+
export interface RtpResult {
|
|
79
|
+
id: string;
|
|
80
|
+
status: RtpStatus;
|
|
81
|
+
error?: RtpError | string;
|
|
82
|
+
data?: unknown;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Outcome of feeding a raw payload into the client. */
|
|
86
|
+
export interface RtpReceiveOutcome {
|
|
87
|
+
/** False when the payload was not an RTP payload. */
|
|
88
|
+
handled: boolean;
|
|
89
|
+
request?: RtpRequest;
|
|
90
|
+
status?: RtpStatus;
|
|
91
|
+
error?: RtpError | string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// --- Injected ports (the portability seams) -------------------------------
|
|
95
|
+
|
|
96
|
+
export interface RtpHttpResponse<T = any> {
|
|
97
|
+
data?: T;
|
|
98
|
+
status?: number;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Minimal HTTP contract. Hosts inject an implementation backed by their own
|
|
103
|
+
* client (`ky`, `axios`, native, ...). The SDK never imports a network library.
|
|
104
|
+
*/
|
|
105
|
+
export interface RtpTransport {
|
|
106
|
+
post: <T = any>(
|
|
107
|
+
path: string,
|
|
108
|
+
body?: unknown,
|
|
109
|
+
headers?: Record<string, string>
|
|
110
|
+
) => Promise<RtpHttpResponse<T>>;
|
|
111
|
+
get: <T = any>(
|
|
112
|
+
path: string,
|
|
113
|
+
headers?: Record<string, string>
|
|
114
|
+
) => Promise<RtpHttpResponse<T>>;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Key/value storage for persistence across cold starts and auth gates. */
|
|
118
|
+
export interface RtpStorage {
|
|
119
|
+
getItem: (key: string) => Promise<string | null> | string | null;
|
|
120
|
+
setItem: (key: string, value: string) => Promise<void> | void;
|
|
121
|
+
removeItem: (key: string) => Promise<void> | void;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface RtpClientOptions {
|
|
125
|
+
/** Injected HTTP client. */
|
|
126
|
+
transport: RtpTransport;
|
|
127
|
+
/**
|
|
128
|
+
* Returns auth headers for an outgoing request. Called per request so the
|
|
129
|
+
* host can refresh tokens / compute MACs.
|
|
130
|
+
*/
|
|
131
|
+
getAuthHeaders?: () =>
|
|
132
|
+
Promise<Record<string, string>> | Record<string, string>;
|
|
133
|
+
/**
|
|
134
|
+
* Returns the currently authenticated user's alias (phone/email), or null
|
|
135
|
+
* when logged out. Used for the auth gate and recipient matching.
|
|
136
|
+
*/
|
|
137
|
+
getCurrentUser?: () => string | null | undefined;
|
|
138
|
+
/** Injectable clock (ms since epoch) for deterministic expiry tests. */
|
|
139
|
+
now?: () => number;
|
|
140
|
+
/** Optional sink for diagnostics. */
|
|
141
|
+
logger?: (...args: unknown[]) => void;
|
|
142
|
+
/** Endpoint base path for RTP routes. Defaults to `/rtp`. */
|
|
143
|
+
basePath?: string;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export type RtpUnsubscribe = () => void;
|
|
147
|
+
|
|
148
|
+
export interface RtpEventMap {
|
|
149
|
+
request: (request: RtpRequest) => void;
|
|
150
|
+
state: (event: { id: string; status: RtpStatus }) => void;
|
|
151
|
+
result: (result: RtpResult) => void;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export interface RtpClient {
|
|
155
|
+
isRtp: (payload: unknown) => payload is RtpPayload;
|
|
156
|
+
receive: (payload: unknown) => Promise<RtpReceiveOutcome>;
|
|
157
|
+
/** Fetch/refresh authorization details for a request from the backend. */
|
|
158
|
+
retrieve: (id: string) => Promise<RtpResult>;
|
|
159
|
+
resumeAuth: () => void;
|
|
160
|
+
accept: (params: {
|
|
161
|
+
id: string;
|
|
162
|
+
credential?: RtpCredential;
|
|
163
|
+
}) => Promise<RtpResult>;
|
|
164
|
+
decline: (params: { id: string; reason?: string }) => Promise<RtpResult>;
|
|
165
|
+
getStatus: (id: string) => RtpStatus | undefined;
|
|
166
|
+
getRequest: (id: string) => RtpRequest | undefined;
|
|
167
|
+
acknowledge: (id: string) => void;
|
|
168
|
+
on: <E extends keyof RtpEventMap>(
|
|
169
|
+
event: E,
|
|
170
|
+
handler: RtpEventMap[E]
|
|
171
|
+
) => RtpUnsubscribe;
|
|
172
|
+
}
|