my fork of the bluesky client

referrers for all platforms (#4514)

authored by hailey.at and committed by

GitHub 8b121af2 83e8522e

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