diff --git a/.gitignore b/.gitignore index 7900232..262e484 100644 --- a/.gitignore +++ b/.gitignore @@ -61,3 +61,6 @@ cache/ .eslintcache .cursor/rules/nx-rules.mdc .github/instructions/nx.instructions.md + +# Generated files for npm publishing +src/lib/README.md diff --git a/README.md b/README.md index 2806983..3aa8111 100644 --- a/README.md +++ b/README.md @@ -163,9 +163,18 @@ const webSocketService = new WebSocketService({ 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 -const tokenGetter = () => authSelectors.token(getState()); // Always get actual token -webSocketService.init(tokenGetter); +webSocketService.init( + tokenGetter, // Always get actual token + { + // Optional handlers + onConnectionStateChange: (currentState) => { + // Do something with the connection state + }, + }, +); // Connect to Pusher instance webSocketService.connect(); diff --git a/nx.json b/nx.json index 3df8e10..2973f4a 100644 --- a/nx.json +++ b/nx.json @@ -15,52 +15,11 @@ "sharedGlobals": [] }, "plugins": [ - { - "plugin": "@nx/expo/plugin", - "options": { - "startTargetName": "start", - "buildTargetName": "build", - "prebuildTargetName": "prebuild", - "serveTargetName": "serve", - "installTargetName": "install", - "exportTargetName": "export", - "submitTargetName": "submit", - "runIosTargetName": "run-ios", - "runAndroidTargetName": "run-android" - } - }, { "plugin": "@nx/eslint/plugin", "options": { "targetName": "lint" } - }, - { - "plugin": "@nx/jest/plugin", - "options": { - "targetName": "test" - } - }, - { - "plugin": "@nx/detox/plugin", - "options": { - "buildTargetName": "build", - "startTargetName": "start", - "testTargetName": "test" - } - }, - { - "plugin": "@nx/rollup/plugin", - "options": { - "buildTargetName": "build" - } - } - ], - "targetDefaults": { - "@nx/rollup:rollup": { - "cache": true, - "dependsOn": ["^build"], - "inputs": ["production", "^production"] } - } + ] } diff --git a/package-lock.json b/package-lock.json index 339c33c..fe93fde 100644 --- a/package-lock.json +++ b/package-lock.json @@ -51,7 +51,6 @@ "@nx/expo": "21.6.4", "@nx/jest": "^21.6.4", "@nx/js": "21.6.4", - "@nx/rollup": "21.6.4", "@nx/workspace": "21.6.4", "@pusher/pusher-websocket-react-native": "^1.3.1", "@react-native-community/cli-server-api": "^20.0.2", @@ -83,7 +82,6 @@ "pusher-js": "^8.4.0", "reactotron-react-native": "^5.1.17", "reactotron-redux": "^3.2.1", - "rollup": "^4.52.4", "syncpack": "^13.0.4", "tsc-files": "^1.1.4", "typescript": "~5.9.2", diff --git a/package.json b/package.json index 67844a4..a6e6f14 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,6 @@ "@nx/expo": "21.6.4", "@nx/jest": "^21.6.4", "@nx/js": "21.6.4", - "@nx/rollup": "21.6.4", "@nx/workspace": "21.6.4", "@pusher/pusher-websocket-react-native": "^1.3.1", "@react-native-community/cli-server-api": "^20.0.2", @@ -85,7 +84,6 @@ "pusher-js": "^8.4.0", "reactotron-react-native": "^5.1.17", "reactotron-redux": "^3.2.1", - "rollup": "^4.52.4", "syncpack": "^13.0.4", "tsc-files": "^1.1.4", "typescript": "~5.9.2", diff --git a/src/lib/README.md b/src/lib/README.md deleted file mode 100644 index 727c2fe..0000000 --- a/src/lib/README.md +++ /dev/null @@ -1,430 +0,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'; - - - Content goes here -; -``` - -### Services - -#### 1. Push notifications - -> **_NOTE:_** Required dependencies: `@pusher/pusher-websocket-react-native`, `pusher-js`, `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 milliseconds to ping a server after last message. -- `pongTimeout` (optional) - time in milliseconds to wait a response after a pinging request. - -`WebSocketService` public methods: - -- `connect` initializes and connects the Pusher client. Optional authorization token is used for secure connections. -- `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({ - apiKey: '1234567890qwertyuiop', - cluster: 'eu', - authURL: 'https://your-api.com/api/v1/broadcasting/auth', -}); - -// Initialize Pusher, e.g. after an app initialization or successful authorization -await webSocketService.connect('your-auth-token'); - -// 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); -}); -``` - -### Utils - -#### 1. `setupReactotron(projectName: string, plugins: Array)` - -> **_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({ storage }), -]); -const enhancers = reactotron ? [reactotron.createEnhancer()] : []; - -const initStore = createStoreInitializer({ - rootReducer: rootReducer as unknown as Reducer, - 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({ language: 'en' }); - -function App(): ReactElement { - return ( - - - - ); -} - -export default function RootLayout(): ReactElement | null { - const [language, setLanguage] = useState('en'); - - useLanguage(language); - - return ( - - - - ); -} -``` - -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 ( - - - - {translate('BUTTON_PRESS_ME')} - - - {translate('BUTTON_LANGUAGE')} - - - - ); -} -``` - -### 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 ( - - - -