Skip to content
Open
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
16 changes: 16 additions & 0 deletions example/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,22 @@ If everything is set up _correctly_, you should see your new app running in your
This is one way to run your app β€” you can also run it directly from within Android Studio and Xcode respectively.


## Testing deep links (Android)

The example's `MainActivity` handles `app://` URLs (`AndroidManifest.xml`), and `urlHandler` in `src/hooks/useIterableApp.tsx` routes `app://commerce`, `app://inbox` and `app://user` to the matching tab. Sign in first: while signed out, `urlHandler` returns `false` and nothing is navigated.

Check the intent filter from a terminal (app running or killed):

```bash
adb shell am start -a android.intent.action.VIEW -c android.intent.category.BROWSABLE -d "app://inbox" iterable.reactnativesdk.example
```

To test an Open URL push end to end, send a push campaign with an **Open URL** action of `app://inbox`, then tap it:

- **App killed** (`adb shell am force-stop iterable.reactnativesdk.example`): the app launches and `urlHandler` receives `app://inbox` after `androidWakeDelayMs` (default 1000 ms). The example registers `urlHandler` only after you tap **Sign in**, so a cold-start push reaches `urlHandler` once you have signed in.
- **App in the foreground**: tap once; `urlHandler` receives the URL and the Inbox tab shows.
- **Wrapped link**: a `https://links.iterable.com/...` link that wraps an `app://` URL is expected to reach `urlHandler` as the unwrapped `app://` URL (the native Iterable SDK does the unwrap). If you see the wrapped `https://` URL instead, report it.

## Congratulations! :tada:

You've successfully run the Iterable Example App. :partying_face:
Expand Down
11 changes: 11 additions & 0 deletions example/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="app" />
</intent>
</queries>

<application
Expand All @@ -30,6 +35,12 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="app" />
</intent-filter>
</activity>
</application>
</manifest>
7 changes: 3 additions & 4 deletions example/src/components/Login/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,11 @@ import {
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';

import { colors, type Route } from '../../constants';
import { colors } from '../../constants';
import { useIterableApp } from '../../hooks';
import type { RootStackScreenProps } from '../../types';
import { styles } from './Login.styles';

export const Login = ({ navigation }: RootStackScreenProps<Route.Login>) => {
export const Login = () => {
const { apiKey, setApiKey, userId, setUserId, initialize, loginInProgress } =
useIterableApp();
const loginIsEnabled = useMemo(() => apiKey && userId, [apiKey, userId]);
Expand Down Expand Up @@ -54,7 +53,7 @@ export const Login = ({ navigation }: RootStackScreenProps<Route.Login>) => {
<Pressable
style={loginIsEnabled ? styles.button : styles.buttonDisabled}
disabled={!loginIsEnabled}
onPressOut={() => initialize(navigation)}
onPressOut={() => initialize()}
>
<Text
style={
Expand Down
25 changes: 13 additions & 12 deletions example/src/hooks/useIterableApp.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { StackNavigationProp } from '@react-navigation/stack';
import {
createContext,
useCallback,
useContext,
useRef,
useState,
type FunctionComponent,
} from 'react';
Expand All @@ -19,22 +19,19 @@ import {
} from '@iterable/react-native-sdk';

import { Route } from '../constants/routes';
import type { RootStackParamList } from '../types/navigation';
import { navigationRef } from '../navigationRef';
import NativeJwtTokenModule from '../NativeJwtTokenModule';

type Navigation = StackNavigationProp<RootStackParamList>;

interface IterableAppProps {
/** The API key for your Iterable instance */
apiKey?: string;
/** The configuration of the RN SDK */
config: IterableConfig | null;
/**
* Call to initialize the Iterable SDK
* @param navigation - The navigation prop from the screen
* @returns Promise<boolean> Whether the initialization was successful
*/
initialize: (navigation: Navigation) => void;
initialize: () => void;
/** Whether the SDK has been initialized */
isInitialized?: boolean;
/** Is the tab in focus the `Inbox` tab? */
Expand Down Expand Up @@ -99,6 +96,8 @@ export const IterableAppProvider: FunctionComponent<
const [isInboxTab, setIsInboxTab] = useState<boolean>(false);
const [itblConfig, setItblConfig] = useState<IterableConfig | null>(null);
const [isLoggedIn, setIsLoggedIn] = useState<boolean>(false);
// Read by `urlHandler`, which is created once and would see stale state
const isLoggedInRef = useRef<boolean>(false);
const [isInitialized, setIsInitialized] = useState<boolean>(false);
const [apiKey, setApiKey] = useState<string | undefined>(
process.env.ITBL_API_KEY
Expand Down Expand Up @@ -141,14 +140,15 @@ export const IterableAppProvider: FunctionComponent<
}

fn(id, token);
isLoggedInRef.current = true;
setIsLoggedIn(true);
setLoginInProgress(false);

return Promise.resolve(true);
}, [getJwtToken, userId]);

const initialize = useCallback(
(navigation: Navigation) => {
() => {
logout();

const config = new IterableConfig();
Expand Down Expand Up @@ -176,13 +176,13 @@ export const IterableAppProvider: FunctionComponent<
};

config.urlHandler = (url: string) => {
const routeNames = [Route.Commerce, Route.Inbox, Route.User];
// Logged out: the tabs do not exist, so leave the URL unhandled
if (!isLoggedInRef.current || !navigationRef.isReady()) return false;

const routeNames = [Route.Commerce, Route.Inbox, Route.User] as const;
for (const route of routeNames) {
if (url.includes(route.toLowerCase())) {
// [MOB-10418](https://iterable.atlassian.net/browse/MOB-10418): Figure out typing for this
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
navigation.navigate(route);
navigationRef.navigate(Route.Main, { screen: route });
return true;
}
}
Expand Down Expand Up @@ -278,6 +278,7 @@ export const IterableAppProvider: FunctionComponent<
Iterable.setUserId(null);
Iterable.logout();
lastTimeStamp = 0;
isLoggedInRef.current = false;
setIsLoggedIn(false);
}, []);

Expand Down
3 changes: 2 additions & 1 deletion example/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ import { SafeAreaProvider } from 'react-native-safe-area-context';

import App from './components/App';
import { IterableAppProvider } from './hooks';
import { navigationRef } from './navigationRef';

export default function Main() {
return (
<SafeAreaProvider>
<NavigationContainer>
<NavigationContainer ref={navigationRef}>
<IterableAppProvider>
<App />
</IterableAppProvider>
Expand Down
10 changes: 10 additions & 0 deletions example/src/navigationRef.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { createNavigationContainerRef } from '@react-navigation/native';

import type { RootStackParamList } from './types/navigation';

/**
* Lets code outside a screen (e.g. the Iterable `urlHandler`) navigate. A
* screen's `navigation` prop goes stale once that screen unmounts (the Login
* screen unmounts on login), so deep links must not use it.
*/
export const navigationRef = createNavigationContainerRef<RootStackParamList>();
Loading