referrers for all platforms (#4514)
parent
83e8522e0a
commit
8b121af2e4
|
@ -221,6 +221,7 @@ module.exports = function (config) {
|
||||||
'./plugins/withAndroidSplashScreenStatusBarTranslucentPlugin.js',
|
'./plugins/withAndroidSplashScreenStatusBarTranslucentPlugin.js',
|
||||||
'./plugins/shareExtension/withShareExtensions.js',
|
'./plugins/shareExtension/withShareExtensions.js',
|
||||||
'./plugins/notificationsExtension/withNotificationsExtension.js',
|
'./plugins/notificationsExtension/withNotificationsExtension.js',
|
||||||
|
'./plugins/withAppDelegateReferrer.js',
|
||||||
].filter(Boolean),
|
].filter(Boolean),
|
||||||
extra: {
|
extra: {
|
||||||
eas: {
|
eas: {
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
package expo.modules.blueskyswissarmy.referrer
|
package expo.modules.blueskyswissarmy.referrer
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.android.installreferrer.api.InstallReferrerClient
|
import com.android.installreferrer.api.InstallReferrerClient
|
||||||
import com.android.installreferrer.api.InstallReferrerStateListener
|
import com.android.installreferrer.api.InstallReferrerStateListener
|
||||||
|
@ -8,10 +11,53 @@ import expo.modules.kotlin.modules.Module
|
||||||
import expo.modules.kotlin.modules.ModuleDefinition
|
import expo.modules.kotlin.modules.ModuleDefinition
|
||||||
|
|
||||||
class ExpoBlueskyReferrerModule : Module() {
|
class ExpoBlueskyReferrerModule : Module() {
|
||||||
|
private var intent: Intent? = null
|
||||||
|
private var activityReferrer: Uri? = null
|
||||||
|
|
||||||
override fun definition() =
|
override fun definition() =
|
||||||
ModuleDefinition {
|
ModuleDefinition {
|
||||||
Name("ExpoBlueskyReferrer")
|
Name("ExpoBlueskyReferrer")
|
||||||
|
|
||||||
|
OnNewIntent {
|
||||||
|
intent = it
|
||||||
|
activityReferrer = appContext.currentActivity?.referrer
|
||||||
|
}
|
||||||
|
|
||||||
|
AsyncFunction("getReferrerInfoAsync") {
|
||||||
|
val intentReferrer =
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
intent?.getParcelableExtra(Intent.EXTRA_REFERRER, Uri::class.java)
|
||||||
|
} else {
|
||||||
|
intent?.getParcelableExtra(Intent.EXTRA_REFERRER)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some apps explicitly set a referrer, like Chrome. In these cases, we prefer this since
|
||||||
|
// it's the actual website that the user came from rather than the app.
|
||||||
|
if (intentReferrer is Uri) {
|
||||||
|
val res =
|
||||||
|
mapOf(
|
||||||
|
"referrer" to intentReferrer.toString(),
|
||||||
|
"hostname" to intentReferrer.host,
|
||||||
|
)
|
||||||
|
intent = null
|
||||||
|
return@AsyncFunction res
|
||||||
|
}
|
||||||
|
|
||||||
|
// In all other cases, we'll just record the app that sent the intent.
|
||||||
|
if (activityReferrer != null) {
|
||||||
|
// referrer could become null here. `.toString()` though can be called on null
|
||||||
|
val res =
|
||||||
|
mapOf(
|
||||||
|
"referrer" to activityReferrer.toString(),
|
||||||
|
"hostname" to (activityReferrer?.host ?: ""),
|
||||||
|
)
|
||||||
|
activityReferrer = null
|
||||||
|
return@AsyncFunction res
|
||||||
|
}
|
||||||
|
|
||||||
|
return@AsyncFunction null
|
||||||
|
}
|
||||||
|
|
||||||
AsyncFunction("getGooglePlayReferrerInfoAsync") { promise: Promise ->
|
AsyncFunction("getGooglePlayReferrerInfoAsync") { promise: Promise ->
|
||||||
val referrerClient = InstallReferrerClient.newBuilder(appContext.reactContext).build()
|
val referrerClient = InstallReferrerClient.newBuilder(appContext.reactContext).build()
|
||||||
referrerClient.startConnection(
|
referrerClient.startConnection(
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
import {requireNativeModule} from 'expo'
|
import {requireNativeModule} from 'expo'
|
||||||
|
|
||||||
import {GooglePlayReferrerInfo} from './types'
|
import {GooglePlayReferrerInfo, ReferrerInfo} from './types'
|
||||||
|
|
||||||
export const NativeModule = requireNativeModule('ExpoBlueskyReferrer')
|
export const NativeModule = requireNativeModule('ExpoBlueskyReferrer')
|
||||||
|
|
||||||
export function getGooglePlayReferrerInfoAsync(): Promise<GooglePlayReferrerInfo> {
|
export function getGooglePlayReferrerInfoAsync(): Promise<GooglePlayReferrerInfo | null> {
|
||||||
return NativeModule.getGooglePlayReferrerInfoAsync()
|
return NativeModule.getGooglePlayReferrerInfoAsync()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getReferrerInfoAsync(): Promise<ReferrerInfo | null> {
|
||||||
|
return NativeModule.getReferrerInfoAsync()
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
import {SharedPrefs} from '../../index'
|
||||||
|
import {NotImplementedError} from '../NotImplemented'
|
||||||
|
import {GooglePlayReferrerInfo, ReferrerInfo} from './types'
|
||||||
|
|
||||||
|
export function getGooglePlayReferrerInfoAsync(): Promise<GooglePlayReferrerInfo> {
|
||||||
|
throw new NotImplementedError()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getReferrerInfoAsync(): Promise<ReferrerInfo | null> {
|
||||||
|
const referrer = SharedPrefs.getString('referrer')
|
||||||
|
if (referrer) {
|
||||||
|
SharedPrefs.removeValue('referrer')
|
||||||
|
try {
|
||||||
|
const url = new URL(referrer)
|
||||||
|
return {
|
||||||
|
referrer,
|
||||||
|
hostname: url.hostname,
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return {
|
||||||
|
referrer,
|
||||||
|
hostname: undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const referrerApp = SharedPrefs.getString('referrerApp')
|
||||||
|
if (referrerApp) {
|
||||||
|
SharedPrefs.removeValue('referrerApp')
|
||||||
|
return {
|
||||||
|
referrer: referrerApp,
|
||||||
|
hostname: referrerApp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
|
@ -1,7 +1,10 @@
|
||||||
import {NotImplementedError} from '../NotImplemented'
|
import {NotImplementedError} from '../NotImplemented'
|
||||||
import {GooglePlayReferrerInfo} from './types'
|
import {GooglePlayReferrerInfo, ReferrerInfo} from './types'
|
||||||
|
|
||||||
// @ts-ignore throws
|
|
||||||
export function getGooglePlayReferrerInfoAsync(): Promise<GooglePlayReferrerInfo> {
|
export function getGooglePlayReferrerInfoAsync(): Promise<GooglePlayReferrerInfo> {
|
||||||
throw new NotImplementedError()
|
throw new NotImplementedError()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getReferrerInfoAsync(): Promise<ReferrerInfo | null> {
|
||||||
|
throw new NotImplementedError()
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
import {Platform} from 'react-native'
|
||||||
|
|
||||||
|
import {NotImplementedError} from '../NotImplemented'
|
||||||
|
import {GooglePlayReferrerInfo, ReferrerInfo} from './types'
|
||||||
|
|
||||||
|
export function getGooglePlayReferrerInfoAsync(): Promise<GooglePlayReferrerInfo> {
|
||||||
|
throw new NotImplementedError()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getReferrerInfoAsync(): Promise<ReferrerInfo | null> {
|
||||||
|
if (
|
||||||
|
Platform.OS === 'web' &&
|
||||||
|
// for ssr
|
||||||
|
typeof document !== 'undefined' &&
|
||||||
|
document != null &&
|
||||||
|
document.referrer
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const url = new URL(document.referrer)
|
||||||
|
if (url.hostname !== 'bsky.app') {
|
||||||
|
return {
|
||||||
|
referrer: url.href,
|
||||||
|
hostname: url.hostname,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// If something happens to the URL parsing, we don't want to actually cause any problems for the user. Just
|
||||||
|
// log the error so we might catch it
|
||||||
|
console.error('Failed to parse referrer URL')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
|
@ -1,7 +1,10 @@
|
||||||
export type GooglePlayReferrerInfo =
|
export type GooglePlayReferrerInfo = {
|
||||||
| {
|
|
||||||
installReferrer?: string
|
installReferrer?: string
|
||||||
clickTimestamp?: number
|
clickTimestamp?: number
|
||||||
installTimestamp?: number
|
installTimestamp?: number
|
||||||
}
|
}
|
||||||
| undefined
|
|
||||||
|
export type ReferrerInfo = {
|
||||||
|
referrer: string
|
||||||
|
hostname: string
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
const {withAppDelegate} = require('@expo/config-plugins')
|
||||||
|
const {mergeContents} = require('@expo/config-plugins/build/utils/generateCode')
|
||||||
|
const path = require('path')
|
||||||
|
const fs = require('fs')
|
||||||
|
|
||||||
|
module.exports = config => {
|
||||||
|
// eslint-disable-next-line no-shadow
|
||||||
|
return withAppDelegate(config, async config => {
|
||||||
|
const delegatePath = path.join(
|
||||||
|
config.modRequest.platformProjectRoot,
|
||||||
|
'AppDelegate.mm',
|
||||||
|
)
|
||||||
|
|
||||||
|
let newContents = config.modResults.contents
|
||||||
|
newContents = mergeContents({
|
||||||
|
src: newContents,
|
||||||
|
anchor: '// Linking API',
|
||||||
|
newSrc: `
|
||||||
|
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
|
||||||
|
[defaults setObject:options[UIApplicationOpenURLOptionsSourceApplicationKey] forKey:@"referrerApp"];\n`,
|
||||||
|
offset: 2,
|
||||||
|
tag: 'referrer info - deep links',
|
||||||
|
comment: '//',
|
||||||
|
}).contents
|
||||||
|
|
||||||
|
newContents = mergeContents({
|
||||||
|
src: newContents,
|
||||||
|
anchor: '// Universal Links',
|
||||||
|
newSrc: `
|
||||||
|
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
|
||||||
|
[defaults setURL:userActivity.referrerURL forKey:@"referrer"];\n`,
|
||||||
|
offset: 2,
|
||||||
|
tag: 'referrer info - universal links',
|
||||||
|
comment: '//',
|
||||||
|
}).contents
|
||||||
|
|
||||||
|
config.modResults.contents = newContents
|
||||||
|
|
||||||
|
return config
|
||||||
|
})
|
||||||
|
}
|
|
@ -31,7 +31,7 @@ import {
|
||||||
} from 'lib/routes/types'
|
} from 'lib/routes/types'
|
||||||
import {RouteParams, State} from 'lib/routes/types'
|
import {RouteParams, State} from 'lib/routes/types'
|
||||||
import {bskyTitle} from 'lib/strings/headings'
|
import {bskyTitle} from 'lib/strings/headings'
|
||||||
import {isAndroid, isNative} from 'platform/detection'
|
import {isAndroid, isNative, isWeb} from 'platform/detection'
|
||||||
import {PreferencesExternalEmbeds} from '#/view/screens/PreferencesExternalEmbeds'
|
import {PreferencesExternalEmbeds} from '#/view/screens/PreferencesExternalEmbeds'
|
||||||
import {AppPasswords} from 'view/screens/AppPasswords'
|
import {AppPasswords} from 'view/screens/AppPasswords'
|
||||||
import {ModerationBlockedAccounts} from 'view/screens/ModerationBlockedAccounts'
|
import {ModerationBlockedAccounts} from 'view/screens/ModerationBlockedAccounts'
|
||||||
|
@ -49,6 +49,7 @@ import {
|
||||||
StarterPackScreenShort,
|
StarterPackScreenShort,
|
||||||
} from '#/screens/StarterPack/StarterPackScreen'
|
} from '#/screens/StarterPack/StarterPackScreen'
|
||||||
import {Wizard} from '#/screens/StarterPack/Wizard'
|
import {Wizard} from '#/screens/StarterPack/Wizard'
|
||||||
|
import {Referrer} from '../modules/expo-bluesky-swiss-army'
|
||||||
import {init as initAnalytics} from './lib/analytics/analytics'
|
import {init as initAnalytics} from './lib/analytics/analytics'
|
||||||
import {useWebScrollRestoration} from './lib/hooks/useWebScrollRestoration'
|
import {useWebScrollRestoration} from './lib/hooks/useWebScrollRestoration'
|
||||||
import {attachRouteToLogEvents, logEvent} from './lib/statsig/statsig'
|
import {attachRouteToLogEvents, logEvent} from './lib/statsig/statsig'
|
||||||
|
@ -769,6 +770,18 @@ function logModuleInitTime() {
|
||||||
initMs,
|
initMs,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (isWeb) {
|
||||||
|
Referrer.getReferrerInfoAsync().then(info => {
|
||||||
|
if (info && info.hostname !== 'bsky.app') {
|
||||||
|
logEvent('deepLink:referrerReceived', {
|
||||||
|
to: window.location.href,
|
||||||
|
referrer: info?.referrer,
|
||||||
|
hostname: info?.hostname,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
// This log is noisy, so keep false committed
|
// This log is noisy, so keep false committed
|
||||||
const shouldLog = false
|
const shouldLog = false
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import * as Linking from 'expo-linking'
|
import * as Linking from 'expo-linking'
|
||||||
|
|
||||||
|
import {logEvent} from 'lib/statsig/statsig'
|
||||||
import {isNative} from 'platform/detection'
|
import {isNative} from 'platform/detection'
|
||||||
import {useComposerControls} from 'state/shell'
|
|
||||||
import {useSession} from 'state/session'
|
import {useSession} from 'state/session'
|
||||||
|
import {useComposerControls} from 'state/shell'
|
||||||
import {useCloseAllActiveElements} from 'state/util'
|
import {useCloseAllActiveElements} from 'state/util'
|
||||||
|
import {Referrer} from '../../../modules/expo-bluesky-swiss-army'
|
||||||
|
|
||||||
type IntentType = 'compose'
|
type IntentType = 'compose'
|
||||||
|
|
||||||
|
@ -15,6 +18,16 @@ export function useIntentHandler() {
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const handleIncomingURL = (url: string) => {
|
const handleIncomingURL = (url: string) => {
|
||||||
|
Referrer.getReferrerInfoAsync().then(info => {
|
||||||
|
if (info && info.hostname !== 'bsky.app') {
|
||||||
|
logEvent('deepLink:referrerReceived', {
|
||||||
|
to: url,
|
||||||
|
referrer: info?.referrer,
|
||||||
|
hostname: info?.hostname,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// We want to be able to support bluesky:// deeplinks. It's unnatural for someone to use a deeplink with three
|
// We want to be able to support bluesky:// deeplinks. It's unnatural for someone to use a deeplink with three
|
||||||
// slashes, like bluesky:///intent/follow. However, supporting just two slashes causes us to have to take care
|
// slashes, like bluesky:///intent/follow. However, supporting just two slashes causes us to have to take care
|
||||||
// of two cases when parsing the url. If we ensure there is a third slash, we can always ensure the first
|
// of two cases when parsing the url. If we ensure there is a third slash, we can always ensure the first
|
||||||
|
|
|
@ -25,6 +25,11 @@ export type LogEvents = {
|
||||||
}
|
}
|
||||||
'state:foreground:sampled': {}
|
'state:foreground:sampled': {}
|
||||||
'router:navigate:sampled': {}
|
'router:navigate:sampled': {}
|
||||||
|
'deepLink:referrerReceived': {
|
||||||
|
to: string
|
||||||
|
referrer: string
|
||||||
|
hostname: string
|
||||||
|
}
|
||||||
|
|
||||||
// Screen events
|
// Screen events
|
||||||
'splash:signInPressed': {}
|
'splash:signInPressed': {}
|
||||||
|
|
|
@ -28,8 +28,6 @@ type StatsigUser = {
|
||||||
bundleDate: number
|
bundleDate: number
|
||||||
refSrc: string
|
refSrc: string
|
||||||
refUrl: string
|
refUrl: string
|
||||||
referrer: string
|
|
||||||
referrerHostname: string
|
|
||||||
appLanguage: string
|
appLanguage: string
|
||||||
contentLanguages: string[]
|
contentLanguages: string[]
|
||||||
}
|
}
|
||||||
|
@ -37,29 +35,12 @@ type StatsigUser = {
|
||||||
|
|
||||||
let refSrc = ''
|
let refSrc = ''
|
||||||
let refUrl = ''
|
let refUrl = ''
|
||||||
let referrer = ''
|
|
||||||
let referrerHostname = ''
|
|
||||||
if (isWeb && typeof window !== 'undefined') {
|
if (isWeb && typeof window !== 'undefined') {
|
||||||
const params = new URLSearchParams(window.location.search)
|
const params = new URLSearchParams(window.location.search)
|
||||||
refSrc = params.get('ref_src') ?? ''
|
refSrc = params.get('ref_src') ?? ''
|
||||||
refUrl = decodeURIComponent(params.get('ref_url') ?? '')
|
refUrl = decodeURIComponent(params.get('ref_url') ?? '')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
|
||||||
isWeb &&
|
|
||||||
typeof document !== 'undefined' &&
|
|
||||||
document != null &&
|
|
||||||
document.referrer
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
const url = new URL(document.referrer)
|
|
||||||
if (url.hostname !== 'bsky.app') {
|
|
||||||
referrer = document.referrer
|
|
||||||
referrerHostname = url.hostname
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type {LogEvents}
|
export type {LogEvents}
|
||||||
|
|
||||||
function createStatsigOptions(prefetchUsers: StatsigUser[]) {
|
function createStatsigOptions(prefetchUsers: StatsigUser[]) {
|
||||||
|
@ -222,8 +203,6 @@ function toStatsigUser(did: string | undefined): StatsigUser {
|
||||||
custom: {
|
custom: {
|
||||||
refSrc,
|
refSrc,
|
||||||
refUrl,
|
refUrl,
|
||||||
referrer,
|
|
||||||
referrerHostname,
|
|
||||||
platform: Platform.OS as 'ios' | 'android' | 'web',
|
platform: Platform.OS as 'ios' | 'android' | 'web',
|
||||||
bundleIdentifier: BUNDLE_IDENTIFIER,
|
bundleIdentifier: BUNDLE_IDENTIFIER,
|
||||||
bundleDate: BUNDLE_DATE,
|
bundleDate: BUNDLE_DATE,
|
||||||
|
|
Loading…
Reference in New Issue