Skip to content
Merged
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
83 changes: 79 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,98 @@ sdk for handling deferred links

## Installation


```sh
npm install detour-react-native
```

#### You need to install additional dependencies

```sh
npm install expo-localization react-native-device-info expo-clipboard @react-native-async-storage/async-storage
```

## Usage

#### Initialize provider in root of your app

```js
import { multiply } from 'detour-react-native';
import { DetourProvider, type Config } from 'detour-react-native';

export default function App() {
const config: Config = {
API_KEY: 'ssss-ssss-ssss',
appID: 'app-id-from-dashboard',
shouldUseClipboard: true,
};

return(
<DetourProvider config={config}>
// rest of app content
</DetourProvider>)
}
```

#### Use values from context

```js
import { useDetourContext } from 'detour-react-native';

// inside component
const { deferredLink, deferredLinkProcessed, route } = useDetourContext();
```

## Types

The package exposes several types to help you with type-checking in your own codebase.

// ...
**Config**

const result = await multiply(3, 7);
This type is used to define the configuration object you pass to the DetourProvider.

```js
export type Config = {
/**
* Your application ID from the Detour dashboard.
*/
appID: string;

/**
* Your API key from the Detour dashboard.
*/
API_KEY: string;

/**
* Optional: A flag to determine if the provider should check the clipboard for a deferred link.
* When true, it displays permission alert to user.
* Defaults to true if not provided.
*/
shouldUseClipboard?: boolean;
};
```

**DeferredLinkContext**

This type represents the object returned by the useDetourContext hook, containing the deferred link and its processing status.

```js
export type DeferredLinkContext = {
/**
* Boolean indicating if the deferred link has been processed.
* This is useful for conditionally rendering UI components.
*/
deferredLinkProcessed: boolean;

/**
* The deferred link value. This can be a string or a URL object, or null if no link was found.
*/
deferredLink: string | URL | null;

/**
* The detected route based on the deferred link, or null if no route was detected.
*/
route: string | null;
};
```

## Contributing

Expand Down
14 changes: 8 additions & 6 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { DetourProvider } from 'detour-react-native';
import { DetourProvider, type Config } from 'detour-react-native';

import { Screen } from './Screen';

export default function App() {
return (
<DetourProvider appID="example-id" API_KEY="sssss-sssss-sssss">
{<Screen />}
</DetourProvider>
);
const config: Config = {
API_KEY: 'ssss-ssss-ssss',
appID: 'app-id-from-dashboard',
shouldUseClipboard: true,
};

return <DetourProvider config={config}>{<Screen />}</DetourProvider>;
}
20 changes: 8 additions & 12 deletions src/DetourContext.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,16 @@
import { createContext, useContext, type ReactNode } from 'react';
import { createContext, useContext, type PropsWithChildren } from 'react';
import { useDeferredLink } from './hooks/useDeferredLink';
import type { Config, DeferredLinkContext } from './types';

type DetourContextType = ReturnType<typeof useDeferredLink>;
type Props = PropsWithChildren & { config: Config };

type DetourContextType = DeferredLinkContext;

const DetourContext = createContext<DetourContextType | undefined>(undefined);

export const DetourProvider = ({
appID,
API_KEY,
children,
}: {
appID: string;
API_KEY: string;
children: ReactNode;
}) => {
const value = useDeferredLink({ API_KEY, appID });
export const DetourProvider = ({ config, children }: Props) => {
const { API_KEY, appID, shouldUseClipboard = true } = config;
const value = useDeferredLink({ API_KEY, appID, shouldUseClipboard });

return (
<DetourContext.Provider value={value}>{children}</DetourContext.Provider>
Expand Down
14 changes: 10 additions & 4 deletions src/api/getDeferredLink.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
import type { RequiredConfig } from '../types';
import { getProbabilisticFingerprint } from '../utils/fingerprint';

const API_URL = 'https://godetour.dev/api/link/match-link';

export const getDeferredLink = async (apiKey: string, appId: string) => {
const probabilisticFingerprint = await getProbabilisticFingerprint();
export const getDeferredLink = async ({
API_KEY,
appID,
shouldUseClipboard,
}: RequiredConfig) => {
const probabilisticFingerprint =
await getProbabilisticFingerprint(shouldUseClipboard);

try {
const response = await fetch(API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`,
'X-App-ID': appId,
'Authorization': `Bearer ${API_KEY}`,
'X-App-ID': appID,
},
body: JSON.stringify(probabilisticFingerprint),
});
Expand Down
23 changes: 12 additions & 11 deletions src/hooks/useDeferredLink.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
import { useEffect, useState } from 'react';
import { checkIsFirstEntrance, markFirstEntrance } from '../utils/appEntrance';
import { getDeferredLink } from '../api/getDeferredLink';
import type { DeferredLinkContext, RequiredConfig } from '../types';
import { checkIsFirstEntrance, markFirstEntrance } from '../utils/appEntrance';

let deferredSessionHandled = false;

type ReturnType = DeferredLinkContext;

export const useDeferredLink = ({
API_KEY,
appID,
}: {
API_KEY: string;
appID: string;
}): {
deferredLinkProcessed: boolean;
deferredLink: string | URL | null;
route: string | null;
} => {
shouldUseClipboard,
}: RequiredConfig): ReturnType => {
const [deferredLinkProcessed, setDeferredProcessed] = useState(false);
const [matchedLink, setMatchedLink] = useState<string | URL | null>(null);
const [route, setRoute] = useState<string | null>(null);
Expand All @@ -37,7 +34,11 @@ export const useDeferredLink = ({
await markFirstEntrance();
}

const link = await getDeferredLink(API_KEY, appID);
const link = await getDeferredLink({
API_KEY,
appID,
shouldUseClipboard,
});
if (!link) {
console.log('No deferred link found');
setDeferredProcessed(true);
Expand Down Expand Up @@ -65,7 +66,7 @@ export const useDeferredLink = ({
setDeferredProcessed(true);
}
})();
}, [API_KEY, appID]);
}, [API_KEY, appID, shouldUseClipboard]);

return {
deferredLinkProcessed,
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { DetourProvider, useDetourContext } from './DetourContext';
export type { Config, DeferredLinkContext } from './types/index';
13 changes: 13 additions & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export type Config = {
appID: string;
API_KEY: string;
shouldUseClipboard?: boolean;
};

export type RequiredConfig = Required<Config>;

export type DeferredLinkContext = {
deferredLinkProcessed: boolean;
deferredLink: string | URL | null;
route: string | null;
};
39 changes: 21 additions & 18 deletions src/utils/fingerprint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,26 @@ type ProbabilisticFingerprint = {
pastedLink?: string;
};

export const getProbabilisticFingerprint =
async (): Promise<ProbabilisticFingerprint> => {
const { width, height } = Dimensions.get('window');
export const getProbabilisticFingerprint = async (
shouldUseClipboard: boolean
): Promise<ProbabilisticFingerprint> => {
const { width, height } = Dimensions.get('window');

return {
platform: Platform.OS,
model: DeviceInfo.getModel(),
manufacturer: await DeviceInfo.getManufacturer(),
systemVersion: DeviceInfo.getSystemVersion(),
screenWidth: width,
screenHeight: height,
scale: PixelRatio.get(),
locale: Localization.getLocales(),
timezone: Localization.getCalendars()[0]?.timeZone,
userAgent: await DeviceInfo.getUserAgent(),
appVersion: DeviceInfo.getVersion(),
timestamp: Date.now(),
pastedLink: await Clipboard.getStringAsync(),
};
return {
platform: Platform.OS,
model: DeviceInfo.getModel(),
manufacturer: await DeviceInfo.getManufacturer(),
systemVersion: DeviceInfo.getSystemVersion(),
screenWidth: width,
screenHeight: height,
scale: PixelRatio.get(),
locale: Localization.getLocales(),
timezone: Localization.getCalendars()[0]?.timeZone,
userAgent: await DeviceInfo.getUserAgent(),
appVersion: DeviceInfo.getVersion(),
timestamp: Date.now(),
pastedLink: shouldUseClipboard
? await Clipboard.getStringAsync()
: undefined,
};
};