npm package: @os1-platform/dispatch-mobile
Description: Dispatch SDK React Native Package
Author: Amit Sanvedi
Latest version: 3.2.0

# React Native Dispatch SDK

## Introduction

Dispatch SDK is an expo based SDK that is written in Typescript and some Modules are in Java/Kotlin.
This SDK can be used to render execution task screens on UI and maintain their states.

`Currently supports SDK>=21 (Android)`

### Dispatch SDK provides the following features:

- `Sync Manager`: Provides Sync Manager for syncing events & Docs in background with retry functionality.
- `Execution Tasks`: Provides a list of Execution Tasks inbuilt UIs which are as follows.
  - `Deliver`
  - `Capture Input`
  - `Deliver Cash`
  - `Complete-Success`
  - `Complete-Failure`
  - `Pickup`
  - `Doodle`
  - `Form`
  - `Display`
  - `Verify Location`
  - `Verify Input`
  - `Scan`
  - `Image Capture`
  - `Init Payment`
  - `Process Payment`
  - `Complete Payment`
  - `Validate OTP`
  - `Collect Payment`
- `Execution Engine`: Provides methods for Executing a given workflow/Job in a dispatch`
- `Firebase Cloud Messaging`:Provides method for receiving Firebase Cloud Messaging
- `Async storage`:Provides Async storage for storing key value pairs
- `Async Events`: Support for async events triggered on start and end of every execution task except end-state tasks, as for them a summary is generated at the end of completion of workflow.

  - Format of events:
    ```js
    const eventName =
      `onTaskStart:{ET_Name}` |
      `onTaskEnd:{ET_name}` |
      `onTaskBack:{ET_name}` |
      `onScan:{ET_name}` |
      `onScanRemove:{ET_name}`;
    ```
  - Format of click events for collect payment ET:
    ```js
    const eventName = `onClick:{clickEventName}:{etName}`
    enum clickEventName { QR_CODE, SEND_LINK, RESEND_LINK, CHECK_STATUS }
    ```
  - Usage:
    ```js
    import { eventListener } from '@os1-platform/dispatch-mobile';
    eventListener.on('{eventName}', (eventData) => {
      console.log(eventData);
    });
    eventListener.remove('{eventName}');
    ```
  - Event data Payload:
    `onTaskStart event`:

    ```js
      `onTaskStart:{ETCustomName}` = {
        "timestamp":"number",
        "taskId":"string",
        "etData":{
          "jobId1":{
              ...ET inputs
          },
          "jobId2":{
              ...ET inputs
          }
        }
      }
    ```

    `onTaskEnd event` | `onTaskBack event`:

    ```js
      `onTaskEnd:{ETCustomName}` = {
        "timestamp": "string",
        "taskId": "string",
        "etData":{
          "jobId1":{
              "success":"boolean",
              "eventCode":"string",
              "reasonCode":"string",
              ...ET output
          },
          "jobId2":{
              "success":"boolean",
              "eventCode":"string",
              "reasonCode":"string",
              ...ET output
          }
        },
        "meta"?:{},
    }
    ```

    `onClick event for QR_CODE and CHECK_STATUS`

    ```js
    {
      timestamp: number;
      taskId: string;
      customData?: any;
      etData: {
        ...etInput
      }
      additionalData: {
        //This is the same refId passed by the app at the start of an objective
        "refId":"string",
        //This amount  will be sum of amount for all the merged objective
        "money":{       
            "amount":"number", // sum of all the merged objectives
            "currency":"string"
        }
      }
    }
    ```

    `onClick event for SEND_LINK and RESEND_LINK`

    ```js
    {
      timestamp: number;
      taskId: string;
      customData?: any;
      etData: {
        ...etInput
      }
      additionalData: {
        //This is the same refId passed by the app at the start of an objective
        "refId": string,
        //This amount  will be sum of amount for all the merged objective
        "money":{       
            "amount": number, // sum of all the merged objectives
            "currency": string
        }
        //This will contain either phoneNumber or email as per user selection
        "contactDetails": string,
        //Static ENV based payment landing page link
        "paymentLink": string
      }
    }
    ```

## Dispatch SDK Installation

### Setup Expo (For non-expo projects only)

```sh
npx install-expo-modules
```

### Install Dispatch SDK Package

```sh
#using npm
npm install @os1-platform/dispatch-mobile
```

### Install these dependencies

```json
{
  "dependencies": {
    "@apollo/client": "^3.5.6",
    "@expo-google-fonts/ibm-plex-sans": "*",
    //MTS SDK has been removed as a dependency now
    //"@delhivery/platform-coreos-mts-sdk": "^2.0.3",
    "@react-native-async-storage/async-storage": "^1.15.5",
    "@react-native-community/datetimepicker": "^3.5.2",
    "@react-native-community/netinfo": "^6.0.2",
    "@react-native-firebase/analytics": "^14.2.2",
    "@react-native-firebase/app": "^14.2.2",
    "@react-native-firebase/crashlytics": "^14.2.2",
    "@react-native-firebase/messaging": "^14.2.2",
    "@react-native-firebase/remote-config": "^14.2.2",
    "@react-navigation/native": "^6.0.6",
    "@react-navigation/native-stack": "^6.2.5",
    "@sentry/react-native": "^3.2.13",
    "axios": "^0.24.0",
    "expo": "~47.0.13",
    "expo-barcode-scanner": "~11.4.0",
    "expo-blur": "~11.0.0",
    "expo-camera": "~13.1.0",
    "expo-file-system": "~15.1.1",
    "expo-font": "~11.0.1",
    "expo-image-picker": "~14.1.0",
    "expo-location": "~15.0.1",
    "expo-sqlite": "~11.0.0",
    "graphql": "^16.2.0",
    "react": "^18.0.0",
    "react-native": "^0.69.5",
    "react-native-dropdown-picker": "^5.4.0",
    "react-native-get-random-values": "^1.8.0",
    "react-native-paper": "^4.9.2",
    "react-native-safe-area-context": "^3.3.2",
    "react-native-screens": "^3.9.0",
    "react-native-vector-icons": "^9.0.0"
  }
}
```

### Async Storage Size Increase

- `Add this line in gradle.properties file for increasing storage (AsyncStorage_db_size_in_MB=10)`

### Changes in Android Manifest File (Android only)

```txt
android:allowBackup="false"
tools:replace="android:allowBackup"
```

```xml
<activity
      android:name=".MainActivity"
      android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode"
      android:label="@string/app_name"
      android:launchMode="singleTask"
      android:windowSoftInputMode="adjustResize"
      android:exported="true">
```

### Add jcenter() if not added already added in the Project-level build.gradle (<project>/build.gradle): (Android only)

```txt
buildscript {

    repositories {
        ...
        jcenter()
    }
}

allprojects {
    repositories {
        ...
        jcenter()
        ...
    }
}

```

### Fix for Apollo GraphQL Client (metro.config.js)

### Add the following changes in metro.config.js file

#### Issue Link https://github.com/apollographql/apollo-client/releases/tag/v3.5.4

```js
const { getDefaultConfig } = require('metro-config');
const { resolver: defaultResolver } = getDefaultConfig.getDefaultValues();
exports.resolver = {
  ...defaultResolver,
  sourceExts: [...defaultResolver.sourceExts, 'cjs'],
};
```

## Dispatch SDK Usage

### Init Dispatch SDK

```tsx
interface EventMapping {
  eventCode: string;
  successEvent: boolean;
  description: string;
}

interface DispatchSDKConfig {
  ...previous configs
  debugLevelLog?: Boolean;
  callbacks?: {
    fetchPhoneNumbersCallback?: (sdsIds: string[]) => Promise<{ key: string; value: string[]}>;
  };
}


const fetchPhoneNumbersCallback = (ids: string[]) => {
  console.log('sds ids: ', ids);
  return {
    key: 'phoneNumber', //This key is same, what was being sent in meta as key
    value: ['8888888888'],
  };
};

import { DispatchSDKManager } from '@os1-platform/dispatch-mobile';

await DispatchSDKManager.getInstance().initDispatchSDK({
  userName: 'testuser',
  userID: 'testID',
  tenantID: tenantId,
  tenantBaseURL: 'baseURL',
  accessToken: 'accessToken',
  headers: {
    'x-user-id': 'testID',
    'x-coreos-tid': tenantId,
    'x-coreos-access': accessToken,
    'Content-Type': 'application/json',
    'x-coreos-request-id': new Date().getTime(),
    'x-coreos-userinfo': JSON.stringify({ id: 'testID' }),
  },
  // Used for Scan ET dropdown list => eventMapping: EventMapping[]
  eventMapping: [
    {
      eventCode: 'E-010',
      successEvent: true,
      description: 'Success',
    },
    {
      eventCode: 'E-011',
      successEvent: false,
      description: 'Failure',
    },
  ],
  /* 
  * Callback to fetch mobile number based on the sds-ids passed
  * This same callback will be used to fetch contacts for collect payment ET.
  */
  callbacks: { fetchPhoneNumbersCallback }, 
});
```

### Init Execution engine

```ts
interface Job {
    id: string;
    jobRef: string;
    jobWorkflowId: string;
    status: string;
    subStatus?: string | null;
    containers: Array<Container>;
    customData: any;
    displayInfo: any;
    start: string;
    objectives: Array<Objective>;
}

interface Workflow {
    name: string;
    id: string;
    tag: IWfTag[];
    description: string;
    flows: Array<ExecutionTask>;
    expire?: boolean;
    meta?: string;
}

interface Workflows {
  [workflowId:string]: Workflow;
}

import { DispatchStateContainer } from '@os1-platform/dispatch-mobile';

/**
 * Call this function when dispatch data is fetched successfully
 * @param dispatchID
 * @param jobs
 * @param logging
 * @param maxTaskReattempt
 */

await DispatchStateContainer.getInstance().initDispatchExecutor(
  dispatchID: string,
  dispatchJobs: Job[],
  dispatchWorkflows: Workflows,
  maxTaskReattempt?: number
);
```

### Start Objective Execution

```tsx
interface sdkError {
  code: string;
  message: string;
}
// Error Handling from the calling Screen
React.useEffect(() => {
  if (route.params?.sdkError) {
    console.log(JSON.stringify(route.params.sdkError));
    Alert.alert('Error', JSON.stringify(route.params.sdkError));
  }
}, [route.params?.sdkError]);

navigation.navigate('DispatchExec', {
  success: true,
  initRoute: 'TaskDetail',
  mergedWI: {
    mwId: route.params.moId,
    customEventsData: {},
    statusFilter: ExecutionStatus[]
  },
  successRoute: 'SuccessScreen',
  failureRoute: 'FailureScreen',
  meta: META,
  paymentConfig: {
    paymentRefId: string, 
    pgConfigId?: string,
    userInfo?:{
      phone?:{
        countryCode:string
        mobileNumber:string
      }
    }
  }

});

```

### Payment Handling
- FCM Notification
  To handle the payment status notification using FCM, following data should be provided while sending the notification to app:

  ```ts
    interface notificationPaymentStatus {
      refId: string;
      status: STATUS;
    }
  ```

  - Example:
    In notification payload data should have: 
      `type: 'PAYMENT_COMPLETED'` and data.data should be json stringified.

    ```js
      var payload = {
        "notification":{
            "title":"Payment Completion",
            "body":"payment complete notify"
        },
        "data": {
          "type": "PAYMENT_COMPLETED",
          "data": JSON.stringify({
            "status":INITIATED | FAILED | SUCCESS,
            "refId":"ref:12345",
          })
        }
      };
    ```
  - Notification Handling
    1. In case of status: success, the user will be redirected to the complete payment screen, from init/ contact details/process payment screens.
    2. In case of status:failure, 
      2.1. If the user is on the Init Payment screen, then it will stay on the same screen.
      2.2. If the user is on the ContactDetails screen, then it will stay on the same screen.
      2.3. If the user is on the Process Payment screen, then it will get redirected to the complete payment screen, and the complete payment screen will have the option for retry or cancel.

- GO Back Feature
  1. If the user is on Init Payment screen and a payment is already initiated for that order(objective), and refId id is in-progress, then going back to previous ET should not be allowed and user to get redirected to home screen instead. 
  2. If the user is on Init Payment screen and the payment is not yet initiated, then going back to previous ET as per the workflow should be allowed.
  

### Objective Summary Route Params

```ts

export interface ObjectiveSummary {
entityCode: string;
eventCode: string;
reasonCode: string;
reasonCodeDesc?: string;
}

// When navigating to success or failure route, following are the route params:
{
summary: {
 success?: ObjectiveSummary[];
 failure?: ObjectiveSummary[];
},
objSuccess: boolean,
}
```

### New functions

```ts
// getObjectiveTaskList
public async getMergedObjectiveList getObjectiveTaskList(
    status: ExecutionStatus[]
  ): Promise<IObjectiveTask[]> {
    return [];
  }

// getObjectiveTaskDetails
public async getMergedObjectiveDetails getObjectiveTaskDetails(
    mwId: string,
    status: ExecutionStatus[]
  ):  Promise< (IObjectiveTask & { workflowData: StoredWorkflowInstance[] | [] }) | {} >
  {
    return [];
  }

// Inventory Data
public async getRiderInventory(): Promise<IDispatch.ItemsInRiderCustody> {
	return {}
}
```

### Interfaces

```ts
export interface IObjectiveTask {
  id: string;
  status: ExecutionStatus;
  tags: IWfTag[];
  inputs: any;
  instanceCount: number;
  location: Location;
  contact: string[];
}

export interface StoredWorkflowInstance {
  id: string;
  mergedObjectiveId: string;
  jobId: string;
  scannableId: string;
  initialMergedObjectiveId: string;
  status: ExecutionStatus;
  jobWorkflowId: string;
  workflowId: string;
  displayInfo: any;
  cashAmount: number;
}

enum ExecutionStatus {
  READY = 'READY',
  IN_PROGRESS = 'IN_PROGRESS',
  NOT_READY = 'NOT_READY',
  COMPLETED_SUCCESS = 'COMPLETED-SUCCESS',
  COMPLETED_FAILURE = 'COMPLETED-FAILURE',
}

interface IWfTag {
  name: string;
  value: string;
}

export interface Location {
  locationId: string;
  address?: any;
  geolocation?: any;
}
export interface ItemsInRiderCustody {
  shipments: {
    totalCount: number;
    deliveredCount: number;
    pickedCount: number;
  };
  cash: {
    expectedAmount: number;
    collectedAmount: number;
  };
}
```

## FCM

#### Setup (android only)

##### Add the Firebase Android configuration file to your app:

- Create a firebase project (Check Firebase instructions for creating app).
- Click Download google-services.json to obtain your Firebase Android config file (google-services.json).
- Move your config file into the module (app-level) directory of your app.
- To enable Firebase products in your app, add the google-services plugin to your Gradle files.

###### In your module (app-level) Gradle file (usually app/build.gradle), apply the Google Services Gradle plugin:

```text
apply plugin: 'com.android.application'
// Add the following line:
apply plugin: 'com.google.gms.google-services'  // Google Services plugin

android {
  // ...
}
```

#### In your root-level (project-level) Gradle file (build.gradle), add rules to include the Google Services Gradle plugin. Check that you have Google's Maven repository, as well

```text
buildscript {

  repositories {
    // Check that you have the following line (if not, add it):
    google()  // Google's Maven repository
  }

  dependencies {
    // ...

    // Add the following line:
    classpath("com.android.tools.build:gradle:7.1.1")
    classpath("com.google.gms:google-services:4.3.10")  // Google Services plugin
    classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
  }
}

allprojects {
  // ...

  repositories {
    // Check that you have the following line (if not, add it):
    google()  // Google's Maven repository
    // ...
  }
}

```

### Get FCM Token

```ts
import {
  getFCMToken,
  requestFirebasePermissions,
} from '@os1-platform/dispatch-mobile';
//Get FCM Token
let token = await getFCMToken();

//Request Notifications permissions (ios)
// It will ask for user permissions for showing alert/notifications
let enabled = await requestFirebasePermissions();
```

### Background FCM Messages

##### Register callback for receiving FCM Messages in background

Note : Call this function in the root component i.e in index.js file of the app

```ts
import { registerBackgroundHandler } from '@os1-platform/dispatch-mobile';

registerBackgroundHandler((message: object) => {
  // Handle FCM Message here
});
```

#### Foreground FCM Messages

###### useFcmMessage() custom hook in functional components to receive FCM Messages in Foregound

```ts
import { useFCMMessage } from '@os1-platform/dispatch-mobile';

const fcmMessage = useFCMMessage();
if (fcmMessage != null) {
  // update UI here to show the message
}
```

OR

###### extend FCM Base class in case of class components

```ts
import { FCM } from '@os1-platform/dispatch-mobile';

class ClassComponent extends FCM {
  //implement these methods
  handleFcmMessage(remoteMessage: object): void {}

  //implement these methods
  handleNotification(remoteMessage: object): void {}
}
```

### Error Codes

```ts
PERMISSIONS_ERROR[2500] = 'Mandatory Android Permissions not provided';
PARAM_MISSING[2502] = 'Mandatory Paramater is missing in request';
```

## Sync Manager

```ts
import { AppSyncManager, SdkSyncType } from '@os1-platform/dispatch-mobile';

// Start Events sync
await AppSyncManager.getInstance().startSyncing(
  false, // pass true for force sync
  SdkSyncType.EVENTS_SYNC
);

//Start Documents sync
await AppSyncManager.getInstance().startSyncing(
  false, // pass true for force sync
  SdkSyncType.DOCUMENT_SYNC
);

//Get All Events By Dispatch ID
await AppSyncManager.getInstance().getAllEvents('dsp_id');

//Get all documents By Dispatch ID
await AppSyncManager.getInstance().getAllDocuments('dsp_id');
```

## Start Sync Manager as a Foreground Service in android

```ts
import { NativeSyncManager } from '@os1-platform/dispatch-mobile';

NativeSyncManager.startSyncManager(
  interval,
  notificationTitle,
  notificationText
);
// interval will be in seconds
NativeSyncManager.startSyncManager(
  2000,
  'Dispatch Service',
  'Syncing events...'
);

//To stop the foreground android service
NativeSyncManager.stopSyncManager();
```

## SDK Utility methods

### Download Firebase Config

```ts
import { SdkUtils } from '@os1-platform/dispatch-mobile';

await SdkUtils.getRemoteConfig(3000);
// 3000 is the number of seconds to cache the config
```

### Download APK from Public URL

```ts
/**
 * Function to download apk file from a public URL
 * @param apkURL - URL where apk is hosted
 * @param version - expected version of apk (used for naming the file)
 * @param callback - callback for getting progress of download
 */
await SdkUtils.downloadAPK('https://apk_url.com', '1', (progress) =>
  console.log(progress.totalBytesWritten)
);
```

### Open & Install an APK File

```ts
/**
 * Opens & Install an APK file
 * @param uri - source of apk file
 */

await SdkUtils.openAPKFile(result.uri);
```

### Send events to Firebase analytics

```ts
/**
 *
 * @param eventName-> string
 * @param tag -> string
 * @param message -> string
 */
await Logger.getInstance().sendToFirebaseAnalytics('ev_name', 'tag', 'message');
```

### Log events to console

```ts
/**
 *
 * @param TAG
 * @param message
 * @param logType
 */
Logger.getInstance().logEvent('tag', 'message', LOG_TYPE.SDK_ERROR);
```

### Error Codes

```ts
const enum BaseErrorCodes {
  InvalidArgumentError = '100100',
  InvalidBaseURL = '100101',
  SyncManagerNotInitialized = '100102',
  MissingOrInvalidProps = '100103',
  SQLiteDBIssue = '100104',
  AppSyncNotInitialized = '100105',
  FMS_FOLDER_CREATION_ERROR = '100106',
  REASON_CODE_API_ERROR = '100107',
  MERGING_ERROR = '100108',
  LOCATION_PERMISSION_DENIED = '100109',
  CAMERA_PERMISSION_DENIED = '100110',
  STORAGE_PERMISSION_DENIED = '100111',
  GRAPHQL_CLIENT_NOT_INITIALIZED = '100112',
  FMS_GRAPHQL_API_ERROR = '100113',
  INTERNET_NOT_ENABLED = '100114',
  LOCATION_OR_GPS_NOT_ENABLED = '100115',
  EXECUTION_ENGINE_ERROR = '100116',
  UNEXPECTED_ERROR = '100117',
}
```