about summary refs log tree commit diff
path: root/modules
diff options
context:
space:
mode:
authorHailey <me@haileyok.com>2024-06-21 21:38:04 -0700
committerGitHub <noreply@github.com>2024-06-21 21:38:04 -0700
commitf089f4578131e83cd177b7809ce0f7b75779dfdc (patch)
tree51978aede2040fb8dc319f0749d3de77c7811fbe /modules
parent35f64535cb8dfa0fe46e740a6398f3b991ecfbc7 (diff)
downloadvoidsky-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')
-rw-r--r--modules/BlueskyClip/AppDelegate.swift32
-rw-r--r--modules/BlueskyClip/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.pngbin0 -> 473960 bytes
-rw-r--r--modules/BlueskyClip/Images.xcassets/AppIcon.appiconset/Contents.json14
-rw-r--r--modules/BlueskyClip/Images.xcassets/Contents.json6
-rw-r--r--modules/BlueskyClip/ViewController.swift133
-rw-r--r--modules/expo-bluesky-swiss-army/android/build.gradle47
-rw-r--r--modules/expo-bluesky-swiss-army/android/src/main/AndroidManifest.xml2
-rw-r--r--modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/deviceprefs/ExpoBlueskyDevicePrefsModule.kt10
-rw-r--r--modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/referrer/ExpoBlueskyReferrerModule.kt54
-rw-r--r--modules/expo-bluesky-swiss-army/expo-module.config.json12
-rw-r--r--modules/expo-bluesky-swiss-army/index.ts4
-rw-r--r--modules/expo-bluesky-swiss-army/ios/DevicePrefs/ExpoBlueskyDevicePrefsModule.swift23
-rw-r--r--modules/expo-bluesky-swiss-army/ios/ExpoBlueskySwissArmy.podspec21
-rw-r--r--modules/expo-bluesky-swiss-army/ios/Referrer/ExpoBlueskyReferrerModule.swift7
-rw-r--r--modules/expo-bluesky-swiss-army/src/DevicePrefs/index.ios.ts18
-rw-r--r--modules/expo-bluesky-swiss-army/src/DevicePrefs/index.ts16
-rw-r--r--modules/expo-bluesky-swiss-army/src/NotImplemented.ts16
-rw-r--r--modules/expo-bluesky-swiss-army/src/Referrer/index.android.ts9
-rw-r--r--modules/expo-bluesky-swiss-army/src/Referrer/index.ts7
-rw-r--r--modules/expo-bluesky-swiss-army/src/Referrer/types.ts7
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