about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--app.config.js1
-rw-r--r--modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/referrer/ExpoBlueskyReferrerModule.kt46
-rw-r--r--modules/expo-bluesky-swiss-army/src/Referrer/index.android.ts8
-rw-r--r--modules/expo-bluesky-swiss-army/src/Referrer/index.ios.ts37
-rw-r--r--modules/expo-bluesky-swiss-army/src/Referrer/index.ts7
-rw-r--r--modules/expo-bluesky-swiss-army/src/Referrer/index.web.ts34
-rw-r--r--modules/expo-bluesky-swiss-army/src/Referrer/types.ts17
-rw-r--r--plugins/withAppDelegateReferrer.js41
-rw-r--r--src/Navigation.tsx15
-rw-r--r--src/lib/hooks/useIntentHandler.ts15
-rw-r--r--src/lib/statsig/events.ts5
-rw-r--r--src/lib/statsig/statsig.tsx21
12 files changed, 213 insertions, 34 deletions
diff --git a/app.config.js b/app.config.js
index 1467f762f..cd8a4b034 100644
--- a/app.config.js
+++ b/app.config.js
@@ -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: {
diff --git a/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/referrer/ExpoBlueskyReferrerModule.kt b/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/referrer/ExpoBlueskyReferrerModule.kt
index ac6ed90b8..bac555233 100644
--- a/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/referrer/ExpoBlueskyReferrerModule.kt
+++ b/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/referrer/ExpoBlueskyReferrerModule.kt
@@ -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(
diff --git a/modules/expo-bluesky-swiss-army/src/Referrer/index.android.ts b/modules/expo-bluesky-swiss-army/src/Referrer/index.android.ts
index 06dfd2d09..ec2bcb57d 100644
--- a/modules/expo-bluesky-swiss-army/src/Referrer/index.android.ts
+++ b/modules/expo-bluesky-swiss-army/src/Referrer/index.android.ts
@@ -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()
+}
diff --git a/modules/expo-bluesky-swiss-army/src/Referrer/index.ios.ts b/modules/expo-bluesky-swiss-army/src/Referrer/index.ios.ts
new file mode 100644
index 000000000..2bf1497a0
--- /dev/null
+++ b/modules/expo-bluesky-swiss-army/src/Referrer/index.ios.ts
@@ -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
+}
diff --git a/modules/expo-bluesky-swiss-army/src/Referrer/index.ts b/modules/expo-bluesky-swiss-army/src/Referrer/index.ts
index 255398552..a60f7b6db 100644
--- a/modules/expo-bluesky-swiss-army/src/Referrer/index.ts
+++ b/modules/expo-bluesky-swiss-army/src/Referrer/index.ts
@@ -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()
+}
diff --git a/modules/expo-bluesky-swiss-army/src/Referrer/index.web.ts b/modules/expo-bluesky-swiss-army/src/Referrer/index.web.ts
new file mode 100644
index 000000000..76f03e7c8
--- /dev/null
+++ b/modules/expo-bluesky-swiss-army/src/Referrer/index.web.ts
@@ -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
+}
diff --git a/modules/expo-bluesky-swiss-army/src/Referrer/types.ts b/modules/expo-bluesky-swiss-army/src/Referrer/types.ts
index 55faaff4d..921e3a692 100644
--- a/modules/expo-bluesky-swiss-army/src/Referrer/types.ts
+++ b/modules/expo-bluesky-swiss-army/src/Referrer/types.ts
@@ -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
+}
diff --git a/plugins/withAppDelegateReferrer.js b/plugins/withAppDelegateReferrer.js
new file mode 100644
index 000000000..de773df07
--- /dev/null
+++ b/plugins/withAppDelegateReferrer.js
@@ -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
+  })
+}
diff --git a/src/Navigation.tsx b/src/Navigation.tsx
index 495435122..8c815a3fe 100644
--- a/src/Navigation.tsx
+++ b/src/Navigation.tsx
@@ -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
diff --git a/src/lib/hooks/useIntentHandler.ts b/src/lib/hooks/useIntentHandler.ts
index 8741530b5..3235e1a6a 100644
--- a/src/lib/hooks/useIntentHandler.ts
+++ b/src/lib/hooks/useIntentHandler.ts
@@ -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
diff --git a/src/lib/statsig/events.ts b/src/lib/statsig/events.ts
index 4946fb7f2..159061eac 100644
--- a/src/lib/statsig/events.ts
+++ b/src/lib/statsig/events.ts
@@ -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': {}
diff --git a/src/lib/statsig/statsig.tsx b/src/lib/statsig/statsig.tsx
index 94a1e63d0..81707d2bf 100644
--- a/src/lib/statsig/statsig.tsx
+++ b/src/lib/statsig/statsig.tsx
@@ -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,