@regantis-sdk/react-native-chat 0.1.2 → 0.1.4
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 +27 -7
- package/RegantisReactNativeChat.podspec +1 -1
- package/android/build.gradle +1 -0
- package/android/src/main/AndroidManifest.xml +22 -1
- package/android/src/main/java/technology/regantis/chat/RegantisChatNativeModule.java +161 -55
- package/android/src/main/res/xml/regantis_chat_file_paths.xml +4 -0
- package/assets/regantis_logo_black.png +0 -0
- package/assets/regantis_logo_white.png +0 -0
- package/assets/soft_glow.png +0 -0
- package/index.d.ts +2 -1
- package/ios/RegantisChatNative.h +1 -1
- package/ios/RegantisChatNative.m +113 -32
- package/package.json +2 -1
- package/src/RegantisChat.js +572 -55
- package/src/client.js +19 -4
- package/src/defaults.js +1 -1
- package/src/native.js +5 -5
package/README.md
CHANGED
|
@@ -25,6 +25,17 @@ pod install
|
|
|
25
25
|
|
|
26
26
|
Android is autolinked by React Native.
|
|
27
27
|
|
|
28
|
+
### iOS camera
|
|
29
|
+
|
|
30
|
+
To use the built-in camera action, add a camera usage description to the host app `Info.plist`:
|
|
31
|
+
|
|
32
|
+
```xml
|
|
33
|
+
<key>NSCameraUsageDescription</key>
|
|
34
|
+
<string>Take photos to attach to support chat messages.</string>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Android declares `CAMERA` in the SDK manifest and requests the permission only when the visitor chooses **Take photo**. If permission was denied permanently, the SDK offers a shortcut to the app settings.
|
|
38
|
+
|
|
28
39
|
## Basic usage
|
|
29
40
|
|
|
30
41
|
```jsx
|
|
@@ -112,19 +123,20 @@ Server configuration remains the default. Per-app overrides are optional:
|
|
|
112
123
|
- WebSocket realtime updates with HTTP polling fallback
|
|
113
124
|
- typing state
|
|
114
125
|
- customer-side translation selector
|
|
115
|
-
-
|
|
116
|
-
-
|
|
117
|
-
- native incoming-message sound
|
|
126
|
+
- image gallery, file picker and direct camera capture with runtime permission handling
|
|
127
|
+
- in-chat image lightbox with pinch zoom, pan, reset and +/- zoom controls
|
|
128
|
+
- native incoming-message sound with persisted user preference
|
|
118
129
|
- close/reopen conversation
|
|
119
130
|
- rating and rating comment
|
|
120
131
|
- transcript email flow
|
|
121
132
|
- read state and unread callback
|
|
122
133
|
- light/dark mode
|
|
123
|
-
- basic/advanced color configuration
|
|
124
|
-
- logo, agent photo toggle and white-label behavior
|
|
134
|
+
- basic/advanced color configuration including background opacity
|
|
135
|
+
- custom logo with fallback, agent photo toggle, profile-name display and white-label behavior
|
|
136
|
+
- mobile welcome screen with the same soft blurred-glow treatment as the web widget
|
|
125
137
|
- CRM-managed localized UI texts
|
|
126
138
|
|
|
127
|
-
The web-only launcher settings such as desktop/mobile corner placement and minimized bubble/bar mode are still returned in the settings object for configuration parity, but they do not change a full-page React Native component because there is no browser launcher to position.
|
|
139
|
+
The web-only launcher settings such as desktop/mobile corner placement, launcher visibility and minimized bubble/bar mode are still returned in the settings object for configuration parity, but they do not change a full-page React Native component because there is no browser launcher to position. `before_you_go_enabled` is also intentionally not used in native because that feature is a desktop mouse-exit prompt in the web loader.
|
|
128
140
|
|
|
129
141
|
## Headless client
|
|
130
142
|
|
|
@@ -167,4 +179,12 @@ The React Native component now mirrors the mobile web widget flow: it opens on t
|
|
|
167
179
|
/>
|
|
168
180
|
```
|
|
169
181
|
|
|
170
|
-
Use `initialView="chat"` when an application should open directly in the conversation. `onClose` can be supplied when the host app wants the widget-style minimize action to navigate away from the chat page.
|
|
182
|
+
Use `initialView="chat"` when an application should open directly in the conversation. `onClose` can be supplied when the host app wants the widget-style minimize action to navigate away from the chat page. `onBack` controls the top-left back button on the welcome screen.
|
|
183
|
+
|
|
184
|
+
```jsx
|
|
185
|
+
<RegantisChat
|
|
186
|
+
apiKey="YOUR_REACT_NATIVE_CHAT_API_KEY"
|
|
187
|
+
onBack={() => navigation.goBack()}
|
|
188
|
+
onClose={() => navigation.goBack()}
|
|
189
|
+
/>
|
|
190
|
+
```
|
|
@@ -12,6 +12,6 @@ Pod::Spec.new do |s|
|
|
|
12
12
|
s.platforms = { :ios => "15.1" }
|
|
13
13
|
s.source = { :git => "https://github.com/regantis/react-native-chat.git", :tag => "#{s.version}" }
|
|
14
14
|
s.source_files = "ios/**/*.{h,m,mm}"
|
|
15
|
-
s.frameworks = "AudioToolbox", "UniformTypeIdentifiers"
|
|
15
|
+
s.frameworks = "AudioToolbox", "AVFoundation", "UniformTypeIdentifiers"
|
|
16
16
|
s.dependency "React-Core"
|
|
17
17
|
end
|
package/android/build.gradle
CHANGED
|
@@ -1 +1,22 @@
|
|
|
1
|
-
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
1
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
2
|
+
<uses-permission android:name="android.permission.CAMERA" />
|
|
3
|
+
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
|
4
|
+
|
|
5
|
+
<queries>
|
|
6
|
+
<intent>
|
|
7
|
+
<action android:name="android.media.action.IMAGE_CAPTURE" />
|
|
8
|
+
</intent>
|
|
9
|
+
</queries>
|
|
10
|
+
|
|
11
|
+
<application>
|
|
12
|
+
<provider
|
|
13
|
+
android:name="androidx.core.content.FileProvider"
|
|
14
|
+
android:authorities="${applicationId}.regantis.chat.fileprovider"
|
|
15
|
+
android:exported="false"
|
|
16
|
+
android:grantUriPermissions="true">
|
|
17
|
+
<meta-data
|
|
18
|
+
android:name="android.support.FILE_PROVIDER_PATHS"
|
|
19
|
+
android:resource="@xml/regantis_chat_file_paths" />
|
|
20
|
+
</provider>
|
|
21
|
+
</application>
|
|
22
|
+
</manifest>
|
|
@@ -1,21 +1,25 @@
|
|
|
1
1
|
package technology.regantis.chat;
|
|
2
2
|
|
|
3
|
+
import android.Manifest;
|
|
3
4
|
import android.app.Activity;
|
|
5
|
+
import android.content.ClipData;
|
|
4
6
|
import android.content.Context;
|
|
5
7
|
import android.content.Intent;
|
|
6
8
|
import android.content.SharedPreferences;
|
|
7
|
-
import android.
|
|
8
|
-
import android.
|
|
9
|
+
import android.content.pm.PackageManager;
|
|
10
|
+
import android.content.pm.ResolveInfo;
|
|
9
11
|
import android.database.Cursor;
|
|
10
12
|
import android.media.Ringtone;
|
|
11
13
|
import android.media.RingtoneManager;
|
|
12
14
|
import android.net.Uri;
|
|
15
|
+
import android.provider.MediaStore;
|
|
13
16
|
import android.provider.OpenableColumns;
|
|
14
|
-
import android.view.View;
|
|
15
17
|
import android.webkit.MimeTypeMap;
|
|
16
18
|
|
|
17
19
|
import androidx.annotation.NonNull;
|
|
18
20
|
import androidx.annotation.Nullable;
|
|
21
|
+
import androidx.core.content.ContextCompat;
|
|
22
|
+
import androidx.core.content.FileProvider;
|
|
19
23
|
|
|
20
24
|
import com.facebook.react.bridge.ActivityEventListener;
|
|
21
25
|
import com.facebook.react.bridge.Arguments;
|
|
@@ -25,46 +29,93 @@ import com.facebook.react.bridge.ReactApplicationContext;
|
|
|
25
29
|
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
|
26
30
|
import com.facebook.react.bridge.ReactMethod;
|
|
27
31
|
import com.facebook.react.bridge.WritableMap;
|
|
32
|
+
import com.facebook.react.modules.core.PermissionAwareActivity;
|
|
33
|
+
import com.facebook.react.modules.core.PermissionListener;
|
|
28
34
|
|
|
29
35
|
import java.io.File;
|
|
30
|
-
import java.
|
|
36
|
+
import java.util.List;
|
|
31
37
|
|
|
32
38
|
public class RegantisChatNativeModule extends ReactContextBaseJavaModule {
|
|
33
39
|
private static final String MODULE_NAME = "RegantisChatNative";
|
|
34
40
|
private static final String STORAGE_NAME = "regantis_react_native_chat";
|
|
35
41
|
private static final int PICK_ATTACHMENT_REQUEST = 17841;
|
|
42
|
+
private static final int TAKE_PHOTO_REQUEST = 17842;
|
|
43
|
+
private static final int CAMERA_PERMISSION_REQUEST = 17843;
|
|
36
44
|
|
|
37
45
|
private final ReactApplicationContext reactContext;
|
|
38
46
|
private Promise pickerPromise;
|
|
47
|
+
private Promise cameraPromise;
|
|
48
|
+
private Promise cameraPermissionPromise;
|
|
49
|
+
private File cameraFile;
|
|
50
|
+
private Uri cameraOutputUri;
|
|
39
51
|
|
|
40
52
|
private final ActivityEventListener activityEventListener = new BaseActivityEventListener() {
|
|
41
53
|
@Override
|
|
42
54
|
public void onActivityResult(Activity activity, int requestCode, int resultCode, @Nullable Intent data) {
|
|
43
|
-
if (requestCode
|
|
55
|
+
if (requestCode == PICK_ATTACHMENT_REQUEST && pickerPromise != null) {
|
|
56
|
+
Promise promise = pickerPromise;
|
|
57
|
+
pickerPromise = null;
|
|
44
58
|
|
|
45
|
-
|
|
46
|
-
|
|
59
|
+
if (resultCode != Activity.RESULT_OK || data == null || data.getData() == null) {
|
|
60
|
+
promise.resolve(null);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
47
63
|
|
|
48
|
-
|
|
49
|
-
|
|
64
|
+
Uri uri = data.getData();
|
|
65
|
+
try {
|
|
66
|
+
int flags = data.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
|
|
67
|
+
if ((flags & Intent.FLAG_GRANT_READ_URI_PERMISSION) != 0) {
|
|
68
|
+
reactContext.getContentResolver().takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
|
69
|
+
}
|
|
70
|
+
} catch (Exception ignored) {
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
WritableMap result = Arguments.createMap();
|
|
74
|
+
result.putString("uri", uri.toString());
|
|
75
|
+
result.putString("name", getDisplayName(uri));
|
|
76
|
+
result.putString("type", getMimeType(uri));
|
|
77
|
+
result.putDouble("size", getSize(uri));
|
|
78
|
+
promise.resolve(result);
|
|
50
79
|
return;
|
|
51
80
|
}
|
|
52
81
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
82
|
+
if (requestCode == TAKE_PHOTO_REQUEST && cameraPromise != null) {
|
|
83
|
+
Promise promise = cameraPromise;
|
|
84
|
+
File file = cameraFile;
|
|
85
|
+
Uri outputUri = cameraOutputUri;
|
|
86
|
+
cameraPromise = null;
|
|
87
|
+
cameraFile = null;
|
|
88
|
+
cameraOutputUri = null;
|
|
89
|
+
|
|
90
|
+
if (outputUri != null) {
|
|
91
|
+
try {
|
|
92
|
+
reactContext.revokeUriPermission(
|
|
93
|
+
outputUri,
|
|
94
|
+
Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
|
95
|
+
);
|
|
96
|
+
} catch (Exception ignored) {
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (resultCode != Activity.RESULT_OK) {
|
|
101
|
+
if (file != null) file.delete();
|
|
102
|
+
promise.resolve(null);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (file == null || !file.exists() || file.length() < 1) {
|
|
107
|
+
if (file != null) file.delete();
|
|
108
|
+
promise.reject("CAMERA_FAILED", "Camera did not return an image");
|
|
109
|
+
return;
|
|
58
110
|
}
|
|
59
|
-
} catch (Exception ignored) {
|
|
60
|
-
}
|
|
61
111
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
112
|
+
WritableMap result = Arguments.createMap();
|
|
113
|
+
result.putString("uri", Uri.fromFile(file).toString());
|
|
114
|
+
result.putString("name", file.getName());
|
|
115
|
+
result.putString("type", "image/jpeg");
|
|
116
|
+
result.putDouble("size", file.length());
|
|
117
|
+
promise.resolve(result);
|
|
118
|
+
}
|
|
68
119
|
}
|
|
69
120
|
};
|
|
70
121
|
|
|
@@ -128,49 +179,104 @@ public class RegantisChatNativeModule extends ReactContextBaseJavaModule {
|
|
|
128
179
|
}
|
|
129
180
|
|
|
130
181
|
@ReactMethod
|
|
131
|
-
public void
|
|
182
|
+
public void takePhoto(Promise promise) {
|
|
132
183
|
Activity activity = getCurrentActivity();
|
|
133
184
|
if (activity == null) {
|
|
134
185
|
promise.reject("NO_ACTIVITY", "No active Android Activity");
|
|
135
186
|
return;
|
|
136
187
|
}
|
|
188
|
+
if (cameraPromise != null || cameraPermissionPromise != null || pickerPromise != null) {
|
|
189
|
+
promise.reject("PICKER_BUSY", "An attachment picker is already open");
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
137
192
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
if (width < 1 || height < 1) {
|
|
144
|
-
promise.reject("SCREENSHOT_FAILED", "Invalid app window size");
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
193
|
+
if (ContextCompat.checkSelfPermission(reactContext, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
|
|
194
|
+
if (!(activity instanceof PermissionAwareActivity)) {
|
|
195
|
+
promise.reject("CAMERA_PERMISSION_UNAVAILABLE", "The current Activity cannot request camera permission");
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
147
198
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
199
|
+
cameraPermissionPromise = promise;
|
|
200
|
+
PermissionAwareActivity permissionActivity = (PermissionAwareActivity) activity;
|
|
201
|
+
permissionActivity.requestPermissions(
|
|
202
|
+
new String[]{Manifest.permission.CAMERA},
|
|
203
|
+
CAMERA_PERMISSION_REQUEST,
|
|
204
|
+
new PermissionListener() {
|
|
205
|
+
@Override
|
|
206
|
+
public boolean onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
|
|
207
|
+
if (requestCode != CAMERA_PERMISSION_REQUEST) return false;
|
|
151
208
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
209
|
+
Promise pendingPromise = cameraPermissionPromise;
|
|
210
|
+
cameraPermissionPromise = null;
|
|
211
|
+
if (pendingPromise == null) return true;
|
|
212
|
+
|
|
213
|
+
boolean granted = grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED;
|
|
214
|
+
if (!granted) {
|
|
215
|
+
pendingPromise.reject("CAMERA_PERMISSION_DENIED", "Camera permission is required to take a photo");
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
Activity currentActivity = getCurrentActivity();
|
|
220
|
+
if (currentActivity == null) {
|
|
221
|
+
pendingPromise.reject("NO_ACTIVITY", "No active Android Activity");
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
launchCamera(currentActivity, pendingPromise);
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
162
228
|
}
|
|
229
|
+
);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
163
232
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
233
|
+
launchCamera(activity, promise);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private void launchCamera(Activity activity, Promise promise) {
|
|
237
|
+
try {
|
|
238
|
+
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
|
|
239
|
+
|
|
240
|
+
File directory = new File(reactContext.getCacheDir(), "regantis-chat/camera");
|
|
241
|
+
if (!directory.exists() && !directory.mkdirs()) {
|
|
242
|
+
promise.reject("CAMERA_FAILED", "Unable to create camera directory");
|
|
243
|
+
return;
|
|
172
244
|
}
|
|
173
|
-
|
|
245
|
+
|
|
246
|
+
File file = File.createTempFile("camera-", ".jpg", directory);
|
|
247
|
+
String authority = reactContext.getPackageName() + ".regantis.chat.fileprovider";
|
|
248
|
+
Uri outputUri = FileProvider.getUriForFile(reactContext, authority, file);
|
|
249
|
+
|
|
250
|
+
intent.putExtra(MediaStore.EXTRA_OUTPUT, outputUri);
|
|
251
|
+
intent.setClipData(ClipData.newRawUri("RegantisChatCamera", outputUri));
|
|
252
|
+
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
|
|
253
|
+
|
|
254
|
+
List<ResolveInfo> cameraActivities = reactContext.getPackageManager().queryIntentActivities(intent, 0);
|
|
255
|
+
for (ResolveInfo info : cameraActivities) {
|
|
256
|
+
reactContext.grantUriPermission(
|
|
257
|
+
info.activityInfo.packageName,
|
|
258
|
+
outputUri,
|
|
259
|
+
Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
cameraPromise = promise;
|
|
264
|
+
cameraFile = file;
|
|
265
|
+
cameraOutputUri = outputUri;
|
|
266
|
+
activity.startActivityForResult(intent, TAKE_PHOTO_REQUEST);
|
|
267
|
+
} catch (android.content.ActivityNotFoundException error) {
|
|
268
|
+
cameraPromise = null;
|
|
269
|
+
if (cameraFile != null) cameraFile.delete();
|
|
270
|
+
cameraFile = null;
|
|
271
|
+
cameraOutputUri = null;
|
|
272
|
+
promise.reject("CAMERA_UNAVAILABLE", "No camera application is available", error);
|
|
273
|
+
} catch (Exception error) {
|
|
274
|
+
cameraPromise = null;
|
|
275
|
+
if (cameraFile != null) cameraFile.delete();
|
|
276
|
+
cameraFile = null;
|
|
277
|
+
cameraOutputUri = null;
|
|
278
|
+
promise.reject("CAMERA_FAILED", error);
|
|
279
|
+
}
|
|
174
280
|
}
|
|
175
281
|
|
|
176
282
|
@ReactMethod
|
|
@@ -180,7 +286,7 @@ public class RegantisChatNativeModule extends ReactContextBaseJavaModule {
|
|
|
180
286
|
promise.reject("NO_ACTIVITY", "No active Android Activity");
|
|
181
287
|
return;
|
|
182
288
|
}
|
|
183
|
-
if (pickerPromise != null) {
|
|
289
|
+
if (pickerPromise != null || cameraPromise != null || cameraPermissionPromise != null) {
|
|
184
290
|
promise.reject("PICKER_BUSY", "An attachment picker is already open");
|
|
185
291
|
return;
|
|
186
292
|
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/index.d.ts
CHANGED
|
@@ -62,6 +62,7 @@ export type RegantisChatProps = {
|
|
|
62
62
|
style?: StyleProp<ViewStyle>;
|
|
63
63
|
initialView?: 'home' | 'chat';
|
|
64
64
|
onClose?: () => void;
|
|
65
|
+
onBack?: () => void;
|
|
65
66
|
onViewChange?: (view: 'home' | 'chat') => void;
|
|
66
67
|
keyboardVerticalOffset?: number;
|
|
67
68
|
onMessage?: (message: any) => void;
|
|
@@ -78,7 +79,7 @@ export class RegantisChatClient {
|
|
|
78
79
|
sendMessage(message: string): Promise<any>;
|
|
79
80
|
saveContact(name: string, email: string): Promise<any>;
|
|
80
81
|
uploadAttachment(file: { uri: string; name?: string; type?: string }, kind: 'image' | 'file'): Promise<any>;
|
|
81
|
-
pickAndUpload(kind: 'image' | 'file' | '
|
|
82
|
+
pickAndUpload(kind: 'image' | 'file' | 'camera'): Promise<any>;
|
|
82
83
|
setTranslationLanguage(languageCode: string): Promise<void>;
|
|
83
84
|
closeChat(): Promise<any>;
|
|
84
85
|
reopenChat(): Promise<any>;
|
package/ios/RegantisChatNative.h
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#import <React/RCTBridgeModule.h>
|
|
2
2
|
#import <UIKit/UIKit.h>
|
|
3
3
|
|
|
4
|
-
@interface RegantisChatNative : NSObject <RCTBridgeModule, UIDocumentPickerDelegate>
|
|
4
|
+
@interface RegantisChatNative : NSObject <RCTBridgeModule, UIDocumentPickerDelegate, UIImagePickerControllerDelegate, UINavigationControllerDelegate>
|
|
5
5
|
@end
|
package/ios/RegantisChatNative.m
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#import "RegantisChatNative.h"
|
|
2
2
|
#import <AudioToolbox/AudioToolbox.h>
|
|
3
|
+
#import <AVFoundation/AVFoundation.h>
|
|
3
4
|
#import <React/RCTUtils.h>
|
|
4
5
|
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
|
|
5
6
|
|
|
@@ -66,50 +67,65 @@ RCT_EXPORT_METHOD(playIncomingSound)
|
|
|
66
67
|
AudioServicesPlaySystemSound(1007);
|
|
67
68
|
}
|
|
68
69
|
|
|
69
|
-
|
|
70
|
-
|
|
70
|
+
- (void)presentCameraWithResolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject
|
|
71
|
+
{
|
|
72
|
+
if (self.pickerResolve != nil) {
|
|
73
|
+
reject(@"PICKER_BUSY", @"An attachment picker is already open", nil);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (![UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) {
|
|
78
|
+
reject(@"CAMERA_UNAVAILABLE", @"Camera is not available on this device", nil);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
UIViewController *controller = RCTPresentedViewController();
|
|
83
|
+
if (controller == nil) {
|
|
84
|
+
reject(@"NO_VIEW_CONTROLLER", @"No active iOS view controller", nil);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
UIImagePickerController *picker = [[UIImagePickerController alloc] init];
|
|
89
|
+
picker.sourceType = UIImagePickerControllerSourceTypeCamera;
|
|
90
|
+
picker.delegate = self;
|
|
91
|
+
picker.allowsEditing = NO;
|
|
92
|
+
|
|
93
|
+
self.pickerResolve = resolve;
|
|
94
|
+
self.pickerReject = reject;
|
|
95
|
+
[controller presentViewController:picker animated:YES completion:nil];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
RCT_REMAP_METHOD(takePhoto,
|
|
99
|
+
takePhotoWithResolver:(RCTPromiseResolveBlock)resolve
|
|
71
100
|
rejecter:(RCTPromiseRejectBlock)reject)
|
|
72
101
|
{
|
|
73
102
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
reject(@"SCREENSHOT_FAILED", @"No active iOS window", nil);
|
|
103
|
+
NSString *usageDescription = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"NSCameraUsageDescription"];
|
|
104
|
+
if (![usageDescription isKindOfClass:[NSString class]] || usageDescription.length == 0) {
|
|
105
|
+
reject(@"CAMERA_USAGE_DESCRIPTION_MISSING", @"NSCameraUsageDescription is required in the host app Info.plist", nil);
|
|
78
106
|
return;
|
|
79
107
|
}
|
|
80
108
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
[
|
|
84
|
-
}];
|
|
85
|
-
NSData *data = UIImagePNGRepresentation(image);
|
|
86
|
-
if (data == nil) {
|
|
87
|
-
reject(@"SCREENSHOT_FAILED", @"Unable to encode screenshot", nil);
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
NSString *filename = [NSString stringWithFormat:@"screenshot-%lld.png", (long long)(NSDate.date.timeIntervalSince1970 * 1000.0)];
|
|
92
|
-
NSString *directory = [NSTemporaryDirectory() stringByAppendingPathComponent:@"regantis-chat"];
|
|
93
|
-
NSError *directoryError = nil;
|
|
94
|
-
[[NSFileManager defaultManager] createDirectoryAtPath:directory withIntermediateDirectories:YES attributes:nil error:&directoryError];
|
|
95
|
-
if (directoryError != nil) {
|
|
96
|
-
reject(@"SCREENSHOT_FAILED", directoryError.localizedDescription, directoryError);
|
|
109
|
+
AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
|
|
110
|
+
if (status == AVAuthorizationStatusAuthorized) {
|
|
111
|
+
[self presentCameraWithResolver:resolve rejecter:reject];
|
|
97
112
|
return;
|
|
98
113
|
}
|
|
99
114
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (![data writeToFile:path options:NSDataWritingAtomic error:&writeError]) {
|
|
103
|
-
reject(@"SCREENSHOT_FAILED", writeError.localizedDescription, writeError);
|
|
115
|
+
if (status == AVAuthorizationStatusDenied || status == AVAuthorizationStatusRestricted) {
|
|
116
|
+
reject(@"CAMERA_PERMISSION_DENIED", @"Camera permission is required to take a photo", nil);
|
|
104
117
|
return;
|
|
105
118
|
}
|
|
106
119
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
120
|
+
[AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) {
|
|
121
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
122
|
+
if (!granted) {
|
|
123
|
+
reject(@"CAMERA_PERMISSION_DENIED", @"Camera permission is required to take a photo", nil);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
[self presentCameraWithResolver:resolve rejecter:reject];
|
|
127
|
+
});
|
|
128
|
+
}];
|
|
113
129
|
});
|
|
114
130
|
}
|
|
115
131
|
|
|
@@ -146,6 +162,71 @@ RCT_REMAP_METHOD(pickAttachment,
|
|
|
146
162
|
});
|
|
147
163
|
}
|
|
148
164
|
|
|
165
|
+
- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker
|
|
166
|
+
{
|
|
167
|
+
RCTPromiseResolveBlock resolve = self.pickerResolve;
|
|
168
|
+
self.pickerResolve = nil;
|
|
169
|
+
self.pickerReject = nil;
|
|
170
|
+
[picker dismissViewControllerAnimated:YES completion:^{
|
|
171
|
+
if (resolve) resolve(nil);
|
|
172
|
+
}];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<UIImagePickerControllerInfoKey,id> *)info
|
|
176
|
+
{
|
|
177
|
+
RCTPromiseResolveBlock resolve = self.pickerResolve;
|
|
178
|
+
RCTPromiseRejectBlock reject = self.pickerReject;
|
|
179
|
+
self.pickerResolve = nil;
|
|
180
|
+
self.pickerReject = nil;
|
|
181
|
+
|
|
182
|
+
UIImage *image = info[UIImagePickerControllerOriginalImage];
|
|
183
|
+
if (image == nil) {
|
|
184
|
+
[picker dismissViewControllerAnimated:YES completion:^{
|
|
185
|
+
if (reject) reject(@"CAMERA_FAILED", @"Camera did not return an image", nil);
|
|
186
|
+
}];
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
NSData *data = UIImageJPEGRepresentation(image, 0.92);
|
|
191
|
+
if (data == nil) {
|
|
192
|
+
[picker dismissViewControllerAnimated:YES completion:^{
|
|
193
|
+
if (reject) reject(@"CAMERA_FAILED", @"Unable to encode camera image", nil);
|
|
194
|
+
}];
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
NSString *directory = [NSTemporaryDirectory() stringByAppendingPathComponent:@"regantis-chat/camera"];
|
|
199
|
+
NSError *directoryError = nil;
|
|
200
|
+
[[NSFileManager defaultManager] createDirectoryAtPath:directory withIntermediateDirectories:YES attributes:nil error:&directoryError];
|
|
201
|
+
if (directoryError != nil) {
|
|
202
|
+
[picker dismissViewControllerAnimated:YES completion:^{
|
|
203
|
+
if (reject) reject(@"CAMERA_FAILED", directoryError.localizedDescription, directoryError);
|
|
204
|
+
}];
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
NSString *filename = [NSString stringWithFormat:@"camera-%lld.jpg", (long long)(NSDate.date.timeIntervalSince1970 * 1000.0)];
|
|
209
|
+
NSString *path = [directory stringByAppendingPathComponent:filename];
|
|
210
|
+
NSError *writeError = nil;
|
|
211
|
+
if (![data writeToFile:path options:NSDataWritingAtomic error:&writeError]) {
|
|
212
|
+
[picker dismissViewControllerAnimated:YES completion:^{
|
|
213
|
+
if (reject) reject(@"CAMERA_FAILED", writeError.localizedDescription, writeError);
|
|
214
|
+
}];
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
NSDictionary *result = @{
|
|
219
|
+
@"uri": [NSURL fileURLWithPath:path].absoluteString ?: @"",
|
|
220
|
+
@"name": filename,
|
|
221
|
+
@"type": @"image/jpeg",
|
|
222
|
+
@"size": @(data.length)
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
[picker dismissViewControllerAnimated:YES completion:^{
|
|
226
|
+
if (resolve) resolve(result);
|
|
227
|
+
}];
|
|
228
|
+
}
|
|
229
|
+
|
|
149
230
|
- (void)documentPickerWasCancelled:(UIDocumentPickerViewController *)controller
|
|
150
231
|
{
|
|
151
232
|
if (self.pickerResolve) self.pickerResolve(nil);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@regantis-sdk/react-native-chat",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Regantis customer support chat SDK for React Native Android and iOS",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"src",
|
|
12
12
|
"android",
|
|
13
13
|
"ios",
|
|
14
|
+
"assets",
|
|
14
15
|
"RegantisReactNativeChat.podspec",
|
|
15
16
|
"README.md"
|
|
16
17
|
],
|