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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
module.exports = {
root: true,
ignorePatterns: ['coverage/**/*'],
extends: [
'@react-native',
'plugin:react/recommended',
Expand Down
29 changes: 18 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ Iterable. It supports JavaScript and TypeScript.

- [Iterable's React Native SDK](#iterables-react-native-sdk)
- [Requirements](#requirements)
- [React Native](#react-native)
- [UI Components require additional peer dependencies](#ui-components-require-additional-peer-dependencies)
- [Optional peer dependencies for enhanced UI](#optional-peer-dependencies-for-enhanced-ui)
- [iOS](#ios)
- [Android](#android)
- [Architecture Support](#architecture-support)
- [Installation](#installation)
- [Features](#features)
Expand All @@ -34,22 +39,24 @@ Iterable. It supports JavaScript and TypeScript.

Iterable's React Native SDK relies on:

- **React Native**
- [React Native 0.75+](https://github.com/facebook/react-native)
- [React 18.1+](https://github.com/facebook/react)
### React Native
- [React Native 0.75+](https://github.com/facebook/react-native)
- [React 18.1+](https://github.com/facebook/react)

_UI Components require additional peer dependencies_
- [React Navigation 6+](https://github.com/react-navigation/react-navigation)
- [React Native Safe Area Context 4+](https://github.com/th3rdwave/react-native-safe-area-context)
- [React Native Vector Icons 10+](https://github.com/oblador/react-native-vector-icons)
- [React Native WebView 13+](https://github.com/react-native-webview/react-native-webview)
#### UI Components require additional peer dependencies
- [React Navigation 6+](https://github.com/react-navigation/react-navigation)

- **iOS**
#### Optional peer dependencies for enhanced UI
- [React Native WebView 13+](https://github.com/react-native-webview/react-native-webview) - Required only for inbox message display functionality. If not installed, the SDK will show a fallback message.
- [React Native Safe Area Context 4+](https://github.com/th3rdwave/react-native-safe-area-context) - Provides proper safe area handling for the inbox component. If not installed, the SDK will use fallback View components.
- [React Native Vector Icons 10+](https://github.com/oblador/react-native-vector-icons) - Provides enhanced icons for the inbox component. If not installed, the SDK will use fallback Unicode symbols.

### iOS
- Xcode 12+
- [Deployment target 13.4+](https://help.apple.com/xcode/mac/current/#/deve69552ee5)
- [Iterable's iOS SDK](https://github.com/Iterable/iterable-swift-sdk)

- **Android**
- Swift 5
### Android
- [`minSdkVersion` 21+, `compileSdkVersion` 31+](https://medium.com/androiddevelopers/picking-your-compilesdkversion-minsdkversion-targetsdkversion-a098a0341ebd)
- [Iterable's Android SDK](https://github.com/Iterable/iterable-android-sdk)

Expand Down
18 changes: 12 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,9 @@
"react-native": "0.79.3",
"react-native-builder-bob": "^0.40.4",
"react-native-gesture-handler": "^2.26.0",
"react-native-safe-area-context": "^5.4.0",
"react-native-safe-area-context": "^5.6.1",
"react-native-screens": "^4.10.0",
"react-native-vector-icons": "^10.2.0",
"react-native-vector-icons": "^10.3.0",
"react-native-webview": "^13.14.1",
"react-test-renderer": "19.0.0",
"release-it": "^17.10.0",
Expand All @@ -111,14 +111,20 @@
"peerDependencies": {
"@react-navigation/native": "*",
"react": "*",
"react-native": "*",
"react-native-safe-area-context": "*",
"react-native-vector-icons": "*",
"react-native-webview": "*"
"react-native": "*"
},
"peerDependenciesMeta": {
"expo": {
"optional": true
},
"react-native-safe-area-context": {
"optional": true
},
"react-native-vector-icons": {
"optional": true
},
"react-native-webview": {
"optional": true
}
},
"sideEffects": false,
Expand Down
1 change: 1 addition & 0 deletions src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './classes';
export * from './enums';
export * from './hooks';
export * from './types';
export * from './utils/SafeAreaContext';
127 changes: 127 additions & 0 deletions src/core/utils/SafeAreaContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/* eslint-disable @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports */
import React from 'react';
import { View, type ViewStyle } from 'react-native';

/**
* Error thrown when react-native-safe-area-context is required but not available
*/
export class SafeAreaContextNotAvailableError extends Error {
constructor(componentName: string) {
super(
`react-native-safe-area-context is required for ${componentName} but is not installed. ` +
'Please install it by running: npm install react-native-safe-area-context ' +
'or yarn add react-native-safe-area-context'
);
this.name = 'SafeAreaContextNotAvailableError';
}
}

/**
* Conditionally imports and returns SafeAreaView from react-native-safe-area-context
* @throws \{SafeAreaContextNotAvailableError\} When the library is not available
*/
export const getSafeAreaView = () => {
try {
const { SafeAreaView } = require('react-native-safe-area-context');
return SafeAreaView;
} catch {
throw new SafeAreaContextNotAvailableError('SafeAreaView');
}
};

/**
* Conditionally imports and returns SafeAreaProvider from react-native-safe-area-context
* @throws \{SafeAreaContextNotAvailableError\} When the library is not available
*/
export const getSafeAreaProvider = () => {
try {
const { SafeAreaProvider } = require('react-native-safe-area-context');
return SafeAreaProvider;
} catch {
throw new SafeAreaContextNotAvailableError('SafeAreaProvider');
}
};

/**
* Conditionally imports and returns useSafeAreaInsets from react-native-safe-area-context
* @throws \{SafeAreaContextNotAvailableError\} When the library is not available
*/
export const getUseSafeAreaInsets = () => {
try {
const { useSafeAreaInsets } = require('react-native-safe-area-context');
return useSafeAreaInsets;
} catch {
throw new SafeAreaContextNotAvailableError('useSafeAreaInsets');
}
};

/**
* Conditionally imports and returns useSafeAreaFrame from react-native-safe-area-context
* @throws \{SafeAreaContextNotAvailableError\} When the library is not available
*/
export const getUseSafeAreaFrame = () => {
try {
const { useSafeAreaFrame } = require('react-native-safe-area-context');
return useSafeAreaFrame;
} catch {
throw new SafeAreaContextNotAvailableError('useSafeAreaFrame');
}
};

/**
* A conditional SafeAreaView component that only loads react-native-safe-area-context when needed
*/
export interface ConditionalSafeAreaViewProps {
style?: ViewStyle;
children: React.ReactNode;
edges?: string[];
mode?: 'padding' | 'margin';
}

export const ConditionalSafeAreaView: React.FC<
ConditionalSafeAreaViewProps
> = ({ style, children, edges, mode }) => {
try {
const SafeAreaView = getSafeAreaView();
return (
<SafeAreaView style={style} edges={edges} mode={mode}>
{children}
</SafeAreaView>
);
} catch {
// Fallback to regular View if SafeAreaView is not available
console.warn(
'SafeAreaView is not available. Falling back to regular View. ' +
'Install react-native-safe-area-context for proper safe area handling.'
);
return <View style={style}>{children}</View>;
}
};

/**
* A conditional SafeAreaProvider component that only loads react-native-safe-area-context when needed
*/
export interface ConditionalSafeAreaProviderProps {
children: React.ReactNode;
initialMetrics?: unknown;
}

export const ConditionalSafeAreaProvider: React.FC<
ConditionalSafeAreaProviderProps
> = ({ children, initialMetrics }) => {
try {
const SafeAreaProvider = getSafeAreaProvider();
return (
<SafeAreaProvider initialMetrics={initialMetrics}>
{children}
</SafeAreaProvider>
);
} catch {
// Fallback to Fragment if SafeAreaProvider is not available
console.warn(
'SafeAreaProvider is not available. Falling back to Fragment. ' +
'Install react-native-safe-area-context for proper safe area handling.'
);
return <>{children}</>;
}
};
5 changes: 2 additions & 3 deletions src/inbox/components/IterableInbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,8 @@ import {
Text,
View,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';

import { useAppStateListener, useDeviceOrientation } from '../../core';
import { ConditionalSafeAreaView } from '../../core/utils/SafeAreaContext';
// expo throws an error if this is not imported directly due to circular
// dependencies
// See: https://github.com/expo/expo/issues/35100
Expand Down Expand Up @@ -500,7 +499,7 @@ export const IterableInbox = ({
);

return safeAreaMode ? (
<SafeAreaView style={styles.container}>{inboxAnimatedView}</SafeAreaView>
<ConditionalSafeAreaView style={styles.container}>{inboxAnimatedView}</ConditionalSafeAreaView>
) : (
<View style={styles.container}>{inboxAnimatedView}</View>
);
Expand Down
41 changes: 41 additions & 0 deletions src/inbox/components/IterableInboxIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Text, StyleSheet, type TextStyle } from 'react-native';

/**
* Props for the IterableInboxIcon component.
*/
export interface IterableInboxIconProps {
/**
* The name of the icon to display.
*/
name: string;
/**
* The style to apply to the icon.
*/
style?: TextStyle;
}

/**
* A fallback icon component that uses Unicode symbols instead of vector icons.
* This allows the inbox to work without requiring react-native-vector-icons.
*/
export const IterableInboxIcon = ({ name, style }: IterableInboxIconProps) => {
// Map of common icon names to Unicode symbols
const iconMap: Record<string, string> = {
'chevron-back-outline': '‹',
'chevron-back': '‹',
'arrow-back': '←',
'back': '←',
};

const iconSymbol = iconMap[name] || '?';

return <Text style={[styles.icon, style]}>{iconSymbol}</Text>;
};

const styles = StyleSheet.create({
icon: {
fontSize: 24,
fontWeight: 'bold',
textAlign: 'center',
},
});
20 changes: 20 additions & 0 deletions src/inbox/components/IterableInboxIconUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/* eslint-disable @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires */
import { type TextStyle } from 'react-native';

// Type for the vector icon component
type VectorIconComponent = React.ComponentType<{
name: string;
style?: TextStyle;
}>;

/**
* Attempts to load the react-native-vector-icons module.
* Returns null if the module is not available.
*/
export function tryLoadVectorIcons(): VectorIconComponent | null {
try {
return require('react-native-vector-icons/Ionicons').default;
} catch {
return null;
}
}
39 changes: 32 additions & 7 deletions src/inbox/components/IterableInboxMessageDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@ import {
TouchableWithoutFeedback,
View,
} from 'react-native';
import Icon from 'react-native-vector-icons/Ionicons';
import { WebView, type WebViewMessageEvent } from 'react-native-webview';
import { IterableInboxSmartIcon } from './IterableInboxSmartIcon';
import {
loadWebView,
FallbackWebView,
WebViewNotAvailableError,
} from '../../utils/WebViewLoader';

import {
IterableAction,
Expand Down Expand Up @@ -78,6 +82,13 @@ export const IterableInboxMessageDisplay = ({
const messageTitle = rowViewModel.inAppMessage.inboxMetadata?.title;
const [inAppContent, setInAppContent] =
useState<IterableHtmlInAppContent | null>(null);
const [WebViewComponent, setWebViewComponent] = useState<React.ComponentType<{
originWhiteList?: string[];
source?: { html: string };
style?: object;
onMessage?: (event: { nativeEvent: { data: string } }) => void;
injectedJavaScript?: string;
}> | null>(null);

const styles = StyleSheet.create({
contentContainer: {
Expand Down Expand Up @@ -172,7 +183,21 @@ export const IterableInboxMessageDisplay = ({
};
});

function handleInAppLinkAction(event: WebViewMessageEvent) {
// Load WebView component dynamically
useEffect(() => {
try {
const WebView = loadWebView();
setWebViewComponent(() => WebView);
} catch (error) {
if (error instanceof WebViewNotAvailableError) {
setWebViewComponent(() => FallbackWebView);
} else {
setWebViewComponent(() => FallbackWebView);
}
}
}, []);

function handleInAppLinkAction(event: { nativeEvent: { data: string } }) {
const URL = event.nativeEvent.data;

const action = new IterableAction('openUrl', URL, '');
Expand Down Expand Up @@ -233,7 +258,7 @@ export const IterableInboxMessageDisplay = ({
}}
>
<View style={styles.returnButton}>
<Icon
<IterableInboxSmartIcon
name="chevron-back-outline"
style={styles.returnButtonIcon}
/>
Expand All @@ -253,13 +278,13 @@ export const IterableInboxMessageDisplay = ({
</View>
</View>
</View>
{inAppContent && (
{inAppContent && WebViewComponent && (
<ScrollView contentContainerStyle={styles.contentContainer}>
<WebView
<WebViewComponent
originWhiteList={['*']}
source={{ html: inAppContent.html }}
style={{ width: contentWidth }}
onMessage={(event) => handleInAppLinkAction(event)}
onMessage={handleInAppLinkAction}
injectedJavaScript={JS}
/>
</ScrollView>
Expand Down
Loading