referrers for all platforms (#4514)
This commit is contained in:
		
							parent
							
								
									83e8522e0a
								
							
						
					
					
						commit
						8b121af2e4
					
				
					 12 changed files with 213 additions and 34 deletions
				
			
		| 
						 | 
				
			
			@ -221,6 +221,7 @@ module.exports = function (config) {
 | 
			
		|||
        './plugins/withAndroidSplashScreenStatusBarTranslucentPlugin.js',
 | 
			
		||||
        './plugins/shareExtension/withShareExtensions.js',
 | 
			
		||||
        './plugins/notificationsExtension/withNotificationsExtension.js',
 | 
			
		||||
        './plugins/withAppDelegateReferrer.js',
 | 
			
		||||
      ].filter(Boolean),
 | 
			
		||||
      extra: {
 | 
			
		||||
        eas: {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,8 @@
 | 
			
		|||
package expo.modules.blueskyswissarmy.referrer
 | 
			
		||||
 | 
			
		||||
import android.content.Intent
 | 
			
		||||
import android.net.Uri
 | 
			
		||||
import android.os.Build
 | 
			
		||||
import android.util.Log
 | 
			
		||||
import com.android.installreferrer.api.InstallReferrerClient
 | 
			
		||||
import com.android.installreferrer.api.InstallReferrerStateListener
 | 
			
		||||
| 
						 | 
				
			
			@ -8,10 +11,53 @@ import expo.modules.kotlin.modules.Module
 | 
			
		|||
import expo.modules.kotlin.modules.ModuleDefinition
 | 
			
		||||
 | 
			
		||||
class ExpoBlueskyReferrerModule : Module() {
 | 
			
		||||
  private var intent: Intent? = null
 | 
			
		||||
  private var activityReferrer: Uri? = null
 | 
			
		||||
 | 
			
		||||
  override fun definition() =
 | 
			
		||||
    ModuleDefinition {
 | 
			
		||||
      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 ->
 | 
			
		||||
        val referrerClient = InstallReferrerClient.newBuilder(appContext.reactContext).build()
 | 
			
		||||
        referrerClient.startConnection(
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,9 +1,13 @@
 | 
			
		|||
import {requireNativeModule} from 'expo'
 | 
			
		||||
 | 
			
		||||
import {GooglePlayReferrerInfo} from './types'
 | 
			
		||||
import {GooglePlayReferrerInfo, ReferrerInfo} from './types'
 | 
			
		||||
 | 
			
		||||
export const NativeModule = requireNativeModule('ExpoBlueskyReferrer')
 | 
			
		||||
 | 
			
		||||
export function getGooglePlayReferrerInfoAsync(): Promise<GooglePlayReferrerInfo> {
 | 
			
		||||
export function getGooglePlayReferrerInfoAsync(): Promise<GooglePlayReferrerInfo | null> {
 | 
			
		||||
  return NativeModule.getGooglePlayReferrerInfoAsync()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getReferrerInfoAsync(): Promise<ReferrerInfo | null> {
 | 
			
		||||
  return NativeModule.getReferrerInfoAsync()
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										37
									
								
								modules/expo-bluesky-swiss-army/src/Referrer/index.ios.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								modules/expo-bluesky-swiss-army/src/Referrer/index.ios.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -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 {GooglePlayReferrerInfo} from './types'
 | 
			
		||||
import {GooglePlayReferrerInfo, ReferrerInfo} from './types'
 | 
			
		||||
 | 
			
		||||
// @ts-ignore throws
 | 
			
		||||
export function getGooglePlayReferrerInfoAsync(): Promise<GooglePlayReferrerInfo> {
 | 
			
		||||
  throw new NotImplementedError()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getReferrerInfoAsync(): Promise<ReferrerInfo | null> {
 | 
			
		||||
  throw new NotImplementedError()
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										34
									
								
								modules/expo-bluesky-swiss-army/src/Referrer/index.web.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								modules/expo-bluesky-swiss-army/src/Referrer/index.web.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -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 =
 | 
			
		||||
  | {
 | 
			
		||||
      installReferrer?: string
 | 
			
		||||
      clickTimestamp?: number
 | 
			
		||||
      installTimestamp?: number
 | 
			
		||||
    }
 | 
			
		||||
  | undefined
 | 
			
		||||
export type GooglePlayReferrerInfo = {
 | 
			
		||||
  installReferrer?: string
 | 
			
		||||
  clickTimestamp?: number
 | 
			
		||||
  installTimestamp?: number
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type ReferrerInfo = {
 | 
			
		||||
  referrer: string
 | 
			
		||||
  hostname: string
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										41
									
								
								plugins/withAppDelegateReferrer.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								plugins/withAppDelegateReferrer.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -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'
 | 
			
		||||
import {RouteParams, State} from 'lib/routes/types'
 | 
			
		||||
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 {AppPasswords} from 'view/screens/AppPasswords'
 | 
			
		||||
import {ModerationBlockedAccounts} from 'view/screens/ModerationBlockedAccounts'
 | 
			
		||||
| 
						 | 
				
			
			@ -49,6 +49,7 @@ import {
 | 
			
		|||
  StarterPackScreenShort,
 | 
			
		||||
} from '#/screens/StarterPack/StarterPackScreen'
 | 
			
		||||
import {Wizard} from '#/screens/StarterPack/Wizard'
 | 
			
		||||
import {Referrer} from '../modules/expo-bluesky-swiss-army'
 | 
			
		||||
import {init as initAnalytics} from './lib/analytics/analytics'
 | 
			
		||||
import {useWebScrollRestoration} from './lib/hooks/useWebScrollRestoration'
 | 
			
		||||
import {attachRouteToLogEvents, logEvent} from './lib/statsig/statsig'
 | 
			
		||||
| 
						 | 
				
			
			@ -769,6 +770,18 @@ function logModuleInitTime() {
 | 
			
		|||
    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__) {
 | 
			
		||||
    // This log is noisy, so keep false committed
 | 
			
		||||
    const shouldLog = false
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,9 +1,12 @@
 | 
			
		|||
import React from 'react'
 | 
			
		||||
import * as Linking from 'expo-linking'
 | 
			
		||||
 | 
			
		||||
import {logEvent} from 'lib/statsig/statsig'
 | 
			
		||||
import {isNative} from 'platform/detection'
 | 
			
		||||
import {useComposerControls} from 'state/shell'
 | 
			
		||||
import {useSession} from 'state/session'
 | 
			
		||||
import {useComposerControls} from 'state/shell'
 | 
			
		||||
import {useCloseAllActiveElements} from 'state/util'
 | 
			
		||||
import {Referrer} from '../../../modules/expo-bluesky-swiss-army'
 | 
			
		||||
 | 
			
		||||
type IntentType = 'compose'
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -15,6 +18,16 @@ export function useIntentHandler() {
 | 
			
		|||
 | 
			
		||||
  React.useEffect(() => {
 | 
			
		||||
    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
 | 
			
		||||
      // 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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -25,6 +25,11 @@ export type LogEvents = {
 | 
			
		|||
  }
 | 
			
		||||
  'state:foreground:sampled': {}
 | 
			
		||||
  'router:navigate:sampled': {}
 | 
			
		||||
  'deepLink:referrerReceived': {
 | 
			
		||||
    to: string
 | 
			
		||||
    referrer: string
 | 
			
		||||
    hostname: string
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Screen events
 | 
			
		||||
  'splash:signInPressed': {}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -28,8 +28,6 @@ type StatsigUser = {
 | 
			
		|||
    bundleDate: number
 | 
			
		||||
    refSrc: string
 | 
			
		||||
    refUrl: string
 | 
			
		||||
    referrer: string
 | 
			
		||||
    referrerHostname: string
 | 
			
		||||
    appLanguage: string
 | 
			
		||||
    contentLanguages: string[]
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			@ -37,29 +35,12 @@ type StatsigUser = {
 | 
			
		|||
 | 
			
		||||
let refSrc = ''
 | 
			
		||||
let refUrl = ''
 | 
			
		||||
let referrer = ''
 | 
			
		||||
let referrerHostname = ''
 | 
			
		||||
if (isWeb && typeof window !== 'undefined') {
 | 
			
		||||
  const params = new URLSearchParams(window.location.search)
 | 
			
		||||
  refSrc = params.get('ref_src') ?? ''
 | 
			
		||||
  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}
 | 
			
		||||
 | 
			
		||||
function createStatsigOptions(prefetchUsers: StatsigUser[]) {
 | 
			
		||||
| 
						 | 
				
			
			@ -222,8 +203,6 @@ function toStatsigUser(did: string | undefined): StatsigUser {
 | 
			
		|||
    custom: {
 | 
			
		||||
      refSrc,
 | 
			
		||||
      refUrl,
 | 
			
		||||
      referrer,
 | 
			
		||||
      referrerHostname,
 | 
			
		||||
      platform: Platform.OS as 'ios' | 'android' | 'web',
 | 
			
		||||
      bundleIdentifier: BUNDLE_IDENTIFIER,
 | 
			
		||||
      bundleDate: BUNDLE_DATE,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue