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
8 changes: 8 additions & 0 deletions packages/plugins/plugin-appsflyer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ Run `pod install` after the installation to autolink the AppsFlyer SDK.

See [AppsFlyer React Native Plugin](https://github.com/AppsFlyerSDK/appsflyer-react-native-plugin) for more details of this dependency.

This plugin requires `react-native-appsflyer` 7.x, which requires React Native 0.76 or later with the New Architecture enabled. For `react-native-appsflyer` 6.x, use an earlier release of this plugin.

## Usage

Follow the [instructions for adding plugins](https://github.com/segmentio/analytics-react-native#adding-plugins) on the main Analytics client:
Expand All @@ -44,6 +46,12 @@ const plugin = new AppsflyerPlugin();
segmentClient.add({ plugin });
```

The plugin registers the AppsFlyer conversion and deep link listeners. `react-native-appsflyer` keeps one callback per listener, so do not register your own. Pass `onInstallConversionData` and `onDeepLink` to the plugin instead.

### App Tracking Transparency on iOS

The plugin does not wait for ATT authorization, because `react-native-appsflyer` 7.x removed `timeToWaitForATTUserAuthorization`. Request ATT authorization before you add the plugin.

### Tracking Deep Links on iOS

The Analytics React Native SDK [requires additonal setup](https://github.com/segmentio/analytics-react-native#ios-deep-link-tracking-setup) to automatically track deep links. If you are also tracking [Universal Links](https://dev.appsflyer.com/hc/docs/ios-sdk-reference-appsflyerlib#continue), add the following to your `AppDelegate.m` :
Expand Down
4 changes: 2 additions & 2 deletions packages/plugins/plugin-appsflyer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,14 @@
"homepage": "https://github.com/segmentio/analytics-react-native/tree/master/packages/plugins/plugin-appsflyer#readme",
"peerDependencies": {
"@segment/analytics-react-native": "^2.18.0",
"react-native-appsflyer": "^6.9.4"
"react-native-appsflyer": "^7.0.2-0"
},
"devDependencies": {
"@segment/analytics-react-native": "^2.18.0",
"@segment/analytics-rn-shared": "workspace:^",
"@segment/sovran-react-native": "^1.1.0",
"jest": "^29.7.0",
"react-native-appsflyer": "^6.9.4",
"react-native-appsflyer": "^7.0.2-0",
"react-native-builder-bob": "^0.23.1",
"rimraf": "^5.0.5",
"semantic-release": "^22.0.8",
Expand Down
269 changes: 139 additions & 130 deletions packages/plugins/plugin-appsflyer/src/AppsflyerPlugin.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
DestinationPlugin,
IdentifyEventType,
JsonMap,
PluginType,
TrackEventType,
UpdateType,
Expand All @@ -11,56 +12,42 @@ import {
import type { SegmentAppsflyerSettings } from './types';
import appsFlyer, {
ConversionData,
OnAppOpenAttributionData,
UnifiedDeepLinkData,
DeepLinkData,
} from 'react-native-appsflyer';
import identify from './methods/identify';
import track from './methods/track';

export class AppsflyerPlugin extends DestinationPlugin {
constructor(props?: {
timeToWaitForATTUserAuthorization: number;
is_adset: boolean;
is_adset_id: boolean;
is_ad_id: boolean;
onDeepLink?: (data: UnifiedDeepLinkData) => void;
onDeepLink?: (data: DeepLinkData) => void;
onInstallConversionData?: (data: ConversionData) => void;
onAppOpenAttribution?: (data: OnAppOpenAttributionData) => void;
}) {
super();
if (props != null) {
this.timeToWaitForATTUserAuthorization =
props.timeToWaitForATTUserAuthorization;
this.is_adset = props.is_adset === undefined ? false : props.is_adset;
this.is_ad_id = props.is_ad_id === undefined ? false : props.is_ad_id;
this.is_adset_id =
props.is_adset_id === undefined ? false : props.is_adset_id;
this.onDeepLink = props.onDeepLink;
this.onInstallConversionData = props.onInstallConversionData;
this.onAppOpenAttribution = props.onAppOpenAttribution;
}
}
type = PluginType.destination;
key = 'AppsFlyer';
is_adset = false;
is_adset_id = false;
is_ad_id = false;
onDeepLink?: (data: UnifiedDeepLinkData) => void;
onDeepLink?: (data: DeepLinkData) => void;
onInstallConversionData?: (data: ConversionData) => void;
onAppOpenAttribution?: (data: OnAppOpenAttributionData) => void;
private settings: SegmentAppsflyerSettings | null = null;
private hasRegisteredInstallCallback = false;
private hasRegisteredDeepLinkCallback = false;
private hasInitialized = false;

timeToWaitForATTUserAuthorization = 60;

async update(settings: SegmentAPISettings, _: UpdateType): Promise<void> {
const defaultOpts = {
isDebug: false,
timeToWaitForATTUserAuthorization: this.timeToWaitForATTUserAuthorization,
onInstallConversionDataListener: true,
};
const appsflyerSettings = settings.integrations[
this.key
] as SegmentAppsflyerSettings;
Expand All @@ -72,39 +59,40 @@ export class AppsflyerPlugin extends DestinationPlugin {

this.settings = appsflyerSettings;

if (
this.settings.trackAttributionData &&
!this.hasRegisteredInstallCallback
) {
this.registerConversionCallback();
this.hasRegisteredInstallCallback = true;
}

// Must reach native before init(): Android permanently drops a deep link that resolves with no listener attached.
if (
clientConfig?.trackDeepLinks === true &&
!this.hasRegisteredDeepLinkCallback
) {
this.registerDeepLinkCallback();
this.registerUnifiedDeepLinkCallback();

this.hasRegisteredDeepLinkCallback = true;
}
if (!this.hasInitialized) {
try {
await appsFlyer.initSdk({

const initialization = this.hasInitialized
? undefined
: appsFlyer.init({
devKey: this.settings.appsFlyerDevKey,
appId: this.settings.appleAppID,
onDeepLinkListener: clientConfig?.trackDeepLinks === true,
...defaultOpts,
});
this.hasInitialized = true;
} catch (error) {
const message = 'AppsFlyer failed to initialize';
this.analytics?.reportInternalError(
new SegmentError(ErrorType.PluginError, message, error)
);
this.analytics?.logger.warn(`${message}: ${JSON.stringify(error)}`);
}
this.hasInitialized = true;

if (
this.settings.trackAttributionData &&
!this.hasRegisteredInstallCallback
) {
this.registerConversionCallback();
this.hasRegisteredInstallCallback = true;
}

if (initialization === undefined) {
return;
}
this.registerSessionReadyCallback();
try {
await initialization;
} catch (error) {
this.hasInitialized = false;
this.reportError('AppsFlyer failed to initialize', error);
}
}

Expand All @@ -118,101 +106,122 @@ export class AppsflyerPlugin extends DestinationPlugin {
return event;
}

registerSessionReadyCallback = () => {
appsFlyer
.registerSessionReadyListener(() => {
appsFlyer
.start()
.catch((error) =>
this.reportError('AppsFlyer failed to start', error)
);
})
.catch((error) =>
this.reportError(
'AppsFlyer failed to register the session ready listener',
error
)
);
};

registerConversionCallback = () => {
appsFlyer.onInstallConversionData((res) => {
const {
af_status,
media_source,
campaign,
is_first_launch,
adset_id,
ad_id,
adset,
} = res?.data;
const properties = {
provider: this.key,
campaign: {
source: media_source,
name: campaign,
appsFlyer
.registerConversionListener({
onConversionDataSuccess: (data) => {
const {
af_status,
media_source,
campaign,
is_first_launch,
adset_id,
ad_id,
adset,
} = data as JsonMap;
const properties = {
provider: this.key,
campaign: {
source: media_source,
name: campaign,
},
};
if (this.is_adset_id) {
Object.assign(properties, { adset_id: adset_id });
}
if (this.is_ad_id) {
Object.assign(properties, { ad_id: ad_id });
}
if (this.is_adset) {
Object.assign(properties, { adset: adset });
}
if (is_first_launch === true || is_first_launch === 'true') {
if (af_status === 'Non-organic') {
this.analytics
?.track('Install Attributed', properties)
.then(() =>
this.analytics?.logger.info(
'Sent Install Attributed event to Segment'
)
);
} else {
this.analytics
?.track('Organic Install', {
provider: 'AppsFlyer',
})
.then(() =>
this.analytics?.logger.info(
'Sent Organic Install event to Segment'
)
);
}
}
this.onInstallConversionData?.(data);
},
};
if (this.is_adset_id) {
Object.assign(properties, { adset_id: adset_id });
}
if (this.is_ad_id) {
Object.assign(properties, { ad_id: ad_id });
}
if (this.is_adset) {
Object.assign(properties, { adset: adset });
}
if (Boolean(is_first_launch) && JSON.parse(is_first_launch) === true) {
if (af_status === 'Non-organic') {
this.analytics
?.track('Install Attributed', properties)
.then(() =>
this.analytics?.logger.info(
'Sent Install Attributed event to Segment'
)
);
} else {
this.analytics
?.track('Organic Install', {
provider: 'AppsFlyer',
})
.then(() =>
this.analytics?.logger.info(
'Sent Organic Install event to Segment'
)
);
}
}
this.onInstallConversionData?.(res);
});
})
.catch((error) =>
this.reportError(
'AppsFlyer failed to register the conversion listener',
error
)
);
};

registerDeepLinkCallback = () => {
appsFlyer.onAppOpenAttribution((res) => {
if (res?.status === 'success') {
const { campaign, media_source } = res.data;
const properties = {
provider: this.key,
campaign: {
name: campaign,
source: media_source,
},
};
this.analytics
?.track('Deep Link Opened', properties)
.then(() =>
this.analytics?.logger.info(
'Sent Deep Link Opened event to Segment'
)
);
}
this.onAppOpenAttribution?.(res);
});
appsFlyer
.registerDeepLinkListener({
onDeepLinking: (data) => {
if (data.status === 'FOUND' && data.deepLink !== undefined) {
const { deep_link_value, media_source, campaign } =
data.deepLink as JsonMap;
const properties = {
provider: this.key,
deepLink: deep_link_value,
campaign: {
name: campaign,
source: media_source,
},
};
this.analytics
?.track('Deep Link Opened', properties)
.then(() =>
this.analytics?.logger.info(
'Sent Deep Link Opened event to Segment'
)
);
}
this.onDeepLink?.(data);
},
})
.catch((error) =>
this.reportError(
'AppsFlyer failed to register the deep link listener',
error
)
);
};

registerUnifiedDeepLinkCallback = () => {
appsFlyer.onDeepLink((res) => {
if (res.deepLinkStatus !== 'NOT_FOUND') {
const { DLValue, media_source, campaign } = res.data;
const properties = {
deepLink: DLValue as string,
campaign: {
name: campaign,
source: media_source,
},
};
this.analytics
?.track('Deep Link Opened', properties)
.then(() =>
this.analytics?.logger.info(
'Sent Deep Link Opened event to Segment'
)
);
}
this.onDeepLink?.(res);
});
};
private reportError(message: string, error: unknown) {
this.analytics?.reportInternalError(
new SegmentError(ErrorType.PluginError, message, error)
);
this.analytics?.logger.warn(`${message}: ${JSON.stringify(error)}`);
}
}
Loading