npm package: @ronas-it/react-native-common-modules
Description: Common modules for React Native apps at Ronas IT
Author: Ronas IT
Latest version: 1.3.0

# React Native Common Modules

Common components for Ronas IT projects.

## Usage

1. Install the package: `npm i @ronas-it/react-native-common-modules`
2. Import modules to your app and use as described below

### UI-components

At the moment this library contains the following components:

#### 1. `AppSafeAreaView`

> **_NOTE:_** Required dependencies: `react-native-safe-area-context`

A component for granular control of safe area edges on each screen. The difference from `SafeAreaView` in [react-native-safe-area-context](https://www.npmjs.com/package/react-native-safe-area-context) is that the container adds padding to the elements inside it, rather than to the entire screen, making it more flexible for use.

**Props:**

- `edges`: An array indicating which edges of the screen to respect. Possible values are 'top', 'right', 'bottom', 'left'. Defaults to all edges.
- `style`: Custom styles to apply to the view. Note that padding values will be adjusted to respect safe area insets.

**Example:**

```jsx
import { AppSafeAreaView } from '@ronas-it/react-native-common-modules/safe-area-view';

<AppSafeAreaView edges={['top', 'bottom']} style={styles.container}>
  <Text>Content goes here</Text>
</AppSafeAreaView>;
```

### Services

#### 1. Push notifications

> **_NOTE:_** Required dependencies: `expo-notifications`, `expo-router`, `expo-constants`, `expo-device`, `expo-modules-core`

##### `PushNotificationsService`

Service for integrating [Expo push notifications](https://docs.expo.dev/push-notifications/overview/) into apps.
Requires [setup](https://docs.expo.dev/push-notifications/push-notifications-setup/) and [backend implementation](https://docs.expo.dev/push-notifications/sending-notifications/) for sending notifications.

`PushNotificationsService` public methods:

- `obtainPushNotificationsToken` - get an Expo token that can be used to send a push notification to the device using Expo's push notifications service.

- `pushToken` - getter for retrieving the token if it was already obtained.

##### `usePushNotifications`

Hook, that automatically subscribes the device to receive push notifications when a user becomes authenticated, and unsubscribes when a user becomes non-authenticated. It supports custom subscription and unsubscription logic through provided functions or API configuration. Listens for [responses](https://docs.expo.dev/push-notifications/receiving-notifications/) to notifications and executes a callback, if provided, when a notification is interacted with.
Used in the root `App` component.

`usePushNotifications` hook arguments:

- `isAuthenticated` (required) - flag, that indicates whether the user is authenticated or not.
- `onNotificationResponse` (optional) - callback when a notification is interacted with.
- `subscribeDevice` (optional) - function for subscribing the device.
- `unsubscribeDevice` (optional) - function for unsubscribing the device.
- `apiConfig` (optional) - API configuration for subscribing and unsubscribing the device (when `subscribeDevice` and `unsubscribeDevice` are not provided).
- `apiErrorHandler` (optional) - API error handler for subscribe/unsubscribe functions.
- `getTokenErrorHandler` (optional) - handler for error that occur when attempting to obtain a push notifications token.

**Example:**

```ts
// Somewhere in a root component of your app:
import { usePushNotifications } from '@ronas-it/react-native-common-modules/push-notifications';

...
const authToken = useSelector(authSelectors.token);
...
usePushNotifications({
  apiConfig: {
    subscribeDeviceUrl: 'https://your-api.com/api/v1/push-notifications/subscribe',
    unsubscribeDeviceUrl: 'https://your-api.com/api/v1/push-notifications/unsubscribe',
    accessToken: authToken,
  },
  isAuthenticated: !!authToken,
})
```

> **_NOTE:_** By default, when using the `apiConfig` option, the hook sends the Expo push token to your server in the following format: `JSON.stringify({ expo_token })`

#### 2. Image Picker

> **_NOTE:_** Required dependencies: `expo-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
import { imagePickerService, ImagePickerSource } from '@ronas-it/react-native-common-modules/image-picker';

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);
};
```

#### 3. WebSocket

> **_NOTE:_** Required dependencies: `@pusher/pusher-websocket-react-native`, `pusher-js`

`WebSocketService` manages WebSocket connections using [Pusher](https://pusher.com/) and can work in both web and mobile applications.
Doesn't support Expo Go.

It's necessary to install [@pusher/pusher-websocket-react-native](https://github.com/pusher/pusher-websocket-react-native)
for a mobile app and [pusher-js](https://github.com/pusher/pusher-js) for a web app.

Options for `WebSocketService` constructor:

- `apiKey` (required) - `APP_KEY` from [Pusher Channels Dashboard](https://dashboard.pusher.com/).
- `cluster` (required) - `APP_CLUSTER` from [Pusher Channels Dashboard](https://dashboard.pusher.com/).
- `authURL` (optional) - a URL that returns the authentication signature needed for private channels.
- `useTLS` (optional) - a flag that indicates whether TLS encrypted transport should be used. Default value is `true`.
- `activityTimeout` (optional) - Time in **ms** before sending a ping when no messages have been sent. Default value is `30000`.
- `pongTimeout` (optional) - Time in **ms** to wait for the pong response.
- `authorizerTimeoutInSeconds` (optional) - Time in **seconds** to wait for the authorizer response. Default value is `60`.

`WebSocketService` public methods:

- `init(tokenGetter?, handlers?)` - Initializes the Pusher client. Should be called only once before calling `connect`. If an authorization token is provided, it will be used for secure connections.
- `connect()` - Connects the client to the Pusher server. Should be called after initializing the client using `init`.
- `disconnect()` - Disconnects the client from the Pusher server. Should be called when the client is no longer needed.
- `subscribeToChannel` subscribes to a specified channel and registers an event listener for incoming messages on that channel.
- `unsubscribeFromChannel` removes an event listener and, if there are no listeners for a specified channel, unsubscribes from it.

**Example:**

```ts
import { WebSocketService } from '@ronas-it/react-native-common-modules/websocket';

// Create a service instance
type ChannelName = `private-conversations.${number}` | `private-users.${number}`;
const webSocketService = new WebSocketService<ChannelName>({
  apiKey: '1234567890qwertyuiop',
  cluster: 'eu',
  authURL: 'https://your-api.com/api/v1/broadcasting/auth',
});

// Callback to get your app auth token for private channels
const tokenGetter = () => authSelectors.token(getState());
// Initialize Pusher, e.g. after an app initialization or successful authorization
webSocketService.init(
  tokenGetter, // Always get actual token
  {
    // Optional handlers
    onConnectionStateChange: (currentState) => {
      // Do something with the connection state
    },
  },
);
// Connect to Pusher instance
webSocketService.connect();

// Subscribe to a channel when it's necessary
webSocketService.subscribeToChannel('private-conversations.123', (event) => {
  console.log('Received event:', event);
});

// Unsubscribe from a channel, e.g. before an app closing or logging out
webSocketService.unsubscribeFromChannel('private-conversations.123', (event) => {
  console.log('Received event:', event);
});
```

##### `usePusherReactNative`

Hook that automatically manages WebSocket connection lifecycle on Android devices. It reconnects the service when the app comes to the foreground and disconnects when the app goes to the background.

> **_NOTE:_** Android can silently lose WebSocket connection when the app is put to background. This hook addresses this issue. See [GitHub issue #135](https://github.com/pusher/pusher-websocket-react-native/issues/135) for more details.

**Example:**

```ts
import { usePusherReactNative } from '@ronas-it/react-native-common-modules/websocket';
import { webSocketService } from '@your-app/mobile/data-access/websocket';

// Use the hook in your root component
usePusherReactNative(webSocketService);
```

### Utils

#### 1. `setupReactotron(projectName: string, plugins: Array<ReactotronPlugin>)`

> **_NOTE:_** Required dependencies: `@reduxjs/toolkit`, `reactotron-react-native`, `reactotron-react-js`, `reactotron-redux`

Configures and initializes [Reactotron debugger](https://github.com/infinitered/reactotron) with [redux plugin](https://docs.infinite.red/reactotron/plugins/redux/) for development purposes.
Install the [Reactotron app](https://github.com/infinitered/reactotron/releases?q=reactotron-app&expanded=true) on your computer for use.

**Example:**

```ts
import { setupReactotron } from '@ronas-it/react-native-common-modules/reactotron';
import { createStoreInitializer } from '@ronas-it/rtkq-entity-api';
import Reactotron from 'reactotron-react-native';
import mmkvPlugin from 'reactotron-react-native-mmkv';
import type { ReactotronReactNative } from 'reactotron-react-native';
import { storage } from './mmkv/storage/instance/location'; // <--- update this to your mmkv instance.

const reactotron = setupReactotron('your-app', [
  // You can add an array of Reactotron plugins here
  (reactotron) => mmkvPlugin<ReactotronReactNative>({ storage }),
]);
const enhancers = reactotron ? [reactotron.createEnhancer()] : [];

const initStore = createStoreInitializer({
  rootReducer: rootReducer as unknown as Reducer<AppState>,
  middlewares,
  enhancers,
});
```

#### 2. `i18n`

> **_NOTE:_** Required dependencies: `i18n-js`, `expo-localization`

Provides functions to set language and use translations using [i18n-js](https://github.com/fnando/i18n-js)

**Example:**

root layout:

```ts
import { setLanguage } from '@ronas-it/react-native-common-modules/i18n';

const translations = {
  en: {
    ...require('i18n/example/en.json')
  },
  fr: {
    ...require('i18n/example/fr.json')
  }
};

const useLanguage = setLanguage(translations, 'en');

interface LanguageContextProps {
  language: string;
  onLanguageChange?: (language: keyof typeof translations) => void;
}

export const LanguageContext = createContext<LanguageContextProps>({ language: 'en' });

function App(): ReactElement {
  return (
    <Stack>
      <Stack.Screen name='index' />
    </Stack>
  );
}

export default function RootLayout(): ReactElement | null {
  const [language, setLanguage] = useState<keyof typeof translations>('en');

  useLanguage(language);

  return (
    <LanguageContext.Provider value={{ language, onLanguageChange: setLanguage }}>
      <App />
    </LanguageContext.Provider>
  );
}
```

screen:

```ts
import { AppSafeAreaView } from '@ronas-it/react-native-common-modules/safe-area-view';
import { useTranslation } from '@ronas-it/react-native-common-modules/i18n';
import { ReactElement, useContext } from 'react';
import { View, Text, Alert, Pressable } from 'react-native';
import { LanguageContext } from './_layout';

export default function RootScreen(): ReactElement {
  const translate = useTranslation('EXAMPLE');
  const { language, onLanguageChange } = useContext(LanguageContext);

  const onPress = () => Alert.alert(translate('TEXT_PRESSED'));

  const handleLanguageChange = (): void => {
    onLanguageChange?.(language === 'en' ? 'fr' : 'en');
  };

  return (
    <AppSafeAreaView edges={['bottom']} style={styles.safeAreaContainer}>
      <View style={styles.container}>
        <Pressable onPress={onPress} hitSlop={10}>
        <Text>{translate('BUTTON_PRESS_ME')}</Text>
        </Pressable>
        <Pressable onPress={handleLanguageChange} hitSlop={10}>
          <Text>{translate('BUTTON_LANGUAGE')}</Text>
        </Pressable>
      </View>
    </AppSafeAreaView>
  );
}
```

### Features

#### 1. Clerk

> **_NOTE:_** Required dependencies: `@clerk/clerk-expo`, `expo-web-browser`, `expo-auth-session`

Hooks and helpers to create user authentication with [Clerk Expo SDK](https://clerk.com/docs/references/expo/overview).

#### `useClerkResources`

Hook, that provides access to essential Clerk methods and objects.

Returned Object:

- `signUp` - provides access to [SignUp](https://clerk.com/docs/references/javascript/sign-up) object.
- `signIn` - provides access to [SignIn](https://clerk.com/docs/references/javascript/sign-in) object.
- `setActive` - A function that sets the active session.
- `signOut` - A function that signs out the current user.

#### `useAuthWithIdentifier`

Hook, that provides functionality to handle user sign-up and sign-in processes using an identifier such as an email, phone number, or username. It supports both OTP (One Time Password) and password-based authentication methods.

Parameters:

- `method`: Specifies the type of identifier used for authentication (e.g., 'emailAddress', 'phoneNumber', 'username').
- `verifyBy`: Specifies the verification method ('otp' for one-time passwords or 'password').

Returned Object:

- `startSignUp`: Initiates a new user registration using the specified identifier and verification method.
- `startSignIn`: Initiates authentication of an existing user using the specified identifier and verification method.
- `startAuthorization`: Determines whether to initiate a sign-up or sign-in based on whether the user has been registered previously.
- `verifyCode`: Verifies an OTP code if the verification method is 'otp'.
- `isLoading`: Indicates whether an authentication request is in progress.
- `isVerifying`: Indicates whether an OTP verification is in progress.

**Example:**

```ts
import React, { useState } from 'react';
import { View, TextInput, Button } from 'react-native';
import { useAuthWithIdentifier } from '@ronas-it/react-native-common-modules/clerk';

export const AuthWithIdentifierComponent = () => {
  const [identifier, setIdentifier] = useState('');
  const [verificationCode, setVerificationCode] = useState('');
  const { startSignUp, verifyCode, isLoading, isVerifying } = useAuthWithIdentifier('emailAddress', 'otp');

  const handleSignUp = async () => {
    await startSignUp({ identifier });
  };

  const handleVerifyCode = async () => {
    const result = await verifyCode({ code: verificationCode });
    console.log(result.sessionToken)
  };

  return (
    <View>
      <TextInput
        placeholder="Enter your email"
        value={identifier}
        onChangeText={setIdentifier}
        keyboardType="email-address"
      />
      <TextInput
        placeholder="Enter verification code"
        value={verificationCode}
        onChangeText={setVerificationCode}
      />
      <Button onPress={handleSignUp} title="Sign Up" disabled={isLoading || isVerifying} />
      <Button onPress={handleVerifyCode} title="Verify code" disabled={isLoading || isVerifying} />
    </View>
  );
};

```

#### `useAuthWithSSO`

Hook provides functionality to handle [SSO](https://clerk.com/docs/references/expo/use-sso) authentication flows.

Returned Object:

- `startSSOFlow`: A function to initiate an SSO flow. It takes a strategy, redirectUrl, and optional tokenTemplate as parameters, starting the SSO authentication and returning session information or errors upon completion.
- `isLoading`: A boolean indicating whether an SSO process is currently ongoing.

#### `useAuthWithTicket`

This hook is a utility that facilitates user authentication using a ticket-based strategy (ticket is a token generated from the Backend API).

Returned Object:

- `startAuthorization`: A function to initiate authentication with a ticket. It accepts an object with ticket and optional tokenTemplate parameters to kick off the authorization process and returns the session details.
- `isLoading`: A boolean indicating whether the ticket-based authorization process is ongoing.

#### `useGetSessionToken`

This hook is a utility for getting session tokens.

Returned Object:

- `getSessionToken`: A function to retrieve the session token. It takes an optional [tokenTemplate](https://clerk.com/docs/backend-requests/jwt-templates) parameter to specify a template for the token.

#### `useAddIdentifier`

Hook provides functionality to add new email or phone number identifiers to a user's account and verify them using verification codes.

Returned Object:

- `createIdentifier`: A function to add a new email or phone number identifier to the user's account and prepare it for verification.
- `verifyCode`: A function to verify a code sent to the identifier, completing the verification process.
- `isCreating`: A boolean indicating whether an identifier is currently being added.
- `isVerifying`: A boolean indicating whether a verification code is currently being processed.

#### `useUpdateIdentifier`

Hook to update the user's primary identifier (email or phone): add and verify a new one, then set it as primary and remove the old one.

Returned Object:

- `createIdentifier`, `verifyCode`, `isCreating`, `isVerifying`, `isUpdating` — see JSDoc.

#### `useOtpVerification`

Hook provides functionality for managing OTP (One Time Password) verification in user authentication workflows, supporting both sign-up and sign-in processes.

Returned Object:

- `sendOtpCode`: Sends an OTP code to the user's identifier (email or phone number) based on the specified strategy.
- `verifyCode`: Verifies the OTP code provided by the user, completing the authentication process.
- `isVerifying`: A boolean indicating whether a verification attempt is currently in progress.

#### `useResetPassword`

Hook provides methods to handle password reset functionality through email or phone-based OTP.

Returned Object:

- `startResetPassword`: A function to initiate the password reset process by sending a verification code to the user's email or phone number.
- `resetPassword`: A function to reset the user's password by verifying the code and setting a new password.
- `isCodeSending`: A boolean indicating if the verification code is being sent.
- `isResetting`: A boolean indicating if the password is being reset.

#### `useChangePassword`

Hook to update the current user's password (requires current and new password).

Returned Object:

- `updatePassword`, `isPasswordUpdating` — see JSDoc.