diff --git a/modules/expo-bluesky-swiss-army/android/build.gradle b/modules/expo-bluesky-swiss-army/android/build.gradle new file mode 100644 index 0000000000..4a823450c7 --- /dev/null +++ b/modules/expo-bluesky-swiss-army/android/build.gradle @@ -0,0 +1,43 @@ +apply plugin: 'com.android.library' + +group = 'expo.modules.blueskyswissarmy' +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.blueskyswissarmy" + defaultConfig { + versionCode 1 + versionName "0.6.0" + } + lintOptions { + abortOnError false + } +} diff --git a/modules/expo-bluesky-swiss-army/android/src/main/AndroidManifest.xml b/modules/expo-bluesky-swiss-army/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..bdae66c8f5 --- /dev/null +++ b/modules/expo-bluesky-swiss-army/android/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/ExpoBlueskySwissArmyModule.kt b/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/ExpoBlueskySwissArmyModule.kt new file mode 100644 index 0000000000..0410240ff3 --- /dev/null +++ b/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/ExpoBlueskySwissArmyModule.kt @@ -0,0 +1,10 @@ +package expo.modules.blueskyswissarmy + +import expo.modules.kotlin.modules.Module +import expo.modules.kotlin.modules.ModuleDefinition + +class ExpoBlueskySwissArmyModule : Module() { + override fun definition() = ModuleDefinition { + Name("ExpoBlueskySwissArmy") + } +} diff --git a/modules/expo-bluesky-swiss-army/expo-module.config.json b/modules/expo-bluesky-swiss-army/expo-module.config.json new file mode 100644 index 0000000000..74e6a561ad --- /dev/null +++ b/modules/expo-bluesky-swiss-army/expo-module.config.json @@ -0,0 +1,9 @@ +{ + "platforms": ["ios", "tvos", "android", "web"], + "ios": { + "modules": ["ExpoBlueskySwissArmyModule"] + }, + "android": { + "modules": ["expo.modules.blueskyswissarmy.ExpoBlueskySwissArmyModule"] + } +} diff --git a/modules/expo-bluesky-swiss-army/index.ts b/modules/expo-bluesky-swiss-army/index.ts new file mode 100644 index 0000000000..e72eae7dfd --- /dev/null +++ b/modules/expo-bluesky-swiss-army/index.ts @@ -0,0 +1,2 @@ +import {ExpoBlueskySwissArmyModule} from './src/ExpoBlueskySwissArmy' +export {ExpoBlueskySwissArmyModule as SwissArmyKnife} diff --git a/modules/expo-bluesky-swiss-army/ios/ExpoBlueskySwissArmy.podspec b/modules/expo-bluesky-swiss-army/ios/ExpoBlueskySwissArmy.podspec new file mode 100644 index 0000000000..be4b0eae45 --- /dev/null +++ b/modules/expo-bluesky-swiss-army/ios/ExpoBlueskySwissArmy.podspec @@ -0,0 +1,21 @@ +Pod::Spec.new do |s| + s.name = 'ExpoBlueskySwissArmy' + s.version = '1.0.0' + s.summary = 'A collection of native tools for Bluesky' + s.description = 'A collection of native tools for Bluesky' + s.author = '' + s.homepage = 'https://github.com/bluesky-social/social-app' + 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 diff --git a/modules/expo-bluesky-swiss-army/ios/ExpoBlueskySwissArmyModule.swift b/modules/expo-bluesky-swiss-army/ios/ExpoBlueskySwissArmyModule.swift new file mode 100644 index 0000000000..ddf7f5055c --- /dev/null +++ b/modules/expo-bluesky-swiss-army/ios/ExpoBlueskySwissArmyModule.swift @@ -0,0 +1,26 @@ +import ExpoModulesCore + +public class ExpoBlueskySwissArmyModule: Module { + func getDefaults(_ useAppGroup: Bool) -> UserDefaults? { + if useAppGroup { + return UserDefaults(suiteName: "group.app.bsky") + } else { + return UserDefaults.standard + } + } + + public func definition() -> ModuleDefinition { + Name("ExpoBlueskySwissArmy") + + AsyncFunction("getStringValueAsync") { (key: String, useAppGroup: Bool) in + let defaults = self.getDefaults(useAppGroup) + + return defaults?.string(forKey: key) + } + + AsyncFunction("setStringValueAsync") { (key: String, value: String?, useAppGroup: Bool) in + let defaults = self.getDefaults(useAppGroup) + defaults?.setValue(value, forKey: key) + } + } +} diff --git a/modules/expo-bluesky-swiss-army/src/ExpoBlueskySwissArmy.ts b/modules/expo-bluesky-swiss-army/src/ExpoBlueskySwissArmy.ts new file mode 100644 index 0000000000..30b75b856f --- /dev/null +++ b/modules/expo-bluesky-swiss-army/src/ExpoBlueskySwissArmy.ts @@ -0,0 +1,13 @@ +type ExpoBlueskySwissArmyModuleType = { + getStringValueAsync( + key: string, + useAppGroup?: boolean, + ): Promise + setStringValueAsync( + key: string, + value: string | null, + useAppGroup?: boolean, + ): Promise +} + +export const ExpoBlueskySwissArmyModule = {} as ExpoBlueskySwissArmyModuleType diff --git a/modules/expo-bluesky-swiss-army/src/ExpoBlueskySwissArmyModule.android.ts b/modules/expo-bluesky-swiss-army/src/ExpoBlueskySwissArmyModule.android.ts new file mode 100644 index 0000000000..cd3f9e59c1 --- /dev/null +++ b/modules/expo-bluesky-swiss-army/src/ExpoBlueskySwissArmyModule.android.ts @@ -0,0 +1,8 @@ +export const ExpoBlueskySwissArmyModule = { + getStringValueAsync(_: string, _?: boolean): Promise { + throw new Error('getStringValueAsync is not available on Android') + }, + setStringValueAsync(_: string, _: string | null, _?: boolean): Promise { + throw new Error('setStringValueAsync is not available on Android') + }, +} diff --git a/modules/expo-bluesky-swiss-army/src/ExpoBlueskySwissArmyModule.ios.ts b/modules/expo-bluesky-swiss-army/src/ExpoBlueskySwissArmyModule.ios.ts new file mode 100644 index 0000000000..d1c2b696e5 --- /dev/null +++ b/modules/expo-bluesky-swiss-army/src/ExpoBlueskySwissArmyModule.ios.ts @@ -0,0 +1,16 @@ +import {requireNativeModule} from 'expo-modules-core' + +const NativeModule = requireNativeModule('ExpoBlueskySwissArmy') + +export const ExpoBlueskySwissArmyModule = { + getStringValueAsync(key: string, useAppGroup = false): Promise { + return NativeModule.getStringValueAsync(key, useAppGroup) + }, + setStringValueAsync( + key: string, + value: string | null, + useAppGroup = false, + ): Promise { + return NativeModule.setStringValueAsync(key, value, useAppGroup) + }, +} diff --git a/modules/expo-bluesky-swiss-army/src/ExpoBlueskySwissArmyModule.web.ts b/modules/expo-bluesky-swiss-army/src/ExpoBlueskySwissArmyModule.web.ts new file mode 100644 index 0000000000..8068da1407 --- /dev/null +++ b/modules/expo-bluesky-swiss-army/src/ExpoBlueskySwissArmyModule.web.ts @@ -0,0 +1,8 @@ +export const ExpoBlueskySwissArmyModule = { + getStringValueAsync(_: string, _?: boolean): Promise { + throw new Error('getStringValueAsync is not available on web') + }, + setStringValueAsync(_: string, _: string | null, _?: boolean): Promise { + throw new Error('setStringValueAsync is not available on web') + }, +} diff --git a/src/components/hooks/useStarterPackEntry.android.ts b/src/components/hooks/useStarterPackEntry.android.ts deleted file mode 100644 index 09664db1b1..0000000000 --- a/src/components/hooks/useStarterPackEntry.android.ts +++ /dev/null @@ -1,45 +0,0 @@ -import React from 'react' - -import {makeStarterPackLink} from 'lib/routes/links' -import {useSetCurrentStarterPack} from 'state/preferences/starter-pack' -import {useUsedStarterPacks} from 'state/preferences/used-starter-packs' -import GooglePlayReferrer from '../../../modules/expo-google-play-referrer' - -export function useStarterPackEntry() { - const [ready, setReady] = React.useState(false) - const setCurrentStarterPack = useSetCurrentStarterPack() - const usedStarterPacks = useUsedStarterPacks() - - React.useEffect(() => { - if (ready) return - ;(async () => { - const res = await GooglePlayReferrer.getReferrerInfoAsync() - - if (res && res.installReferrer) { - // The `utm_content` parameter should be `starterpack--rkey>`. Let's try to get it from the parameters - // and create a URI for it if it's valid. - const parts = res.installReferrer.split('&') - const starterPackSource = parts - .find(part => part.startsWith('utm_content=')) - ?.split('=')[1] - const sourceParts = starterPackSource?.split('-') - - if (sourceParts?.length === 3 && sourceParts[0] === 'starterpack') { - // Android doesn't clear the referrer info for a total of 90 days. We want to make sure that we don't - // retrigger this on accident. - // We won't actually set `lastUsedUri` until a _successful_ use of the starter pack, meaning a new account - // has actually completed onboarding with the given starter pack. - const uri = makeStarterPackLink(sourceParts[1], sourceParts[2]) - if (!usedStarterPacks?.includes(uri)) { - setCurrentStarterPack({ - uri: makeStarterPackLink(sourceParts[1], sourceParts[2]), - }) - } - } - } - setReady(true) - })() - }, [ready, setCurrentStarterPack, usedStarterPacks]) - - return ready -} diff --git a/src/components/hooks/useStarterPackEntry.ios.ts b/src/components/hooks/useStarterPackEntry.ios.ts deleted file mode 100644 index 5ac77efddb..0000000000 --- a/src/components/hooks/useStarterPackEntry.ios.ts +++ /dev/null @@ -1,13 +0,0 @@ -import React from 'react' - -export function useStarterPackEntry() { - const [ready, setReady] = React.useState(false) - - React.useEffect(() => { - ;(async () => { - setReady(true) - })() - }, []) - - return ready -} diff --git a/src/components/hooks/useStarterPackEntry.native.ts b/src/components/hooks/useStarterPackEntry.native.ts new file mode 100644 index 0000000000..35711c2b95 --- /dev/null +++ b/src/components/hooks/useStarterPackEntry.native.ts @@ -0,0 +1,48 @@ +import React from 'react' + +import {makeStarterPackLink} from 'lib/routes/links' +import {isAndroid} from 'platform/detection' +import {useSetCurrentStarterPack} from 'state/preferences/starter-pack' +import {useUsedStarterPacks} from 'state/preferences/used-starter-packs' +import {SwissArmyKnife} from '../../../modules/expo-bluesky-swiss-army' +import GooglePlayReferrer from '../../../modules/expo-google-play-referrer' + +export function useStarterPackEntry() { + const [ready, setReady] = React.useState(false) + const setCurrentStarterPack = useSetCurrentStarterPack() + const usedStarterPacks = useUsedStarterPacks() + + React.useEffect(() => { + if (ready) return + ;(async () => { + let uri: string | null | undefined + + if (isAndroid) { + const res = await GooglePlayReferrer.getReferrerInfoAsync() + + if (res && res.installReferrer) { + const parts = res.installReferrer.split('&') + const starterPackSource = parts + .find(part => part.startsWith('utm_content=')) + ?.split('=')[1] + const sourceParts = starterPackSource?.split('-') + if (sourceParts?.length === 3 && sourceParts[0] === 'starterpack') { + uri = makeStarterPackLink(sourceParts[1], sourceParts[2]) + } + } + } else { + uri = await SwissArmyKnife.getStringValueAsync('starterPackUri', true) + } + + if (uri && !usedStarterPacks?.includes(uri)) { + setCurrentStarterPack({ + uri, + }) + } + + setReady(true) + })() + }, [ready, setCurrentStarterPack, usedStarterPacks]) + + return ready +}