diff --git a/.devrev/repo.yml b/.devrev/repo.yml new file mode 100644 index 0000000..a1d97bf --- /dev/null +++ b/.devrev/repo.yml @@ -0,0 +1 @@ +deployable: true diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..4def115 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,2 @@ +# Global code owner +* @rohan-devrev diff --git a/.gitignore b/.gitignore index ced8b27..365c296 100644 --- a/.gitignore +++ b/.gitignore @@ -8,22 +8,7 @@ !**/*.xcworkspacedata !**/*.xcsettings !**/*.xcscheme -*.pbxuser -!default.pbxuser -*.mode1v3 -!default.mode1v3 -*.mode2v3 -!default.mode2v3 -*.perspectivev3 -!default.perspectivev3 -xcuserdata -*.xccheckout -*.moved-aside DerivedData -*.hmap -*.ipa -*.xcuserstate -project.xcworkspace **/.xcode.env.local # Gradle @@ -68,10 +53,6 @@ buck-out .watchmanconfig # Android -.idea -.gradle -local.properties -*.iml /packages/react-native/android/* !/packages/react-native/android/README.md @@ -81,9 +62,6 @@ node_modules .nvm package-lock.json -# OS X -.DS_Store - # Test generated files *.js.meta @@ -152,7 +130,6 @@ vendor/ # Visual Studio Code (config dir - if present, this merges user defined # workspace settings on top of react-native.code-workspace) -/.vscode # Visual Studio .vs @@ -166,11 +143,7 @@ vendor/ # CircleCI .circleci/generated_config.yml -# XDE -.expo/ - # VSCode -.vscode/ jsconfig.json # Ruby diff --git a/CHANGELOG.md b/CHANGELOG.md index 7af0b6e..7e907f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,57 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.2.1] - 2025-10-17 + +### Added +- Added the ability to pause and resume user interaction event tracking, offering more security on the confidential screens. + +### Changed +- Simplified identification flow by deprecating redundant anonymous API usage. + +### Fixed +- Resolved potential out of memory crashes. +- Corrected incorrect or missing device model names on certain iPhone versions. +- Fixed visual distortion issues when zooming inside web view. +- Fixed issues in the logout process to ensure complete session termination and improved reliability. + +## [2.2.0] - 2025-09-29 + +### Changed +- iOS only: Improved the infrastructure for custom masking. +- Unified the screen tracking feature naming across platforms. + +### Deprecated +- Deprecated the `startScreenTransition` and `endScreenTransition` functions, use the `setInScreenTransitioning` function instead. + +### Fixed +- iOS only: Fixed push notifications not opening the conversations. +- Android only: Resolved potential issues that could cause app crashes during network operations. +- Android only: When adding session properties before initialization, they are now properly queued and executed. +- Android only: Added extra safeguards to prevent crashes related to uninitialized properties. +- Android only: Enhanced the stability of session-recording flows for a more reliable experience. + +## [2.1.3] - 2025-09-05 + +## Added +- Added detailed observability for SDK configuration and identification steps. + +## [2.1.2] - 2025-09-01 + +## Added +- Added a caching mechanism for identification calls. + +## Fixed +- Improved the presentation of support widget in edge to edge scenarios. +- Improved the session analytics and metrics. +- Fixed an issue related to long session engagement times. + +## [2.1.1] - 2025-07-24 + +## Fixed +- Fixed an issue with manual unmasking of input components. +- Fixed an issue with session uploads when the app is rapidly killed. + ## [2.1.0] - 2025-06-27 ### Added diff --git a/MIGRATION.md b/MIGRATION.md index e50f41e..b337178 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -1,17 +1,19 @@ -# Migration Guide -This guide and chart should help facilitate the transition from the legacy UserExperior SDK to the new DevRev SDK in your React Native application, providing insights into feature equivalents and method changes. +# Migration guide -## Feature Equivalence Chart -| Feature | UserExperior SDK | DevRev SDK | -|-|-|-| -| Installation | `npm install react-native-userexperior` | `npm install @devrev/sdk-react-native` | -| Initialization | `UserExperior.startRecording(string)` | `DevRev.configure(appID: string)` | -| User Identification | `UserExperior.setUserIdentifier(string)` | `DevRev.identifyAnonymousUser(userID: string)`
`DevRev.identifyUnverifiedUser(identity: Identity)`
`DevRev.updateUser(identity: Identity)`
`DevRev.identifyVerifiedUser(userID: string, sessionToken: string)`
`DevRev.logout(deviceID: string)` | -| Event Tracking | `UserExperior.logEvent(string, Map)` | `DevRev.trackEvent(name: string, properties?: Map)` | -| Session Recording | `UserExperior.stopRecording()`
`UserExperior.pauseRecording()`
`UserExperior.resumeRecording()` | `DevRev.startRecording()`
`DevRev.stopRecording()`
`DevRev.pauseRecording()`
`DevRev.resumeRecording()`
`DevRev.processAllOnDemandSessions()` | -| Opting in/out | Not supported. | `DevRev.stopAllMonitoring()`
`DevRev.resumeAllMonitoring()` | -| Session Properties | `UserExperior.setUserProperties(Map)` | `DevRev.addSessionProperties(properties: Map)`
`DevRev.clearSessionProperties()` | -| Masking Sensitive Data | `UserExperior.addInSecureViewBucket(any[])`
`UserExperior.removeFromSecureViewBucket(any[])` | `DevRev.markSensitiveViews(tags: any[])`
`DevRev.unmarkSensitiveViews(tags: any[])` | -| Timers | Not supported. | `DevRev.startTimer()`
`DevRev.stopTimer()` | -| PLuG support chat | Not supported. | `DevRev.showSupport()`
`DevRev.createSupportConversation()`
`DevRev.setShouldDismissModalsOnOpenLink()`
`DevRev.setInAppLinkHandler()` | -| Push Notifications | Not supported. | `DevRev.registerDeviceToken()`
`DevRev.unregisterDevice(deviceID: string)`
`DevRev.processPushNotification()` | +This guide and chart should help facilitate the transition from the legacy UserExperior SDK to the new DevRev SDK in your React Native application, providing insights into feature equivalents and method changes. + +## Feature equivalence chart + +| Feature | UserExperior SDK | DevRev SDK | +|-|-|-| +| Installation | `npm install react-native-userexperior` | `npm install @devrev/sdk-react-native` | +| Initialization | `UserExperior.startRecording(string)` | `DevRev.configure(appID: string)` | +| User Identification | `UserExperior.setUserIdentifier(string)` | `DevRev.identifyAnonymousUser(userID: string)`
`DevRev.identifyUnverifiedUser(identity: Identity)`
`DevRev.updateUser(identity: Identity)`
`DevRev.identifyVerifiedUser(userID: string, sessionToken: string)`
`DevRev.logout(deviceID: string)` | +| Event Tracking | `UserExperior.logEvent(string, Map)` | `DevRev.trackEvent(name: string, properties?: { [key: string]: string })` | +| Session Recording | `UserExperior.stopRecording()`
`UserExperior.pauseRecording()`
`UserExperior.resumeRecording()` | `DevRev.startRecording()`
`DevRev.stopRecording()`
`DevRev.pauseRecording()`
`DevRev.resumeRecording()`
`DevRev.processAllOnDemandSessions()` | +| Opting in/out | Not supported. | `DevRev.stopAllMonitoring()`
`DevRev.resumeAllMonitoring()` | +| Session Properties | `UserExperior.setUserProperties(Map)` | `DevRev.addSessionProperties(properties: { [key: string]: string })`
`DevRev.clearSessionProperties()` | +| Masking Sensitive Data | `UserExperior.addInSecureViewBucket(any[])`
`UserExperior.removeFromSecureViewBucket(any[])` | `DevRev.markSensitiveViews(tags: any[])`
`DevRev.unmarkSensitiveViews(tags: any[])` | +| Timers | Not supported. | `DevRev.startTimer(name: string, properties: { [key: string]: string })`
`DevRev.endTimer(name: string, properties: { [key: string]: string })` | +| Support chat | Not supported. | `DevRev.showSupport()`
`DevRev.createSupportConversation()`
`DevRev.setShouldDismissModalsOnOpenLink()`
`DevRev.setInAppLinkHandler()` | +| Push Notifications | Not supported. | `DevRev.registerDeviceToken()`
`DevRev.unregisterDevice(deviceID: string)`
`DevRev.processPushNotification()` | diff --git a/README.md b/README.md index f93b6b3..c5cf993 100644 --- a/README.md +++ b/README.md @@ -1,141 +1,295 @@ -# DevRev SDK for React Native -DevRev SDK, used for integrating DevRev services into your React Native app. +# DevRev SDK for React Native and Expo -## Table of contents -- [DevRev SDK for React Native](#devrev-sdk-for-react-native) - - [Table of contents](#table-of-contents) +DevRev SDK, used for integrating DevRev services into your React Native and Expo apps. + +- [DevRev SDK for React Native and Expo](#devrev-sdk-for-react-native-and-expo) - [Quickstart](#quickstart) + - [Requirements](#requirements) - [Installation](#installation) + - [Expo](#expo) - [Set up the DevRev SDK](#set-up-the-devrev-sdk) - [Features](#features) - [Identification](#identification) - - [Anonymous identification](#anonymous-identification) - - [Unverified identification](#unverified-identification) - - [Verified identification](#verified-identification) - - [Updating the user](#updating-the-user) + - [Identify an unverified user](#identify-an-unverified-user) + - [Identify a verified user](#identify-a-verified-user) + - [Generate an AAT](#generate-an-aat) + - [Exchange your AAT for a session token](#exchange-your-aat-for-a-session-token) + - [Identify the verified user](#identify-the-verified-user) + - [Update the user](#update-the-user) - [Logout](#logout) - - [PLuG support chat](#plug-support-chat) - - [Creating a new support conversation](#creating-a-new-support-conversation) - - [In-app link handling](#in-app-link-handling) - - [In-app link callback](#in-app-link-callback) + - [Identity model](#identity-model) + - [Properties](#properties) + - [User traits](#user-traits) + - [Organization traits](#organization-traits) + - [Account traits](#account-traits) + - [Support chat](#support-chat) + - [Create a new support conversation](#create-a-new-support-conversation) + - [In-app link handling](#in-app-link-handling) + - [In-app link callback](#in-app-link-callback) - [Dynamic theme configuration](#dynamic-theme-configuration) - [Analytics](#analytics) - [Session analytics](#session-analytics) - - [Opting-in or out](#opting-in-or-out) + - [Opt in or out](#opt-in-or-out) - [Session recording](#session-recording) - [Session properties](#session-properties) - - [Masking sensitive data](#masking-sensitive-data) + - [Mask sensitive data](#mask-sensitive-data) + - [Mask elements inside web views](#mask-elements-inside-web-views) + - [User interaction tracking](#user-interaction-tracking) - [Timers](#timers) - - [Screen tracking](#screen-tracking) - - [Screen transition tracking (Android only)](#screen-transition-tracking-android-only) + - [Track screens](#track-screens) + - [Manage screen transitions (Android only)](#manage-screen-transitions-android-only) - [Push notifications](#push-notifications) - [Configuration](#configuration) - [Register for push notifications](#register-for-push-notifications) - [Unregister from push notifications](#unregister-from-push-notifications) - - [Processing push notifications](#processing-push-notifications) + - [Handle push notifications](#handle-push-notifications) - [Android](#android) - - [Example](#example) - [iOS](#ios) - - [Example](#example-1) - - [Sample app](#sample-app) + - [Sample app (without framework)](#sample-app-without-framework) + - [Sample app (Expo)](#sample-app-expo) - [Troubleshooting](#troubleshooting) + - [ProGuard (Android only)](#proguard-android-only) - [Migration Guide](#migration-guide) ## Quickstart ### Requirements -- Minimum deployment target Android SDK 24 or iOS 15.1. + +- React Native 0.79.0 or later. +- For Expo apps, Expo 50.0.0 or later. +- Android: minimum API level 24. +- iOS: minimum deployment target 15.1. +- (Recommended) An SSH key configured locally and registered with [GitHub](https://docs.github.com/en/github/authenticating-to-github/connecting-to-github-with-ssh). ### Installation + To install the DevRev SDK, run the following command: -```sh +```bash npm install @devrev/sdk-react-native ``` +#### Expo + +1. To install the DevRev SDK, run the following command: + ```bash + npx expo install @devrev/sdk-react-native-expo-plugin + ``` +2. Configure the Expo config plugin in your `app.json` or `app.config.js`: + ```json + { + "expo": { + "plugins": [ + "@devrev/sdk-react-native-expo-plugin" + ] + } + } + ``` +3. Rebuild your app: + ```bash + npx expo prebuild --clean + ``` + ### Set up the DevRev SDK + 1. Open the DevRev web app at [https://app.devrev.ai](https://app.devrev.ai) and go to the **Settings** page. 2. Under **PLuG settings** copy the value under **Your unique App ID**. -3. After obtaining the credentials, you can configure the DevRev SDK in your app. +3. Configure the DevRev SDK in your app using the obtained credentials. + +> [!WARNING] +> The DevRev SDK must be configured before you can use any of its features. -The SDK will be ready for use once you execute the following configuration method. +The SDK becomes ready for use once the following configuration method is executed. ```typescript DevRev.configure(appID: string) ``` ## Features + ### Identification + To access certain features of the DevRev SDK, user identification is required. -The identification function should be placed appropriately in your app after the user logs in. If you have the user information available at app launch, call the function after the `DevRev.configure(appID:)` method. +The identification function should be placed appropriately in your app after the user logs in. If you have the user information available at app launch, call the function after the `DevRev.configure(appID: string)` method. -> [!IMPORTANT] -> On iOS, if you haven't previously identified the user, the DevRev SDK will automatically create an anonymous user for you immediately after the SDK is configured. +> [!TIP] +> If you haven't previously identified the user, the DevRev SDK will automatically create an anonymous user for you immediately after the SDK is configured. -> [!IMPORTANT] +> [!TIP] > The `Identity` structure allows for custom fields in the user, organization, and account traits. These fields must be configured through the DevRev app before they can be used. For more information, refer to [Object customization](https://devrev.ai/docs/product/object-customization). You can select from the following methods to identify users within your application: -#### Anonymous identification -The anonymous identification method allows you to create an anonymous user with an optional user identifier, ensuring that no other data is stored or associated with the user. +#### Identify an unverified user -```typescript -DevRev.identifyAnonymousUser(userID: string) -``` - -#### Unverified identification The unverified identification method identifies users with a unique identifier, but it does not verify their identity with the DevRev backend. ```typescript DevRev.identifyUnverifiedUser(userID: string, organizationID?: string) ``` -#### Verified identification -The verified identification method identifies users with a unique identifier and verifies their identity with the DevRev backend. +#### Identify a verified user + +The verified identification method is used to identify users with an identifier unique to your system within the DevRev platform. The verification is done through a token exchange process between you and the DevRev backend. + +The steps to identify a verified user are as follows: +1. Generate an AAT for your system (preferably through your backend). +2. Exchange your AAT for a session token for each user of your system. +3. Pass the user identifier and the exchanged session token to the `DevRev.identifyVerifiedUser(userID: string, sessionToken: string)` method. + +> [!WARNING] +> For security reasons, it is **strongly recommended** that the token exchange is executed on your backend to prevent exposing your application access token (AAT). + +##### Generate an AAT + +1. Open the DevRev web app at [https://app.devrev.ai](https://app.devrev.ai) and go to the **Settings** page. +2. Open the **PLuG Tokens** page. +3. Under the **Application access tokens** panel, click **New token** and copy the token that's displayed. + +> [!WARNING] +> Ensure that you copy the generated application access token, as you cannot view it again. + +##### Exchange your AAT for a session token + +To proceed with identifying the user, you need to exchange your AAT for a session token. This step helps you identify a user of your own system within the DevRev platform. + +Here is a simple example of an API request to the DevRev backend to exchange your AAT for a session token: + +> [!WARNING] +> Make sure that you replace the `` and `` with the actual values. + +```bash +curl \ +--location 'https://api.devrev.ai/auth-tokens.create' \ +--header 'accept: application/json, text/plain, */*' \ +--header 'content-type: application/json' \ +--header 'authorization: ' \ +--data '{ + "rev_info": { + "user_ref": "" + } +}' +``` + +The response of the API call contains a session token that you can use with the verified identification method in your app. + +> [!WARNING] +> As a good practice, **your** app should retrieve the exchanged session token from **your** backend at app launch or any relevant app lifecycle event. + +##### Identify the verified user + +Pass the user identifier and the exchanged session token to the verified identification method: ```typescript DevRev.identifyVerifiedUser(userID: string, sessionToken: string) ``` -#### Updating the user +#### Update the user + You can update the user's information using the following method: ```typescript DevRev.updateUser(identity: Identity) ``` -> [!IMPORTANT] +> [!WARNING] > The `userID` property cannot be updated. #### Logout + You can logout of the current user by using the following method: ```typescript DevRev.logout(deviceID: string) ``` -The user will be logged out by clearing their credentials, as well as unregistering the device from receiving push notifications, and stopping the session recording. +### Identity model + +The `Identity` interface is used to provide user, organization, and account information when identifying users or updating their details. This class is used primarily with the `identifyUnverifiedUser` and `updateUser` methods. + +#### Properties + +The `Identity` class contains the following properties: + +| Property | Type | Required | Description | +|----------|------|----------|-------------| +| `userRef` | `string` | ✅ | A unique identifier for the user | +| `organizationRef` | `string?` | ❌ | An identifier for the user's organization | +| `accountRef` | `string?` | ❌ | An identifier for the user's account | +| `userTraits` | `UserTraits?` | ❌ | Additional information about the user | +| `organizationTraits` | `OrganizationTraits?` | ❌ | Additional information about the organization | +| `accountTraits` | `AccountTraits?` | ❌ | Additional information about the account | + +> [!NOTE] +> The custom fields properties defined as part of the user, organization and account traits, must be configured in the DevRev web app **before** they can be used. See [Object customization](https://devrev.ai/docs/product/object-customization) for more information. + +##### User traits + +The `UserTraits` class contains detailed information about the user: + +> [!NOTE] +> All properties in `UserTraits` are optional. + +| Property | Type | Description | +|----------|------|-------------| +| `displayName` | `string?` | The displayed name of the user | +| `email` | `string?` | The user's email address | +| `fullName` | `string?` | The user's full name | +| `description` | `string?` | A description of the user | +| `customFields` | `{ [key: string]: any }` | Dictionary of custom fields configured in DevRev | -### PLuG support chat -Once user identification is complete, you can start using the chat (conversations) dialog supported by our DevRev SDK. The support chat feature can be shown as a modal screen from the top-most screen. +##### Organization traits -> [!IMPORTANT] -> This feature requires the SDK to be configured and the user to be identified, whether they are unverified or anonymous. +The `OrganizationTraits` class contains detailed information about the organization: + +> [!NOTE] +> All properties in `OrganizationTraits` are optional. + +| Property | Type | Description | +|----------|------|-------------| +| `displayName` | `string?` | The displayed name of the organization | +| `domain` | `string?` | The organization's domain | +| `description` | `string?` | A description of the organization | +| `phoneNumbers` | `string[]?` | Array of the organization's phone numbers | +| `tier` | `string?` | The organization's tier or plan level | +| `customFields` | `{ [key: string]: any }` | Dictionary of custom fields configured in DevRev | + +##### Account traits + +The `AccountTraits` class contains detailed information about the account: + +> [!NOTE] +> All properties in `AccountTraits` are optional. + +| Property | Type | Description | +|----------|------|-------------| +| `displayName` | `string?` | The displayed name of the account | +| `domains` | `string[]?` | Array of domains associated with the account | +| `description` | `string?` | A description of the account | +| `phoneNumbers` | `string[]?` | Array of the account's phone numbers | +| `websites` | `string[]?` | Array of websites associated with the account | +| `tier` | `string?` | The account's tier or plan level | +| `customFields` | `{ [key: string]: any }` | Dictionary of custom fields configured in DevRev | + +### Support chat + +Once the user identification is complete, you can start using the chat (conversations) dialog supported by our DevRev SDK. The support chat feature can be shown as a modal screen from the top-most screen. ```typescript DevRev.showSupport() ``` -#### Creating a new support conversation +#### Create a new support conversation + You can initiate a new support conversation directly from your app. This method displays the support chat screen and simultaneously creates a new conversation. ```typescript DevRev.createSupportConversation() ``` -#### In-app link handling +### In-app link handling + In certain cases, the links opened from the support chat are opened in the app instead of a browser. You can control whether the chat modal should be dismissed after the link is opened by calling the following method: ```typescript @@ -144,9 +298,10 @@ DevRev.setShouldDismissModalsOnOpenLink(value: boolean) Setting this flag to true applies the system's default behavior for opening links, which includes dismissing any DevRev modal screens to facilitate handling your own deep links. -#### In-app link callback -> [!NOTE] -> This feature is for Android only. +### In-app link callback + +> [!TIP] +> This feature is supported only on Android. For scenarios where custom handling is needed, links from the support chat can be captured with the following method: @@ -157,15 +312,14 @@ DevRev.setInAppLinkHandler((url) => { ``` ### Dynamic theme configuration -The DevRev SDK allows you to configure the theme dynamically based on the system appearance, or use the theme configured on the DevRev portal. By default, the theme will be dynamic and follow the system appearance. -```swift +The DevRev SDK allows you to configure the theme dynamically based on the system appearance, or use the theme configured on the DevRev portal. By default, the theme is dynamic and follows the system appearance. + +```typescript DevRev.setPrefersSystemTheme(value: boolean) ``` ### Analytics -> [!IMPORTANT] -> This feature requires the SDK to be configured and the user to be identified, whether they are unverified or anonymous. The DevRev SDK allows you to send custom analytic events by using a properties map. You can track these events using the following function: @@ -174,9 +328,11 @@ DevRev.trackEvent(name: string, properties?: { [key: string]: string }) ``` ### Session analytics + The DevRev SDK offers session analytics features to help you understand how users interact with your app. -#### Opting-in or out +#### Opt in or out + Session analytics features are opted-in by default, enabling them from the start. However, you can opt-out using the following method: ```typescript @@ -190,24 +346,28 @@ DevRev.resumeAllMonitoring() ``` #### Session recording + You can enable session recording to record user interactions with your app. -> [!CAUTION] +> [!NOTE] > The session recording feature is opt-out and is enabled by default. The session recording feature includes the following methods to control the recording: -- `DevRev.startRecording()`: Starts the session recording. -- `DevRev.stopRecording()`: Stops the session recording and uploads it to the portal. -- `DevRev.pauseRecording()`: Pauses the ongoing session recording. -- `DevRev.resumeRecording()`: Resumes a paused session recording. -- `DevRev.processAllOnDemandSessions()`: Stops the ongoing session recording and uploads all offline sessions on demand, including the current one. +| Method | Action | +|--------------------------------------------------------------------|-----------------------------------------------------------| +|`DevRev.startRecording()` | Starts the session recording. | +|`DevRev.stopRecording()` | Stops the session recording and uploads it to the portal. | +|`DevRev.pauseRecording()` | Pauses the ongoing session recording. | +|`DevRev.resumeRecording()` | Resumes a paused session recording. | +|`DevRev.processAllOnDemandSessions()` | Stops the ongoing user recording and sends all on-demand sessions along with the current recording. | #### Session properties + You can add custom properties to the session recording to help you understand the context of the session. The properties are defined as a map of string values. ```typescript -DevRev.addSessionProperties(properties: { [key: string]: any }) +DevRev.addSessionProperties(properties: { [key: string]: string }) ``` To clear the session properties in scenarios such as user logout or when the session ends, use the following method: @@ -216,10 +376,11 @@ To clear the session properties in scenarios such as user logout or when the ses DevRev.clearSessionProperties() ``` -#### Masking sensitive data -To protect sensitive data, the DevRev SDK provides an auto-masking feature that masks data before sending to the server. Input views such as text fields, text views, and web views are automatically masked. +#### Mask sensitive data + +To protect sensitive data, the DevRev SDK provides an auto-masking feature that masks data before sending to the server. Input views such as password text fields are automatically masked. -While the auto-masking feature may be sufficient for most situations, you can manually mark additional views as sensitive using the following method: +While the auto-masking feature is sufficient for most situations, you can manually mark additional views as sensitive using the following method: ```typescript DevRev.markSensitiveViews(tags: any[]) @@ -231,7 +392,77 @@ If any previously masked views need to be unmasked, you can use the following me DevRev.unmarkSensitiveViews(tags: any[]) ``` +For example: + +```typescript +import * as DevRev from '@devrev/sdk-react-native'; +import { View, Text, findNodeHandle } from "react-native"; +import { useRef, useEffect } from "react"; + +const YourComponent = () => { + const sensitiveViewRef = useRef(null); + const insensitiveViewRef = useRef(null); + + useEffect(() => { + // Mark sensitive view + const sensitiveId = findNodeHandle(sensitiveViewRef.current); + if (sensitiveId) { + DevRev.markSensitiveViews([sensitiveId]); + } + + // Unmark insensitive view + const insensitiveId = findNodeHandle(insensitiveViewRef.current); + if (insensitiveId) { + DevRev.unmarkSensitiveViews([insensitiveId]); + } + }, []); + + return ( + + + Sensitive content (masked in recordings) + + + Insensitive content (visible in recordings) + + + ); +}; + +export default YourComponent; +``` + +#### Mask elements inside web views + +To mark elements as sensitive inside a web view (`WebView`), apply the `devrev-mask` CSS class. To unmark them, use `devrev-unmask`. + +- Mark an element as masked: + ```html + + ``` +- Mark an element as unmasked: + ```html + + ``` + +#### User interaction tracking + +The DevRev SDK automatically tracks user interactions such as taps, swipes, and scrolls. However, in some cases you may want to disable this tracking to prevent sensitive user actions from being recorded. + +To **temporarily disable** user interaction tracking, use the following method: + +```typescript +DevRev.pauseUserInteractionTracking() +``` + +To **resume** user interaction tracking, use the following method: + +```typescript +DevRev.resumeUserInteractionTracking() +``` + #### Timers + The DevRev SDK offers a timer mechanism to measure the time spent on specific tasks, allowing you to track events such as response time, loading time, or any other duration-based metrics. The mechanism uses balanced start and stop methods, both of which accept a timer name and an optional dictionary of properties. @@ -245,41 +476,43 @@ DevRev.startTimer(name: string, properties: { [key: string]: string }) To stop a timer, use the following method: ```typescript -DevRev.stopTimer(name: string, properties: { [key: string]: string }) +DevRev.endTimer(name: string, properties: { [key: string]: string }) ``` -#### Screen tracking -The DevRev SDK offers automatic screen tracking to help you understand how users navigate through your app. Although view controllers are automatically tracked, you can manually track screens using the following method: +#### Track screens + +The DevRev SDK offers automatic screen tracking to help you understand how users navigate through your app. Although screens are automatically tracked, you can manually track screens using the following method: ```typescript -DevRev.trackScreen(name: string) +DevRev.trackScreenName(name: string) ``` -#### Screen transition tracking (Android only) -On Android, the DevRev SDK provides methods to manually track the screen transitions. +#### Manage screen transitions (Android only) -When a screen transition begins, you must call the following method: +The DevRev SDK allows tracking of screen transitions to understand the user navigation within your app. +You can manually update the state using the following methods: -```typescript -DevRev.startScreenTransition() -``` +```javascript +// Mark the transition as started. +DevRev.setInScreenTransitioning(true) -When a screen transition ends, you must call the following method: - -```typescript -DevRev.endScreenTransition() +// Mark the transition as ended. +DevRev.setInScreenTransitioning(false) ``` ### Push notifications + You can configure your app to receive push notifications from the DevRev SDK. The SDK is able to handle push notifications and execute actions based on the notification's content. -The DevRev backend sends push notifications to your app to notify users about new messages in the PLuG support chat. +The DevRev backend sends push notifications to your app to notify users about new messages in the support chat. #### Configuration -To receive push notifications, you need to configure your DevRev organization by following the instructions in the [push notifications](https://developer.devrev.ai/public/sdks/mobile/push-notification) section. + +To receive push notifications, you need to configure your DevRev organization by following the instructions in the [push notifications](https://developer.devrev.ai/sdks/mobile/push-notifications) section. #### Register for push notifications -> [!IMPORTANT] + +> [!TIP] > Push notifications require that the SDK has been configured and the user has been identified, to ensure delivery to the correct user. The DevRev SDK offers a method to register your device for receiving push notifications. You can register for push notifications using the following method: @@ -288,9 +521,10 @@ The DevRev SDK offers a method to register your device for receiving push notifi DevRev.registerDeviceToken(deviceToken: string, deviceID: string) ``` -On Android devices, the `deviceToken` should be the Firebase Cloud Messaging (FCM) token value, while on iOS devices, it should be the Apple Push Notification Service (APNs) token. +On Android devices, the `deviceToken` should be the Firebase Cloud Messaging (FCM) token value, while on iOS devices, it should be the Apple Push Notification service (APNs) token. #### Unregister from push notifications + If your app no longer needs to receive push notifications, you can unregister the device. Use the following method to unregister the device: @@ -301,8 +535,10 @@ DevRev.unregisterDevice(deviceID: string) The method requires the device identifier, which should be the same as the one used when registering the device. -#### Processing push notifications +#### Handle push notifications + ##### Android + On Android, notifications are implemented as data messages to offer flexibility. However, this means that automatic click processing isn't available. To handle notification clicks, developers need to intercept the click event, extract the payload, and pass it to a designated method for processing. This custom approach enables tailored notification handling in Android applications. To process the notification, use the following method: @@ -313,7 +549,7 @@ DevRev.processPushNotification(payload: string) Here, the `message` object from the notification payload needs to be passed to this function. -###### Example +For example: ```typescript const notificationPayload = { @@ -327,50 +563,111 @@ DevRev.processPushNotification(JSON.stringify(messageJson)); ``` ##### iOS -On iOS devices, you must pass the received push notification payload to the DevRev SDK for processing. The SDK will then handle the notification and execute the necessary actions. + +On iOS devices, you must pass the received push notification payload to the DevRev SDK for processing. The SDK handles the notification and executes the necessary actions. ```typescript DevRev.processPushNotification(payload: string) ``` -###### Example +For example: ```typescript DevRev.processPushNotification(JSON.stringify(payload)); ``` -## Sample app +## Sample app (without framework) -A sample app with use cases for the DevRev React Native plugin has been provided as a part of our [public repository](https://github.com/devrev/devrev-sdk-react-native). To set up and run the sample app: +A sample app with use cases for the DevRev SDK for React Native has been provided as a part of our [public repository](https://github.com/devrev/devrev-sdk-react-native). To set up and run the sample app, follow these steps: 1. Go to the sample directory: - ```sh - cd sample + ```bash + cd sample/react-native ``` - -2. Install dependencies: - ```sh +2. Install the dependencies: + ```bash yarn install ``` - 3. For iOS, run: - ```sh - cd ios - pod install + ```bash + pod install --project-directory=ios --repo-update ``` - -4. Run the app on Android or iOS using: - ```sh +4. Start the React Native development server: + ```bash npx react-native start ``` +5. Run the app on Android using: + ```bash + npx react-native run-android + ``` + or open the `android` directory in Android Studio and run the app from there. +6. Run the app on iOS using: + ```bash + npx react-native run-ios + ``` + or open `ios/DevRevSDKSampleRN.xcworkspace` in Xcode and run the app from there. -Or open `android` directory in Android Studio or `ios/DevRevSDKSample.xcworkspace` in Xcode and run the app from there. +## Sample app (Expo) + +A sample app with use cases for the DevRev SDK for Expo has been provided as a part of our [public repository](https://github.com/devrev/devrev-sdk-react-native). To set up and run the sample app, follow these steps: + +1. Go to the sample directory: + ```bash + cd sample/expo + ``` +2. Install the dependencies: + ```bash + yarn install + ``` +3. Run clean and prebuild: + ```bash + npx expo prebuild --clean + ``` +4. Run the app on Android using: + ```bash + npx expo run:android + ``` + OR open the `android` directory in Android Studio and run the app. +5. Run the app on iOS: + ```bash + npx expo run:ios + ``` + OR open `ios/DevRevSDKSample.xcworkspace` in Xcode and run the app. ## Troubleshooting -- **Issue**: Support chat won't show. - **Solution**: Ensure you have correctly called one of the identification methods: `DevRev.identifyUnverifiedUser(...)`, `DevRev.identifyVerifiedUser(...)`, or `DevRev.identifyAnonymousUser(...)`. + +- **Issue**: Support chat doesn't show. + **Solution**: Ensure you have correctly called one of the identification methods: `DevRev.identifyUnverifiedUser(...)` or `DevRev.identifyVerifiedUser(...)`. - **Issue**: Not receiving push notifications. - **Solution**: Ensure that your app is configured to receive push notifications and that your device is registered with the DevRev SDK. + **Solution**: Ensure that your app is configured to receive push notifications and that your device is registered with the DevRev SDK. + +### ProGuard (Android only) + +When trying to build your app for Android with ProGuard enabled, refer to these common issues and their solutions. + +> [!NOTE] +> You can always refer to the [Android ProGuard documentation](https://developer.android.com/topic/performance/app-optimization/enable-app-optimization#proguard) for more information. + +- **Issue**: Missing class `com.google.android.play.core.splitcompat.SplitCompatApplication`. + **Solution**: Add the following line to your `proguard-rules.pro` file: + ```proguard + -dontwarn com.google.android.play.core.** + ``` + +- **Issue**: Missing class issue due to transitive Flutter dependencies. + **Solution**: Add the following lines to your `proguard-rules.pro` file: + ```proguard + -keep class io.flutter.** { *; } + -keep class io.flutter.plugins.** { *; } + -keep class GeneratedPluginRegistrant { *; } + ``` + +- **Issue**: Missing class `org.s1f4j.impl.StaticLoggerBinder`. + **Solution**: Add the following line to your `proguard-rules.pro` file: + ```proguard + -dontwarn org.slf4j.impl.StaticLoggerBinder + ``` ## Migration Guide + If you are migrating from the legacy UserExperior SDK to the new DevRev SDK, please refer to the [Migration Guide](./MIGRATION.md) for detailed instructions and feature equivalence. diff --git a/devrev-sdk-react-native-2.2.1.tgz b/devrev-sdk-react-native-2.2.1.tgz new file mode 100644 index 0000000..9b32ad1 Binary files /dev/null and b/devrev-sdk-react-native-2.2.1.tgz differ diff --git a/sample/README.md b/sample/README.md deleted file mode 100644 index 12470c3..0000000 --- a/sample/README.md +++ /dev/null @@ -1,79 +0,0 @@ -This is a new [**React Native**](https://reactnative.dev) project, bootstrapped using [`@react-native-community/cli`](https://github.com/react-native-community/cli). - -# Getting Started - ->**Note**: Make sure you have completed the [React Native - Environment Setup](https://reactnative.dev/docs/environment-setup) instructions till "Creating a new application" step, before proceeding. - -## Step 1: Start the Metro Server - -First, you will need to start **Metro**, the JavaScript _bundler_ that ships _with_ React Native. - -To start Metro, run the following command from the _root_ of your React Native project: - -```bash -# using npm -npm start - -# OR using Yarn -yarn start -``` - -## Step 2: Start your Application - -Let Metro Bundler run in its _own_ terminal. Open a _new_ terminal from the _root_ of your React Native project. Run the following command to start your _Android_ or _iOS_ app: - -### For Android - -```bash -# using npm -npm run android - -# OR using Yarn -yarn android -``` - -### For iOS - -```bash -# using npm -npm run ios - -# OR using Yarn -yarn ios -``` - -If everything is set up _correctly_, you should see your new app running in your _Android Emulator_ or _iOS Simulator_ shortly provided you have set up your emulator/simulator correctly. - -This is one way to run your app — you can also run it directly from within Android Studio and Xcode respectively. - -## Step 3: Modifying your App - -Now that you have successfully run the app, let's modify it. - -1. Open `App.tsx` in your text editor of choice and edit some lines. -2. For **Android**: Press the R key twice or select **"Reload"** from the **Developer Menu** (Ctrl + M (on Window and Linux) or Cmd ⌘ + M (on macOS)) to see your changes! - - For **iOS**: Hit Cmd ⌘ + R in your iOS Simulator to reload the app and see your changes! - -## Congratulations! :tada: - -You've successfully run and modified your React Native App. :partying_face: - -### Now what? - -- If you want to add this new React Native code to an existing application, check out the [Integration guide](https://reactnative.dev/docs/integration-with-existing-apps). -- If you're curious to learn more about React Native, check out the [Introduction to React Native](https://reactnative.dev/docs/getting-started). - -# Troubleshooting - -If you can't get this to work, see the [Troubleshooting](https://reactnative.dev/docs/troubleshooting) page. - -# Learn More - -To learn more about React Native, take a look at the following resources: - -- [React Native Website](https://reactnative.dev) - learn more about React Native. -- [Getting Started](https://reactnative.dev/docs/environment-setup) - an **overview** of React Native and how setup your environment. -- [Learn the Basics](https://reactnative.dev/docs/getting-started) - a **guided tour** of the React Native **basics**. -- [Blog](https://reactnative.dev/blog) - read the latest official React Native **Blog** posts. -- [`@facebook/react-native`](https://github.com/facebook/react-native) - the Open Source; GitHub **repository** for React Native. diff --git a/sample/android/app/google-services.json b/sample/android/app/google-services.json deleted file mode 100644 index aca92f3..0000000 --- a/sample/android/app/google-services.json +++ /dev/null @@ -1,83 +0,0 @@ -{ - "project_info": { - "project_number": "1001237930288", - "firebase_url": "https://ue-wallet.firebaseio.com", - "project_id": "ue-wallet", - "storage_bucket": "ue-wallet.appspot.com" - }, - "client": [ - { - "client_info": { - "mobilesdk_app_id": "1:1001237930288:android:7b2c19eb3181f8c4ede8f5", - "android_client_info": { - "package_name": "ai.devrev.sdk.bridge.reactnative.sample" - } - }, - "oauth_client": [ - { - "client_id": "1001237930288-2ak63an8u4p9jjbtdusd90f04su3pdm9.apps.googleusercontent.com", - "client_type": 3 - } - ], - "api_key": [ - { - "current_key": "AIzaSyBNEHH0CBGTCSh1R-QyeQC4t7U0HmDii9g" - } - ], - "services": { - "appinvite_service": { - "other_platform_oauth_client": [ - { - "client_id": "1001237930288-2ak63an8u4p9jjbtdusd90f04su3pdm9.apps.googleusercontent.com", - "client_type": 3 - }, - { - "client_id": "1001237930288-hotgoru17eoib0brnplbtcd73h5c4oug.apps.googleusercontent.com", - "client_type": 2, - "ios_info": { - "bundle_id": "com.ue.wallet" - } - } - ] - } - } - }, - { - "client_info": { - "mobilesdk_app_id": "1:1001237930288:android:70f638c00cf42f64ede8f5", - "android_client_info": { - "package_name": "com.userexperior.uewallet" - } - }, - "oauth_client": [ - { - "client_id": "1001237930288-2ak63an8u4p9jjbtdusd90f04su3pdm9.apps.googleusercontent.com", - "client_type": 3 - } - ], - "api_key": [ - { - "current_key": "AIzaSyBNEHH0CBGTCSh1R-QyeQC4t7U0HmDii9g" - } - ], - "services": { - "appinvite_service": { - "other_platform_oauth_client": [ - { - "client_id": "1001237930288-2ak63an8u4p9jjbtdusd90f04su3pdm9.apps.googleusercontent.com", - "client_type": 3 - }, - { - "client_id": "1001237930288-hotgoru17eoib0brnplbtcd73h5c4oug.apps.googleusercontent.com", - "client_type": 2, - "ios_info": { - "bundle_id": "com.ue.wallet" - } - } - ] - } - } - } - ], - "configuration_version": "1" -} \ No newline at end of file diff --git a/sample/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/sample/android/app/src/main/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index a2f5908..0000000 Binary files a/sample/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and /dev/null differ diff --git a/sample/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/sample/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png deleted file mode 100644 index 1b52399..0000000 Binary files a/sample/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png and /dev/null differ diff --git a/sample/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/sample/android/app/src/main/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index ff10afd..0000000 Binary files a/sample/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and /dev/null differ diff --git a/sample/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/sample/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png deleted file mode 100644 index 115a4c7..0000000 Binary files a/sample/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png and /dev/null differ diff --git a/sample/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/sample/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index dcd3cd8..0000000 Binary files a/sample/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ diff --git a/sample/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/sample/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png deleted file mode 100644 index 459ca60..0000000 Binary files a/sample/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png and /dev/null differ diff --git a/sample/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/sample/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index 8ca12fe..0000000 Binary files a/sample/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/sample/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/sample/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png deleted file mode 100644 index 8e19b41..0000000 Binary files a/sample/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png and /dev/null differ diff --git a/sample/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/sample/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index b824ebd..0000000 Binary files a/sample/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ diff --git a/sample/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/sample/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png deleted file mode 100644 index 4c19a13..0000000 Binary files a/sample/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and /dev/null differ diff --git a/sample/android/app/src/main/res/values/strings.xml b/sample/android/app/src/main/res/values/strings.xml deleted file mode 100644 index ec18647..0000000 --- a/sample/android/app/src/main/res/values/strings.xml +++ /dev/null @@ -1,3 +0,0 @@ - - DevRevSDKSample - diff --git a/sample/android/build.gradle b/sample/android/build.gradle deleted file mode 100644 index 0675088..0000000 --- a/sample/android/build.gradle +++ /dev/null @@ -1,35 +0,0 @@ -buildscript { - ext { - buildToolsVersion = "35.0.0" - minSdkVersion = 24 - compileSdkVersion = 35 - targetSdkVersion = 35 - ndkVersion = "26.1.10909125" - kotlinVersion = "2.0.21" - } - repositories { - google() - mavenCentral() - jcenter() - mavenLocal() - } - dependencies { - classpath("com.android.tools.build:gradle") - classpath("com.facebook.react:react-native-gradle-plugin") - classpath("org.jetbrains.kotlin:kotlin-gradle-plugin") - classpath("com.google.gms:google-services:4.4.2") - } -} - -rootProject.allprojects { - repositories { - google() - mavenCentral() - mavenLocal() - maven { - url "https://s01.oss.sonatype.org/content/repositories/staging/" - } - } -} - -apply plugin: "com.facebook.react.rootproject" diff --git a/sample/android/settings.gradle b/sample/android/settings.gradle deleted file mode 100644 index 08e07cf..0000000 --- a/sample/android/settings.gradle +++ /dev/null @@ -1,7 +0,0 @@ -pluginManagement { includeBuild("../node_modules/@react-native/gradle-plugin") } -plugins { id("com.facebook.react.settings") } -extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autolinkLibrariesFromCommand() } -rootProject.name = 'DevRevSDKSample' - -include ':app' -includeBuild('../node_modules/@react-native/gradle-plugin') diff --git a/sample/app.json b/sample/app.json deleted file mode 100644 index 654a6a0..0000000 --- a/sample/app.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "DevRevSDKSample", - "displayName": "DevRevSDKSample" -} diff --git a/sample/expo/.gitignore b/sample/expo/.gitignore new file mode 100644 index 0000000..45f5589 --- /dev/null +++ b/sample/expo/.gitignore @@ -0,0 +1,197 @@ +# Dependencies +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Expo +.expo/ +dist/ +web-build/ +.expo-shared/ + +# Native build outputs +android/ +ios/ + +# But keep source files and configuration +!android/app/src/ +!android/app/build.gradle +!android/app/proguard-rules.pro +!android/app/debug.keystore +!android/build.gradle +!android/gradle.properties +!android/settings.gradle +!android/gradle/ +!android/gradlew* +!ios/*.plist +!ios/*.entitlements +!ios/Podfile* +!ios/Sources/ + +# OSX +.DS_Store + +# Xcode +build/ +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata/ +*.xccheckout +*.moved-aside +DerivedData/ +*.hmap +*.ipa +*.xcuserstate +*.xcworkspace/ +!*.xcworkspace/contents.xcworkspacedata +!*.xcworkspace/xcshareddata/ + +# CocoaPods +ios/Pods/ +ios/.symlinks/ +Podfile.lock + +# Android +android/.gradle/ +android/app/build/ +android/build/ +android/gradle/ +android/gradlew +android/gradlew.bat +android/local.properties +android/.idea/ +android/*.iml +android/.cxx/ +*.keystore +!debug.keystore +*.hprof + +# React Native Metro +.metro-health-check* + +# BUCK +buck-out/ +\.buckd/ +*.keystore +!debug.keystore + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the +# screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/ + +*/fastlane/report.xml +*/fastlane/Preview.html +*/fastlane/screenshots +*/fastlane/test_output + +# Bundle artifacts +*.jsbundle + +# CocoaPods +/ios/Pods/ + +# Expo +.expo/ +dist/ +web-build/ +.expo-shared/ + +# Temporary files created by Metro to check the health of the file watcher +.metro-health-check* + +# Environment variables +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage/ +*.lcov + +# nyc test coverage +.nyc_output + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# next.js build output +.next + +# nuxt.js build output +.nuxt + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# Flipper +android/app/src/debug/ +ios/Flipper/ + +# Misc +*.swp +*.swo +.yarn/ +*~ diff --git a/sample/expo/App.tsx b/sample/expo/App.tsx new file mode 100644 index 0000000..8bc2a8e --- /dev/null +++ b/sample/expo/App.tsx @@ -0,0 +1,32 @@ +import * as DevRev from '@devrev/sdk-react-native'; +import { Linking } from 'react-native'; +import PushNotificationsService from './PushNotificationsService'; +import React from 'react'; +import firebase from '@react-native-firebase/app'; +import { NavigationContainer } from '@react-navigation/native'; +import Navigator from './navigator/Navigator'; + +const App = () => { + React.useEffect(() => { + try { + firebase.app(); + DevRev.configure('YOUR_APP_ID'); + DevRev.setShouldDismissModalsOnOpenLink(true); + DevRev.setInAppLinkHandler((url) => { + Linking.openURL(url); + }); + + PushNotificationsService.initialize(); + } catch (error) { + console.log(error); + } + }, []); + + return ( + + + + ); +}; + +export default App; diff --git a/sample/expo/GoogleService-Info.plist b/sample/expo/GoogleService-Info.plist new file mode 100644 index 0000000..4f25e41 --- /dev/null +++ b/sample/expo/GoogleService-Info.plist @@ -0,0 +1,16 @@ + + + + + API_KEY + DUMMY_API_KEY + CLIENT_ID + DUMMY_CLIENT_ID + GOOGLE_APP_ID + 1:1234567890:ios:abcdef123456 + PROJECT_ID + dummy-project + BUNDLE_ID + ai.devrev.sdk.bridge.reactnative.expo.sample + + diff --git a/sample/expo/PushNotificationsService.tsx b/sample/expo/PushNotificationsService.tsx new file mode 100644 index 0000000..ff1ba2a --- /dev/null +++ b/sample/expo/PushNotificationsService.tsx @@ -0,0 +1,312 @@ +import messaging, { + FirebaseMessagingTypes, +} from '@react-native-firebase/messaging'; +import { Platform } from 'react-native'; +import * as DevRev from '@devrev/sdk-react-native'; +import DeviceInfo from 'react-native-device-info'; +import * as Notifications from 'expo-notifications'; + +interface NotificationActor { + display_handle: string; + display_id: string; + display_name: string; + full_name: string; + id: string; + id_v1: string; + state: string; + thumbnail: string; + type: string; +} + +interface NotificationDevice { + android: { + channel: string; + channel_id: string; + }; + device_type: string; +} + +interface NotificationItem { + display_id: string; + id: string; + id_v1: string; + target: string; + title: string; + type: string; +} + +interface DevRevNotification { + actor: NotificationActor; + body: string; + device: NotificationDevice; + id: string; + item: NotificationItem; + notification_id: string; + notification_id_v1: string; + source_id: string; + state: string; + subtitle: string; + title: string; + type: string; + url: string; +} + +interface StaleNotificationData { + stale_notification_ids?: string[]; +} + +interface SilentNotificationData { + silent?: string; +} + +const isAndroid = Platform.OS === 'android'; + +Notifications.setNotificationHandler({ + handleNotification: async () => { + return { + shouldShowAlert: true, + shouldPlaySound: true, + shouldSetBadge: true, + }; + }, +}); + +const handleSilentNotification = async (data: SilentNotificationData) => { + try { + if (data.silent) { + const silentData: StaleNotificationData = JSON.parse(data.silent); + if (silentData.stale_notification_ids?.length) { + console.log( + 'Handling stale notifications:', + silentData.stale_notification_ids + ); + // Cancel stale notifications + for (const notificationId of silentData.stale_notification_ids) { + await Notifications.cancelScheduledNotificationAsync(notificationId); + } + } + } + } catch (error) { + console.error('Error handling silent notification:', error); + } +}; + +// Create default channel for Android +const createDefaultChannel = async (): Promise => { + if (Platform.OS === 'android') { + await Notifications.setNotificationChannelAsync('default', { + name: 'Default Channel', + importance: Notifications.AndroidImportance.HIGH, + vibrationPattern: [0, 250, 250, 250], + lightColor: '#FF231F7C', + }); + } +}; + +// Request user permission for notifications +const requestUserPermission = async (): Promise => { + // Check permissions + const { status: existingStatus } = await Notifications.getPermissionsAsync(); + let finalStatus = existingStatus; + + if (existingStatus !== 'granted') { + const { status } = await Notifications.requestPermissionsAsync(); + finalStatus = status; + } + + if (finalStatus !== 'granted') { + console.error('Failed to get the device token for push notifications!'); + return false; + } + return true; +}; + +const getDeviceToken = async (): Promise => { + try { + console.log('Device token: Starting token retrieval...'); + + if (isAndroid) { + console.log('Device token: Getting Android token...'); + const token = await Notifications.getDevicePushTokenAsync(); + console.log('Device token: Android token received:', token); + return token.data; + } else { + console.log('Device token: Getting iOS token...'); + const token = await Notifications.getDevicePushTokenAsync(); + console.log('Device token: iOS token received:', token); + return token.data; + } + } catch (error) { + console.error('Device token: Error in getDeviceToken:', error); + throw error; + } +}; + +// Register device with DevRev +const registerDevice = async (): Promise => { + try { + console.log('Starting device registration...'); + const hasPermission = await requestUserPermission(); + console.log('Permission status:', hasPermission); + + if (hasPermission) { + console.log('Getting device ID...'); + const deviceId = await DeviceInfo.getUniqueId(); + console.log('Device ID:', deviceId); + console.log('Getting device token...'); + const token = await getDeviceToken(); + console.log('Token received:', token); + + if (token) { + console.log('Registering with DevRev...'); + await DevRev.registerDeviceToken(token, deviceId); + console.log('Successfully registered with DevRev'); + } else { + console.warn('Failed to get device token'); + } + } else { + console.warn('Notification permission not granted'); + } + } catch (error) { + console.error('Error registering device:', error); + throw error; + } +}; + +// Display notification +const displayNotification = async ( + remoteMessage: FirebaseMessagingTypes.RemoteMessage +): Promise => { + try { + if (remoteMessage.data?.silent) { + await handleSilentNotification(remoteMessage.data); + return; + } + + let notification: DevRevNotification | null = null; + if (remoteMessage.data?.message) { + try { + notification = JSON.parse(remoteMessage.data.message as string); + } catch (parseError) { + console.error('Error parsing notification:', parseError); + } + } + + if (!notification) { + console.warn('No valid notification data'); + return; + } + + await Notifications.scheduleNotificationAsync({ + content: { + title: notification.title, + body: notification.body, + data: remoteMessage.data, + }, + trigger: null, // null means show immediately + }); + } catch (error) { + console.error('Error displaying notification:', error); + } +}; + +// Handle notification press +const handleNotificationPress = async (notification: any): Promise => { + console.log('Notification pressed:', notification); + + if (notification.notification?.request) { + console.log( + 'Unmarshalling: ', + notification.notification.request.content.data.message + ); + DevRev.processPushNotification(notification.data.message); + return; + } + + if (notification.data?.message) { + DevRev.processPushNotification(notification.data.message as string); + return; + } + + console.warn('Unexpected notification format:', notification); +}; + +// Setup notification listeners +const setupNotificationListeners = (): void => { + // Handle token refresh + messaging().onTokenRefresh(async (token) => { + try { + const deviceId = await DeviceInfo.getUniqueId(); + DevRev.registerDeviceToken(token, deviceId); + } catch (error) { + console.error('Error handling token refresh:', error); + } + }); + + // Handle background messages + messaging().setBackgroundMessageHandler(async (remoteMessage) => { + console.log('BACKGROUND MESSAGE: ', remoteMessage); + if (Platform.OS === 'android') { + await displayNotification(remoteMessage); + } + }); + + // Handle foreground messages + messaging().onMessage(async (remoteMessage) => { + console.log('FOREGROUND MESSAGE: ', remoteMessage); + await displayNotification(remoteMessage); + }); + + // Handle notification interaction + Notifications.addNotificationResponseReceivedListener((response) => { + handleNotificationPress(response); + }); + + // Handle foreground notifications + Notifications.addNotificationReceivedListener((notification) => { + console.log('Foreground notification received:', notification); + }); + + // Check if app was opened from a notification + messaging() + .getInitialNotification() + .then(async (remoteMessage) => { + if (remoteMessage) { + console.log('App opened from quit state:', remoteMessage); + await handleNotificationPress(remoteMessage); + } + }) + .catch((error) => { + console.error('Error checking initial notification:', error); + }); +}; + +// Cancel all notifications +const cancelAllNotifications = async (): Promise => { + try { + await Notifications.cancelAllScheduledNotificationsAsync(); + } catch (error) { + console.error('Error canceling notifications:', error); + } +}; + +// Initialize notification service +const initializeNotifications = async (): Promise => { + try { + if (isAndroid) { + await createDefaultChannel(); + } + setupNotificationListeners(); + } catch (error) { + console.error('Error initializing notifications:', error); + } +}; + +export const PushNotificationsService = { + initialize: initializeNotifications, + register: registerDevice, + displayNotification, + cancelAllNotifications, +}; + +export default PushNotificationsService; diff --git a/sample/expo/app.json b/sample/expo/app.json new file mode 100644 index 0000000..d33b3cd --- /dev/null +++ b/sample/expo/app.json @@ -0,0 +1,74 @@ +{ + "expo": { + "name": "DevRevSDK (Expo)", + "slug": "devrev-sdk-expo-sample", + "version": "1.0.0", + "orientation": "portrait", + "icon": "./assets/icon.png", + "userInterfaceStyle": "light", + "splash": { + "image": "./assets/splash-icon.png", + "resizeMode": "contain", + "backgroundColor": "#ffffff" + }, + "ios": { + "supportsTablet": true, + "bundleIdentifier": "ai.devrev.sdk.bridge.reactnative.expo.sample", + "config": { + "usesNonExemptEncryption": false + }, + "infoPlist": { + "UIBackgroundModes": [ + "remote-notification" + ], + "UIUserInterfaceStyle": "light", + "NSAppTransportSecurity": { + "NSAllowsArbitraryLoads": true + } + }, + "googleServicesFile": "./GoogleService-Info.plist" + }, + "android": { + "adaptiveIcon": { + "foregroundImage": "./assets/adaptive-icon.png", + "backgroundColor": "#ffffff" + }, + "package": "ai.devrev.sdk.bridge.reactnative.expo.sample", + "googleServicesFile": "./google-services.json", + "permissions": [ + "NOTIFICATIONS" + ] + }, + "web": { + "favicon": "./assets/favicon.png" + }, + "plugins": [ + [ + "../../app.plugin.js" + ], + [ + "expo-notifications", + { + "icon": "./assets/splash-icon.png", + "enableBackgroundRemoteNotifications": true + } + ], + [ + "expo-build-properties", + { + "ios": { + "useFrameworks": "static" + }, + "podfileProperties": { + "use_modular_headers!": true + } + } + ] + ], + "extra": { + "eas": { + "projectId": "501ce10b-b735-428d-8429-6616662accd7" + } + } + } +} diff --git a/sample/expo/assets/adaptive-icon.png b/sample/expo/assets/adaptive-icon.png new file mode 100644 index 0000000..03d6f6b Binary files /dev/null and b/sample/expo/assets/adaptive-icon.png differ diff --git a/sample/expo/assets/favicon.png b/sample/expo/assets/favicon.png new file mode 100644 index 0000000..e75f697 Binary files /dev/null and b/sample/expo/assets/favicon.png differ diff --git a/sample/expo/assets/icon.png b/sample/expo/assets/icon.png new file mode 100644 index 0000000..a0b1526 Binary files /dev/null and b/sample/expo/assets/icon.png differ diff --git a/sample/expo/assets/sample.html b/sample/expo/assets/sample.html new file mode 100644 index 0000000..5204157 --- /dev/null +++ b/sample/expo/assets/sample.html @@ -0,0 +1,56 @@ + + + + + + Sample Page with Masking + + + +

Sample HTML for WebView

+

This page demonstrates content masking within a WebView.

+
+ + +

This field (devrev-mask) is masked in recordings.

+
+ +
+ + +

This password field (type="password") is automatically masked by the SDK without requiring the devrev-mask class.

+
+ +
+ + +

This field (devrev-unmask) is visible in recordings.

+
+ +
+ + +

This number (devrev-mask) is masked in recordings.

+
+ +

This paragraph contains a masked phrase and an unmasked phrase within the text.

+ +
+

This entire div and its contents are masked:

+ + +
+ +
+ + + diff --git a/sample/expo/assets/splash-icon.png b/sample/expo/assets/splash-icon.png new file mode 100644 index 0000000..03d6f6b Binary files /dev/null and b/sample/expo/assets/splash-icon.png differ diff --git a/sample/expo/babel.config.js b/sample/expo/babel.config.js new file mode 100644 index 0000000..9d89e13 --- /dev/null +++ b/sample/expo/babel.config.js @@ -0,0 +1,6 @@ +module.exports = function (api) { + api.cache(true); + return { + presets: ['babel-preset-expo'], + }; +}; diff --git a/sample/expo/components/TouchableOpacityButton.tsx b/sample/expo/components/TouchableOpacityButton.tsx new file mode 100644 index 0000000..ca02b39 --- /dev/null +++ b/sample/expo/components/TouchableOpacityButton.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { + TouchableOpacity, + Text, + StyleSheet, + type TextStyle, + type ViewStyle, +} from 'react-native'; + +interface TouchableOpacityProps { + onPress: () => void; + buttonText: string; + buttonStyle?: ViewStyle; + textStyle?: TextStyle; +} + +const TouchableOpacityButton: React.FC = ({ + onPress, + buttonText, + buttonStyle, + textStyle, +}) => { + return ( + + {buttonText} + + ); +}; + +const styles = StyleSheet.create({ + button: { + margin: 4, + padding: 8, + paddingLeft: 4, + backgroundColor: '#FFFFFF', + width: '100%', + alignItems: 'flex-start', + }, + buttonText: { + color: '#000000', + fontSize: 16, + textAlign: 'left', + }, +}); + +export default TouchableOpacityButton; diff --git a/sample/expo/eas.json b/sample/expo/eas.json new file mode 100644 index 0000000..9a6f64d --- /dev/null +++ b/sample/expo/eas.json @@ -0,0 +1,21 @@ +{ + "cli": { + "version": ">= 14.4.1", + "appVersionSource": "remote" + }, + "build": { + "development": { + "developmentClient": true, + "distribution": "internal" + }, + "preview": { + "distribution": "internal" + }, + "production": { + "autoIncrement": true + } + }, + "submit": { + "production": {} + } +} diff --git a/sample/expo/google-services.json b/sample/expo/google-services.json new file mode 100644 index 0000000..375296b --- /dev/null +++ b/sample/expo/google-services.json @@ -0,0 +1,18 @@ +{ + "project_info": { + "project_number": "1234567890", + "project_id": "dummy-project" + }, + "client": [{ + "client_info": { + "mobilesdk_app_id": "1:1234567890:android:abcdef123456", + "android_client_info": { + "package_name": "ai.devrev.sdk.bridge.reactnative.expo.sample" + } + }, + "api_key": [{ + "current_key": "DUMMY_API_KEY" + }] + }], + "configuration_version": "1" +} diff --git a/sample/expo/index.ts b/sample/expo/index.ts new file mode 100644 index 0000000..1d6e981 --- /dev/null +++ b/sample/expo/index.ts @@ -0,0 +1,8 @@ +import { registerRootComponent } from 'expo'; + +import App from './App'; + +// registerRootComponent calls AppRegistry.registerComponent('main', () => App); +// It also ensures that whether you load the app in Expo Go or in a native build, +// the environment is set up appropriately +registerRootComponent(App); diff --git a/sample/expo/metro.config.js b/sample/expo/metro.config.js new file mode 100644 index 0000000..2a7734c --- /dev/null +++ b/sample/expo/metro.config.js @@ -0,0 +1,38 @@ +// Learn more https://docs.expo.io/guides/customizing-metro +const { getDefaultConfig } = require('expo/metro-config'); +const path = require('path'); + +const config = getDefaultConfig(__dirname); + +// npm v7+ will install ../node_modules/react and ../node_modules/react-native because of peerDependencies. +// To prevent the incompatible react-native between ./node_modules/react-native and ../node_modules/react-native, +// excludes the one from the parent folder when bundling. +config.resolver.blockList = [ + ...Array.from(config.resolver.blockList ?? []), + new RegExp(path.resolve('..', 'node_modules', 'react')), + new RegExp(path.resolve('..', 'node_modules', 'react-native')), +]; + +config.resolver.nodeModulesPaths = [ + path.resolve(__dirname, './node_modules'), + path.resolve(__dirname, '../node_modules'), + path.resolve(__dirname, '../../node_modules'), +]; + +config.resolver.extraNodeModules = { + 'devrev-sdk-react-native': '../..', +}; + +config.watchFolders = [ + path.resolve(__dirname, '..'), + path.resolve(__dirname, '../..'), +]; + +config.transformer.getTransformOptions = async () => ({ + transform: { + experimentalImportSupport: false, + inlineRequires: true, + }, +}); + +module.exports = config; diff --git a/sample/expo/navigator/Navigator.tsx b/sample/expo/navigator/Navigator.tsx new file mode 100644 index 0000000..466764b --- /dev/null +++ b/sample/expo/navigator/Navigator.tsx @@ -0,0 +1,108 @@ +import React from 'react'; +import { createStackNavigator } from '@react-navigation/stack'; +import IdentificationScreen from '../screens/IdentificationScreen'; +import PushNotificationsScreen from '../screens/PushNotificationsScreen'; +import SessionAnalyticsScreen from '../screens/SessionAnalyticsScreen'; +import SupportChatScreen from '../screens/SupportChatScreen'; +import HomeScreen from '../screens/HomeScreen'; +import DelayedScreen from '../screens/DelayScreen'; +import WebViewScreen from '../screens/WebViewScreen'; +import FlatListScreen from '../screens/FlatListScreen'; +import { TouchableOpacity, StyleSheet, Text } from 'react-native'; + +export type RootStackParamList = { + Home: undefined; + Identification: undefined; + PushNotifications: undefined; + SessionAnalytics: undefined; + SupportChat: undefined; + DelayScreen: undefined; + WebViewScreen: undefined; + FlatListScreen: undefined; +}; + +const Stack = createStackNavigator(); + +const screens = [ + { name: 'Home', component: HomeScreen, title: 'DevRev SDK' }, + { + name: 'Identification', + component: IdentificationScreen, + title: 'Identification', + }, + { + name: 'PushNotifications', + component: PushNotificationsScreen, + title: 'Push Notifications', + }, + { + name: 'SessionAnalytics', + component: SessionAnalyticsScreen, + title: 'Session Analytics', + }, + { + name: 'SupportChat', + component: SupportChatScreen, + title: 'Support Chat', + }, + { + name: 'DelayScreen', + component: DelayedScreen, + title: 'Delayed Screen', + }, + { + name: 'WebViewScreen', + component: WebViewScreen, + title: 'Web View', + }, + { + name: 'FlatListScreen', + component: FlatListScreen, + title: 'Large Scrollable List', + }, +] as const; + +const createScreen = ( + name: keyof RootStackParamList, + component: React.ComponentType, + title: string, + index: number +) => ( + ({ + title, + headerRight: () => ( + navigation.replace(name)} + style={styles.refreshButton} + > + + + ), + })} + /> +); + +export default function Navigator() { + return ( + + {screens.map((screen, index) => + createScreen(screen.name, screen.component, screen.title, index) + )} + + ); +} + +const styles = StyleSheet.create({ + refreshButton: { + marginRight: 20, + }, + refreshIcon: { + fontSize: 24, + fontWeight: 'bold', + transform: [{ rotate: '90deg' }], + }, +}); diff --git a/sample/expo/package.json b/sample/expo/package.json new file mode 100644 index 0000000..3bbe00b --- /dev/null +++ b/sample/expo/package.json @@ -0,0 +1,49 @@ +{ + "name": "@devrev/sdk-react-native-expo-sample", + "version": "2.2.0", + "main": "index.ts", + "description": "DevRev SDK for Expo sample app.", + "scripts": { + "start": "expo start", + "android": "expo run:android", + "ios": "expo run:ios", + "web": "expo start --web", + "prebuild": "expo prebuild --clean" + }, + "dependencies": { + "@devrev/sdk-react-native": "workspace:*", + "@react-native-community/cli-server-api": "^17.0.0", + "@react-native-firebase/app": "^21.7.1", + "@react-native-firebase/installations": "^21.0.0", + "@react-native-firebase/messaging": "^21.7.1", + "@react-navigation/native": "^7.0.15", + "@react-navigation/native-stack": "^7.3.14", + "@react-navigation/stack": "^7.1.2", + "expo": "^53.0.0", + "expo-build-properties": "~0.14.8", + "expo-firebase-messaging": "^2.0.0", + "expo-notifications": "~0.31.4", + "expo-system-ui": "~5.0.11", + "react": "19.0.0", + "react-native": "0.79.5", + "react-native-device-info": "^14.0.2", + "react-native-gesture-handler": "~2.24.0", + "react-native-safe-area-context": "5.4.0", + "react-native-screens": "~4.11.1", + "react-native-webview": "13.13.5" + }, + "devDependencies": { + "@babel/core": "^7.25.2", + "@types/react": "~19.0.10", + "typescript": "~5.8.3" + }, + "private": true, + "expo": { + "autolinking": { + "nativeModulesDir": "../../", + "ios": { + "deploymentTarget": "15.1" + } + } + } +} diff --git a/sample/expo/screens/DelayScreen.tsx b/sample/expo/screens/DelayScreen.tsx new file mode 100644 index 0000000..54ee81b --- /dev/null +++ b/sample/expo/screens/DelayScreen.tsx @@ -0,0 +1,30 @@ +import React, { useEffect } from 'react'; +import { Text, View, StyleSheet } from 'react-native'; +import * as DevRev from '@devrev/sdk-react-native'; + +const DelayScreen: React.FC = () => { + useEffect(() => { + try { + DevRev.endScreenTransition(); + } catch (error) { + console.error('Error ending screen transition:', error); + } + }, []); + + return ( + + Delayed Screen + This screen opened after a 2-second delay + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, +}); + +export default DelayScreen; diff --git a/sample/expo/screens/FlatListScreen.tsx b/sample/expo/screens/FlatListScreen.tsx new file mode 100644 index 0000000..c6f810b --- /dev/null +++ b/sample/expo/screens/FlatListScreen.tsx @@ -0,0 +1,86 @@ +import React, { useRef } from 'react'; +import { View, Text, FlatList, StyleSheet, findNodeHandle } from 'react-native'; +import * as DevRev from '@devrev/sdk-react-native'; + +const DATA = Array.from({ length: 100 }, (_, i) => ({ + id: i.toString(), + title: `Item #${i}`, +})); + +const refs = DATA.map(() => React.createRef()); + +const CardView = React.forwardRef(({ title }: { title: string }, ref) => ( + + {title} + +)); + +const FlatListScreen: React.FC = () => { + const lastSensitiveTagsRef = useRef([]); + + const onViewableItemsChanged = React.useCallback(({ viewableItems }) => { + const sensitiveTags: number[] = []; + viewableItems.forEach(({ index }) => { + if (index % 2 === 0 && refs[index]?.current) { + const tag = findNodeHandle(refs[index].current); + if (tag) sensitiveTags.push(tag); + } + }); + + // Determine views to unmark and mark based on visibility changes + const currentlyMarked = lastSensitiveTagsRef.current; + const toUnmark = currentlyMarked.filter( + (tag) => !sensitiveTags.includes(tag) + ); + const toMark = sensitiveTags.filter( + (tag) => !currentlyMarked.includes(tag) + ); + + // Unmark views that are no longer visible or will be remarked + if (toUnmark.length > 0) { + DevRev.unmarkSensitiveViews(toUnmark); + } + + // Mark newly visible sensitive views + if (toMark.length > 0) { + DevRev.markSensitiveViews(toMark); + } + + // Update the record of currently marked sensitive views + // This includes views that remained visible and newly marked views + lastSensitiveTagsRef.current = sensitiveTags.filter( + (tag) => toUnmark.indexOf(tag) === -1 || toMark.indexOf(tag) !== -1 + ); + }, []); + + return ( + item.id} + renderItem={({ item, index }) => ( + + )} + contentContainerStyle={styles.list} + onViewableItemsChanged={onViewableItemsChanged} + viewabilityConfig={{ itemVisiblePercentThreshold: 50 }} + /> + ); +}; + +const styles = StyleSheet.create({ + list: { padding: 16 }, + card: { + backgroundColor: '#fff', + borderRadius: 8, + elevation: 3, + shadowColor: '#000', + shadowOpacity: 0.1, + shadowRadius: 4, + shadowOffset: { width: 0, height: 2 }, + padding: 16, + marginBottom: 12, + }, + cardText: { fontSize: 16, color: '#333' }, +}); + +export default FlatListScreen; diff --git a/sample/expo/screens/HomeScreen.tsx b/sample/expo/screens/HomeScreen.tsx new file mode 100644 index 0000000..8e627c6 --- /dev/null +++ b/sample/expo/screens/HomeScreen.tsx @@ -0,0 +1,163 @@ +import * as React from 'react'; + +import { + StyleSheet, + View, + Text, + Animated, + Platform, + TouchableOpacity, +} from 'react-native'; +import TouchableOpacityButton from '../components/TouchableOpacityButton'; +import viewModel from '../viewmodel/ViewModel'; +import { useRef, useState } from 'react'; + +const navigateButtons = [ + { text: 'Identification', route: 'Identification' }, + { text: 'Support Chat', route: 'SupportChat' }, + { text: 'Push Notifications', route: 'PushNotifications' }, + { text: 'Session Analytics', route: 'SessionAnalytics' }, +] as const; + +const testButtons = [ + { + text: 'Simulate ANR', + onPress: viewModel.simulateANR, + platform: 'android', + }, + { + text: 'Simulate Crash', + onPress: viewModel.simulateCrash, + platform: 'both', + }, +] as const; + +const buttons = testButtons.filter( + (button) => button.platform === 'both' || button.platform === Platform.OS +); + +const HomeScreen = ({ navigation }: { navigation: any }) => { + const [isAnimating, setIsAnimating] = useState(false); + const scaleValue = useRef(new Animated.Value(1)).current; + const animation = useRef(null); + + const startAnimation = () => { + if (isAnimating) return; + + setIsAnimating(true); + scaleValue.setValue(1); + + animation.current = Animated.loop( + Animated.sequence([ + Animated.spring(scaleValue, { + toValue: 1.2, + friction: 2, + tension: 60, + useNativeDriver: true, + }), + Animated.spring(scaleValue, { + toValue: 1, + friction: 2, + tension: 60, + useNativeDriver: true, + }), + ]), + { iterations: 4 } + ); + + animation.current.start(({ finished: _finished }) => { + setIsAnimating(false); + }); + }; + + return ( + + Features + {navigateButtons.map((button, index) => ( + navigation.navigate(button.route)} + buttonStyle={styles.button} + textStyle={styles.buttonText} + /> + ))} + Debug + {buttons.map((button, index) => ( + + ))} + Animation + + + + Play an Animation + + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + alignItems: 'flex-start', + paddingLeft: 8, + paddingRight: 16, + backgroundColor: '#FFFFFF', + }, + textbox: { + width: '80%', + padding: 8, + color: '#000000', + }, + titleBar: { + width: '100%', + padding: 20, + }, + title: { + color: '#000000', + fontSize: 24, + fontWeight: 'bold', + }, + heading: { + color: '#454141', + fontSize: 14, + textAlign: 'left', + paddingTop: 8, + margin: 8, + }, + buttonContainer: { + width: '100%', + }, + button: { + backgroundColor: '#D3D3D3', + padding: 12, + borderRadius: 12, + marginVertical: 4, + alignItems: 'flex-start', + paddingBottom: 8, + }, + buttonText: { + color: '#000000', + fontSize: 16, + paddingLeft: 8, + }, + buttonTouchable: { + width: '100%', + }, +}); + +export default HomeScreen; diff --git a/sample/expo/screens/IdentificationScreen.tsx b/sample/expo/screens/IdentificationScreen.tsx new file mode 100644 index 0000000..3103a55 --- /dev/null +++ b/sample/expo/screens/IdentificationScreen.tsx @@ -0,0 +1,138 @@ +import React from 'react'; + +import * as DevRev from '@devrev/sdk-react-native'; +import type { Identity } from '@devrev/sdk-react-native'; +import { View, Text, TextInput, StyleSheet } from 'react-native'; +import viewModel from '../viewmodel/ViewModel'; +import TouchableOpacityButton from '../components/TouchableOpacityButton'; + +const IdentificationScreen: React.FC = () => { + const [userID, setUserID] = React.useState(''); + const [sessionToken, setSessionToken] = React.useState(''); + const [userEmail, setUserEmail] = React.useState(''); + + const handleUpdateUser = () => { + const identity: Identity = { + userRef: userID, + userTraits: { + email: userEmail, + }, + }; + DevRev.updateUser(identity); + }; + + return ( + + Unverified User + + + { + console.log('Identifying user with:', userID); + DevRev.identifyUnverifiedUser(userID); + }} + buttonText="Identify Unverified User" + buttonStyle={styles.button} + textStyle={styles.buttonText} + /> + + Verified User + + + + { + console.log( + 'Identifying user with:', + userID, + 'and session token:', + sessionToken + ); + DevRev.identifyVerifiedUser(userID, sessionToken); + }} + buttonText="Identify Verified User" + buttonStyle={styles.button} + textStyle={styles.buttonText} + /> + + Update User + + + { + console.log('Updating user...'); + handleUpdateUser(); + }} + buttonText="Update User" + buttonStyle={styles.button} + textStyle={styles.buttonText} + /> + + Logout + { + viewModel.logout(); + }} + buttonText="Logout" + buttonStyle={styles.button} + textStyle={styles.buttonText} + /> + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#FFFFFF', + paddingHorizontal: 8, + paddingRight: 16, + }, + heading: { + color: '#454141', + fontSize: 12, + marginTop: 16, + paddingHorizontal: 8, + paddingTop: 16, + }, + input: { + backgroundColor: '#FFFFFF', + borderWidth: 1, + borderColor: '#D3D3D3', + borderRadius: 4, + padding: 12, + marginVertical: 4, + fontSize: 16, + }, + button: { + backgroundColor: '#D3D3D3', + padding: 12, + borderRadius: 12, + marginVertical: 4, + alignItems: 'flex-start', + paddingBottom: 8, + }, + buttonText: { + color: '#000000', + fontSize: 16, + paddingLeft: 8, + }, +}); + +export default IdentificationScreen; diff --git a/sample/expo/screens/PushNotificationsScreen.tsx b/sample/expo/screens/PushNotificationsScreen.tsx new file mode 100644 index 0000000..8f7939b --- /dev/null +++ b/sample/expo/screens/PushNotificationsScreen.tsx @@ -0,0 +1,125 @@ +import viewModel from '../viewmodel/ViewModel'; +import React, { useState } from 'react'; +import { View, Text, Button, StyleSheet } from 'react-native'; +import TouchableOpacityButton from '../components/TouchableOpacityButton'; + +const PushNotificationsScreen: React.FC = () => { + const [showDialog, setShowDialog] = useState<{ + title: string; + message: string; + } | null>(null); + + const handleRegister = async () => { + const isRegistered = viewModel.registerDeviceToken(); + if (await isRegistered) { + setShowDialog({ + title: 'Success', + message: 'Successfully registered for push notifications.', + }); + } else { + setShowDialog({ + title: 'Error', + message: 'Could not register the device.', + }); + } + }; + + const handleUnregister = async () => { + const isUnregistered = viewModel.unregisterDevice(); + if (await isUnregistered) { + setShowDialog({ + title: 'Success', + message: 'Successfully unregistered for push notifications.', + }); + } else { + setShowDialog({ + title: 'Error', + message: 'Could not unregister the device.', + }); + return; + } + }; + + const buttons = [ + { text: 'Register for push notifications', onPress: handleRegister }, + { text: 'Unregister for push notifications', onPress: handleUnregister }, + ] as const; + + return ( + + {buttons.map((button, index) => ( + + ))} + + {showDialog && ( + setShowDialog(null)} + /> + )} + + ); +}; + +const AlertDialog: React.FC<{ + title: string; + message: string; + onDismiss: () => void; +}> = ({ title, message, onDismiss }) => { + return ( + + {title} + {message} + + + diff --git a/sample/react-native/android/app/src/main/res/values/strings.xml b/sample/react-native/android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..efc9115 --- /dev/null +++ b/sample/react-native/android/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + DevRevSDK (React Native) + diff --git a/sample/android/app/src/main/res/values/styles.xml b/sample/react-native/android/app/src/main/res/values/styles.xml similarity index 100% rename from sample/android/app/src/main/res/values/styles.xml rename to sample/react-native/android/app/src/main/res/values/styles.xml diff --git a/sample/react-native/android/build.gradle b/sample/react-native/android/build.gradle new file mode 100644 index 0000000..f37f1ec --- /dev/null +++ b/sample/react-native/android/build.gradle @@ -0,0 +1,42 @@ +buildscript { + repositories { + google() + mavenCentral() + jcenter() + mavenLocal() + } + dependencies { + classpath("com.android.tools.build:gradle:8.6.1") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.0.21") + classpath("com.google.gms:google-services:4.4.2") + } +} + +plugins { + id "com.facebook.react.rootproject" apply false +} + +ext { + buildToolsVersion = "35.0.0" + minSdkVersion = 24 + compileSdkVersion = 35 + targetSdkVersion = 35 + ndkVersion = "26.1.10909125" + kotlinVersion = "2.0.21" +} + +apply plugin: "com.facebook.react.rootproject" + +rootProject.allprojects { + repositories { + google() + mavenCentral() + mavenLocal() + maven { + url "https://central.sonatype.com/repository/maven-snapshots/" + mavenContent { + snapshotsOnly() + } + } + } +} diff --git a/sample/android/gradle.properties b/sample/react-native/android/gradle.properties similarity index 100% rename from sample/android/gradle.properties rename to sample/react-native/android/gradle.properties diff --git a/sample/android/gradle/wrapper/gradle-wrapper.jar b/sample/react-native/android/gradle/wrapper/gradle-wrapper.jar similarity index 100% rename from sample/android/gradle/wrapper/gradle-wrapper.jar rename to sample/react-native/android/gradle/wrapper/gradle-wrapper.jar diff --git a/sample/android/gradle/wrapper/gradle-wrapper.properties b/sample/react-native/android/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from sample/android/gradle/wrapper/gradle-wrapper.properties rename to sample/react-native/android/gradle/wrapper/gradle-wrapper.properties diff --git a/sample/android/gradlew b/sample/react-native/android/gradlew similarity index 100% rename from sample/android/gradlew rename to sample/react-native/android/gradlew diff --git a/sample/android/gradlew.bat b/sample/react-native/android/gradlew.bat similarity index 100% rename from sample/android/gradlew.bat rename to sample/react-native/android/gradlew.bat diff --git a/sample/react-native/android/settings.gradle b/sample/react-native/android/settings.gradle new file mode 100644 index 0000000..454405a --- /dev/null +++ b/sample/react-native/android/settings.gradle @@ -0,0 +1,9 @@ +pluginManagement { includeBuild("../node_modules/@react-native/gradle-plugin") } +plugins { id("com.facebook.react.settings") } +reactSettings { autolinkLibrariesFromCommand() } +rootProject.name = 'DevRevSDKSample' + +include ':app' + +include ':react-native-notifications' +project(':react-native-notifications').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-notifications/lib/android/app') diff --git a/sample/react-native/app.json b/sample/react-native/app.json new file mode 100644 index 0000000..0b791a2 --- /dev/null +++ b/sample/react-native/app.json @@ -0,0 +1,4 @@ +{ + "name": "DevRevSDKSampleRN", + "displayName": "DevRevSDK (React Native)" +} diff --git a/sample/react-native/babel.config.js b/sample/react-native/babel.config.js new file mode 100644 index 0000000..565292e --- /dev/null +++ b/sample/react-native/babel.config.js @@ -0,0 +1,11 @@ +module.exports = { + presets: ['module:@react-native/babel-preset'], + plugins: [ + [ + 'module-resolver', + { + extensions: ['.tsx', '.ts', '.js', '.json'], + }, + ], + ], +}; diff --git a/sample/index.js b/sample/react-native/index.js similarity index 100% rename from sample/index.js rename to sample/react-native/index.js diff --git a/sample/ios/.editorconfig b/sample/react-native/ios/.editorconfig similarity index 100% rename from sample/ios/.editorconfig rename to sample/react-native/ios/.editorconfig diff --git a/sample/ios/.xcode.env b/sample/react-native/ios/.xcode.env similarity index 100% rename from sample/ios/.xcode.env rename to sample/react-native/ios/.xcode.env diff --git a/sample/ios/DevRevSDKSample-Bridging-Header.h b/sample/react-native/ios/DevRevSDKSampleRN-Bridging-Header.h similarity index 100% rename from sample/ios/DevRevSDKSample-Bridging-Header.h rename to sample/react-native/ios/DevRevSDKSampleRN-Bridging-Header.h diff --git a/sample/ios/DevRevSDKSample.entitlements b/sample/react-native/ios/DevRevSDKSampleRN.entitlements similarity index 100% rename from sample/ios/DevRevSDKSample.entitlements rename to sample/react-native/ios/DevRevSDKSampleRN.entitlements diff --git a/sample/ios/DevRevSDKSample.xcodeproj/project.pbxproj b/sample/react-native/ios/DevRevSDKSampleRN.xcodeproj/project.pbxproj similarity index 88% rename from sample/ios/DevRevSDKSample.xcodeproj/project.pbxproj rename to sample/react-native/ios/DevRevSDKSampleRN.xcodeproj/project.pbxproj index 34740b4..203aae0 100644 --- a/sample/ios/DevRevSDKSample.xcodeproj/project.pbxproj +++ b/sample/react-native/ios/DevRevSDKSampleRN.xcodeproj/project.pbxproj @@ -12,21 +12,21 @@ 1CA606002D13FC7000B2293B /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 1CA605FF2D13FC7000B2293B /* GoogleService-Info.plist */; }; 343065452DDCF35A009F6083 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 343065442DDCF35A009F6083 /* AppDelegate.swift */; }; 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; }; - D17B58719925A0DC175BCCAB /* Pods_DevRevSDKSample.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8281D88BA7622C9E8AE79A31 /* Pods_DevRevSDKSample.framework */; }; + EF1AA09255E944CC774FB0C1 /* Pods_DevRevSDKSampleRN.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E3EE8C8BC611A9CAA5574249 /* Pods_DevRevSDKSampleRN.framework */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ - 13B07F961A680F5B00A75B9A /* DevRevSDKSample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DevRevSDKSample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 13B07F961A680F5B00A75B9A /* DevRevSDKSampleRN.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DevRevSDKSampleRN.app; sourceTree = BUILT_PRODUCTS_DIR; }; 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = Sources/Images.xcassets; sourceTree = ""; }; 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Sources/Info.plist; sourceTree = ""; }; 1CA605FF2D13FC7000B2293B /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; - 1CA606012D13FCE400B2293B /* DevRevSDKSample.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DevRevSDKSample.entitlements; sourceTree = ""; }; + 1CA606012D13FCE400B2293B /* DevRevSDKSampleRN.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DevRevSDKSampleRN.entitlements; sourceTree = ""; }; 343065442DDCF35A009F6083 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = Sources/AppDelegate.swift; sourceTree = ""; }; - 6D78A72DEE821DC6473B0DE8 /* Pods-DevRevSDKSample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DevRevSDKSample.debug.xcconfig"; path = "Target Support Files/Pods-DevRevSDKSample/Pods-DevRevSDKSample.debug.xcconfig"; sourceTree = ""; }; + 60508B310A9753744EE0782C /* Pods-DevRevSDKSampleRN.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DevRevSDKSampleRN.release.xcconfig"; path = "Target Support Files/Pods-DevRevSDKSampleRN/Pods-DevRevSDKSampleRN.release.xcconfig"; sourceTree = ""; }; 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = Sources/LaunchScreen.storyboard; sourceTree = ""; }; - 8281D88BA7622C9E8AE79A31 /* Pods_DevRevSDKSample.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_DevRevSDKSample.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 8AC6DBD62B5701538664BE92 /* Pods-DevRevSDKSampleRN.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DevRevSDKSampleRN.debug.xcconfig"; path = "Target Support Files/Pods-DevRevSDKSampleRN/Pods-DevRevSDKSampleRN.debug.xcconfig"; sourceTree = ""; }; A2B83FA1ACA1BE40552EEA27 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = Sources/PrivacyInfo.xcprivacy; sourceTree = ""; }; - BDFEDCAEC4000AD1D0CC60AE /* Pods-DevRevSDKSample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DevRevSDKSample.release.xcconfig"; path = "Target Support Files/Pods-DevRevSDKSample/Pods-DevRevSDKSample.release.xcconfig"; sourceTree = ""; }; + E3EE8C8BC611A9CAA5574249 /* Pods_DevRevSDKSampleRN.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_DevRevSDKSampleRN.framework; sourceTree = BUILT_PRODUCTS_DIR; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; /* End PBXFileReference section */ @@ -35,7 +35,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - D17B58719925A0DC175BCCAB /* Pods_DevRevSDKSample.framework in Frameworks */, + EF1AA09255E944CC774FB0C1 /* Pods_DevRevSDKSampleRN.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -59,7 +59,7 @@ isa = PBXGroup; children = ( ED297162215061F000B7C4FE /* JavaScriptCore.framework */, - 8281D88BA7622C9E8AE79A31 /* Pods_DevRevSDKSample.framework */, + E3EE8C8BC611A9CAA5574249 /* Pods_DevRevSDKSampleRN.framework */, ); name = Frameworks; sourceTree = ""; @@ -74,7 +74,7 @@ 83CBB9F61A601CBA00E9B192 = { isa = PBXGroup; children = ( - 1CA606012D13FCE400B2293B /* DevRevSDKSample.entitlements */, + 1CA606012D13FCE400B2293B /* DevRevSDKSampleRN.entitlements */, 13B07FAE1A68108700A75B9A /* Sample */, 832341AE1AAA6A7D00B99B32 /* Libraries */, 83CBBA001A601CBA00E9B192 /* Products */, @@ -90,7 +90,7 @@ 83CBBA001A601CBA00E9B192 /* Products */ = { isa = PBXGroup; children = ( - 13B07F961A680F5B00A75B9A /* DevRevSDKSample.app */, + 13B07F961A680F5B00A75B9A /* DevRevSDKSampleRN.app */, ); name = Products; sourceTree = ""; @@ -105,8 +105,8 @@ BBD78D7AC51CEA395F1C20DB /* Pods */ = { isa = PBXGroup; children = ( - 6D78A72DEE821DC6473B0DE8 /* Pods-DevRevSDKSample.debug.xcconfig */, - BDFEDCAEC4000AD1D0CC60AE /* Pods-DevRevSDKSample.release.xcconfig */, + 8AC6DBD62B5701538664BE92 /* Pods-DevRevSDKSampleRN.debug.xcconfig */, + 60508B310A9753744EE0782C /* Pods-DevRevSDKSampleRN.release.xcconfig */, ); path = Pods; sourceTree = ""; @@ -114,26 +114,26 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ - 13B07F861A680F5B00A75B9A /* DevRevSDKSample */ = { + 13B07F861A680F5B00A75B9A /* DevRevSDKSampleRN */ = { isa = PBXNativeTarget; - buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "DevRevSDKSample" */; + buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "DevRevSDKSampleRN" */; buildPhases = ( - 1C09263E11E9B37D983C0685 /* [CP] Check Pods Manifest.lock */, + 79D99AFFD42C79566699A194 /* [CP] Check Pods Manifest.lock */, 13B07F871A680F5B00A75B9A /* Sources */, 13B07F8C1A680F5B00A75B9A /* Frameworks */, 13B07F8E1A680F5B00A75B9A /* Resources */, 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, - AE323BC7D371C3E3133066BC /* [CP] Embed Pods Frameworks */, - 48158488B74EA6A93A67A6F5 /* [CP] Copy Pods Resources */, - D43ADF06748524F5D3925B6E /* [CP-User] [RNFB] Core Configuration */, + 9EEBE2A4BA4AAA08EBC649BE /* [CP] Embed Pods Frameworks */, + 2DB4C6E723704CE6C198EFF3 /* [CP] Copy Pods Resources */, + 84F618E3FF9BDBCFDD8C71E6 /* [CP-User] [RNFB] Core Configuration */, ); buildRules = ( ); dependencies = ( ); - name = DevRevSDKSample; - productName = DevRevSDKSample; - productReference = 13B07F961A680F5B00A75B9A /* DevRevSDKSample.app */; + name = DevRevSDKSampleRN; + productName = DevRevSDKSampleRN; + productReference = 13B07F961A680F5B00A75B9A /* DevRevSDKSampleRN.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ @@ -149,7 +149,7 @@ }; }; }; - buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "DevRevSDKSample" */; + buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "DevRevSDKSampleRN" */; compatibilityVersion = "Xcode 12.0"; developmentRegion = en; hasScannedForEncodings = 0; @@ -162,7 +162,7 @@ projectDirPath = ""; projectRoot = ""; targets = ( - 13B07F861A680F5B00A75B9A /* DevRevSDKSample */, + 13B07F861A680F5B00A75B9A /* DevRevSDKSampleRN */, ); }; /* End PBXProject section */ @@ -198,7 +198,24 @@ shellPath = /bin/sh; shellScript = "set -e\n\nWITH_ENVIRONMENT=\"$REACT_NATIVE_PATH/scripts/xcode/with-environment.sh\"\nREACT_NATIVE_XCODE=\"$REACT_NATIVE_PATH/scripts/react-native-xcode.sh\"\n\n/bin/sh -c \"$WITH_ENVIRONMENT $REACT_NATIVE_XCODE\"\n"; }; - 1C09263E11E9B37D983C0685 /* [CP] Check Pods Manifest.lock */ = { + 2DB4C6E723704CE6C198EFF3 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-DevRevSDKSampleRN/Pods-DevRevSDKSampleRN-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-DevRevSDKSampleRN/Pods-DevRevSDKSampleRN-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-DevRevSDKSampleRN/Pods-DevRevSDKSampleRN-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + 79D99AFFD42C79566699A194 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -213,60 +230,43 @@ outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-DevRevSDKSample-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-DevRevSDKSampleRN-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 48158488B74EA6A93A67A6F5 /* [CP] Copy Pods Resources */ = { + 84F618E3FF9BDBCFDD8C71E6 /* [CP-User] [RNFB] Core Configuration */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-DevRevSDKSample/Pods-DevRevSDKSample-resources-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Copy Pods Resources"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-DevRevSDKSample/Pods-DevRevSDKSample-resources-${CONFIGURATION}-output-files.xcfilelist", + inputPaths = ( + "$(BUILT_PRODUCTS_DIR)/$(INFOPLIST_PATH)", ); + name = "[CP-User] [RNFB] Core Configuration"; runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-DevRevSDKSample/Pods-DevRevSDKSample-resources.sh\"\n"; - showEnvVarsInLog = 0; + shellScript = "#!/usr/bin/env bash\n#\n# Copyright (c) 2016-present Invertase Limited & Contributors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this library except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n##########################################################################\n##########################################################################\n#\n# NOTE THAT IF YOU CHANGE THIS FILE YOU MUST RUN pod install AFTERWARDS\n#\n# This file is installed as an Xcode build script in the project file\n# by cocoapods, and you will not see your changes until you pod install\n#\n##########################################################################\n##########################################################################\n\nset -e\n\n_MAX_LOOKUPS=2;\n_SEARCH_RESULT=''\n_RN_ROOT_EXISTS=''\n_CURRENT_LOOKUPS=1\n_JSON_ROOT=\"'react-native'\"\n_JSON_FILE_NAME='firebase.json'\n_JSON_OUTPUT_BASE64='e30=' # { }\n_CURRENT_SEARCH_DIR=${PROJECT_DIR}\n_PLIST_BUDDY=/usr/libexec/PlistBuddy\n_TARGET_PLIST=\"${BUILT_PRODUCTS_DIR}/${INFOPLIST_PATH}\"\n_DSYM_PLIST=\"${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Info.plist\"\n\n# plist arrays\n_PLIST_ENTRY_KEYS=()\n_PLIST_ENTRY_TYPES=()\n_PLIST_ENTRY_VALUES=()\n\nfunction setPlistValue {\n echo \"info: setting plist entry '$1' of type '$2' in file '$4'\"\n ${_PLIST_BUDDY} -c \"Add :$1 $2 '$3'\" $4 || echo \"info: '$1' already exists\"\n}\n\nfunction getFirebaseJsonKeyValue () {\n if [[ ${_RN_ROOT_EXISTS} ]]; then\n ruby -Ku -e \"require 'rubygems';require 'json'; output=JSON.parse('$1'); puts output[$_JSON_ROOT]['$2']\"\n else\n echo \"\"\n fi;\n}\n\nfunction jsonBoolToYesNo () {\n if [[ $1 == \"false\" ]]; then\n echo \"NO\"\n elif [[ $1 == \"true\" ]]; then\n echo \"YES\"\n else echo \"NO\"\n fi\n}\n\necho \"info: -> RNFB build script started\"\necho \"info: 1) Locating ${_JSON_FILE_NAME} file:\"\n\nif [[ -z ${_CURRENT_SEARCH_DIR} ]]; then\n _CURRENT_SEARCH_DIR=$(pwd)\nfi;\n\nwhile true; do\n _CURRENT_SEARCH_DIR=$(dirname \"$_CURRENT_SEARCH_DIR\")\n if [[ \"$_CURRENT_SEARCH_DIR\" == \"/\" ]] || [[ ${_CURRENT_LOOKUPS} -gt ${_MAX_LOOKUPS} ]]; then break; fi;\n echo \"info: ($_CURRENT_LOOKUPS of $_MAX_LOOKUPS) Searching in '$_CURRENT_SEARCH_DIR' for a ${_JSON_FILE_NAME} file.\"\n _SEARCH_RESULT=$(find \"$_CURRENT_SEARCH_DIR\" -maxdepth 2 -name ${_JSON_FILE_NAME} -print | /usr/bin/head -n 1)\n if [[ ${_SEARCH_RESULT} ]]; then\n echo \"info: ${_JSON_FILE_NAME} found at $_SEARCH_RESULT\"\n break;\n fi;\n _CURRENT_LOOKUPS=$((_CURRENT_LOOKUPS+1))\ndone\n\nif [[ ${_SEARCH_RESULT} ]]; then\n _JSON_OUTPUT_RAW=$(cat \"${_SEARCH_RESULT}\")\n _RN_ROOT_EXISTS=$(ruby -Ku -e \"require 'rubygems';require 'json'; output=JSON.parse('$_JSON_OUTPUT_RAW'); puts output[$_JSON_ROOT]\" || echo '')\n\n if [[ ${_RN_ROOT_EXISTS} ]]; then\n if ! python3 --version >/dev/null 2>&1; then echo \"python3 not found, firebase.json file processing error.\" && exit 1; fi\n _JSON_OUTPUT_BASE64=$(python3 -c 'import json,sys,base64;print(base64.b64encode(bytes(json.dumps(json.loads(open('\"'${_SEARCH_RESULT}'\"', '\"'rb'\"').read())['${_JSON_ROOT}']), '\"'utf-8'\"')).decode())' || echo \"e30=\")\n fi\n\n _PLIST_ENTRY_KEYS+=(\"firebase_json_raw\")\n _PLIST_ENTRY_TYPES+=(\"string\")\n _PLIST_ENTRY_VALUES+=(\"$_JSON_OUTPUT_BASE64\")\n\n # config.app_data_collection_default_enabled\n _APP_DATA_COLLECTION_ENABLED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"app_data_collection_default_enabled\")\n if [[ $_APP_DATA_COLLECTION_ENABLED ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseDataCollectionDefaultEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_APP_DATA_COLLECTION_ENABLED\")\")\n fi\n\n # config.analytics_auto_collection_enabled\n _ANALYTICS_AUTO_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_auto_collection_enabled\")\n if [[ $_ANALYTICS_AUTO_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"FIREBASE_ANALYTICS_COLLECTION_ENABLED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AUTO_COLLECTION\")\")\n fi\n\n # config.analytics_collection_deactivated\n _ANALYTICS_DEACTIVATED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_collection_deactivated\")\n if [[ $_ANALYTICS_DEACTIVATED ]]; then\n _PLIST_ENTRY_KEYS+=(\"FIREBASE_ANALYTICS_COLLECTION_DEACTIVATED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_DEACTIVATED\")\")\n fi\n\n # config.analytics_idfv_collection_enabled\n _ANALYTICS_IDFV_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_idfv_collection_enabled\")\n if [[ $_ANALYTICS_IDFV_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_IDFV_COLLECTION_ENABLED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_IDFV_COLLECTION\")\")\n fi\n\n # config.analytics_default_allow_analytics_storage\n _ANALYTICS_STORAGE=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_analytics_storage\")\n if [[ $_ANALYTICS_STORAGE ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_ANALYTICS_STORAGE\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_STORAGE\")\")\n fi\n\n # config.analytics_default_allow_ad_storage\n _ANALYTICS_AD_STORAGE=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_ad_storage\")\n if [[ $_ANALYTICS_AD_STORAGE ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_AD_STORAGE\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AD_STORAGE\")\")\n fi\n\n # config.analytics_default_allow_ad_user_data\n _ANALYTICS_AD_USER_DATA=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_ad_user_data\")\n if [[ $_ANALYTICS_AD_USER_DATA ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_AD_USER_DATA\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AD_USER_DATA\")\")\n fi\n\n # config.analytics_default_allow_ad_personalization_signals\n _ANALYTICS_PERSONALIZATION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_ad_personalization_signals\")\n if [[ $_ANALYTICS_PERSONALIZATION ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_AD_PERSONALIZATION_SIGNALS\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_PERSONALIZATION\")\")\n fi\n\n # config.analytics_registration_with_ad_network_enabled\n _ANALYTICS_REGISTRATION_WITH_AD_NETWORK=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"google_analytics_registration_with_ad_network_enabled\")\n if [[ $_ANALYTICS_REGISTRATION_WITH_AD_NETWORK ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_REGISTRATION_WITH_AD_NETWORK_ENABLED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_REGISTRATION_WITH_AD_NETWORK\")\")\n fi\n\n # config.google_analytics_automatic_screen_reporting_enabled\n _ANALYTICS_AUTO_SCREEN_REPORTING=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"google_analytics_automatic_screen_reporting_enabled\")\n if [[ $_ANALYTICS_AUTO_SCREEN_REPORTING ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseAutomaticScreenReportingEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AUTO_SCREEN_REPORTING\")\")\n fi\n\n # config.perf_auto_collection_enabled\n _PERF_AUTO_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"perf_auto_collection_enabled\")\n if [[ $_PERF_AUTO_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"firebase_performance_collection_enabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_PERF_AUTO_COLLECTION\")\")\n fi\n\n # config.perf_collection_deactivated\n _PERF_DEACTIVATED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"perf_collection_deactivated\")\n if [[ $_PERF_DEACTIVATED ]]; then\n _PLIST_ENTRY_KEYS+=(\"firebase_performance_collection_deactivated\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_PERF_DEACTIVATED\")\")\n fi\n\n # config.messaging_auto_init_enabled\n _MESSAGING_AUTO_INIT=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"messaging_auto_init_enabled\")\n if [[ $_MESSAGING_AUTO_INIT ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseMessagingAutoInitEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_MESSAGING_AUTO_INIT\")\")\n fi\n\n # config.in_app_messaging_auto_colllection_enabled\n _FIAM_AUTO_INIT=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"in_app_messaging_auto_collection_enabled\")\n if [[ $_FIAM_AUTO_INIT ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseInAppMessagingAutomaticDataCollectionEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_FIAM_AUTO_INIT\")\")\n fi\n\n # config.app_check_token_auto_refresh\n _APP_CHECK_TOKEN_AUTO_REFRESH=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"app_check_token_auto_refresh\")\n if [[ $_APP_CHECK_TOKEN_AUTO_REFRESH ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseAppCheckTokenAutoRefreshEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_APP_CHECK_TOKEN_AUTO_REFRESH\")\")\n fi\n\n # config.crashlytics_disable_auto_disabler - undocumented for now - mainly for debugging, document if becomes useful\n _CRASHLYTICS_AUTO_DISABLE_ENABLED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"crashlytics_disable_auto_disabler\")\n if [[ $_CRASHLYTICS_AUTO_DISABLE_ENABLED == \"true\" ]]; then\n echo \"Disabled Crashlytics auto disabler.\" # do nothing\n else\n _PLIST_ENTRY_KEYS+=(\"FirebaseCrashlyticsCollectionEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"NO\")\n fi\nelse\n _PLIST_ENTRY_KEYS+=(\"firebase_json_raw\")\n _PLIST_ENTRY_TYPES+=(\"string\")\n _PLIST_ENTRY_VALUES+=(\"$_JSON_OUTPUT_BASE64\")\n echo \"warning: A firebase.json file was not found, whilst this file is optional it is recommended to include it to configure firebase services in React Native Firebase.\"\nfi;\n\necho \"info: 2) Injecting Info.plist entries: \"\n\n# Log out the keys we're adding\nfor i in \"${!_PLIST_ENTRY_KEYS[@]}\"; do\n echo \" -> $i) ${_PLIST_ENTRY_KEYS[$i]}\" \"${_PLIST_ENTRY_TYPES[$i]}\" \"${_PLIST_ENTRY_VALUES[$i]}\"\ndone\n\nfor plist in \"${_TARGET_PLIST}\" \"${_DSYM_PLIST}\" ; do\n if [[ -f \"${plist}\" ]]; then\n\n # paths with spaces break the call to setPlistValue. temporarily modify\n # the shell internal field separator variable (IFS), which normally\n # includes spaces, to consist only of line breaks\n oldifs=$IFS\n IFS=\"\n\"\n\n for i in \"${!_PLIST_ENTRY_KEYS[@]}\"; do\n setPlistValue \"${_PLIST_ENTRY_KEYS[$i]}\" \"${_PLIST_ENTRY_TYPES[$i]}\" \"${_PLIST_ENTRY_VALUES[$i]}\" \"${plist}\"\n done\n\n # restore the original internal field separator value\n IFS=$oldifs\n else\n echo \"warning: A Info.plist build output file was not found (${plist})\"\n fi\ndone\n\necho \"info: <- RNFB build script finished\"\n"; }; - AE323BC7D371C3E3133066BC /* [CP] Embed Pods Frameworks */ = { + 9EEBE2A4BA4AAA08EBC649BE /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-DevRevSDKSample/Pods-DevRevSDKSample-frameworks-${CONFIGURATION}-input-files.xcfilelist", + "${PODS_ROOT}/Target Support Files/Pods-DevRevSDKSampleRN/Pods-DevRevSDKSampleRN-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-DevRevSDKSample/Pods-DevRevSDKSample-frameworks-${CONFIGURATION}-output-files.xcfilelist", + "${PODS_ROOT}/Target Support Files/Pods-DevRevSDKSampleRN/Pods-DevRevSDKSampleRN-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-DevRevSDKSample/Pods-DevRevSDKSample-frameworks.sh\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-DevRevSDKSampleRN/Pods-DevRevSDKSampleRN-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - D43ADF06748524F5D3925B6E /* [CP-User] [RNFB] Core Configuration */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "$(BUILT_PRODUCTS_DIR)/$(INFOPLIST_PATH)", - ); - name = "[CP-User] [RNFB] Core Configuration"; - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "#!/usr/bin/env bash\n#\n# Copyright (c) 2016-present Invertase Limited & Contributors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this library except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n##########################################################################\n##########################################################################\n#\n# NOTE THAT IF YOU CHANGE THIS FILE YOU MUST RUN pod install AFTERWARDS\n#\n# This file is installed as an Xcode build script in the project file\n# by cocoapods, and you will not see your changes until you pod install\n#\n##########################################################################\n##########################################################################\n\nset -e\n\n_MAX_LOOKUPS=2;\n_SEARCH_RESULT=''\n_RN_ROOT_EXISTS=''\n_CURRENT_LOOKUPS=1\n_JSON_ROOT=\"'react-native'\"\n_JSON_FILE_NAME='firebase.json'\n_JSON_OUTPUT_BASE64='e30=' # { }\n_CURRENT_SEARCH_DIR=${PROJECT_DIR}\n_PLIST_BUDDY=/usr/libexec/PlistBuddy\n_TARGET_PLIST=\"${BUILT_PRODUCTS_DIR}/${INFOPLIST_PATH}\"\n_DSYM_PLIST=\"${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Info.plist\"\n\n# plist arrays\n_PLIST_ENTRY_KEYS=()\n_PLIST_ENTRY_TYPES=()\n_PLIST_ENTRY_VALUES=()\n\nfunction setPlistValue {\n echo \"info: setting plist entry '$1' of type '$2' in file '$4'\"\n ${_PLIST_BUDDY} -c \"Add :$1 $2 '$3'\" $4 || echo \"info: '$1' already exists\"\n}\n\nfunction getFirebaseJsonKeyValue () {\n if [[ ${_RN_ROOT_EXISTS} ]]; then\n ruby -Ku -e \"require 'rubygems';require 'json'; output=JSON.parse('$1'); puts output[$_JSON_ROOT]['$2']\"\n else\n echo \"\"\n fi;\n}\n\nfunction jsonBoolToYesNo () {\n if [[ $1 == \"false\" ]]; then\n echo \"NO\"\n elif [[ $1 == \"true\" ]]; then\n echo \"YES\"\n else echo \"NO\"\n fi\n}\n\necho \"info: -> RNFB build script started\"\necho \"info: 1) Locating ${_JSON_FILE_NAME} file:\"\n\nif [[ -z ${_CURRENT_SEARCH_DIR} ]]; then\n _CURRENT_SEARCH_DIR=$(pwd)\nfi;\n\nwhile true; do\n _CURRENT_SEARCH_DIR=$(dirname \"$_CURRENT_SEARCH_DIR\")\n if [[ \"$_CURRENT_SEARCH_DIR\" == \"/\" ]] || [[ ${_CURRENT_LOOKUPS} -gt ${_MAX_LOOKUPS} ]]; then break; fi;\n echo \"info: ($_CURRENT_LOOKUPS of $_MAX_LOOKUPS) Searching in '$_CURRENT_SEARCH_DIR' for a ${_JSON_FILE_NAME} file.\"\n _SEARCH_RESULT=$(find \"$_CURRENT_SEARCH_DIR\" -maxdepth 2 -name ${_JSON_FILE_NAME} -print | /usr/bin/head -n 1)\n if [[ ${_SEARCH_RESULT} ]]; then\n echo \"info: ${_JSON_FILE_NAME} found at $_SEARCH_RESULT\"\n break;\n fi;\n _CURRENT_LOOKUPS=$((_CURRENT_LOOKUPS+1))\ndone\n\nif [[ ${_SEARCH_RESULT} ]]; then\n _JSON_OUTPUT_RAW=$(cat \"${_SEARCH_RESULT}\")\n _RN_ROOT_EXISTS=$(ruby -Ku -e \"require 'rubygems';require 'json'; output=JSON.parse('$_JSON_OUTPUT_RAW'); puts output[$_JSON_ROOT]\" || echo '')\n\n if [[ ${_RN_ROOT_EXISTS} ]]; then\n if ! python3 --version >/dev/null 2>&1; then echo \"python3 not found, firebase.json file processing error.\" && exit 1; fi\n _JSON_OUTPUT_BASE64=$(python3 -c 'import json,sys,base64;print(base64.b64encode(bytes(json.dumps(json.loads(open('\"'${_SEARCH_RESULT}'\"', '\"'rb'\"').read())['${_JSON_ROOT}']), '\"'utf-8'\"')).decode())' || echo \"e30=\")\n fi\n\n _PLIST_ENTRY_KEYS+=(\"firebase_json_raw\")\n _PLIST_ENTRY_TYPES+=(\"string\")\n _PLIST_ENTRY_VALUES+=(\"$_JSON_OUTPUT_BASE64\")\n\n # config.app_data_collection_default_enabled\n _APP_DATA_COLLECTION_ENABLED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"app_data_collection_default_enabled\")\n if [[ $_APP_DATA_COLLECTION_ENABLED ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseDataCollectionDefaultEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_APP_DATA_COLLECTION_ENABLED\")\")\n fi\n\n # config.analytics_auto_collection_enabled\n _ANALYTICS_AUTO_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_auto_collection_enabled\")\n if [[ $_ANALYTICS_AUTO_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"FIREBASE_ANALYTICS_COLLECTION_ENABLED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AUTO_COLLECTION\")\")\n fi\n\n # config.analytics_collection_deactivated\n _ANALYTICS_DEACTIVATED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_collection_deactivated\")\n if [[ $_ANALYTICS_DEACTIVATED ]]; then\n _PLIST_ENTRY_KEYS+=(\"FIREBASE_ANALYTICS_COLLECTION_DEACTIVATED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_DEACTIVATED\")\")\n fi\n\n # config.analytics_idfv_collection_enabled\n _ANALYTICS_IDFV_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_idfv_collection_enabled\")\n if [[ $_ANALYTICS_IDFV_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_IDFV_COLLECTION_ENABLED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_IDFV_COLLECTION\")\")\n fi\n\n # config.analytics_default_allow_analytics_storage\n _ANALYTICS_STORAGE=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_analytics_storage\")\n if [[ $_ANALYTICS_STORAGE ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_ANALYTICS_STORAGE\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_STORAGE\")\")\n fi\n\n # config.analytics_default_allow_ad_storage\n _ANALYTICS_AD_STORAGE=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_ad_storage\")\n if [[ $_ANALYTICS_AD_STORAGE ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_AD_STORAGE\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AD_STORAGE\")\")\n fi\n\n # config.analytics_default_allow_ad_user_data\n _ANALYTICS_AD_USER_DATA=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_ad_user_data\")\n if [[ $_ANALYTICS_AD_USER_DATA ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_AD_USER_DATA\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AD_USER_DATA\")\")\n fi\n\n # config.analytics_default_allow_ad_personalization_signals\n _ANALYTICS_PERSONALIZATION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_ad_personalization_signals\")\n if [[ $_ANALYTICS_PERSONALIZATION ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_AD_PERSONALIZATION_SIGNALS\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_PERSONALIZATION\")\")\n fi\n\n # config.analytics_registration_with_ad_network_enabled\n _ANALYTICS_REGISTRATION_WITH_AD_NETWORK=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"google_analytics_registration_with_ad_network_enabled\")\n if [[ $_ANALYTICS_REGISTRATION_WITH_AD_NETWORK ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_REGISTRATION_WITH_AD_NETWORK_ENABLED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_REGISTRATION_WITH_AD_NETWORK\")\")\n fi\n\n # config.google_analytics_automatic_screen_reporting_enabled\n _ANALYTICS_AUTO_SCREEN_REPORTING=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"google_analytics_automatic_screen_reporting_enabled\")\n if [[ $_ANALYTICS_AUTO_SCREEN_REPORTING ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseAutomaticScreenReportingEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AUTO_SCREEN_REPORTING\")\")\n fi\n\n # config.perf_auto_collection_enabled\n _PERF_AUTO_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"perf_auto_collection_enabled\")\n if [[ $_PERF_AUTO_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"firebase_performance_collection_enabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_PERF_AUTO_COLLECTION\")\")\n fi\n\n # config.perf_collection_deactivated\n _PERF_DEACTIVATED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"perf_collection_deactivated\")\n if [[ $_PERF_DEACTIVATED ]]; then\n _PLIST_ENTRY_KEYS+=(\"firebase_performance_collection_deactivated\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_PERF_DEACTIVATED\")\")\n fi\n\n # config.messaging_auto_init_enabled\n _MESSAGING_AUTO_INIT=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"messaging_auto_init_enabled\")\n if [[ $_MESSAGING_AUTO_INIT ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseMessagingAutoInitEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_MESSAGING_AUTO_INIT\")\")\n fi\n\n # config.in_app_messaging_auto_colllection_enabled\n _FIAM_AUTO_INIT=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"in_app_messaging_auto_collection_enabled\")\n if [[ $_FIAM_AUTO_INIT ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseInAppMessagingAutomaticDataCollectionEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_FIAM_AUTO_INIT\")\")\n fi\n\n # config.app_check_token_auto_refresh\n _APP_CHECK_TOKEN_AUTO_REFRESH=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"app_check_token_auto_refresh\")\n if [[ $_APP_CHECK_TOKEN_AUTO_REFRESH ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseAppCheckTokenAutoRefreshEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_APP_CHECK_TOKEN_AUTO_REFRESH\")\")\n fi\n\n # config.crashlytics_disable_auto_disabler - undocumented for now - mainly for debugging, document if becomes useful\n _CRASHLYTICS_AUTO_DISABLE_ENABLED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"crashlytics_disable_auto_disabler\")\n if [[ $_CRASHLYTICS_AUTO_DISABLE_ENABLED == \"true\" ]]; then\n echo \"Disabled Crashlytics auto disabler.\" # do nothing\n else\n _PLIST_ENTRY_KEYS+=(\"FirebaseCrashlyticsCollectionEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"NO\")\n fi\nelse\n _PLIST_ENTRY_KEYS+=(\"firebase_json_raw\")\n _PLIST_ENTRY_TYPES+=(\"string\")\n _PLIST_ENTRY_VALUES+=(\"$_JSON_OUTPUT_BASE64\")\n echo \"warning: A firebase.json file was not found, whilst this file is optional it is recommended to include it to configure firebase services in React Native Firebase.\"\nfi;\n\necho \"info: 2) Injecting Info.plist entries: \"\n\n# Log out the keys we're adding\nfor i in \"${!_PLIST_ENTRY_KEYS[@]}\"; do\n echo \" -> $i) ${_PLIST_ENTRY_KEYS[$i]}\" \"${_PLIST_ENTRY_TYPES[$i]}\" \"${_PLIST_ENTRY_VALUES[$i]}\"\ndone\n\nfor plist in \"${_TARGET_PLIST}\" \"${_DSYM_PLIST}\" ; do\n if [[ -f \"${plist}\" ]]; then\n\n # paths with spaces break the call to setPlistValue. temporarily modify\n # the shell internal field separator variable (IFS), which normally\n # includes spaces, to consist only of line breaks\n oldifs=$IFS\n IFS=\"\n\"\n\n for i in \"${!_PLIST_ENTRY_KEYS[@]}\"; do\n setPlistValue \"${_PLIST_ENTRY_KEYS[$i]}\" \"${_PLIST_ENTRY_TYPES[$i]}\" \"${_PLIST_ENTRY_VALUES[$i]}\" \"${plist}\"\n done\n\n # restore the original internal field separator value\n IFS=$oldifs\n else\n echo \"warning: A Info.plist build output file was not found (${plist})\"\n fi\ndone\n\necho \"info: <- RNFB build script finished\"\n"; - }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -283,13 +283,13 @@ /* Begin XCBuildConfiguration section */ 13B07F941A680F5B00A75B9A /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 6D78A72DEE821DC6473B0DE8 /* Pods-DevRevSDKSample.debug.xcconfig */; + baseConfigurationReference = 8AC6DBD62B5701538664BE92 /* Pods-DevRevSDKSampleRN.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = DevRevSDKSample.entitlements; + CODE_SIGN_ENTITLEMENTS = DevRevSDKSampleRN.entitlements; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = NJDA6Y3XRS; + DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; INFOPLIST_FILE = Sources/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -302,8 +302,8 @@ "-ObjC", "-lc++", ); - PRODUCT_BUNDLE_IDENTIFIER = ai.devrev.sdk.bridge.reactnative.DevRevSDKSample; - PRODUCT_NAME = DevRevSDKSample; + PRODUCT_BUNDLE_IDENTIFIER = ai.devrev.sdk.bridge.reactnative.sample; + PRODUCT_NAME = DevRevSDKSampleRN; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; @@ -312,13 +312,13 @@ }; 13B07F951A680F5B00A75B9A /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = BDFEDCAEC4000AD1D0CC60AE /* Pods-DevRevSDKSample.release.xcconfig */; + baseConfigurationReference = 60508B310A9753744EE0782C /* Pods-DevRevSDKSampleRN.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = DevRevSDKSample.entitlements; + CODE_SIGN_ENTITLEMENTS = DevRevSDKSampleRN.entitlements; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = NJDA6Y3XRS; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = Sources/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -330,8 +330,8 @@ "-ObjC", "-lc++", ); - PRODUCT_BUNDLE_IDENTIFIER = ai.devrev.sdk.bridge.reactnative.DevRevSDKSample; - PRODUCT_NAME = DevRevSDKSample; + PRODUCT_BUNDLE_IDENTIFIER = ai.devrev.sdk.bridge.reactnative.sample; + PRODUCT_NAME = DevRevSDKSampleRN; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; @@ -399,7 +399,7 @@ "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers/react/renderer/graphics/platform/ios", ); - IPHONEOS_DEPLOYMENT_TARGET = 13.4; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD = ""; LDPLUSPLUS = ""; LD_RUNPATH_SEARCH_PATHS = ( @@ -487,7 +487,7 @@ "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers/react/renderer/graphics/platform/ios", ); - IPHONEOS_DEPLOYMENT_TARGET = 13.4; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD = ""; LDPLUSPLUS = ""; LD_RUNPATH_SEARCH_PATHS = ( @@ -522,7 +522,7 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ - 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "DevRevSDKSample" */ = { + 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "DevRevSDKSampleRN" */ = { isa = XCConfigurationList; buildConfigurations = ( 13B07F941A680F5B00A75B9A /* Debug */, @@ -531,7 +531,7 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "DevRevSDKSample" */ = { + 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "DevRevSDKSampleRN" */ = { isa = XCConfigurationList; buildConfigurations = ( 83CBBA201A601CBA00E9B192 /* Debug */, diff --git a/sample/ios/DevRevSDKSample.xcodeproj/xcshareddata/xcschemes/DevRevSDKSample.xcscheme b/sample/react-native/ios/DevRevSDKSampleRN.xcodeproj/xcshareddata/xcschemes/DevRevSDKSampleRN.xcscheme similarity index 74% rename from sample/ios/DevRevSDKSample.xcodeproj/xcshareddata/xcschemes/DevRevSDKSample.xcscheme rename to sample/react-native/ios/DevRevSDKSampleRN.xcodeproj/xcshareddata/xcschemes/DevRevSDKSampleRN.xcscheme index d83aa1b..d3c6ac5 100644 --- a/sample/ios/DevRevSDKSample.xcodeproj/xcshareddata/xcschemes/DevRevSDKSample.xcscheme +++ b/sample/react-native/ios/DevRevSDKSampleRN.xcodeproj/xcshareddata/xcschemes/DevRevSDKSampleRN.xcscheme @@ -1,10 +1,11 @@ + LastUpgradeVersion = "1640" + version = "1.7"> + buildImplicitDependencies = "YES" + buildArchitectures = "Automatic"> + BuildableName = "DevRevSDKSampleRN.app" + BlueprintName = "DevRevSDKSampleRN" + ReferencedContainer = "container:DevRevSDKSampleRN.xcodeproj"> @@ -26,9 +27,8 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES"> - - + shouldUseLaunchSchemeArgsEnv = "YES" + shouldAutocreateTestPlan = "YES"> + BuildableName = "DevRevSDKSampleRN.app" + BlueprintName = "DevRevSDKSampleRN" + ReferencedContainer = "container:DevRevSDKSampleRN.xcodeproj"> @@ -62,9 +62,9 @@ + BuildableName = "DevRevSDKSampleRN.app" + BlueprintName = "DevRevSDKSampleRN" + ReferencedContainer = "container:DevRevSDKSampleRN.xcodeproj"> diff --git a/sample/ios/DevRevSDKSample.xcworkspace/xcshareddata/swiftpm/Package.resolved b/sample/react-native/ios/DevRevSDKSampleRN.xcworkspace/xcshareddata/swiftpm/Package.resolved similarity index 76% rename from sample/ios/DevRevSDKSample.xcworkspace/xcshareddata/swiftpm/Package.resolved rename to sample/react-native/ios/DevRevSDKSampleRN.xcworkspace/xcshareddata/swiftpm/Package.resolved index 036f31a..ae2d07c 100644 --- a/sample/ios/DevRevSDKSample.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/sample/react-native/ios/DevRevSDKSampleRN.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -6,8 +6,8 @@ "kind" : "remoteSourceControl", "location" : "git@github.com:devrev/devrev-sdk-ios.git", "state" : { - "revision" : "21d5a4e3d61b8cac599d95af2f75bc8442c8e3a0", - "version" : "2.0.2" + "revision" : "b394b096d9b8dc015adbe3f58218c985a6a71787", + "version" : "2.2.4" } } ], diff --git a/sample/react-native/ios/GoogleService-Info.plist b/sample/react-native/ios/GoogleService-Info.plist new file mode 100644 index 0000000..ba10bf1 --- /dev/null +++ b/sample/react-native/ios/GoogleService-Info.plist @@ -0,0 +1,16 @@ + + + + + API_KEY + DUMMY_API_KEY + CLIENT_ID + DUMMY_CLIENT_ID + GOOGLE_APP_ID + 1:1234567890:ios:abcdef123456 + PROJECT_ID + dummy-project + BUNDLE_ID + ai.devrev.sdk.bridge.reactnative.sample + + diff --git a/sample/ios/Podfile b/sample/react-native/ios/Podfile similarity index 69% rename from sample/ios/Podfile rename to sample/react-native/ios/Podfile index 377a38d..90c2000 100644 --- a/sample/ios/Podfile +++ b/sample/react-native/ios/Podfile @@ -1,30 +1,29 @@ -# Resolve react_native_pods.rb with node to allow for hoisting require Pod::Executable.execute_command('node', ['-p', 'require.resolve( "react-native/scripts/react_native_pods.rb", {paths: [process.argv[1]]}, )', __dir__]).strip -platform :ios, "15.0" +platform :ios, '15.1' prepare_react_native_project! -use_frameworks! :linkage => :static +use_frameworks! :linkage => :dynamic # !!!! Uncomment the following line for development !!!! # source 'git@github.com:devrev/ios-podspecs-test.git' source 'https://cdn.cocoapods.org/' -target 'DevRevSDKSample' do +project 'DevRevSDKSampleRN.xcodeproj' + +target 'DevRevSDKSampleRN' do config = use_native_modules! use_react_native!( :path => config[:reactNativePath], - # An absolute path to your application root. :app_path => "#{Pod::Config.instance.installation_root}/.." ) post_install do |installer| - # https://github.com/facebook/react-native/blob/main/packages/react-native/scripts/react_native_pods.rb#L197-L202 react_native_post_install( installer, config[:reactNativePath], diff --git a/sample/ios/Podfile.lock b/sample/react-native/ios/Podfile.lock similarity index 98% rename from sample/ios/Podfile.lock rename to sample/react-native/ios/Podfile.lock index 24f3cd8..51f5824 100644 --- a/sample/ios/Podfile.lock +++ b/sample/react-native/ios/Podfile.lock @@ -1,6 +1,6 @@ PODS: - boost (1.84.0) - - devrev-sdk-react-native (2.0.0): + - devrev-sdk-react-native (2.2.1): - DoubleConversion - glog - hermes-engine @@ -1419,6 +1419,8 @@ PODS: - React-jsiexecutor - React-RCTFBReactNativeSpec - ReactCommon/turbomodule/core + - react-native-notifications (5.1.0): + - React-Core - react-native-safe-area-context (5.4.0): - DoubleConversion - glog @@ -1494,6 +1496,30 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga + - react-native-webview (13.16.0): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga - React-NativeModulesApple (0.79.2): - glog - hermes-engine @@ -1855,17 +1881,12 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - RNNotifee (9.1.8): - - React-Core - - RNNotifee/NotifeeCore (= 9.1.8) - - RNNotifee/NotifeeCore (9.1.8): - - React-Core - SocketRocket (0.7.1) - Yoga (0.0.0) DEPENDENCIES: - boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`) - - devrev-sdk-react-native (from `../..`) + - devrev-sdk-react-native (from `../../..`) - DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) - fast_float (from `../node_modules/react-native/third-party-podspecs/fast_float.podspec`) - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`) @@ -1905,7 +1926,9 @@ DEPENDENCIES: - React-logger (from `../node_modules/react-native/ReactCommon/logger`) - React-Mapbuffer (from `../node_modules/react-native/ReactCommon`) - React-microtasksnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`) + - react-native-notifications (from `../node_modules/react-native-notifications`) - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`) + - react-native-webview (from `../node_modules/react-native-webview`) - React-NativeModulesApple (from `../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`) - React-oscompat (from `../node_modules/react-native/ReactCommon/oscompat`) - React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`) @@ -1942,7 +1965,6 @@ DEPENDENCIES: - "RNFBInstallations (from `../node_modules/@react-native-firebase/installations`)" - "RNFBMessaging (from `../node_modules/@react-native-firebase/messaging`)" - RNGestureHandler (from `../node_modules/react-native-gesture-handler`) - - "RNNotifee (from `../node_modules/@notifee/react-native`)" - Yoga (from `../node_modules/react-native/ReactCommon/yoga`) SPEC REPOS: @@ -1963,7 +1985,7 @@ EXTERNAL SOURCES: boost: :podspec: "../node_modules/react-native/third-party-podspecs/boost.podspec" devrev-sdk-react-native: - :path: "../.." + :path: "../../.." DoubleConversion: :podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec" fast_float: @@ -2039,8 +2061,12 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon" React-microtasksnativemodule: :path: "../node_modules/react-native/ReactCommon/react/nativemodule/microtasks" + react-native-notifications: + :path: "../node_modules/react-native-notifications" react-native-safe-area-context: :path: "../node_modules/react-native-safe-area-context" + react-native-webview: + :path: "../node_modules/react-native-webview" React-NativeModulesApple: :path: "../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios" React-oscompat: @@ -2113,14 +2139,12 @@ EXTERNAL SOURCES: :path: "../node_modules/@react-native-firebase/messaging" RNGestureHandler: :path: "../node_modules/react-native-gesture-handler" - RNNotifee: - :path: "../node_modules/@notifee/react-native" Yoga: :path: "../node_modules/react-native/ReactCommon/yoga" SPEC CHECKSUMS: boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90 - devrev-sdk-react-native: 8338db515da7a10a8681e1e54c9ef8b46864f632 + devrev-sdk-react-native: 1444b3f76c3954dcf104e92c73a9b96d69ec0738 DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6 FBLazyVector: 84b955f7b4da8b895faf5946f73748267347c975 @@ -2168,7 +2192,9 @@ SPEC CHECKSUMS: React-logger: 8edfcedc100544791cd82692ca5a574240a16219 React-Mapbuffer: da73f30b000114058d6bc41490dcce204a8ede32 React-microtasksnativemodule: 444c5701aece79629bb73bd9e7ad8937ae65238c + react-native-notifications: 3bafa1237ae8a47569a84801f17d80242fe9f6a5 react-native-safe-area-context: 5928d84c879db2f9eb6969ca70e68f58623dbf25 + react-native-webview: f0d598682cbc88a97d66da4121873d3467f76635 React-NativeModulesApple: df8e5bc59e78ca3040ffbf41336889f3bd0fad68 React-oscompat: ef5df1c734f19b8003e149317d041b8ce1f7d29c React-perflogger: 9a151e0b4c933c9205fd648c246506a83f31395d @@ -2205,10 +2231,9 @@ SPEC CHECKSUMS: RNFBInstallations: e44e81b6eb5966481315f4576134748591c9dd8e RNFBMessaging: c1dc973d87a82fd9e58bd8c3a8c269dcca337669 RNGestureHandler: 381f81c675883fec9f8f10976aa278babc48d8eb - RNNotifee: 5e3b271e8ea7456a36eec994085543c9adca9168 SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 Yoga: 50518ade05048235d91a78b803336dbb5b159d5d -PODFILE CHECKSUM: 4a89df2116af06f5e357ab5fb1908d851492d825 +PODFILE CHECKSUM: 77a3bbff7d76f2ae1551ede8ef62e45eff2a9e3b COCOAPODS: 1.16.2 diff --git a/sample/ios/Sources/AppDelegate.swift b/sample/react-native/ios/Sources/AppDelegate.swift similarity index 55% rename from sample/ios/Sources/AppDelegate.swift rename to sample/react-native/ios/Sources/AppDelegate.swift index a7a0411..70e4ddc 100644 --- a/sample/ios/Sources/AppDelegate.swift +++ b/sample/react-native/ios/Sources/AppDelegate.swift @@ -3,19 +3,24 @@ import React import React_RCTAppDelegate import ReactAppDependencyProvider import Firebase +import RNNotifications @main class AppDelegate: UIResponder, UIApplicationDelegate { - var window: UIWindow? + // MARK: - Properties + var window: UIWindow? var reactNativeDelegate: ReactNativeDelegate? var reactNativeFactory: RCTReactNativeFactory? + // MARK: - App lifecycle + func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { FirebaseApp.configure() + RNNotifications.startMonitorNotifications() let delegate = ReactNativeDelegate() let factory = RCTReactNativeFactory(delegate: delegate) @@ -27,18 +32,45 @@ class AppDelegate: UIResponder, UIApplicationDelegate { window = UIWindow(frame: UIScreen.main.bounds) factory.startReactNative( - withModuleName: "DevRevSDKSample", + withModuleName: "DevRevSDKSampleRN", in: window, launchOptions: launchOptions ) return true } + + // MARK: - Push notifications + + func application( + _ application: UIApplication, + didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data + ) { + RNNotifications.didRegisterForRemoteNotifications(withDeviceToken: deviceToken) + } + + func application( + _ application: UIApplication, + didFailToRegisterForRemoteNotificationsWithError error: any Error + ) { + RNNotifications.didFailToRegisterForRemoteNotificationsWithError(error) + } + + func application( + _ application: UIApplication, + didReceiveRemoteNotification userInfo: [AnyHashable : Any], + fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void + ) { + RNNotifications.didReceiveBackgroundNotification( + userInfo, + withCompletionHandler: completionHandler + ) + } } class ReactNativeDelegate: RCTDefaultReactNativeFactoryDelegate { override func sourceURL(for bridge: RCTBridge) -> URL? { - self.bundleURL() + bundleURL() } override func bundleURL() -> URL? { diff --git a/sample/react-native/ios/Sources/Images.xcassets/AppIcon.appiconset/Contents.json b/sample/react-native/ios/Sources/Images.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..4812f00 --- /dev/null +++ b/sample/react-native/ios/Sources/Images.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,14 @@ +{ + "images" : [ + { + "filename" : "React Native.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/sample/react-native/ios/Sources/Images.xcassets/AppIcon.appiconset/React Native.png b/sample/react-native/ios/Sources/Images.xcassets/AppIcon.appiconset/React Native.png new file mode 100644 index 0000000..0c1b4bb Binary files /dev/null and b/sample/react-native/ios/Sources/Images.xcassets/AppIcon.appiconset/React Native.png differ diff --git a/sample/ios/Sources/Images.xcassets/Contents.json b/sample/react-native/ios/Sources/Images.xcassets/Contents.json similarity index 100% rename from sample/ios/Sources/Images.xcassets/Contents.json rename to sample/react-native/ios/Sources/Images.xcassets/Contents.json diff --git a/sample/ios/Sources/Info.plist b/sample/react-native/ios/Sources/Info.plist similarity index 91% rename from sample/ios/Sources/Info.plist rename to sample/react-native/ios/Sources/Info.plist index 91822d7..471f9b3 100644 --- a/sample/ios/Sources/Info.plist +++ b/sample/react-native/ios/Sources/Info.plist @@ -5,7 +5,7 @@ CFBundleDevelopmentRegion en CFBundleDisplayName - DevRevSDKSample + DevRev SDK (React Native) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier @@ -33,6 +33,10 @@ NSLocationWhenInUseUsageDescription + UIBackgroundModes + + remote-notification + UILaunchStoryboardName LaunchScreen UIRequiredDeviceCapabilities diff --git a/sample/react-native/ios/Sources/LaunchScreen.storyboard b/sample/react-native/ios/Sources/LaunchScreen.storyboard new file mode 100644 index 0000000..ec2b117 --- /dev/null +++ b/sample/react-native/ios/Sources/LaunchScreen.storyboard @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sample/ios/Sources/PrivacyInfo.xcprivacy b/sample/react-native/ios/Sources/PrivacyInfo.xcprivacy similarity index 100% rename from sample/ios/Sources/PrivacyInfo.xcprivacy rename to sample/react-native/ios/Sources/PrivacyInfo.xcprivacy diff --git a/sample/jest.config.js b/sample/react-native/jest.config.js similarity index 100% rename from sample/jest.config.js rename to sample/react-native/jest.config.js diff --git a/sample/react-native/metro.config.js b/sample/react-native/metro.config.js new file mode 100644 index 0000000..f4acabb --- /dev/null +++ b/sample/react-native/metro.config.js @@ -0,0 +1,48 @@ +const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config'); +const path = require('path'); +const escape = require('escape-string-regexp'); +const exclusionList = require('metro-config/src/defaults/exclusionList'); +const pak = require('../../package.json'); + +const root = path.resolve(__dirname, '../..'); +const modules = Object.keys({ ...pak.peerDependencies }); + +const extraNodeModules = modules.reduce((acc, name) => { + acc[name] = path.join(__dirname, 'node_modules', name); + return acc; +}, {}); +extraNodeModules[pak.name] = root; + +/** + * Metro configuration + * https://facebook.github.io/metro/docs/configuration + * + * @type {import('@react-native/metro-config').MetroConfig} + */ +const config = { + watchFolders: [root], + + // We need to make sure that only one version is loaded for peerDependencies + // So we block them at the root, and alias them to the versions in samples's node_modules + resolver: { + blacklistRE: exclusionList( + modules.map( + (m) => + new RegExp(`^${escape(path.join(root, 'node_modules', m))}\\/.*$`) + ) + ), + + extraNodeModules, + }, + + transformer: { + getTransformOptions: async () => ({ + transform: { + experimentalImportSupport: false, + inlineRequires: true, + }, + }), + }, +}; + +module.exports = mergeConfig(getDefaultConfig(__dirname), config); diff --git a/sample/react-native/package.json b/sample/react-native/package.json new file mode 100644 index 0000000..fb31339 --- /dev/null +++ b/sample/react-native/package.json @@ -0,0 +1,76 @@ +{ + "name": "@devrev/sdk-react-native-sample", + "version": "2.2.1", + "description": "DevRev SDK for React Native sample app.", + "scripts": { + "android": "react-native run-android", + "ios": "react-native run-ios", + "start": "react-native start", + "build:android": "react-native build-android --extra-params \"--no-daemon --console=plain -PreactNativeArchitectures=arm64-v8a\"", + "build:ios": "react-native build-ios --scheme DevRevSDKSample --mode Debug --extra-params \"-sdk iphonesimulator CC=clang CPLUSPLUS=clang++ LD=clang LDPLUSPLUS=clang++ GCC_OPTIMIZATION_LEVEL=0 GCC_PRECOMPILE_PREFIX_HEADER=YES ASSETCATALOG_COMPILER_OPTIMIZATION=time DEBUG_INFORMATION_FORMAT=dwarf COMPILER_INDEX_STORE_ENABLE=NO\"" + }, + "dependencies": { + "@devrev/sdk-react-native": "workspace:*", + "@react-native-firebase/app": "^21.0.0", + "@react-native-firebase/installations": "^21.0.0", + "@react-native-firebase/messaging": "^21.0.0", + "react-native-device-info": "^14.0.1", + "react-native-notifications": "^5.1.0", + "react-native-webview": "^13.15.0" + }, + "devDependencies": { + "@babel/core": "^7.20.0", + "@babel/preset-env": "^7.20.0", + "@babel/runtime": "^7.20.0", + "@react-native-community/cli": "^18.0.0", + "@react-native-community/cli-platform-android": "^18.0.0", + "@react-native-community/cli-platform-ios": "^18.0.0", + "@react-native/babel-preset": "^0.79.0", + "@react-native/metro-config": "^0.79.0", + "@react-native/typescript-config": "0.74.85", + "@react-navigation/native": "^6.0.8", + "@react-navigation/stack": "^6.3.21", + "babel-plugin-module-resolver": "^5.0.0", + "react": "19.0.0", + "react-native": "^0.79.0", + "react-native-gesture-handler": "^2.24.0", + "react-native-safe-area-context": "^5.1.0" + }, + "engines": { + "node": ">=18" + }, + "rnx-kit": { + "kitType": "library", + "alignDeps": { + "requirements": { + "development": [ + "react-native@0.79" + ], + "production": [ + "react-native@0.79" + ] + }, + "capabilities": [ + "babel-preset-react-native", + "community/cli-ios", + "core", + "core-android", + "core-ios", + "core/metro-config", + "gestures", + "navigation/native", + "navigation/stack", + "react", + "safe-area" + ] + } + }, + "peerDependencies": { + "@react-navigation/native": "^6.0.8", + "@react-navigation/stack": "^6.3.21", + "react": "19.0.0", + "react-native": "^0.79.0", + "react-native-gesture-handler": "^2.24.0", + "react-native-safe-area-context": "^5.1.0" + } +} diff --git a/sample/react-native/react-native.config.js b/sample/react-native/react-native.config.js new file mode 100644 index 0000000..cecf7a3 --- /dev/null +++ b/sample/react-native/react-native.config.js @@ -0,0 +1,15 @@ +const path = require('path'); +const pak = require('./package.json'); + +module.exports = { + project: { + ios: { + automaticPodsInstallation: true, + }, + }, + dependencies: { + '@devrev/sdk-react-native': { + root: path.join(__dirname, '../..'), + }, + }, +}; diff --git a/sample/src/App.tsx b/sample/react-native/src/App.tsx similarity index 57% rename from sample/src/App.tsx rename to sample/react-native/src/App.tsx index 4e59957..c729d62 100644 --- a/sample/src/App.tsx +++ b/sample/react-native/src/App.tsx @@ -1,8 +1,8 @@ import * as React from 'react'; -import { Text, Linking, TouchableOpacity, StyleSheet } from 'react-native'; +import { Text, TouchableOpacity } from 'react-native'; import * as DevRev from '@devrev/sdk-react-native'; -import NotificationService from './NotificationService'; +import PushNotificationsService from './PushNotificationsService'; import { createStackNavigator } from '@react-navigation/stack'; import { NavigationContainer } from '@react-navigation/native'; import IdentificationScreen from './screens/IdentificationScreen'; @@ -11,6 +11,8 @@ import SessionAnalyticsScreen from './screens/SessionAnalyticsScreen'; import SupportChatScreen from './screens/SupportChatScreen'; import HomeScreen from './screens/HomeScreen'; import DelayedScreen from './screens/DelayedScreen'; +import WebViewScreen from './screens/WebViewScreen'; +import FlatListScreen from './screens/FlatListScreen'; import { commonStyles } from './styles/styles'; export type RootStackParamList = { @@ -20,17 +22,49 @@ export type RootStackParamList = { SessionAnalytics: undefined; SupportChat: undefined; DelayedScreen: undefined; + WebViewScreen: undefined; + FlatListScreen: undefined; }; const Stack = createStackNavigator(); const screens = [ { name: 'Home', component: HomeScreen, title: 'DevRev SDK' }, - { name: 'Identification', component: IdentificationScreen, title: 'Identification' }, - { name: 'PushNotifications', component: PushNotificationsScreen, title: 'Push Notifications' }, - { name: 'SessionAnalytics', component: SessionAnalyticsScreen, title: 'Session Analytics' }, - { name: 'SupportChat', component: SupportChatScreen, title: 'Support Chat' }, - { name: 'DelayedScreen', component: DelayedScreen, title: 'Delayed Screen' }, + { + name: 'Identification', + component: IdentificationScreen, + title: 'Identification', + }, + { + name: 'PushNotifications', + component: PushNotificationsScreen, + title: 'Push Notifications', + }, + { + name: 'SessionAnalytics', + component: SessionAnalyticsScreen, + title: 'Session Analytics', + }, + { + name: 'SupportChat', + component: SupportChatScreen, + title: 'Support Chat', + }, + { + name: 'DelayedScreen', + component: DelayedScreen, + title: 'Delayed Screen', + }, + { + name: 'WebViewScreen', + component: WebViewScreen, + title: 'Web View', + }, + { + name: 'FlatListScreen', + component: FlatListScreen, + title: 'Large Scrollable List', + }, ] as const; const createScreen = ( @@ -47,25 +81,24 @@ const createScreen = ( options={({ navigation }) => ({ title: title, headerRight: () => ( - navigation.replace(name)} + navigation.replace(name)} style={commonStyles.refreshButton} > ), - })} /> - ) -} + })} + /> + ); +}; const App = () => { React.useEffect(() => { try { - DevRev.configure( - 'YOUR_APP_ID', - ); + DevRev.configure('YOUR_APP_ID'); DevRev.setShouldDismissModalsOnOpenLink(true); - - NotificationService.initialize(); + PushNotificationsService.configure(); } catch (error) { console.log(error); } @@ -75,7 +108,12 @@ const App = () => { {screens.map((screen, index) => - createScreen(screen.name as keyof RootStackParamList, screen.component, screen.title, index) + createScreen( + screen.name as keyof RootStackParamList, + screen.component, + screen.title, + index + ) )} diff --git a/sample/react-native/src/PushNotificationsService.tsx b/sample/react-native/src/PushNotificationsService.tsx new file mode 100644 index 0000000..abe5722 --- /dev/null +++ b/sample/react-native/src/PushNotificationsService.tsx @@ -0,0 +1,99 @@ +import { + NotificationBackgroundFetchResult, + Notifications, + Registered, + RegistrationError, + Notification, + NotificationCompletion, +} from 'react-native-notifications'; +import * as DevRev from '@devrev/sdk-react-native'; +import DeviceInfo from 'react-native-device-info'; +import { Platform } from 'react-native'; + +const register = async (): Promise => { + Notifications.registerRemoteNotifications(); +}; + +// Configure the notification service. +const configure = async (): Promise => { + try { + register(); + + if (Platform.OS === 'android') { + Notifications.setNotificationChannel({ + channelId: 'default', + name: 'Default Channel', + enableLights: true, + enableVibration: true, + showBadge: true, + importance: 4, + }); + } + + Notifications.events().registerRemoteNotificationsRegistered( + (event: Registered) => { + console.log('Device token received:', event.deviceToken); + const deviceToken = event.deviceToken; + + DeviceInfo.getUniqueId() + .then((deviceId) => DevRev.registerDeviceToken(deviceToken, deviceId)) + .catch((error) => { + console.error('Could not register the device token!', error); + }); + } + ); + + Notifications.events().registerRemoteNotificationsRegistrationFailed( + (event: RegistrationError) => { + console.error('Failed to register the device token!', event); + } + ); + + Notifications.events().registerNotificationReceivedForeground( + ( + notification: Notification, + completion: (response: NotificationCompletion) => void + ) => { + processPushNotification(notification); + + completion({ alert: true, badge: true, sound: true }); + } + ); + + Notifications.events().registerNotificationReceivedBackground( + ( + notification: Notification, + completion: (response: NotificationBackgroundFetchResult) => void + ) => { + processPushNotification(notification); + + completion(NotificationBackgroundFetchResult.NO_DATA); + } + ); + + Notifications.events().registerNotificationOpened(processPushNotification); + Notifications.getInitialNotification().then(processPushNotification); + } catch (error) { + console.error('Error initializing notifications:', error); + } +}; + +const processPushNotification = (notification: Notification | undefined) => { + if (!notification) { + return; + } + + const message = JSON.stringify(notification.payload); + if (!message) { + return; + } + + DevRev.processPushNotification(message); +}; + +export const PushNotificationsService = { + configure, + register, +}; + +export default PushNotificationsService; diff --git a/sample/react-native/src/assets/sample.html b/sample/react-native/src/assets/sample.html new file mode 100644 index 0000000..5204157 --- /dev/null +++ b/sample/react-native/src/assets/sample.html @@ -0,0 +1,56 @@ + + + + + + Sample Page with Masking + + + +

Sample HTML for WebView

+

This page demonstrates content masking within a WebView.

+
+ + +

This field (devrev-mask) is masked in recordings.

+
+ +
+ + +

This password field (type="password") is automatically masked by the SDK without requiring the devrev-mask class.

+
+ +
+ + +

This field (devrev-unmask) is visible in recordings.

+
+ +
+ + +

This number (devrev-mask) is masked in recordings.

+
+ +

This paragraph contains a masked phrase and an unmasked phrase within the text.

+ +
+

This entire div and its contents are masked:

+ + +
+ +
+ + + diff --git a/sample/src/components/AlertDialog.tsx b/sample/react-native/src/components/AlertDialog.tsx similarity index 88% rename from sample/src/components/AlertDialog.tsx rename to sample/react-native/src/components/AlertDialog.tsx index 01ebf0e..2c372b4 100644 --- a/sample/src/components/AlertDialog.tsx +++ b/sample/react-native/src/components/AlertDialog.tsx @@ -8,7 +8,11 @@ interface AlertDialogProps { onDismiss: () => void; } -const AlertDialog: React.FC = ({ title, message, onDismiss }) => { +const AlertDialog: React.FC = ({ + title, + message, + onDismiss, +}) => { return ( = ({ label, checked }) => { return ( {label} - + {checked && ( diff --git a/sample/react-native/src/components/TouchableOpacityButton.tsx b/sample/react-native/src/components/TouchableOpacityButton.tsx new file mode 100644 index 0000000..edc2c41 --- /dev/null +++ b/sample/react-native/src/components/TouchableOpacityButton.tsx @@ -0,0 +1,32 @@ +import React, { forwardRef } from 'react'; +import { + TouchableOpacity, + Text, + type TextStyle, + type ViewStyle, + View, +} from 'react-native'; +import { commonStyles } from '../styles/styles'; + +interface TouchableOpacityProps { + onPress: () => void; + buttonText: string; + buttonStyle?: ViewStyle; + textStyle?: TextStyle; +} + +const TouchableOpacityButton = forwardRef( + ({ onPress, buttonText, buttonStyle, textStyle }, ref) => { + return ( + + {buttonText} + + ); + } +); + +export default TouchableOpacityButton; diff --git a/sample/src/model/DevRevNotification.ts b/sample/react-native/src/model/DevRevNotification.ts similarity index 100% rename from sample/src/model/DevRevNotification.ts rename to sample/react-native/src/model/DevRevNotification.ts diff --git a/sample/react-native/src/screens/DelayedScreen.tsx b/sample/react-native/src/screens/DelayedScreen.tsx new file mode 100644 index 0000000..932f395 --- /dev/null +++ b/sample/react-native/src/screens/DelayedScreen.tsx @@ -0,0 +1,20 @@ +import * as React from 'react'; +import { useEffect } from 'react'; +import { View, Text } from 'react-native'; +import * as DevRev from '@devrev/sdk-react-native'; +import { commonStyles } from '../styles/styles'; + +const DelayedScreen: React.FC = () => { + useEffect(() => { + // End the screen transition when the screen is loaded (Android only) + DevRev.setInScreenTransitioning(false); + }, []); + + return ( + + Delayed Screen + + ); +}; + +export default DelayedScreen; diff --git a/sample/react-native/src/screens/FlatListScreen.tsx b/sample/react-native/src/screens/FlatListScreen.tsx new file mode 100644 index 0000000..c6f810b --- /dev/null +++ b/sample/react-native/src/screens/FlatListScreen.tsx @@ -0,0 +1,86 @@ +import React, { useRef } from 'react'; +import { View, Text, FlatList, StyleSheet, findNodeHandle } from 'react-native'; +import * as DevRev from '@devrev/sdk-react-native'; + +const DATA = Array.from({ length: 100 }, (_, i) => ({ + id: i.toString(), + title: `Item #${i}`, +})); + +const refs = DATA.map(() => React.createRef()); + +const CardView = React.forwardRef(({ title }: { title: string }, ref) => ( + + {title} + +)); + +const FlatListScreen: React.FC = () => { + const lastSensitiveTagsRef = useRef([]); + + const onViewableItemsChanged = React.useCallback(({ viewableItems }) => { + const sensitiveTags: number[] = []; + viewableItems.forEach(({ index }) => { + if (index % 2 === 0 && refs[index]?.current) { + const tag = findNodeHandle(refs[index].current); + if (tag) sensitiveTags.push(tag); + } + }); + + // Determine views to unmark and mark based on visibility changes + const currentlyMarked = lastSensitiveTagsRef.current; + const toUnmark = currentlyMarked.filter( + (tag) => !sensitiveTags.includes(tag) + ); + const toMark = sensitiveTags.filter( + (tag) => !currentlyMarked.includes(tag) + ); + + // Unmark views that are no longer visible or will be remarked + if (toUnmark.length > 0) { + DevRev.unmarkSensitiveViews(toUnmark); + } + + // Mark newly visible sensitive views + if (toMark.length > 0) { + DevRev.markSensitiveViews(toMark); + } + + // Update the record of currently marked sensitive views + // This includes views that remained visible and newly marked views + lastSensitiveTagsRef.current = sensitiveTags.filter( + (tag) => toUnmark.indexOf(tag) === -1 || toMark.indexOf(tag) !== -1 + ); + }, []); + + return ( + item.id} + renderItem={({ item, index }) => ( + + )} + contentContainerStyle={styles.list} + onViewableItemsChanged={onViewableItemsChanged} + viewabilityConfig={{ itemVisiblePercentThreshold: 50 }} + /> + ); +}; + +const styles = StyleSheet.create({ + list: { padding: 16 }, + card: { + backgroundColor: '#fff', + borderRadius: 8, + elevation: 3, + shadowColor: '#000', + shadowOpacity: 0.1, + shadowRadius: 4, + shadowOffset: { width: 0, height: 2 }, + padding: 16, + marginBottom: 12, + }, + cardText: { fontSize: 16, color: '#333' }, +}); + +export default FlatListScreen; diff --git a/sample/src/screens/HomeScreen.tsx b/sample/react-native/src/screens/HomeScreen.tsx similarity index 76% rename from sample/src/screens/HomeScreen.tsx rename to sample/react-native/src/screens/HomeScreen.tsx index e7dac80..6bea2b6 100644 --- a/sample/src/screens/HomeScreen.tsx +++ b/sample/react-native/src/screens/HomeScreen.tsx @@ -18,31 +18,35 @@ type Button = NavigationButton | ActionButton; // Feature Section Buttons const featureButtons: Button[] = [ - { text: "Identification", route: "Identification" }, - { text: "Support Chat", route: "SupportChat" }, - { text: "Push Notifications", route: "PushNotifications" }, - { text: "Session Analytics", route: "SessionAnalytics" }, + { text: 'Identification', route: 'Identification' }, + { text: 'Support Chat', route: 'SupportChat' }, + { text: 'Push Notifications', route: 'PushNotifications' }, + { text: 'Session Analytics', route: 'SessionAnalytics' }, ]; // Debug Section Buttons const debugButtons: Button[] = [ - ...(Platform.OS === 'android' ? [{ - text: "Simulate ANR", - onPress: () => viewModel.simulateANR() - }] : []), + ...(Platform.OS === 'android' + ? [ + { + text: 'Simulate ANR', + onPress: () => viewModel.simulateANR(), + }, + ] + : []), { - text: "Simulate Crash", - onPress: () => viewModel.simulateCrash() - } + text: 'Simulate Crash', + onPress: () => viewModel.simulateCrash(), + }, ]; const animationButton: Button[] = [ { - text: "Bounce Animation", + text: 'Bounce Animation', onPress: () => { console.log('Bounce Animation Triggered'); - } - } + }, + }, ]; const HomeScreen = ({ navigation }: { navigation: any }) => { @@ -59,12 +63,12 @@ const HomeScreen = ({ navigation }: { navigation: any }) => { toValue: 1, friction: 3, useNativeDriver: true, - }) + }), ]).start(); }; const renderButton = (button: Button, index: number) => { - if (button.text === "Bounce Animation") { + if (button.text === 'Bounce Animation') { return ( { navigation.navigate(button.route) : button.onPress} + onPress={ + 'route' in button + ? () => navigation.navigate(button.route) + : button.onPress + } buttonStyle={commonStyles.button} textStyle={commonStyles.buttonText} /> @@ -104,6 +112,6 @@ const HomeScreen = ({ navigation }: { navigation: any }) => { )} ); -} +}; export default HomeScreen; diff --git a/sample/react-native/src/screens/IdentificationScreen.tsx b/sample/react-native/src/screens/IdentificationScreen.tsx new file mode 100644 index 0000000..5cd9af8 --- /dev/null +++ b/sample/react-native/src/screens/IdentificationScreen.tsx @@ -0,0 +1,121 @@ +import React from 'react'; +import * as DevRev from '@devrev/sdk-react-native'; +import { View, Text, TextInput } from 'react-native'; +import viewModel from '../viewmodel/ViewModel'; +import TouchableOpacityButton from '../components/TouchableOpacityButton'; +import { commonStyles } from '../styles/styles'; + +const IdentificationScreen: React.FC = () => { + const [userID, setUserID] = React.useState(''); + const [sessionToken, setSessionToken] = React.useState(''); + const [email, setEmail] = React.useState(''); + const [currentUserId, setCurrentUserId] = React.useState(null); + + const handleUnverifiedUser = () => { + console.log('Identifying unverified user with:', userID); + DevRev.identifyUnverifiedUser(userID); + setCurrentUserId(userID); + }; + + const handleVerifiedUser = () => { + console.log( + 'Identifying verified user with:', + userID, + 'and session token:', + sessionToken + ); + DevRev.identifyVerifiedUser(userID, sessionToken); + setCurrentUserId(userID); + }; + + const handleUpdateUser = () => { + if (!currentUserId) { + console.error('No user ID found. Please identify a user first.'); + return; + } + + const identity = { + userRef: currentUserId, + userTraits: { + email: email, + }, + }; + + console.log('Updating user with:', identity); + DevRev.updateUser(identity); + }; + + const handleLogout = () => { + viewModel.logout(); + setCurrentUserId(null); + }; + + return ( + + Unverified User + + + + + + + Verified User + + + + + + + + Update User + + + + + + + Logout + + + ); +}; + +export default IdentificationScreen; diff --git a/sample/src/screens/PushNotificationsScreen.tsx b/sample/react-native/src/screens/PushNotificationsScreen.tsx similarity index 59% rename from sample/src/screens/PushNotificationsScreen.tsx rename to sample/react-native/src/screens/PushNotificationsScreen.tsx index cd96e7e..d53ff56 100644 --- a/sample/src/screens/PushNotificationsScreen.tsx +++ b/sample/react-native/src/screens/PushNotificationsScreen.tsx @@ -1,26 +1,38 @@ import React, { useState } from 'react'; import viewModel from '../viewmodel/ViewModel'; -import { View, Text, StyleSheet } from 'react-native'; +import { View } from 'react-native'; import TouchableOpacityButton from '../components/TouchableOpacityButton'; import AlertDialog from '../components/AlertDialog'; import { commonStyles } from '../styles/styles'; const PushNotificationsScreen: React.FC = () => { - const [showDialog, setShowDialog] = useState<{ title: string; message: string } | null>(null); + const [showDialog, setShowDialog] = useState<{ + title: string; + message: string; + } | null>(null); - const handleRegister = () => { - viewModel.registerDeviceToken(); - setShowDialog({ title: 'Success', message: 'Successfully registered for push notifications.' }); + const registerPushNotifications = () => { + viewModel.registerPushNotifications(); + setShowDialog({ + title: 'Success', + message: 'Successfully registered for push notifications!', + }); }; - const handleUnregister = () => { + const unregisterDevice = () => { viewModel.unregisterDevice(); - setShowDialog({ title: 'Success', message: 'Successfully unregistered for push notifications.' }); + setShowDialog({ + title: 'Success', + message: 'Successfully unregistered the device!', + }); }; const buttons = [ - { text: "Register for push notifications", onPress: handleRegister }, - { text: "Unregister for push notifications", onPress: handleUnregister }, + { + text: 'Register for push notifications', + onPress: registerPushNotifications, + }, + { text: 'Unregister the device', onPress: unregisterDevice }, ] as const; return ( diff --git a/sample/react-native/src/screens/SessionAnalyticsScreen.tsx b/sample/react-native/src/screens/SessionAnalyticsScreen.tsx new file mode 100644 index 0000000..dc35835 --- /dev/null +++ b/sample/react-native/src/screens/SessionAnalyticsScreen.tsx @@ -0,0 +1,218 @@ +import React, { useRef, useEffect } from 'react'; +import { + View, + Text, + TextInput, + ScrollView, + Platform, + findNodeHandle, +} from 'react-native'; +import * as DevRev from '@devrev/sdk-react-native'; +import TouchableOpacityButton from '../components/TouchableOpacityButton'; +import { commonStyles } from '../styles/styles'; + +const SessionMonitoringButtons = [ + { text: 'Stop All Monitoring', onPress: DevRev.stopAllMonitoring }, + { text: 'Resume All Monitoring', onPress: DevRev.resumeAllMonitoring }, +] as const; + +const SessionRecordingButtons = [ + { text: 'Start Recording', onPress: DevRev.startRecording }, + { text: 'Stop Recording', onPress: DevRev.stopRecording }, + { text: 'Pause Recording', onPress: DevRev.pauseRecording }, + { text: 'Resume Recording', onPress: DevRev.resumeRecording }, + { + text: 'Pause User Interaction Tracking', + onPress: DevRev.pauseUserInteractionTracking, + }, + { + text: 'Resume User Interaction Tracking', + onPress: DevRev.resumeUserInteractionTracking, + }, +] as const; + +const TimerButtons = [ + { + text: 'Start Timer', + onPress: () => + DevRev.startTimer('test-event', { test_key1: 'test-value1' }), + }, + { + text: 'Stop Timer', + onPress: () => DevRev.endTimer('test-event', { test_key2: 'test-value2' }), + }, +] as const; + +const OnDemandSessionButtons = [ + { + text: 'Process All On-Demand Sessions', + onPress: DevRev.processAllOnDemandSessions, + }, +] as const; + +const WebViewButton = [ + { + text: 'Open Web View', + }, +] as const; + +const ListViewButton = [ + { + text: 'Open Large Scrollable List', + }, +] as const; + +const SessionAnalyticsScreen: React.FC<{ navigation: any }> = ({ + navigation, +}) => { + const sensitiveLabelRef = useRef(null); + const textFieldRef = useRef(null); + + try { + DevRev.addSessionProperties({ + test_user_id: 'test_001', + }); + DevRev.trackScreenName('SessionAnalytics'); + } catch (error) { + console.error('Error in session tracking:', error); + } + + useEffect(() => { + // Mark sensitive view + if (sensitiveLabelRef.current) { + const viewTag = findNodeHandle(sensitiveLabelRef.current); + console.log('Marking sensitive view with tag:', viewTag); + if (viewTag) { + DevRev.markSensitiveViews([viewTag]); + } + } + + // Unmark text field + if (textFieldRef.current) { + const viewTag = findNodeHandle(textFieldRef.current); + console.log('Unmarking text field with tag:', viewTag); + if (viewTag) { + DevRev.unmarkSensitiveViews([viewTag]); + } + } + }, []); // Run once on mount + + const handleDelayedScreenNavigation = () => { + try { + DevRev.setInScreenTransitioning(true); + + setTimeout(() => { + navigation.navigate('DelayedScreen'); + }, 2000); + } catch (error) { + console.error('Error in delayed screen navigation:', error); + DevRev.setInScreenTransitioning(false); + navigation.navigate('DelayedScreen'); + } + }; + + return ( + + + Session Monitoring + {SessionMonitoringButtons.map((button, index) => ( + + ))} + + Session Recording + {SessionRecordingButtons.map((button, index) => ( + + ))} + + Timer + {TimerButtons.map((button, index) => ( + + ))} + + Manual Masking / Unmasking + {}} + buttonText="Manually Masked UI Item" + buttonStyle={commonStyles.button} + textStyle={commonStyles.buttonText} + /> + + + On-Demand Sessions + {OnDemandSessionButtons.map((button, index) => ( + + ))} + + Web View + {WebViewButton.map((button, index) => ( + navigation.navigate('WebViewScreen')} + buttonText={button.text} + buttonStyle={commonStyles.button} + textStyle={commonStyles.buttonText} + /> + ))} + + Large Scrollable List + {ListViewButton.map((button, index) => ( + navigation.navigate('FlatListScreen')} + buttonText={button.text} + buttonStyle={commonStyles.button} + textStyle={commonStyles.buttonText} + /> + ))} + + {Platform.OS === 'android' && ( + <> + + Navigate to the Delayed Screen + + + + )} + + + ); +}; + +export default SessionAnalyticsScreen; diff --git a/sample/src/screens/SupportChatScreen.tsx b/sample/react-native/src/screens/SupportChatScreen.tsx similarity index 100% rename from sample/src/screens/SupportChatScreen.tsx rename to sample/react-native/src/screens/SupportChatScreen.tsx diff --git a/sample/react-native/src/screens/WebViewScreen.tsx b/sample/react-native/src/screens/WebViewScreen.tsx new file mode 100644 index 0000000..d68e2d8 --- /dev/null +++ b/sample/react-native/src/screens/WebViewScreen.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { View, StyleSheet } from 'react-native'; +import { WebView } from 'react-native-webview'; + +const WebViewScreen: React.FC = () => { + return ( + + + + ); +}; + +const styles = StyleSheet.create({ + container: { flex: 1 }, + webview: { flex: 1 }, +}); + +export default WebViewScreen; diff --git a/sample/src/styles/colors.ts b/sample/react-native/src/styles/colors.ts similarity index 92% rename from sample/src/styles/colors.ts rename to sample/react-native/src/styles/colors.ts index 5858ee6..6f86535 100644 --- a/sample/src/styles/colors.ts +++ b/sample/react-native/src/styles/colors.ts @@ -40,4 +40,7 @@ export const systemColors = { }; // Export the appropriate colors based on platform -export const colors = Platform.OS === 'ios' || Platform.OS === 'android' ? systemColors : customColors; +export const colors = + Platform.OS === 'ios' || Platform.OS === 'android' + ? systemColors + : customColors; diff --git a/sample/src/styles/styles.ts b/sample/react-native/src/styles/styles.ts similarity index 97% rename from sample/src/styles/styles.ts rename to sample/react-native/src/styles/styles.ts index 29e357d..1d90de8 100644 --- a/sample/src/styles/styles.ts +++ b/sample/react-native/src/styles/styles.ts @@ -92,8 +92,8 @@ export const commonStyles = StyleSheet.create({ }, refreshIcon: { fontSize: 24, - fontWeight: "bold", - transform: [{ rotate: '90deg' }] + fontWeight: 'bold', + transform: [{ rotate: '90deg' }], }, }); @@ -102,7 +102,7 @@ export const roundCheckboxStyles = StyleSheet.create({ flexDirection: 'row', alignItems: 'center', marginVertical: 8, - paddingRight: 16 + paddingRight: 16, }, label: { flex: 1, diff --git a/sample/react-native/src/viewmodel/ViewModel.tsx b/sample/react-native/src/viewmodel/ViewModel.tsx new file mode 100644 index 0000000..a9fe3be --- /dev/null +++ b/sample/react-native/src/viewmodel/ViewModel.tsx @@ -0,0 +1,66 @@ +import * as DevRev from '@devrev/sdk-react-native'; +import PushNotificationsService from '../PushNotificationsService'; +import { Platform } from 'react-native'; +import DeviceInfo from 'react-native-device-info'; + +function ViewModel() { + const registerPushNotifications = async () => { + try { + await PushNotificationsService.register(); + } catch (error) { + console.error('Error configuring the push notifications:', error); + } + }; + + const unregisterDevice = async () => { + DeviceInfo.getUniqueId() + .then((id) => DevRev.unregisterDevice(id)) + .catch((error) => { + console.error('Error unregistering the device:', error); + }); + }; + + const logout = async () => { + DeviceInfo.getUniqueId() + .then((id) => DevRev.logout(id)) + .catch((error) => { + console.error('Error logging out:', error); + }); + }; + + const simulateCrash = () => { + // Force a crash by accessing an undefined property. + console.log('Simulating a crash by accessing an undefined property.'); + const obj: any = undefined; + obj.nonExistentMethod(); + }; + + const simulateANR = () => { + if (Platform.OS === 'android') { + console.log( + 'Simulating an ANR on Android by blocking the main thread for 5 seconds.' + ); + // Simulate an ANR by blocking the main thread. + const startTime = Date.now(); + while (Date.now() - startTime < 5000) { + // Block the main thread for five seconds. + Math.random(); + } + + return; + } + + console.log('Simulating ANRs on iOS is not supported'); + }; + + return { + registerPushNotifications, + unregisterDevice, + logout, + simulateCrash, + simulateANR, + }; +} + +const viewModel = ViewModel(); +export default viewModel; diff --git a/sample/react-native/tsconfig.json b/sample/react-native/tsconfig.json new file mode 100644 index 0000000..535576e --- /dev/null +++ b/sample/react-native/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@devrev/sdk-react-native": [ + "../../lib/typescript/src" + ], + "@devrev/sdk-react-native/*": [ + "../../lib/typescript/src/*" + ] + }, + "jsx": "react", + }, + "include": [ + "./src", + "./index.ts", + "./index.tsx" + ] +} diff --git a/sample/src/NotificationService.tsx b/sample/src/NotificationService.tsx deleted file mode 100644 index 84d5e29..0000000 --- a/sample/src/NotificationService.tsx +++ /dev/null @@ -1,257 +0,0 @@ -import notifee, { EventType, AndroidImportance } from '@notifee/react-native'; -import { getMessaging, FirebaseMessagingTypes } from '@react-native-firebase/messaging'; -import { getApp } from '@react-native-firebase/app'; -import { Platform, PermissionsAndroid } from 'react-native'; -import * as DevRev from '@devrev/sdk-react-native'; -import DeviceInfo from 'react-native-device-info'; -import type { DevRevNotification, StaleNotificationData, SilentNotificationData } from './model/DevRevNotification'; - -const messaging = getMessaging(getApp()); -const isAndroid = Platform.OS === 'android'; - -const handleSilentNotification = async (data: SilentNotificationData) => { - try { - if (data.silent) { - const silentData: StaleNotificationData = JSON.parse(data.silent); - if (silentData.stale_notification_ids?.length) { - console.log('Handling stale notifications:', silentData.stale_notification_ids); - // Cancel stale notifications - for (const notificationId of silentData.stale_notification_ids) { - await notifee.cancelNotification(notificationId); - } - } - } - } catch (error) { - console.error('Error handling silent notification:', error); - } -}; - -// Create default channel for Android -const createDefaultChannel = async (): Promise => { - if (Platform.OS === 'android') { - await notifee.createChannel({ - id: 'default', - name: 'Default Channel', - importance: AndroidImportance.HIGH, - }); - } -}; - -// Request user permission for notifications -const requestUserPermission = async (): Promise => { - if (Platform.OS === 'ios') { - const settings = await notifee.requestPermission(); - return settings.authorizationStatus >= 0; - } else { - const granted = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS); - return granted === PermissionsAndroid.RESULTS.GRANTED; - } -}; - -const getDeviceToken = async (): Promise => { - try { - console.log('getDeviceToken: Starting token retrieval...'); - - if (isAndroid) { - console.log('getDeviceToken: Getting Android token...'); - const token = await messaging.getToken(); - console.log('getDeviceToken: Android token received:', token); - return token; - } else { - console.log('getDeviceToken: Getting iOS token...'); - const token = await messaging.getAPNSToken(); - console.log('getDeviceToken: iOS token received:', token); - return token; - } - } catch (error) { - console.error('getDeviceToken: Error in getDeviceToken:', error); - throw error; - } -}; - -// Register device with DevRev -const registerDevice = async (): Promise => { - try { - console.log('Starting device registration...'); - const hasPermission = await requestUserPermission(); - console.log('Permission status:', hasPermission); - - if (hasPermission) { - console.log('Getting device ID...'); - const deviceId = await DeviceInfo.getUniqueId(); - console.log('Device ID:', deviceId); - console.log('Getting device token...'); - const token = await getDeviceToken(); - console.log('Token received:', token); - - if (token) { - console.log('Registering with DevRev...'); - await DevRev.registerDeviceToken(token, deviceId); - console.log('Successfully registered with DevRev'); - } else { - console.warn('Failed to get device token'); - } - } else { - console.warn('Notification permission not granted'); - } - } catch (error) { - console.error('Error registering device:', error); - throw error; - } -}; - -// Display notification -const displayNotification = async ( - remoteMessage: FirebaseMessagingTypes.RemoteMessage -): Promise => { - try { - if (remoteMessage.data?.silent) { - await handleSilentNotification(remoteMessage.data); - return; - } - - let notification: DevRevNotification | null = null; - - if (remoteMessage.data?.message) { - try { - notification = JSON.parse(remoteMessage.data.message as string); - } catch (parseError) { - console.error('Error parsing notification:', parseError); - } - } - - if (!notification) { - console.warn('No valid notification data'); - return; - } - - await notifee.displayNotification({ - title: notification.title, - body: notification.body, - android: { - channelId: 'default', - pressAction: { - id: 'default', - }, - smallIcon: 'ic_launcher_round', - importance: AndroidImportance.HIGH, - }, - ios: { - foregroundPresentationOptions: { - badge: true, - sound: true, - banner: true, - list: true, - }, - }, - data: remoteMessage.data, - }); - } catch (error) { - console.error('Error displaying notification:', error); - } -}; - -// Handle notification press -const handleNotificationPress = async (notification: any): Promise => { - console.log('Notification pressed:', notification); - if (notification.data?.message) { - DevRev.processPushNotification(notification.data.message as string) - } -}; - -// Setup notification listeners -const setupNotificationListeners = (): void => { - // Handle token refresh - messaging.onTokenRefresh(async (token) => { - try { - const deviceId = await DeviceInfo.getUniqueId(); - DevRev.registerDeviceToken(token, deviceId); - } catch (error) { - console.error('Error handling token refresh:', error); - } - }); - - // Handle background messages - messaging.setBackgroundMessageHandler(async (remoteMessage) => { - console.log("BACKGROUND MESSAGE: ", remoteMessage); - if (Platform.OS === "android") { - await displayNotification(remoteMessage); - } - }); - - // Handle foreground messages - messaging.onMessage(async (remoteMessage) => { - console.log("FOREGROUND MESSAGE: ", remoteMessage); - await displayNotification(remoteMessage); - }); - - // Handle foreground notification press events - notifee.onForegroundEvent(async ({ type, detail }) => { - try { - switch (type) { - case EventType.PRESS: - await handleNotificationPress(detail.notification); - break; - case EventType.DISMISSED: - console.log('User dismissed notification', detail.notification); - break; - } - } catch (error) { - console.error('Error handling foreground event:', error); - } - }); - - // Handle background/quit state notification press - notifee.onBackgroundEvent(async ({ type, detail }) => { - try { - if (type === EventType.PRESS) { - await handleNotificationPress(detail.notification); - } - } catch (error) { - console.error('Error handling background event:', error); - } - }); - - // Check if app was opened from a notification - messaging - .getInitialNotification() - .then(async (remoteMessage) => { - if (remoteMessage) { - console.log('App opened from quit state:', remoteMessage); - await handleNotificationPress(remoteMessage); - } - }) - .catch((error) => { - console.error('Error checking initial notification:', error); - }); -}; - -// Cancel all notifications -const cancelAllNotifications = async (): Promise => { - try { - await notifee.cancelAllNotifications(); - } catch (error) { - console.error('Error canceling notifications:', error); - } -}; - -// Initialize notification service -const initializeNotifications = async (): Promise => { - try { - if (isAndroid) { - await createDefaultChannel(); - } - setupNotificationListeners(); - } catch (error) { - console.error('Error initializing notifications:', error); - } -}; - -export const NotificationService = { - initialize: initializeNotifications, - register: registerDevice, - displayNotification, - cancelAllNotifications, -}; - -export default NotificationService; diff --git a/sample/src/components/TouchableOpacityButton.tsx b/sample/src/components/TouchableOpacityButton.tsx deleted file mode 100644 index e32f69b..0000000 --- a/sample/src/components/TouchableOpacityButton.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import React, { forwardRef } from 'react'; -import { TouchableOpacity, Text, type TextStyle, type ViewStyle, View } from 'react-native'; -import { commonStyles } from '../styles/styles'; - -interface TouchableOpacityProps { - onPress: () => void; - buttonText: string; - buttonStyle?: ViewStyle; - textStyle?: TextStyle; -} - -const TouchableOpacityButton = forwardRef(({ - onPress, - buttonText, - buttonStyle, - textStyle, -}, ref) => { - return ( - - - {buttonText} - - - ); -}); - -export default TouchableOpacityButton; diff --git a/sample/src/screens/DelayedScreen.tsx b/sample/src/screens/DelayedScreen.tsx deleted file mode 100644 index a3b7716..0000000 --- a/sample/src/screens/DelayedScreen.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React, { useEffect } from 'react'; -import { View, Text, Platform } from 'react-native'; -import * as DevRev from '@devrev/sdk-react-native'; -import { commonStyles } from '../styles/styles'; - -const DelayedScreen: React.FC = () => { - useEffect(() => { - // End the screen transition when the screen is loaded (Android only) - if (Platform.OS === 'android') { - try { - DevRev.endScreenTransition(); - } catch (error) { - console.error('Error ending screen transition:', error); - } - } - }, []); - - return ( - - Delayed Screen - - ); -}; - -export default DelayedScreen; diff --git a/sample/src/screens/IdentificationScreen.tsx b/sample/src/screens/IdentificationScreen.tsx deleted file mode 100644 index 09ce3df..0000000 --- a/sample/src/screens/IdentificationScreen.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import React from 'react'; -import * as DevRev from '@devrev/sdk-react-native'; -import { View, Text, TextInput, type ViewStyle, type TextStyle } from 'react-native'; -import viewModel from '../viewmodel/ViewModel'; -import TouchableOpacityButton from '../components/TouchableOpacityButton'; -import { commonStyles } from '../styles/styles'; - -const IdentificationScreen: React.FC = () => { - const [userID, setUserID] = React.useState(''); - const [sessionToken, setSessionToken] = React.useState(''); - const [email, setEmail] = React.useState(''); - const [currentUserId, setCurrentUserId] = React.useState(null); - - const handleUnverifiedUser = () => { - console.log('Identifying unverified user with:', userID); - DevRev.identifyUnverifiedUser(userID); - setCurrentUserId(userID); - }; - - const handleVerifiedUser = () => { - console.log('Identifying verified user with:', userID, 'and session token:', sessionToken); - DevRev.identifyVerifiedUser(userID, sessionToken); - setCurrentUserId(userID); - }; - - const handleUpdateUser = () => { - if (!currentUserId) { - console.error('No user ID found. Please identify a user first.'); - return; - } - - const identity = { - userRef: currentUserId, - userTraits: { - email: email - } - }; - - console.log('Updating user with:', identity); - DevRev.updateUser(identity); - }; - - const handleLogout = () => { - viewModel.logout(); - setCurrentUserId(null); - }; - - return ( - - Unverified User - - - - - - - Verified User - - - - - - - - Update User - - - - - - - Logout - - - ); -}; - -export default IdentificationScreen; diff --git a/sample/src/screens/SessionAnalyticsScreen.tsx b/sample/src/screens/SessionAnalyticsScreen.tsx deleted file mode 100644 index e2ea9e0..0000000 --- a/sample/src/screens/SessionAnalyticsScreen.tsx +++ /dev/null @@ -1,159 +0,0 @@ -import React, { useRef, useEffect } from 'react'; -import { View, Text, TextInput, ScrollView, Platform, findNodeHandle } from 'react-native'; -import * as DevRev from '@devrev/sdk-react-native'; -import TouchableOpacityButton from '../components/TouchableOpacityButton'; -import { commonStyles } from '../styles/styles'; - -const SessionMonitoringButtons = [ - { text: "Stop All Monitoring", onPress: DevRev.stopAllMonitoring }, - { text: "Resume All Monitoring", onPress: DevRev.resumeAllMonitoring }, -] as const; - -const SessionRecordingButtons = [ - { text: "Start Recording", onPress: DevRev.startRecording }, - { text: "Stop Recording", onPress: DevRev.stopRecording }, - { text: "Pause Recording", onPress: DevRev.pauseRecording }, - { text: "Resume Recording", onPress: DevRev.resumeRecording }, -] as const; - -const TimerButtons = [ - { - text: "Start Timer", - onPress: () => DevRev.startTimer("test-event", { test_key1: "test-value1" }) - }, - { - text: "Stop Timer", - onPress: () => DevRev.endTimer("test-event", { test_key2: "test-value2" }) - }, -] as const; - -const OnDemandSessionButtons = [ - { text: "Process All On-Demand Sessions", onPress: DevRev.processAllOnDemandSessions }, -] as const; - -const SessionAnalyticsScreen: React.FC<{ navigation: any }> = ({ navigation }) => { - const sensitiveLabelRef = useRef(null); - const textFieldRef = useRef(null); - - try { - DevRev.addSessionProperties({ - test_user_id: "test_001" - }); - DevRev.trackScreen("SessionAnalytics"); - } catch (error) { - console.error('Error in session tracking:', error); - } - - useEffect(() => { - // Mark sensitive view - if (sensitiveLabelRef.current) { - const viewTag = findNodeHandle(sensitiveLabelRef.current); - console.log('Marking sensitive view with tag:', viewTag); - if (viewTag) { - DevRev.markSensitiveViews([viewTag]); - } - } - - // Unmark text field - if (textFieldRef.current) { - const viewTag = findNodeHandle(textFieldRef.current); - console.log('Unmarking text field with tag:', viewTag); - if (viewTag) { - DevRev.unmarkSensitiveViews([viewTag]); - } - } - }, []); // Run once on mount - - const handleDelayedScreenNavigation = () => { - try { - if (Platform.OS === 'android') { - DevRev.startScreenTransition(); - } - - setTimeout(() => { - navigation.navigate('DelayedScreen'); - }, 2000); - } catch (error) { - console.error('Error in delayed screen navigation:', error); - navigation.navigate('DelayedScreen'); - } - }; - - return ( - - - Session Monitoring - {SessionMonitoringButtons.map((button, index) => ( - - ))} - - Session Recording - {SessionRecordingButtons.map((button, index) => ( - - ))} - - Timer - {TimerButtons.map((button, index) => ( - - ))} - - Manual Masking / Unmasking - {}} - buttonText="Manually Masked UI Item" - buttonStyle={commonStyles.button} - textStyle={commonStyles.buttonText} - /> - - - On-Demand Sessions - {OnDemandSessionButtons.map((button, index) => ( - - ))} - - {Platform.OS === 'android' && ( - <> - Navigate to the Delayed Screen - - - )} - - - ); -}; - -export default SessionAnalyticsScreen; diff --git a/sample/src/usePushNotifications.tsx b/sample/src/usePushNotifications.tsx deleted file mode 100644 index 6166538..0000000 --- a/sample/src/usePushNotifications.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import messaging from '@react-native-firebase/messaging'; -import {PermissionsAndroid, Platform} from 'react-native'; - -const usePushNotification = () => { - const requestUserPermission = async () => { - if (Platform.OS === 'ios') { - //Request iOS permission - const authStatus = await messaging().requestPermission(); - const enabled = - authStatus === messaging.AuthorizationStatus.AUTHORIZED || - authStatus === messaging.AuthorizationStatus.PROVISIONAL; - - if (enabled) { - console.log('Authorization status:', authStatus); - } - } else if (Platform.OS === 'android') { - //Request Android permission (For API level 33+, for 32 or below is not required) - if (PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS) { - try { - await PermissionsAndroid.request( - PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS - ); - } catch (error) { - console.error('Error requesting notification permission:', error); - } - } else { - console.log('POST_NOTIFICATIONS permission is not available on this Android version'); - } - } - } - - const listenToForegroundNotifications = async () => { - const unsubscribe = messaging().onMessage(async remoteMessage => { - console.log( - 'A new message arrived! (FOREGROUND)', - JSON.stringify(remoteMessage), - ); - }); - return unsubscribe; - } - - const listenToBackgroundNotifications = async () => { - const unsubscribe = messaging().setBackgroundMessageHandler( - async remoteMessage => { - console.log( - 'A new message arrived! (BACKGROUND)', - JSON.stringify(remoteMessage), - ); - }, - ); - return unsubscribe; - } - - const onNotificationOpenedAppFromBackground = async () => { - const unsubscribe = messaging().onNotificationOpenedApp( - async remoteMessage => { - console.log( - 'App opened from BACKGROUND by tapping notification:', - JSON.stringify(remoteMessage), - ); - }, - ); - return unsubscribe; - }; - - const onNotificationOpenedAppFromQuit = async () => { - const message = await messaging().getInitialNotification(); - - if(message) { - console.log('App opened from QUIT by tapping notification:', JSON.stringify(message)); - } - }; - - return { - requestUserPermission, - listenToForegroundNotifications, - listenToBackgroundNotifications, - onNotificationOpenedAppFromBackground, - onNotificationOpenedAppFromQuit, - }; -}; - -export default usePushNotification; diff --git a/sample/src/viewmodel/ViewModel.tsx b/sample/src/viewmodel/ViewModel.tsx deleted file mode 100644 index a4896cf..0000000 --- a/sample/src/viewmodel/ViewModel.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import * as DevRev from '@devrev/sdk-react-native'; -import installations from '@react-native-firebase/installations'; -import NotificationService from '../NotificationService'; -import { Platform } from 'react-native'; - -function ViewModel() { - const registerDeviceToken = async () => { - try { - await NotificationService.register() - } catch (error) { - console.error('Error registering device:', error); - } - }; - - const unregisterDevice = async () => { - try { - const id = await installations().getId(); - DevRev.unregisterDevice(id); - console.log('Device unregistered with ID:', id); - } catch (error) { - console.error('Error registering device:', error); - } - } - - const logout = async () => { - try { - const id = await installations().getId(); - DevRev.logout(id); - console.log('Logged out'); - } catch (error) { - console.error('Error logging out:', error); - } - } - - const simulateCrash = () => { - // Force a crash by accessing undefined property - const obj: any = undefined; - obj.nonExistentMethod(); - }; - - const simulateANR = () => { - if (Platform.OS === 'android') { - // Simulate ANR by blocking the main thread - const startTime = Date.now(); - while (Date.now() - startTime < 5000) { - // Block the main thread for 5 seconds - Math.random(); - } - } - }; - - return { - registerDeviceToken, - unregisterDevice, - logout, - simulateCrash, - simulateANR - } -} - -const viewModel = ViewModel(); -export default viewModel;