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>
This commit is contained in:
parent
35f64535cb
commit
f089f45781
115 changed files with 6336 additions and 237 deletions
32
modules/BlueskyClip/AppDelegate.swift
Normal file
32
modules/BlueskyClip/AppDelegate.swift
Normal file
|
@ -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
|
||||
}
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 463 KiB |
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "App-Icon-1024x1024@1x.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
6
modules/BlueskyClip/Images.xcassets/Contents.json
Normal file
6
modules/BlueskyClip/Images.xcassets/Contents.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
133
modules/BlueskyClip/ViewController.swift
Normal file
133
modules/BlueskyClip/ViewController.swift
Normal file
|
@ -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?
|
||||
}
|
47
modules/expo-bluesky-swiss-army/android/build.gradle
Normal file
47
modules/expo-bluesky-swiss-army/android/build.gradle
Normal file
|
@ -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")
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
<manifest>
|
||||
</manifest>
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
12
modules/expo-bluesky-swiss-army/expo-module.config.json
Normal file
12
modules/expo-bluesky-swiss-army/expo-module.config.json
Normal file
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
4
modules/expo-bluesky-swiss-army/index.ts
Normal file
4
modules/expo-bluesky-swiss-army/index.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
import * as DevicePrefs from './src/DevicePrefs'
|
||||
import * as Referrer from './src/Referrer'
|
||||
|
||||
export {DevicePrefs, Referrer}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -0,0 +1,7 @@
|
|||
import ExpoModulesCore
|
||||
|
||||
public class ExpoBlueskyReferrerModule: Module {
|
||||
public func definition() -> ModuleDefinition {
|
||||
Name("ExpoBlueskyReferrer")
|
||||
}
|
||||
}
|
18
modules/expo-bluesky-swiss-army/src/DevicePrefs/index.ios.ts
Normal file
18
modules/expo-bluesky-swiss-army/src/DevicePrefs/index.ios.ts
Normal file
|
@ -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)
|
||||
}
|
16
modules/expo-bluesky-swiss-army/src/DevicePrefs/index.ts
Normal file
16
modules/expo-bluesky-swiss-army/src/DevicePrefs/index.ts
Normal file
|
@ -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})
|
||||
}
|
16
modules/expo-bluesky-swiss-army/src/NotImplemented.ts
Normal file
16
modules/expo-bluesky-swiss-army/src/NotImplemented.ts
Normal file
|
@ -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')
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
7
modules/expo-bluesky-swiss-army/src/Referrer/index.ts
Normal file
7
modules/expo-bluesky-swiss-army/src/Referrer/index.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import {NotImplementedError} from '../NotImplemented'
|
||||
import {GooglePlayReferrerInfo} from './types'
|
||||
|
||||
// @ts-ignore throws
|
||||
export function getGooglePlayReferrerInfoAsync(): Promise<GooglePlayReferrerInfo> {
|
||||
throw new NotImplementedError()
|
||||
}
|
7
modules/expo-bluesky-swiss-army/src/Referrer/types.ts
Normal file
7
modules/expo-bluesky-swiss-army/src/Referrer/types.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
export type GooglePlayReferrerInfo =
|
||||
| {
|
||||
installReferrer?: string
|
||||
clickTimestamp?: number
|
||||
installTimestamp?: number
|
||||
}
|
||||
| undefined
|
Loading…
Add table
Add a link
Reference in a new issue