@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 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
- - images and files
116
- - current-app screenshot attachment
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
@@ -21,4 +21,5 @@ repositories {
21
21
 
22
22
  dependencies {
23
23
  implementation "com.facebook.react:react-android"
24
+ implementation "androidx.core:core:1.13.1"
24
25
  }
@@ -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.graphics.Bitmap;
8
- import android.graphics.Canvas;
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.io.FileOutputStream;
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 != PICK_ATTACHMENT_REQUEST || pickerPromise == null) return;
55
+ if (requestCode == PICK_ATTACHMENT_REQUEST && pickerPromise != null) {
56
+ Promise promise = pickerPromise;
57
+ pickerPromise = null;
44
58
 
45
- Promise promise = pickerPromise;
46
- pickerPromise = null;
59
+ if (resultCode != Activity.RESULT_OK || data == null || data.getData() == null) {
60
+ promise.resolve(null);
61
+ return;
62
+ }
47
63
 
48
- if (resultCode != Activity.RESULT_OK || data == null || data.getData() == null) {
49
- promise.resolve(null);
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
- Uri uri = data.getData();
54
- try {
55
- int flags = data.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
56
- if ((flags & Intent.FLAG_GRANT_READ_URI_PERMISSION) != 0) {
57
- reactContext.getContentResolver().takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
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
- WritableMap result = Arguments.createMap();
63
- result.putString("uri", uri.toString());
64
- result.putString("name", getDisplayName(uri));
65
- result.putString("type", getMimeType(uri));
66
- result.putDouble("size", getSize(uri));
67
- promise.resolve(result);
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 captureScreenshot(Promise promise) {
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
- activity.runOnUiThread(() -> {
139
- try {
140
- View root = activity.getWindow().getDecorView().getRootView();
141
- int width = root.getWidth();
142
- int height = root.getHeight();
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
- Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
149
- Canvas canvas = new Canvas(bitmap);
150
- root.draw(canvas);
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
- File directory = new File(reactContext.getCacheDir(), "regantis-chat");
153
- if (!directory.exists() && !directory.mkdirs()) {
154
- promise.reject("SCREENSHOT_FAILED", "Unable to create screenshot directory");
155
- return;
156
- }
157
- File file = new File(directory, "screenshot-" + System.currentTimeMillis() + ".png");
158
- try (FileOutputStream stream = new FileOutputStream(file)) {
159
- bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream);
160
- } finally {
161
- bitmap.recycle();
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
- WritableMap result = Arguments.createMap();
165
- result.putString("uri", Uri.fromFile(file).toString());
166
- result.putString("name", file.getName());
167
- result.putString("type", "image/png");
168
- result.putDouble("size", file.length());
169
- promise.resolve(result);
170
- } catch (Exception error) {
171
- promise.reject("SCREENSHOT_FAILED", error);
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
  }
@@ -0,0 +1,4 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <paths xmlns:android="http://schemas.android.com/apk/res/android">
3
+ <cache-path name="regantis_chat_cache" path="regantis-chat/" />
4
+ </paths>
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' | 'screenshot'): Promise<any>;
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>;
@@ -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
@@ -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
- RCT_REMAP_METHOD(captureScreenshot,
70
- captureScreenshotWithResolver:(RCTPromiseResolveBlock)resolve
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
- UIViewController *controller = RCTPresentedViewController();
75
- UIWindow *window = controller.view.window ?: UIApplication.sharedApplication.keyWindow;
76
- if (window == nil) {
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
- UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithBounds:window.bounds];
82
- UIImage *image = [renderer imageWithActions:^(UIGraphicsImageRendererContext * _Nonnull rendererContext) {
83
- [window drawViewHierarchyInRect:window.bounds afterScreenUpdates:YES];
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
- NSString *path = [directory stringByAppendingPathComponent:filename];
101
- NSError *writeError = nil;
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
- resolve(@{
108
- @"uri": [NSURL fileURLWithPath:path].absoluteString ?: @"",
109
- @"name": filename,
110
- @"type": @"image/png",
111
- @"size": @(data.length)
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.2",
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
  ],