diff --git a/example/README.md b/example/README.md
index 8d07c8365..4c021535a 100644
--- a/example/README.md
+++ b/example/README.md
@@ -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:
diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml
index 4bebef5b8..593ef811d 100644
--- a/example/android/app/src/main/AndroidManifest.xml
+++ b/example/android/app/src/main/AndroidManifest.xml
@@ -8,6 +8,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/example/src/components/Login/Login.tsx b/example/src/components/Login/Login.tsx
index 988e9c8db..dae36b45c 100644
--- a/example/src/components/Login/Login.tsx
+++ b/example/src/components/Login/Login.tsx
@@ -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) => {
+export const Login = () => {
const { apiKey, setApiKey, userId, setUserId, initialize, loginInProgress } =
useIterableApp();
const loginIsEnabled = useMemo(() => apiKey && userId, [apiKey, userId]);
@@ -54,7 +53,7 @@ export const Login = ({ navigation }: RootStackScreenProps) => {
initialize(navigation)}
+ onPressOut={() => initialize()}
>
;
-
interface IterableAppProps {
/** The API key for your Iterable instance */
apiKey?: string;
@@ -31,10 +29,9 @@ interface IterableAppProps {
config: IterableConfig | null;
/**
* Call to initialize the Iterable SDK
- * @param navigation - The navigation prop from the screen
* @returns Promise 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? */
@@ -99,6 +96,8 @@ export const IterableAppProvider: FunctionComponent<
const [isInboxTab, setIsInboxTab] = useState(false);
const [itblConfig, setItblConfig] = useState(null);
const [isLoggedIn, setIsLoggedIn] = useState(false);
+ // Read by `urlHandler`, which is created once and would see stale state
+ const isLoggedInRef = useRef(false);
const [isInitialized, setIsInitialized] = useState(false);
const [apiKey, setApiKey] = useState(
process.env.ITBL_API_KEY
@@ -141,6 +140,7 @@ export const IterableAppProvider: FunctionComponent<
}
fn(id, token);
+ isLoggedInRef.current = true;
setIsLoggedIn(true);
setLoginInProgress(false);
@@ -148,7 +148,7 @@ export const IterableAppProvider: FunctionComponent<
}, [getJwtToken, userId]);
const initialize = useCallback(
- (navigation: Navigation) => {
+ () => {
logout();
const config = new IterableConfig();
@@ -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;
}
}
@@ -278,6 +278,7 @@ export const IterableAppProvider: FunctionComponent<
Iterable.setUserId(null);
Iterable.logout();
lastTimeStamp = 0;
+ isLoggedInRef.current = false;
setIsLoggedIn(false);
}, []);
diff --git a/example/src/index.tsx b/example/src/index.tsx
index f860ddb1b..6571cc9ad 100644
--- a/example/src/index.tsx
+++ b/example/src/index.tsx
@@ -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 (
-
+
diff --git a/example/src/navigationRef.ts b/example/src/navigationRef.ts
new file mode 100644
index 000000000..ce9fb341b
--- /dev/null
+++ b/example/src/navigationRef.ts
@@ -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();