referrers for all platforms
This commit is contained in:
@@ -209,6 +209,7 @@ module.exports = function (config) {
|
||||
'./plugins/withAndroidSplashScreenStatusBarTranslucentPlugin.js',
|
||||
'./plugins/shareExtension/withShareExtensions.js',
|
||||
'./plugins/notificationsExtension/withNotificationsExtension.js',
|
||||
'./plugins/withAppDelegateReferrer.js',
|
||||
].filter(Boolean),
|
||||
extra: {
|
||||
eas: {
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
apply plugin: 'com.android.library'
|
||||
|
||||
group = 'expo.modules.getreferrer'
|
||||
version = '0.6.0'
|
||||
|
||||
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
|
||||
apply from: expoModulesCorePlugin
|
||||
applyKotlinExpoModulesCorePlugin()
|
||||
useCoreDependencies()
|
||||
useExpoPublishing()
|
||||
|
||||
// If you want to use the managed Android SDK versions from expo-modules-core, set this to true.
|
||||
// The Android SDK versions will be bumped from time to time in SDK releases and may introduce breaking changes in your module code.
|
||||
// Most of the time, you may like to manage the Android SDK versions yourself.
|
||||
def useManagedAndroidSdkVersions = false
|
||||
if (useManagedAndroidSdkVersions) {
|
||||
useDefaultAndroidSdkVersions()
|
||||
} else {
|
||||
buildscript {
|
||||
// Simple helper that allows the root project to override versions declared by this library.
|
||||
ext.safeExtGet = { prop, fallback ->
|
||||
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
||||
}
|
||||
}
|
||||
project.android {
|
||||
compileSdkVersion safeExtGet("compileSdkVersion", 34)
|
||||
defaultConfig {
|
||||
minSdkVersion safeExtGet("minSdkVersion", 21)
|
||||
targetSdkVersion safeExtGet("targetSdkVersion", 34)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
namespace "expo.modules.getreferrer"
|
||||
defaultConfig {
|
||||
versionCode 1
|
||||
versionName "0.6.0"
|
||||
}
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("com.android.installreferrer:installreferrer:2.2")
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
<manifest>
|
||||
</manifest>
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
package expo.modules.getreferrer
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import com.android.installreferrer.api.InstallReferrerClient
|
||||
import com.android.installreferrer.api.InstallReferrerStateListener
|
||||
import expo.modules.kotlin.modules.Module
|
||||
import expo.modules.kotlin.modules.ModuleDefinition
|
||||
import expo.modules.kotlin.Promise
|
||||
|
||||
class ExpoGetReferrerModule : Module() {
|
||||
private var intent: Intent? = null
|
||||
private var referrer: Uri? = null
|
||||
|
||||
override fun definition() = ModuleDefinition {
|
||||
Name("ExpoGetReferrer")
|
||||
|
||||
OnNewIntent {
|
||||
intent = it
|
||||
referrer = 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) {
|
||||
return@AsyncFunction mapOf(
|
||||
"referrer" to intentReferrer.toString(),
|
||||
"hostname" to intentReferrer.host,
|
||||
)
|
||||
}
|
||||
|
||||
// In all other cases, we'll just record the app that sent the intent.
|
||||
if (referrer != null) {
|
||||
// referrer could become null here. `.toString()` though can be called on null
|
||||
return@AsyncFunction mapOf(
|
||||
"referrer" to referrer.toString(),
|
||||
"hostname" to (referrer?.host ?: ""),
|
||||
)
|
||||
}
|
||||
|
||||
return@AsyncFunction null
|
||||
}
|
||||
|
||||
AsyncFunction("getGooglePlayReferrerInfoAsync") { promise: Promise ->
|
||||
val referrerClient = InstallReferrerClient.newBuilder(appContext.reactContext).build()
|
||||
referrerClient.startConnection(object : InstallReferrerStateListener {
|
||||
override fun onInstallReferrerSetupFinished(responseCode: Int) {
|
||||
if (responseCode == InstallReferrerClient.InstallReferrerResponse.OK) {
|
||||
val response = referrerClient.installReferrer
|
||||
promise.resolve(
|
||||
mapOf(
|
||||
"installReferrer" to response.installReferrer,
|
||||
"clickTimestamp" to response.referrerClickTimestampSeconds,
|
||||
"installTimestamp" to response.installBeginTimestampSeconds
|
||||
)
|
||||
)
|
||||
} else {
|
||||
promise.reject(
|
||||
"ERR_GOOGLE_PLAY_REFERRER_UNKNOWN",
|
||||
"Failed to get referrer info",
|
||||
Exception("Failed to get referrer info")
|
||||
)
|
||||
}
|
||||
referrerClient.endConnection()
|
||||
}
|
||||
|
||||
override fun onInstallReferrerServiceDisconnected() {
|
||||
promise.reject(
|
||||
"ERR_GOOGLE_PLAY_REFERRER_DISCONNECTED",
|
||||
"Failed to get referrer info",
|
||||
Exception("Failed to get referrer info")
|
||||
)
|
||||
referrerClient.endConnection()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"platforms": ["ios", "tvos", "android", "web"],
|
||||
"ios": {
|
||||
"modules": ["ExpoGetReferrerModule"]
|
||||
},
|
||||
"android": {
|
||||
"modules": ["expo.modules.getreferrer.ExpoGetReferrerModule"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import {EventEmitter, NativeModulesProxy, Subscription} from 'expo-modules-core'
|
||||
|
||||
import {
|
||||
ChangeEventPayload,
|
||||
ExpoGetReferrerViewProps,
|
||||
} from './src/ExpoGetReferrer.types'
|
||||
// Import the native module. On web, it will be resolved to ExpoGetReferrer.web.ts
|
||||
// and on native platforms to ExpoGetReferrer.ts
|
||||
import ExpoGetReferrerModule from './src/ExpoGetReferrerModule'
|
||||
import ExpoGetReferrerView from './src/ExpoGetReferrerView'
|
||||
|
||||
// Get the native constant value.
|
||||
export const PI = ExpoGetReferrerModule.PI
|
||||
|
||||
export function hello(): string {
|
||||
return ExpoGetReferrerModule.hello()
|
||||
}
|
||||
|
||||
export async function setValueAsync(value: string) {
|
||||
return await ExpoGetReferrerModule.setValueAsync(value)
|
||||
}
|
||||
|
||||
const emitter = new EventEmitter(
|
||||
ExpoGetReferrerModule ?? NativeModulesProxy.ExpoGetReferrer,
|
||||
)
|
||||
|
||||
export function addChangeListener(
|
||||
listener: (event: ChangeEventPayload) => void,
|
||||
): Subscription {
|
||||
return emitter.addListener<ChangeEventPayload>('onChange', listener)
|
||||
}
|
||||
|
||||
export {ChangeEventPayload, ExpoGetReferrerView, ExpoGetReferrerViewProps}
|
||||
@@ -0,0 +1,21 @@
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'ExpoGetReferrer'
|
||||
s.version = '1.0.0'
|
||||
s.summary = 'A sample project summary'
|
||||
s.description = 'A sample project description'
|
||||
s.author = ''
|
||||
s.homepage = 'https://docs.expo.dev/modules/'
|
||||
s.platforms = { :ios => '13.4', :tvos => '13.4' }
|
||||
s.source = { git: '' }
|
||||
s.static_framework = true
|
||||
|
||||
s.dependency 'ExpoModulesCore'
|
||||
|
||||
# Swift/Objective-C compatibility
|
||||
s.pod_target_xcconfig = {
|
||||
'DEFINES_MODULE' => 'YES',
|
||||
'SWIFT_COMPILATION_MODE' => 'wholemodule'
|
||||
}
|
||||
|
||||
s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}"
|
||||
end
|
||||
@@ -0,0 +1,37 @@
|
||||
import UIKit
|
||||
import ExpoModulesCore
|
||||
|
||||
public class ExpoGetReferrerModule: Module {
|
||||
public func definition() -> ModuleDefinition {
|
||||
Name("ExpoGetReferrer")
|
||||
|
||||
AsyncFunction("getReferrerInfoAsync") { (promise: Promise) in
|
||||
let defaults = UserDefaults.standard
|
||||
let referrerUrlString = defaults.string(forKey: "referrer")
|
||||
let referrerApp = defaults.string(forKey: "referrerApp")
|
||||
|
||||
if let referrerUrlString = defaults.string(forKey: "referrer"),
|
||||
let url = URL(string: referrerUrlString)
|
||||
{
|
||||
if #available(iOS 16.0, *) {
|
||||
promise.resolve([
|
||||
"referrer": url.absoluteString,
|
||||
"hostname": url.host() ?? ""
|
||||
])
|
||||
} else {
|
||||
promise.resolve([
|
||||
"referrer": url.absoluteString,
|
||||
"hostname": url.host ?? ""
|
||||
])
|
||||
}
|
||||
} else if let referrerApp = defaults.string(forKey: "referrerApp") {
|
||||
promise.resolve([
|
||||
"referrer": referrerApp,
|
||||
"hostname": referrerApp,
|
||||
])
|
||||
} else {
|
||||
promise.resolve(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
export type ExpoGetReferrerModule = {
|
||||
getGooglePlayReferrerInfoAsync: () => Promise<GooglePlayReferrerInfo>
|
||||
getReferrerInfoAsync: () => Promise<{
|
||||
referrer: string
|
||||
hostname: string
|
||||
} | null>
|
||||
}
|
||||
|
||||
export type GooglePlayReferrerInfo =
|
||||
| {
|
||||
installReferrer?: string
|
||||
clickTimestamp?: number
|
||||
installTimestamp?: number
|
||||
}
|
||||
| undefined
|
||||
@@ -0,0 +1,14 @@
|
||||
import {requireNativeModule} from 'expo-modules-core'
|
||||
|
||||
import {ExpoGetReferrerModule} from './ExpoGetReferrer.types'
|
||||
|
||||
const NativeModule =
|
||||
requireNativeModule<ExpoGetReferrerModule>('ExpoGetReferrer')
|
||||
|
||||
export const GetReferrerModule: ExpoGetReferrerModule = {
|
||||
getGooglePlayReferrerInfoAsync: async () => {
|
||||
console.error('getGooglePlayReferrerInfo is only available on Android')
|
||||
throw new Error('getGooglePlayReferrerInfo is only available on Android')
|
||||
},
|
||||
getReferrerInfoAsync: NativeModule.getReferrerInfoAsync,
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import {requireNativeModule} from 'expo-modules-core'
|
||||
|
||||
import {ExpoGetReferrerModule} from './ExpoGetReferrer.types'
|
||||
|
||||
const NativeModule =
|
||||
requireNativeModule<ExpoGetReferrerModule>('ExpoGetReferrer')
|
||||
|
||||
export const GetReferrerModule: ExpoGetReferrerModule = {
|
||||
getGooglePlayReferrerInfoAsync: NativeModule.getGooglePlayReferrerInfoAsync,
|
||||
getReferrerInfoAsync: NativeModule.getReferrerInfoAsync,
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import {isWeb} from 'platform/detection'
|
||||
import {ExpoGetReferrerModule} from './ExpoGetReferrer.types'
|
||||
|
||||
export const GetReferrerModule: ExpoGetReferrerModule = {
|
||||
getGooglePlayReferrerInfoAsync: async () => {
|
||||
console.error('getReferrerInfoAsync is only available on Android')
|
||||
throw new Error('getGooglePlayReferrerInfo is only available on Android')
|
||||
},
|
||||
getReferrerInfoAsync: async () => {
|
||||
try {
|
||||
if (
|
||||
isWeb &&
|
||||
typeof document !== 'undefined' &&
|
||||
document != null &&
|
||||
document.referrer
|
||||
) {
|
||||
const url = new URL(document.referrer)
|
||||
if (url.hostname !== 'bsky.app') {
|
||||
return {
|
||||
referrer: url.href,
|
||||
hostname: url.hostname,
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
return null
|
||||
},
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
+13
-1
@@ -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'
|
||||
@@ -43,6 +43,7 @@ import HashtagScreen from '#/screens/Hashtag'
|
||||
import {ModerationScreen} from '#/screens/Moderation'
|
||||
import {ProfileKnownFollowersScreen} from '#/screens/Profile/KnownFollowers'
|
||||
import {ProfileLabelerLikedByScreen} from '#/screens/Profile/ProfileLabelerLikedBy'
|
||||
import {GetReferrerModule} from '../modules/expo-get-referrer/src/ExpoGetReferrerModule'
|
||||
import {init as initAnalytics} from './lib/analytics/analytics'
|
||||
import {useWebScrollRestoration} from './lib/hooks/useWebScrollRestoration'
|
||||
import {attachRouteToLogEvents, logEvent} from './lib/statsig/statsig'
|
||||
@@ -728,6 +729,17 @@ function logModuleInitTime() {
|
||||
initMs,
|
||||
})
|
||||
|
||||
if (isWeb) {
|
||||
GetReferrerModule.getReferrerInfoAsync().then(info => {
|
||||
if (info && info.hostname !== 'bsky.app') {
|
||||
logEvent('deepLink:referrerReceived', {
|
||||
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 {GetReferrerModule} from '../../../modules/expo-get-referrer/src/ExpoGetReferrerModule'
|
||||
|
||||
type IntentType = 'compose'
|
||||
|
||||
@@ -15,6 +18,17 @@ export function useIntentHandler() {
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleIncomingURL = (url: string) => {
|
||||
GetReferrerModule.getReferrerInfoAsync().then(info => {
|
||||
console.log(info)
|
||||
|
||||
if (info && info.hostname !== 'bsky.app') {
|
||||
logEvent('deepLink:referrerReceived', {
|
||||
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,10 @@ export type LogEvents = {
|
||||
}
|
||||
'state:foreground:sampled': {}
|
||||
'router:navigate:sampled': {}
|
||||
'deepLink:referrerReceived': {
|
||||
referrer: string
|
||||
hostname: string
|
||||
}
|
||||
|
||||
// Screen events
|
||||
'splash:signInPressed': {}
|
||||
|
||||
@@ -26,8 +26,6 @@ type StatsigUser = {
|
||||
bundleDate: number
|
||||
refSrc: string
|
||||
refUrl: string
|
||||
referrer: string
|
||||
referrerHostname: string
|
||||
appLanguage: string
|
||||
contentLanguages: string[]
|
||||
}
|
||||
@@ -35,29 +33,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[]) {
|
||||
@@ -217,8 +198,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,
|
||||
|
||||
Reference in New Issue
Block a user