Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,38 @@ appStorageService.token.set('new_token');
appStorageService.token.remove();
```

#### 3. Image Picker

`ImagePickerService` gives the application access to the camera and image gallery.

Public methods:
- `getImage` - initializes the application (camera or gallery) and returns a result containing an image.
- `launchGallery` - launches the gallery application and returns a result containing the selected images.
- `launchCamera` - launches the camera application and returns the taken photo.
- `requestGalleryAccess` - requests the application access to the gallery.
- `requestCameraAccess` - requests the application access to the camera.
- `getFormData` - creates a [FormData](https://developer.mozilla.org/en-US/docs/Web/API/FormData) object with image.

**Example**

Pick image and send request:

```ts
const handlePickImage = async (source: ImagePickerSource) => {
const image = await imagePickerService.getImage(source);
const asset = image?.assets?.[0];

if (!asset) {
return;
}

const data = imagePickerService.getFormData(asset.uri);

// API call
createMedia(data);
};
```

### Utils

#### 1. `setupReactotron(projectName: string)`
Expand Down
1 change: 1 addition & 0 deletions src/lib/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"@react-native-async-storage/async-storage": "^1.23.1",
"@reduxjs/toolkit": "^2.2.6",
"expo-device": "~6.0.2",
"expo-image-picker": "^15.0.5",
"expo-localization": "^15.0.3",
"expo-notifications": "^0.28.9",
"expo-secure-store": "^13.0.2",
Expand Down
4 changes: 4 additions & 0 deletions src/lib/src/data-access/image-picker/enums/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export enum ImagePickerError {
PERMISSION_DENIED = 'denied',
UNAVAILABLE = 'unavailable'
}
2 changes: 2 additions & 0 deletions src/lib/src/data-access/image-picker/enums/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './error';
export * from './source';
4 changes: 4 additions & 0 deletions src/lib/src/data-access/image-picker/enums/source.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export enum ImagePickerSource {
CAMERA = 'camera',
GALLERY = 'gallery'
}
2 changes: 2 additions & 0 deletions src/lib/src/data-access/image-picker/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './service';
export * from './enums';
73 changes: 73 additions & 0 deletions src/lib/src/data-access/image-picker/service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import * as ImagePicker from 'expo-image-picker';
import { ImagePickerSource, ImagePickerError } from './enums';
import { Alert, Linking } from 'react-native';

export class ImagePickerService {
public defaultOptions: ImagePicker.ImagePickerOptions = {
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsEditing: true,
base64: true,
quality: 0.3,
};

public getFormData(uri: string, name?: string): FormData {
const formData = new FormData();
const match = /\.(\w+)$/.exec(uri);
const type = match ? `image/${match[1]}` : 'image';
formData.append(name || 'file', { uri, type, name: match?.[1] } as any);

return formData;
}

public getImage(source: ImagePickerSource, onPermissionDenied?: () => void): Promise<ImagePicker.ImagePickerResult | ImagePickerError> {
return this.pickImage(source, onPermissionDenied);
}

public requestGalleryAccess(): Promise<ImagePicker.PermissionResponse> {
return ImagePicker.requestMediaLibraryPermissionsAsync();
}

public launchGallery(): Promise<ImagePicker.ImagePickerResult> {
return ImagePicker.launchImageLibraryAsync(this.defaultOptions);
}

public requestCameraAccess(): Promise<ImagePicker.PermissionResponse> {
return ImagePicker.requestCameraPermissionsAsync();
}

public launchCamera(): Promise<ImagePicker.ImagePickerResult> {
return ImagePicker.launchCameraAsync(this.defaultOptions);
}

private async pickImage(source: ImagePickerSource, onPermissionDenied?: () => void): Promise<ImagePicker.ImagePickerResult | ImagePickerError> {
const isCamera = source === ImagePickerSource.CAMERA;
const response = isCamera ? await this.requestCameraAccess() : await this.requestGalleryAccess();

const handlePermissionDeniedResponse = (): void => {
if (onPermissionDenied) {
onPermissionDenied();

return;
}

const permissionSubject = isCamera ? 'camera' : 'gallery';
const errorMessage = `Sorry, we need ${permissionSubject} permissions to make this work! Please enable ${permissionSubject} permissions in your device settings`;

Alert.alert('', errorMessage, [{ text: 'Close' }, { onPress: Linking.openSettings, text: 'Open settings' }]);
};

if (!response.canAskAgain) {
handlePermissionDeniedResponse();

return ImagePickerError.PERMISSION_DENIED;
}

if (response.status !== ImagePicker.PermissionStatus.GRANTED) {
return ImagePickerError.UNAVAILABLE;
}

return isCamera ? this.launchCamera() : this.launchGallery();
}
}

export const imagePickerService = new ImagePickerService();
1 change: 1 addition & 0 deletions src/lib/src/data-access/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './push-notifications';
export * from './store';
export * from './storage';
export * from './image-picker';
72 changes: 63 additions & 9 deletions src/lib/src/data-access/push-notifications/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Constants from 'expo-constants';
import * as Device from 'expo-device';
import * as Notifications from 'expo-notifications';
import { PermissionStatus } from 'expo-modules-core';
import { Platform } from 'react-native';
import { Alert, Linking, Platform } from 'react-native';

export interface ObtainPushNotificationsTokenArgs {
getTokenErrorHandler?: (permissionResponse: Notifications.PermissionResponse) => void;
Expand Down Expand Up @@ -36,22 +36,76 @@ class PushNotificationsService {
getTokenErrorHandler,
}: ObtainPushNotificationsTokenArgs): Promise<string | undefined> {
if (Device.isDevice) {
const { status: existingPermissionsStatus } = await Notifications.getPermissionsAsync();
const settings = await Notifications.getPermissionsAsync();

if (existingPermissionsStatus !== PermissionStatus.GRANTED) {
const response = await Notifications.requestPermissionsAsync();
const handlePermissionDeniedResponse = (response: Notifications.NotificationPermissionsStatus): void => {
if (getTokenErrorHandler) {
getTokenErrorHandler(response);
} else {
const errorMessage = `Sorry, we need notification permissions to make this work! Please enable notification permissions in your device settings`;

if (response.status !== PermissionStatus.GRANTED) {
getTokenErrorHandler?.(response) ||
console.warn(
'Failed to obtain push notifications token.\nPlease specify the "getTokenErrorHandler" callback in the usePushNotifications hook to clear this warning'
);
Alert.alert('', errorMessage, [{ text: 'Close' }, { onPress: Linking.openSettings, text: 'Open settings' }]);
}
};

const requestPermissionsIOS = async (
settings: Notifications.NotificationPermissionsStatus
): Promise<void> => {
const permissionsGrantedStatuses = [
Notifications.IosAuthorizationStatus.AUTHORIZED,
Notifications.IosAuthorizationStatus.PROVISIONAL,
Notifications.IosAuthorizationStatus.EPHEMERAL
];

if (settings.ios?.status === Notifications.IosAuthorizationStatus.NOT_DETERMINED) {
const permissions = await Notifications.requestPermissionsAsync();
const arePermissionsGranted = !!permissions.ios && permissionsGrantedStatuses.includes(permissions.ios?.status);

if (!arePermissionsGranted) {
handlePermissionDeniedResponse(permissions);
}

return;
}

const arePermissionsGranted = !!settings.ios && permissionsGrantedStatuses.includes(settings.ios?.status);

if (!arePermissionsGranted) {
handlePermissionDeniedResponse(settings);
}
};

const requestPermissionsAndroid = async (
settings: Notifications.NotificationPermissionsStatus
): Promise<void> => {
if (settings.status !== PermissionStatus.GRANTED) {
const permissions = await Notifications.requestPermissionsAsync();
const arePermissionsGranted = permissions.status === PermissionStatus.GRANTED;

if (!arePermissionsGranted) {
handlePermissionDeniedResponse(permissions);
}

return;
}

const arePermissionsGranted = settings.status === PermissionStatus.GRANTED;

if (!arePermissionsGranted) {
handlePermissionDeniedResponse(settings);
}
};

if (Platform.OS === 'ios') {
await requestPermissionsIOS(settings);
}

if (Platform.OS === 'android') {
await requestPermissionsAndroid(settings);
}

const projectId = Constants.expoConfig?.extra?.eas.projectId;

if (!projectId) {
console.error('EAS projectId is not specified in app.config.ts. Push notifications may not work.');
}
Expand Down