diff options
author | Hailey <me@haileyok.com> | 2024-06-21 21:38:04 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-06-21 21:38:04 -0700 |
commit | f089f4578131e83cd177b7809ce0f7b75779dfdc (patch) | |
tree | 51978aede2040fb8dc319f0749d3de77c7811fbe /modules | |
parent | 35f64535cb8dfa0fe46e740a6398f3b991ecfbc7 (diff) | |
download | voidsky-f089f4578131e83cd177b7809ce0f7b75779dfdc.tar.zst |
Starter Packs (#4332)
Co-authored-by: Dan Abramov <dan.abramov@gmail.com> Co-authored-by: Paul Frazee <pfrazee@gmail.com> Co-authored-by: Eric Bailey <git@esb.lol> Co-authored-by: Samuel Newman <mozzius@protonmail.com>
Diffstat (limited to 'modules')
20 files changed, 438 insertions, 0 deletions
diff --git a/modules/BlueskyClip/AppDelegate.swift b/modules/BlueskyClip/AppDelegate.swift new file mode 100644 index 000000000..684194953 --- /dev/null +++ b/modules/BlueskyClip/AppDelegate.swift @@ -0,0 +1,32 @@ +import UIKit + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + var window: UIWindow? + var controller: ViewController? + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + let window = UIWindow() + self.window = UIWindow() + + let controller = ViewController(window: window) + self.controller = controller + + window.rootViewController = self.controller + window.makeKeyAndVisible() + + return true + } + + func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { + self.controller?.handleURL(url: url) + return true + } + + func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool { + if let incomingURL = userActivity.webpageURL { + self.controller?.handleURL(url: incomingURL) + } + return true + } +} diff --git a/modules/BlueskyClip/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png b/modules/BlueskyClip/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png new file mode 100644 index 000000000..75ce4b813 --- /dev/null +++ b/modules/BlueskyClip/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png Binary files differdiff --git a/modules/BlueskyClip/Images.xcassets/AppIcon.appiconset/Contents.json b/modules/BlueskyClip/Images.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..c3bb428db --- /dev/null +++ b/modules/BlueskyClip/Images.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,14 @@ +{ + "images" : [ + { + "filename" : "App-Icon-1024x1024@1x.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/modules/BlueskyClip/Images.xcassets/Contents.json b/modules/BlueskyClip/Images.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/modules/BlueskyClip/Images.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/modules/BlueskyClip/ViewController.swift b/modules/BlueskyClip/ViewController.swift new file mode 100644 index 000000000..b178644b8 --- /dev/null +++ b/modules/BlueskyClip/ViewController.swift @@ -0,0 +1,133 @@ +import UIKit +import WebKit +import StoreKit + +class ViewController: UIViewController, WKScriptMessageHandler, WKNavigationDelegate { + let defaults = UserDefaults(suiteName: "group.app.bsky") + + var window: UIWindow + var webView: WKWebView? + + var prevUrl: URL? + var starterPackUrl: URL? + + init(window: UIWindow) { + self.window = window + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + let contentController = WKUserContentController() + contentController.add(self, name: "onMessage") + let configuration = WKWebViewConfiguration() + configuration.userContentController = contentController + + let webView = WKWebView(frame: self.view.bounds, configuration: configuration) + webView.translatesAutoresizingMaskIntoConstraints = false + webView.contentMode = .scaleToFill + webView.navigationDelegate = self + self.view.addSubview(webView) + self.webView = webView + } + + func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { + guard let response = message.body as? String, + let data = response.data(using: .utf8), + let payload = try? JSONDecoder().decode(WebViewActionPayload.self, from: data) else { + return + } + + switch payload.action { + case .present: + guard let url = self.starterPackUrl else { + return + } + + self.presentAppStoreOverlay() + defaults?.setValue(url.absoluteString, forKey: "starterPackUri") + + case .store: + guard let keyToStoreAs = payload.keyToStoreAs, let jsonToStore = payload.jsonToStore else { + return + } + + self.defaults?.setValue(jsonToStore, forKey: keyToStoreAs) + } + } + + func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction) async -> WKNavigationActionPolicy { + // Detect when we land on the right URL. This is incase of a short link opening the app clip + guard let url = navigationAction.request.url else { + return .allow + } + + // Store the previous one to compare later, but only set starterPackUrl when we find the right one + prevUrl = url + // pathComponents starts with "/" as the first component, then each path name. so... + // ["/", "start", "name", "rkey"] + if url.pathComponents.count == 4, + url.pathComponents[1] == "start" { + self.starterPackUrl = url + } + + return .allow + } + + func handleURL(url: URL) { + let urlString = "\(url.absoluteString)?clip=true" + if let url = URL(string: urlString) { + self.webView?.load(URLRequest(url: url)) + } + } + + func presentAppStoreOverlay() { + guard let windowScene = self.window.windowScene else { + return + } + + let configuration = SKOverlay.AppClipConfiguration(position: .bottomRaised) + let overlay = SKOverlay(configuration: configuration) + + overlay.present(in: windowScene) + } + + func getHost(_ url: URL?) -> String? { + if #available(iOS 16.0, *) { + return url?.host() + } else { + return url?.host + } + } + + func getQuery(_ url: URL?) -> String? { + if #available(iOS 16.0, *) { + return url?.query() + } else { + return url?.query + } + } + + func urlMatchesPrevious(_ url: URL?) -> Bool { + if #available(iOS 16.0, *) { + return url?.query() == prevUrl?.query() && url?.host() == prevUrl?.host() && url?.query() == prevUrl?.query() + } else { + return url?.query == prevUrl?.query && url?.host == prevUrl?.host && url?.query == prevUrl?.query + } + } +} + +struct WebViewActionPayload: Decodable { + enum Action: String, Decodable { + case present, store + } + + let action: Action + let keyToStoreAs: String? + let jsonToStore: String? +} 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 000000000..b031cde57 --- /dev/null +++ b/modules/expo-bluesky-swiss-army/android/build.gradle @@ -0,0 +1,47 @@ +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 + } +} + +dependencies { + implementation("com.android.installreferrer:installreferrer:2.2") +} 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 000000000..bdae66c8f --- /dev/null +++ b/modules/expo-bluesky-swiss-army/android/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ +<manifest> +</manifest> diff --git a/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/deviceprefs/ExpoBlueskyDevicePrefsModule.kt b/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/deviceprefs/ExpoBlueskyDevicePrefsModule.kt new file mode 100644 index 000000000..29017f17a --- /dev/null +++ b/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/deviceprefs/ExpoBlueskyDevicePrefsModule.kt @@ -0,0 +1,10 @@ +package expo.modules.blueskyswissarmy.deviceprefs + +import expo.modules.kotlin.modules.Module +import expo.modules.kotlin.modules.ModuleDefinition + +class ExpoBlueskyDevicePrefsModule : Module() { + override fun definition() = ModuleDefinition { + Name("ExpoBlueskyDevicePrefs") + } +} 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 new file mode 100644 index 000000000..3589b364e --- /dev/null +++ b/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/referrer/ExpoBlueskyReferrerModule.kt @@ -0,0 +1,54 @@ +package expo.modules.blueskyswissarmy.referrer + +import android.util.Log +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 ExpoBlueskyReferrerModule : Module() { + override fun definition() = ModuleDefinition { + Name("ExpoBlueskyReferrer") + + 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) { + Log.d("ExpoGooglePlayReferrer", "Successfully retrieved referrer info.") + + val response = referrerClient.installReferrer + Log.d("ExpoGooglePlayReferrer", "Install referrer: ${response.installReferrer}") + + promise.resolve( + mapOf( + "installReferrer" to response.installReferrer, + "clickTimestamp" to response.referrerClickTimestampSeconds, + "installTimestamp" to response.installBeginTimestampSeconds + ) + ) + } else { + Log.d("ExpoGooglePlayReferrer", "Failed to get referrer info. Unknown error.") + promise.reject( + "ERR_GOOGLE_PLAY_REFERRER_UNKNOWN", + "Failed to get referrer info", + Exception("Failed to get referrer info") + ) + } + referrerClient.endConnection() + } + + override fun onInstallReferrerServiceDisconnected() { + Log.d("ExpoGooglePlayReferrer", "Failed to get referrer info. Service disconnected.") + referrerClient.endConnection() + promise.reject( + "ERR_GOOGLE_PLAY_REFERRER_DISCONNECTED", + "Failed to get referrer info", + Exception("Failed to get referrer info") + ) + } + }) + } + } +} \ No newline at end of file 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 000000000..730bc6114 --- /dev/null +++ b/modules/expo-bluesky-swiss-army/expo-module.config.json @@ -0,0 +1,12 @@ +{ + "platforms": ["ios", "tvos", "android", "web"], + "ios": { + "modules": ["ExpoBlueskyDevicePrefsModule", "ExpoBlueskyReferrerModule"] + }, + "android": { + "modules": [ + "expo.modules.blueskyswissarmy.deviceprefs.ExpoBlueskyDevicePrefsModule", + "expo.modules.blueskyswissarmy.referrer.ExpoBlueskyReferrerModule" + ] + } +} diff --git a/modules/expo-bluesky-swiss-army/index.ts b/modules/expo-bluesky-swiss-army/index.ts new file mode 100644 index 000000000..1b2f89249 --- /dev/null +++ b/modules/expo-bluesky-swiss-army/index.ts @@ -0,0 +1,4 @@ +import * as DevicePrefs from './src/DevicePrefs' +import * as Referrer from './src/Referrer' + +export {DevicePrefs, Referrer} diff --git a/modules/expo-bluesky-swiss-army/ios/DevicePrefs/ExpoBlueskyDevicePrefsModule.swift b/modules/expo-bluesky-swiss-army/ios/DevicePrefs/ExpoBlueskyDevicePrefsModule.swift new file mode 100644 index 000000000..b13a9fe3f --- /dev/null +++ b/modules/expo-bluesky-swiss-army/ios/DevicePrefs/ExpoBlueskyDevicePrefsModule.swift @@ -0,0 +1,23 @@ +import ExpoModulesCore + +public class ExpoBlueskyDevicePrefsModule: Module { + func getDefaults(_ useAppGroup: Bool) -> UserDefaults? { + if useAppGroup { + return UserDefaults(suiteName: "group.app.bsky") + } else { + return UserDefaults.standard + } + } + + public func definition() -> ModuleDefinition { + Name("ExpoBlueskyDevicePrefs") + + AsyncFunction("getStringValueAsync") { (key: String, useAppGroup: Bool) in + return self.getDefaults(useAppGroup)?.string(forKey: key) + } + + AsyncFunction("setStringValueAsync") { (key: String, value: String?, useAppGroup: Bool) in + self.getDefaults(useAppGroup)?.setValue(value, forKey: key) + } + } +} 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 000000000..be4b0eae4 --- /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/Referrer/ExpoBlueskyReferrerModule.swift b/modules/expo-bluesky-swiss-army/ios/Referrer/ExpoBlueskyReferrerModule.swift new file mode 100644 index 000000000..fd28c51e6 --- /dev/null +++ b/modules/expo-bluesky-swiss-army/ios/Referrer/ExpoBlueskyReferrerModule.swift @@ -0,0 +1,7 @@ +import ExpoModulesCore + +public class ExpoBlueskyReferrerModule: Module { + public func definition() -> ModuleDefinition { + Name("ExpoBlueskyReferrer") + } +} diff --git a/modules/expo-bluesky-swiss-army/src/DevicePrefs/index.ios.ts b/modules/expo-bluesky-swiss-army/src/DevicePrefs/index.ios.ts new file mode 100644 index 000000000..427185086 --- /dev/null +++ b/modules/expo-bluesky-swiss-army/src/DevicePrefs/index.ios.ts @@ -0,0 +1,18 @@ +import {requireNativeModule} from 'expo-modules-core' + +const NativeModule = requireNativeModule('ExpoBlueskyDevicePrefs') + +export function getStringValueAsync( + key: string, + useAppGroup?: boolean, +): Promise<string | null> { + return NativeModule.getStringValueAsync(key, useAppGroup) +} + +export function setStringValueAsync( + key: string, + value: string | null, + useAppGroup?: boolean, +): Promise<void> { + return NativeModule.setStringValueAsync(key, value, useAppGroup) +} diff --git a/modules/expo-bluesky-swiss-army/src/DevicePrefs/index.ts b/modules/expo-bluesky-swiss-army/src/DevicePrefs/index.ts new file mode 100644 index 000000000..f1eee6c28 --- /dev/null +++ b/modules/expo-bluesky-swiss-army/src/DevicePrefs/index.ts @@ -0,0 +1,16 @@ +import {NotImplementedError} from '../NotImplemented' + +export function getStringValueAsync( + key: string, + useAppGroup?: boolean, +): Promise<string | null> { + throw new NotImplementedError({key, useAppGroup}) +} + +export function setStringValueAsync( + key: string, + value: string | null, + useAppGroup?: boolean, +): Promise<string | null> { + throw new NotImplementedError({key, value, useAppGroup}) +} diff --git a/modules/expo-bluesky-swiss-army/src/NotImplemented.ts b/modules/expo-bluesky-swiss-army/src/NotImplemented.ts new file mode 100644 index 000000000..876cd7b32 --- /dev/null +++ b/modules/expo-bluesky-swiss-army/src/NotImplemented.ts @@ -0,0 +1,16 @@ +import {Platform} from 'react-native' + +export class NotImplementedError extends Error { + constructor(params = {}) { + if (__DEV__) { + const caller = new Error().stack?.split('\n')[2] + super( + `Not implemented on ${Platform.OS}. Given params: ${JSON.stringify( + params, + )} ${caller}`, + ) + } else { + super('Not implemented') + } + } +} diff --git a/modules/expo-bluesky-swiss-army/src/Referrer/index.android.ts b/modules/expo-bluesky-swiss-army/src/Referrer/index.android.ts new file mode 100644 index 000000000..06dfd2d09 --- /dev/null +++ b/modules/expo-bluesky-swiss-army/src/Referrer/index.android.ts @@ -0,0 +1,9 @@ +import {requireNativeModule} from 'expo' + +import {GooglePlayReferrerInfo} from './types' + +export const NativeModule = requireNativeModule('ExpoBlueskyReferrer') + +export function getGooglePlayReferrerInfoAsync(): Promise<GooglePlayReferrerInfo> { + return NativeModule.getGooglePlayReferrerInfoAsync() +} diff --git a/modules/expo-bluesky-swiss-army/src/Referrer/index.ts b/modules/expo-bluesky-swiss-army/src/Referrer/index.ts new file mode 100644 index 000000000..255398552 --- /dev/null +++ b/modules/expo-bluesky-swiss-army/src/Referrer/index.ts @@ -0,0 +1,7 @@ +import {NotImplementedError} from '../NotImplemented' +import {GooglePlayReferrerInfo} from './types' + +// @ts-ignore throws +export function getGooglePlayReferrerInfoAsync(): Promise<GooglePlayReferrerInfo> { + throw new NotImplementedError() +} diff --git a/modules/expo-bluesky-swiss-army/src/Referrer/types.ts b/modules/expo-bluesky-swiss-army/src/Referrer/types.ts new file mode 100644 index 000000000..55faaff4d --- /dev/null +++ b/modules/expo-bluesky-swiss-army/src/Referrer/types.ts @@ -0,0 +1,7 @@ +export type GooglePlayReferrerInfo = + | { + installReferrer?: string + clickTimestamp?: number + installTimestamp?: number + } + | undefined |