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();