react-native-iinstall 0.2.12 → 0.2.14
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/INTEGRATION_GUIDE.md +43 -5
- package/README.md +37 -2
- package/RELEASE_MANIFEST.json +8 -0
- package/lib/IInstallWrapper.d.ts +4 -0
- package/lib/IInstallWrapper.js +2 -2
- package/lib/index.d.ts +5 -0
- package/lib/index.js +56 -2
- package/lib/pushRegistration.d.ts +22 -0
- package/lib/pushRegistration.js +70 -0
- package/lib/scripts/check-release-manifest.d.mts +2 -0
- package/lib/scripts/check-release-manifest.mjs +38 -0
- package/lib/scripts/sync-release-manifest.d.mts +2 -0
- package/lib/scripts/sync-release-manifest.mjs +38 -0
- package/lib/src/FeedbackModal.d.ts +12 -0
- package/lib/src/FeedbackModal.js +393 -0
- package/lib/src/IInstallWrapper.d.ts +16 -0
- package/lib/src/IInstallWrapper.js +18 -0
- package/lib/src/ShakeDetector.d.ts +8 -0
- package/lib/src/ShakeDetector.js +68 -0
- package/lib/src/index.d.ts +17 -0
- package/lib/src/index.js +349 -0
- package/lib/src/nativeModules.d.ts +37 -0
- package/lib/src/nativeModules.js +231 -0
- package/lib/src/pushRegistration.d.ts +22 -0
- package/lib/src/pushRegistration.js +70 -0
- package/package.json +18 -8
- package/src/FeedbackModal.tsx +46 -41
- package/src/IInstallWrapper.tsx +12 -0
- package/src/index.tsx +92 -8
- package/src/nativeModules.ts +308 -0
- package/src/pushRegistration.ts +111 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { Platform } from 'react-native';
|
|
2
|
+
|
|
3
|
+
type CaptureScreenOptions = {
|
|
4
|
+
format?: 'png' | 'jpg' | 'webm' | 'raw';
|
|
5
|
+
quality?: number;
|
|
6
|
+
result?: 'tmpfile' | 'base64' | 'data-uri' | 'zip-base64';
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
type DeviceInfoModule = {
|
|
10
|
+
getModel?: () => string;
|
|
11
|
+
getSystemName?: () => string;
|
|
12
|
+
getSystemVersion?: () => string;
|
|
13
|
+
getVersion?: () => string;
|
|
14
|
+
getBuildNumber?: () => string;
|
|
15
|
+
getBrand?: () => string;
|
|
16
|
+
isEmulator?: () => Promise<boolean>;
|
|
17
|
+
getUniqueId?: () => Promise<string>;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type ViewShotModule = {
|
|
21
|
+
captureScreen?: (options?: CaptureScreenOptions) => Promise<string>;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type RecordScreenModule = {
|
|
25
|
+
startRecording?: (config?: { mic?: boolean; fps?: number; bitrate?: number }) => Promise<unknown>;
|
|
26
|
+
stopRecording?: () => Promise<unknown>;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
type AudioRecorderModule = {
|
|
30
|
+
default?: new () => AudioRecorderPlayerLike;
|
|
31
|
+
AVEncodingOption?: { aac?: string };
|
|
32
|
+
AVEncoderAudioQualityIOSType?: { high?: number };
|
|
33
|
+
AudioEncoderAndroidType?: { AAC?: number };
|
|
34
|
+
AudioSourceAndroidType?: { MIC?: number };
|
|
35
|
+
OutputFormatAndroidType?: { MPEG_4?: number };
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
type RecordBackEvent = {
|
|
39
|
+
currentPosition: number;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type AudioRecorderPlayerLike = {
|
|
43
|
+
startRecorder: (uri?: string, audioSets?: Record<string, unknown>) => Promise<string>;
|
|
44
|
+
stopRecorder: () => Promise<string>;
|
|
45
|
+
addRecordBackListener: (callback: (recordingMeta: RecordBackEvent) => void) => void;
|
|
46
|
+
removeRecordBackListener: () => void;
|
|
47
|
+
mmssss: (milisecs: number) => string;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export type DeviceMetadata = {
|
|
51
|
+
device: string;
|
|
52
|
+
systemName: string;
|
|
53
|
+
systemVersion: string;
|
|
54
|
+
appVersion: string;
|
|
55
|
+
buildNumber: string;
|
|
56
|
+
brand: string;
|
|
57
|
+
isEmulator: boolean;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const warnedKeys = new Set<string>();
|
|
61
|
+
|
|
62
|
+
function warnOnce(key: string, message: string) {
|
|
63
|
+
if (warnedKeys.has(key)) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
warnedKeys.add(key);
|
|
67
|
+
console.warn(message);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function getDefaultExport<T>(moduleValue: unknown): T | null {
|
|
71
|
+
if (!moduleValue) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
if (typeof moduleValue === 'object' && moduleValue !== null && 'default' in moduleValue) {
|
|
75
|
+
return (moduleValue as { default: T }).default;
|
|
76
|
+
}
|
|
77
|
+
return moduleValue as T;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function loadRawModule<T>(moduleName: string): T | null {
|
|
81
|
+
try {
|
|
82
|
+
return require(moduleName) as T;
|
|
83
|
+
} catch (error) {
|
|
84
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
85
|
+
warnOnce(
|
|
86
|
+
`missing:${moduleName}`,
|
|
87
|
+
`[iInstall SDK] Missing native dependency "${moduleName}". Install it in your app root and rebuild. (${reason})`
|
|
88
|
+
);
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let cachedDeviceInfoModule: DeviceInfoModule | null | undefined;
|
|
94
|
+
let cachedCaptureScreen: ((options?: CaptureScreenOptions) => Promise<string>) | null | undefined;
|
|
95
|
+
let cachedRecordScreenModule: RecordScreenModule | null | undefined;
|
|
96
|
+
let cachedAudioModule: AudioRecorderModule | null | undefined;
|
|
97
|
+
|
|
98
|
+
function getDeviceInfoModule(): DeviceInfoModule | null {
|
|
99
|
+
if (cachedDeviceInfoModule !== undefined) {
|
|
100
|
+
return cachedDeviceInfoModule;
|
|
101
|
+
}
|
|
102
|
+
const required = loadRawModule<unknown>('react-native-device-info');
|
|
103
|
+
cachedDeviceInfoModule = required ? getDefaultExport<DeviceInfoModule>(required) : null;
|
|
104
|
+
return cachedDeviceInfoModule;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function getCaptureScreenFn(): ((options?: CaptureScreenOptions) => Promise<string>) | null {
|
|
108
|
+
if (cachedCaptureScreen !== undefined) {
|
|
109
|
+
return cachedCaptureScreen;
|
|
110
|
+
}
|
|
111
|
+
const required = loadRawModule<ViewShotModule>('react-native-view-shot');
|
|
112
|
+
cachedCaptureScreen = required?.captureScreen ?? null;
|
|
113
|
+
if (!cachedCaptureScreen) {
|
|
114
|
+
warnOnce(
|
|
115
|
+
'missing:react-native-view-shot:captureScreen',
|
|
116
|
+
'[iInstall SDK] Screenshot capture unavailable: react-native-view-shot is not linked correctly.'
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
return cachedCaptureScreen;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function getRecordScreenModule(): RecordScreenModule | null {
|
|
123
|
+
if (cachedRecordScreenModule !== undefined) {
|
|
124
|
+
return cachedRecordScreenModule;
|
|
125
|
+
}
|
|
126
|
+
const required = loadRawModule<unknown>('react-native-record-screen');
|
|
127
|
+
cachedRecordScreenModule = required ? getDefaultExport<RecordScreenModule>(required) : null;
|
|
128
|
+
return cachedRecordScreenModule;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function getAudioRecorderModule(): AudioRecorderModule | null {
|
|
132
|
+
if (cachedAudioModule !== undefined) {
|
|
133
|
+
return cachedAudioModule;
|
|
134
|
+
}
|
|
135
|
+
cachedAudioModule = loadRawModule<AudioRecorderModule>('react-native-audio-recorder-player');
|
|
136
|
+
return cachedAudioModule;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function fallbackString(value: string | undefined, defaultValue: string): string {
|
|
140
|
+
if (!value || value.trim().length === 0) {
|
|
141
|
+
return defaultValue;
|
|
142
|
+
}
|
|
143
|
+
return value;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export async function isEmulatorDevice(): Promise<boolean> {
|
|
147
|
+
const moduleRef = getDeviceInfoModule();
|
|
148
|
+
if (!moduleRef?.isEmulator) {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
return await moduleRef.isEmulator();
|
|
153
|
+
} catch {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export async function getDeviceUniqueId(): Promise<string | undefined> {
|
|
159
|
+
const moduleRef = getDeviceInfoModule();
|
|
160
|
+
if (!moduleRef?.getUniqueId) {
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
try {
|
|
164
|
+
const value = await moduleRef.getUniqueId();
|
|
165
|
+
return fallbackString(value, 'unknown');
|
|
166
|
+
} catch {
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export async function getDeviceMetadata(): Promise<DeviceMetadata> {
|
|
172
|
+
const moduleRef = getDeviceInfoModule();
|
|
173
|
+
const systemName = Platform.OS === 'ios' ? 'iOS' : Platform.OS;
|
|
174
|
+
const systemVersion = String(Platform.Version ?? 'unknown');
|
|
175
|
+
|
|
176
|
+
const metadata: DeviceMetadata = {
|
|
177
|
+
device: 'Unknown Device',
|
|
178
|
+
systemName,
|
|
179
|
+
systemVersion,
|
|
180
|
+
appVersion: 'unknown',
|
|
181
|
+
buildNumber: 'unknown',
|
|
182
|
+
brand: Platform.OS === 'ios' ? 'Apple' : 'Unknown',
|
|
183
|
+
isEmulator: false,
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
if (!moduleRef) {
|
|
187
|
+
return metadata;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
metadata.device = fallbackString(moduleRef.getModel?.(), metadata.device);
|
|
192
|
+
} catch {
|
|
193
|
+
// noop
|
|
194
|
+
}
|
|
195
|
+
try {
|
|
196
|
+
metadata.systemName = fallbackString(moduleRef.getSystemName?.(), metadata.systemName);
|
|
197
|
+
} catch {
|
|
198
|
+
// noop
|
|
199
|
+
}
|
|
200
|
+
try {
|
|
201
|
+
metadata.systemVersion = fallbackString(moduleRef.getSystemVersion?.(), metadata.systemVersion);
|
|
202
|
+
} catch {
|
|
203
|
+
// noop
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
metadata.appVersion = fallbackString(moduleRef.getVersion?.(), metadata.appVersion);
|
|
207
|
+
} catch {
|
|
208
|
+
// noop
|
|
209
|
+
}
|
|
210
|
+
try {
|
|
211
|
+
metadata.buildNumber = fallbackString(moduleRef.getBuildNumber?.(), metadata.buildNumber);
|
|
212
|
+
} catch {
|
|
213
|
+
// noop
|
|
214
|
+
}
|
|
215
|
+
try {
|
|
216
|
+
metadata.brand = fallbackString(moduleRef.getBrand?.(), metadata.brand);
|
|
217
|
+
} catch {
|
|
218
|
+
// noop
|
|
219
|
+
}
|
|
220
|
+
try {
|
|
221
|
+
metadata.isEmulator = moduleRef.isEmulator ? await moduleRef.isEmulator() : false;
|
|
222
|
+
} catch {
|
|
223
|
+
metadata.isEmulator = false;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return metadata;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function hasScreenRecordingSupport(): boolean {
|
|
230
|
+
const moduleRef = getRecordScreenModule();
|
|
231
|
+
return Boolean(moduleRef?.startRecording && moduleRef?.stopRecording);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export async function startScreenRecording(config: { mic?: boolean } = { mic: true }): Promise<void> {
|
|
235
|
+
const moduleRef = getRecordScreenModule();
|
|
236
|
+
if (!moduleRef?.startRecording) {
|
|
237
|
+
throw new Error(
|
|
238
|
+
'Screen recording dependency missing. Install react-native-record-screen and rebuild the app.'
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
await moduleRef.startRecording(config);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export async function stopScreenRecording(): Promise<unknown> {
|
|
245
|
+
const moduleRef = getRecordScreenModule();
|
|
246
|
+
if (!moduleRef?.stopRecording) {
|
|
247
|
+
throw new Error(
|
|
248
|
+
'Screen recording dependency missing. Install react-native-record-screen and rebuild the app.'
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
return moduleRef.stopRecording();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export async function captureScreenImage(options?: CaptureScreenOptions): Promise<string> {
|
|
255
|
+
const captureScreen = getCaptureScreenFn();
|
|
256
|
+
if (!captureScreen) {
|
|
257
|
+
throw new Error(
|
|
258
|
+
'Screenshot dependency missing. Install react-native-view-shot and rebuild the app.'
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
return captureScreen(options);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function hasAudioRecordingSupport(): boolean {
|
|
265
|
+
const moduleRef = getAudioRecorderModule();
|
|
266
|
+
const recorderCtor = moduleRef?.default;
|
|
267
|
+
return typeof recorderCtor === 'function';
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function createAudioRecorderPlayer(): AudioRecorderPlayerLike | null {
|
|
271
|
+
const moduleRef = getAudioRecorderModule();
|
|
272
|
+
const recorderCtor = moduleRef?.default;
|
|
273
|
+
if (typeof recorderCtor !== 'function') {
|
|
274
|
+
warnOnce(
|
|
275
|
+
'missing:react-native-audio-recorder-player:ctor',
|
|
276
|
+
'[iInstall SDK] Audio recording unavailable: react-native-audio-recorder-player is not linked correctly.'
|
|
277
|
+
);
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
try {
|
|
282
|
+
return new recorderCtor();
|
|
283
|
+
} catch (error) {
|
|
284
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
285
|
+
warnOnce(
|
|
286
|
+
'missing:react-native-audio-recorder-player:new',
|
|
287
|
+
`[iInstall SDK] Failed to initialize audio recorder. (${reason})`
|
|
288
|
+
);
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export function getAudioRecordingPreset(): Record<string, string | number> {
|
|
294
|
+
const moduleRef = getAudioRecorderModule();
|
|
295
|
+
return {
|
|
296
|
+
AVFormatIDKeyIOS: moduleRef?.AVEncodingOption?.aac ?? 'aac',
|
|
297
|
+
AVEncoderAudioQualityKeyIOS: moduleRef?.AVEncoderAudioQualityIOSType?.high ?? 96,
|
|
298
|
+
AVSampleRateKeyIOS: 44100,
|
|
299
|
+
AVNumberOfChannelsKeyIOS: 1,
|
|
300
|
+
AVEncoderBitRateKeyIOS: 128000,
|
|
301
|
+
AudioEncoderAndroid: moduleRef?.AudioEncoderAndroidType?.AAC ?? 3,
|
|
302
|
+
AudioSourceAndroid: moduleRef?.AudioSourceAndroidType?.MIC ?? 1,
|
|
303
|
+
OutputFormatAndroid: moduleRef?.OutputFormatAndroidType?.MPEG_4 ?? 2,
|
|
304
|
+
AudioEncodingBitRateAndroid: 128000,
|
|
305
|
+
AudioSamplingRateAndroid: 44100,
|
|
306
|
+
AudioChannelsAndroid: 1,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { Platform } from 'react-native';
|
|
2
|
+
|
|
3
|
+
export type PushPlatform = 'IOS' | 'ANDROID';
|
|
4
|
+
|
|
5
|
+
export interface RegisterPushTokenParams {
|
|
6
|
+
token: string;
|
|
7
|
+
apiKey: string;
|
|
8
|
+
apiEndpoint?: string;
|
|
9
|
+
deviceUdid?: string;
|
|
10
|
+
projectId?: string;
|
|
11
|
+
platform?: PushPlatform;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface UnregisterPushTokenParams {
|
|
15
|
+
token: string;
|
|
16
|
+
apiKey: string;
|
|
17
|
+
apiEndpoint?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface PushRegistrationResult {
|
|
21
|
+
success: boolean;
|
|
22
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
23
|
+
data?: any;
|
|
24
|
+
error?: string;
|
|
25
|
+
status: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function normalizeApiEndpoint(apiEndpoint: string): string {
|
|
29
|
+
return apiEndpoint.replace(/\/+$/, '');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function resolvePlatform(platform?: PushPlatform): PushPlatform {
|
|
33
|
+
if (platform) return platform;
|
|
34
|
+
return Platform.OS === 'ios' ? 'IOS' : 'ANDROID';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function registerPushToken({
|
|
38
|
+
token,
|
|
39
|
+
apiKey,
|
|
40
|
+
apiEndpoint = 'https://iinstall.app',
|
|
41
|
+
deviceUdid,
|
|
42
|
+
projectId,
|
|
43
|
+
platform,
|
|
44
|
+
}: RegisterPushTokenParams): Promise<PushRegistrationResult> {
|
|
45
|
+
try {
|
|
46
|
+
const response = await fetch(
|
|
47
|
+
`${normalizeApiEndpoint(apiEndpoint)}/api/notifications/push/register`,
|
|
48
|
+
{
|
|
49
|
+
method: 'POST',
|
|
50
|
+
headers: { 'Content-Type': 'application/json' },
|
|
51
|
+
body: JSON.stringify({
|
|
52
|
+
token,
|
|
53
|
+
apiKey,
|
|
54
|
+
deviceUdid,
|
|
55
|
+
projectId,
|
|
56
|
+
platform: resolvePlatform(platform),
|
|
57
|
+
}),
|
|
58
|
+
}
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
62
|
+
const data: any = await response.json().catch(() => ({}));
|
|
63
|
+
return {
|
|
64
|
+
success: response.ok,
|
|
65
|
+
status: response.status,
|
|
66
|
+
data,
|
|
67
|
+
error: response.ok ? undefined : data?.error || 'Failed to register push token',
|
|
68
|
+
};
|
|
69
|
+
} catch (error) {
|
|
70
|
+
return {
|
|
71
|
+
success: false,
|
|
72
|
+
status: 500,
|
|
73
|
+
error: error instanceof Error ? error.message : 'Unknown register push error',
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function unregisterPushToken({
|
|
79
|
+
token,
|
|
80
|
+
apiKey,
|
|
81
|
+
apiEndpoint = 'https://iinstall.app',
|
|
82
|
+
}: UnregisterPushTokenParams): Promise<PushRegistrationResult> {
|
|
83
|
+
try {
|
|
84
|
+
const response = await fetch(
|
|
85
|
+
`${normalizeApiEndpoint(apiEndpoint)}/api/notifications/push/register`,
|
|
86
|
+
{
|
|
87
|
+
method: 'DELETE',
|
|
88
|
+
headers: { 'Content-Type': 'application/json' },
|
|
89
|
+
body: JSON.stringify({
|
|
90
|
+
token,
|
|
91
|
+
apiKey,
|
|
92
|
+
}),
|
|
93
|
+
}
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
97
|
+
const data: any = await response.json().catch(() => ({}));
|
|
98
|
+
return {
|
|
99
|
+
success: response.ok,
|
|
100
|
+
status: response.status,
|
|
101
|
+
data,
|
|
102
|
+
error: response.ok ? undefined : data?.error || 'Failed to unregister push token',
|
|
103
|
+
};
|
|
104
|
+
} catch (error) {
|
|
105
|
+
return {
|
|
106
|
+
success: false,
|
|
107
|
+
status: 500,
|
|
108
|
+
error: error instanceof Error ? error.message : 'Unknown unregister push error',
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
}
|