diff --git a/package.json b/package.json index c193e2f774..a88f31ebf9 100644 --- a/package.json +++ b/package.json @@ -105,6 +105,7 @@ "expo-image": "~1.10.3", "expo-image-manipulator": "^11.8.0", "expo-image-picker": "~14.7.1", + "expo-linking": "^6.2.2", "expo-localization": "~14.8.2", "expo-media-library": "~15.9.1", "expo-notifications": "~0.27.3", @@ -161,6 +162,7 @@ "react-native-safe-area-context": "4.8.2", "react-native-screens": "~3.29.0", "react-native-svg": "14.1.0", + "react-native-ui-text-view": "link:./modules/react-native-ui-text-view", "react-native-url-polyfill": "^1.3.0", "react-native-uuid": "^2.0.1", "react-native-version-number": "^0.3.6", @@ -175,8 +177,7 @@ "tlds": "^1.234.0", "use-deep-compare": "^1.1.0", "zeego": "^1.6.2", - "zod": "^3.20.2", - "react-native-ui-text-view": "link:./modules/react-native-ui-text-view" + "zod": "^3.20.2" }, "devDependencies": { "@atproto/dev-env": "^0.2.28", diff --git a/src/App.native.tsx b/src/App.native.tsx index 41b78fc98a..38ae73db79 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -45,6 +45,7 @@ import {Splash} from '#/Splash' import {Provider as PortalProvider} from '#/components/Portal' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {useUTMSetup} from '#/lib/utm' SplashScreen.preventAutoHideAsync() @@ -54,6 +55,7 @@ function InnerApp() { const {resumeSession} = useSessionApi() const theme = useColorModeTheme(colorMode) const {_} = useLingui() + useUTMSetup() // init useEffect(() => { diff --git a/src/App.web.tsx b/src/App.web.tsx index 1efa0567c7..3db91b4807 100644 --- a/src/App.web.tsx +++ b/src/App.web.tsx @@ -32,12 +32,14 @@ import { import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread' import * as persisted from '#/state/persisted' import {Provider as PortalProvider} from '#/components/Portal' +import {useUTMSetup} from '#/lib/utm' function InnerApp() { const {isInitialLoad, currentAccount} = useSession() const {resumeSession} = useSessionApi() const colorMode = useColorMode() const theme = useColorModeTheme(colorMode) + useUTMSetup() // init useEffect(() => { diff --git a/src/lib/utm.ts b/src/lib/utm.ts new file mode 100644 index 0000000000..0e41237204 --- /dev/null +++ b/src/lib/utm.ts @@ -0,0 +1,91 @@ +import React from 'react' +import {nanoid} from 'nanoid/non-secure' +import * as Linking from 'expo-linking' + +import {logger} from '#/logger' +import {load, save} from '#/lib/storage' + +const STORAGE_KEY = 'BSKY_UTM_PARAMS' + +let PARAMS: Record = {} + +async function loadParams() { + PARAMS = (await load(STORAGE_KEY)) || {} +} + +async function upsertParams(params: Record) { + await loadParams() // make sure we're ready + + for (const key in params) { + const value = params[key] + + // only overwrite if we have a new value + if (value !== null && value !== undefined) { + PARAMS[key] = value + } + } + + await save(STORAGE_KEY, PARAMS) + + logger.debug('utm: updated', {params: PARAMS}) +} + +export function handleIncomingURL(url: string) { + const {searchParams} = new URL(url, 'http://throwaway.com') + const params = Object.fromEntries( + Array.from(searchParams.entries()).filter(([key]) => + key.startsWith('utm_'), + ), + ) + + upsertParams(params) +} + +export function getParams() { + return PARAMS +} + +const UTM_TO_MP: {[key: string]: string} = { + utm_id: 'cid', + utm_source: 'cs', + utm_medium: 'cm', + utm_campaign: 'cn', + utm_term: 'ck', + utm_content: 'cc', + // unsure how to map these + // utm_source_platform: '', + // utm_creative_format: '', + // utm_marketing_tactic: '', +} + +export function getUTMParamsAsMeasurementProtocol() { + return Object.entries(PARAMS).reduce( + (acc, [key, value]) => { + acc[UTM_TO_MP[key]] = value + return acc + }, + { + v: 1, + tid: 'UA-xxx', // TODO + t: 'event', + } as Record, + ) +} + +export function emitEvent() { + const params = getUTMParamsAsMeasurementProtocol() + // TODO events? + const payload = new URLSearchParams(params).toString() + const url = new URL('/collect', 'https://www.google-analytics.com') + url.searchParams.set('payload_data', encodeURIComponent(payload)) + url.searchParams.set('z', Date.now().toString() + '-' + nanoid()) // TODO + + fetch(url.toString()).catch(() => {}) +} + +export function useUTMSetup() { + const url = Linking.useURL() + React.useEffect(() => { + if (url) handleIncomingURL(url) + }, [url]) +} diff --git a/yarn.lock b/yarn.lock index d26d098729..0c89fde1f8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11725,6 +11725,14 @@ expo-keep-awake@~12.8.1: resolved "https://registry.yarnpkg.com/expo-keep-awake/-/expo-keep-awake-12.8.1.tgz#3c8df9d86c265741b5e7bdd36965aa0c6fc17df0" integrity sha512-P/VZFV02Rzgj13skMwH+ceGOGZSEdaUu5n7pCS3wThh2LppZjPJ7sBxUwyzeLa3DXEVUtwLZi+BiQ91wPwy9Gg== +expo-linking@^6.2.2: + version "6.2.2" + resolved "https://registry.yarnpkg.com/expo-linking/-/expo-linking-6.2.2.tgz#b7e148068ae49fd9ad814428c16fdf7a236e8aca" + integrity sha512-FEe6lP4f7xFT/vjoHRG+tt6EPVtkEGaWNK1smpaUevmNdyCJKqW0PDB8o8sfG6y7fly8ULe8qg3HhKh5J7aqUQ== + dependencies: + expo-constants "~15.4.3" + invariant "^2.2.4" + expo-localization@~14.8.2: version "14.8.2" resolved "https://registry.yarnpkg.com/expo-localization/-/expo-localization-14.8.2.tgz#e0bbed2293265834d21a1c58d3a5f8d265bd04ae"