present app clip

This commit is contained in:
Hailey
2024-06-16 21:10:37 -07:00
parent 07696d8eb7
commit a09d699249
3 changed files with 154 additions and 13 deletions
+1 -1
View File
@@ -10,7 +10,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
self.window = UIWindow()
self.controller = ViewController()
self.controller = ViewController(window: self.window!)
self.window?.rootViewController = self.controller
self.window?.makeKeyAndVisible()
+73 -4
View File
@@ -1,26 +1,95 @@
import UIKit
import WebKit
import StoreKit
class ViewController: UIViewController {
class ViewController: UIViewController, WKScriptMessageHandler {
let defaults = UserDefaults(suiteName: "group.app.bsky")
var window: UIWindow
var webView: WKWebView?
var userContentController: WKUserContentController?
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 webView = WKWebView(frame: self.view.bounds)
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
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, allowLossyConversion: false),
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: "appClipStarterPackUri")
case .store:
guard let keyToStoreAs = payload.keyToStoreAs, let jsonToStore = payload.jsonToStore else {
return
}
self.defaults?.setValue(jsonToStore, forKey: keyToStoreAs)
}
}
func handleURL(url: URL) {
self.starterPackUrl = 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)
}
}
struct WebViewActionPayload: Decodable {
enum Action: String, Decodable {
case present, store
}
let action: Action
let keyToStoreAs: String?
let jsonToStore: String?
}
@@ -1,5 +1,6 @@
import React from 'react'
import {ScrollView, View} from 'react-native'
import {Pressable, ScrollView, View} from 'react-native'
import Animated, {FadeIn, FadeOut} from 'react-native-reanimated'
import {AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
@@ -23,6 +24,8 @@ import {ListMaybePlaceholder} from '#/components/Lists'
import {Default as ProfileCardInner} from '#/components/ProfileCard'
import {Text} from '#/components/Typography'
const AnimatedPressable = Animated.createAnimatedComponent(Pressable)
function parseStarterPackHttpUri(uri: string): {name?: string; rkey?: string} {
const parsed = new URL(uri)
const [_, _path, name, rkey] = parsed.pathname.split('/')
@@ -32,6 +35,16 @@ function parseStarterPackHttpUri(uri: string): {name?: string; rkey?: string} {
}
}
interface AppClipMessage {
action: 'present' | 'store'
keyToStoreAs?: string
jsonToStore?: string
}
function postAppClipMessage(message: AppClipMessage) {
window.webkit.messageHandlers.onMessage.postMessage(JSON.stringify(message))
}
export function LandingScreen({
setScreenState,
}: {
@@ -92,8 +105,26 @@ function LandingScreenInner({
const usedStarterPack = useUsedStarterPack()
const {isTabletOrDesktop} = useWebMediaQueries()
const [appClipOverlayVisible, setAppClipOverlayVisible] =
React.useState(false)
const listItemsCount = starterPack.list?.listItemCount ?? 0
const onJoinPress = () => {
if (usedStarterPack?.isClip) {
setAppClipOverlayVisible(true)
postAppClipMessage({
action: 'present',
})
} else {
setUsedStarterPack({
uri: starterPack.uri,
cid: starterPack.cid,
})
setScreenState(LoggedOutScreenState.S_CreateAccount)
}
}
if (!AppBskyGraphStarterpack.isRecord(record)) {
return null
}
@@ -142,13 +173,7 @@ function LandingScreenInner({
) : null}
<Button
label={_(msg`Join the conversation now!`)}
onPress={() => {
setUsedStarterPack({
uri: starterPack.uri,
cid: starterPack.cid,
})
setScreenState(LoggedOutScreenState.S_CreateAccount)
}}
onPress={onJoinPress}
variant="solid"
color="primary"
size="large">
@@ -246,6 +271,53 @@ function LandingScreenInner({
</View>
</View>
</ScrollView>
<AppClipOverlay
visible={appClipOverlayVisible}
setIsVisible={setAppClipOverlayVisible}
/>
</CenteredView>
)
}
function AppClipOverlay({
visible,
setIsVisible,
}: {
visible: boolean
setIsVisible: (visible: boolean) => void
}) {
if (!visible) return
return (
<AnimatedPressable
accessibilityRole="button"
style={[
a.absolute,
{
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.95)',
zIndex: 1,
},
]}
entering={FadeIn}
exiting={FadeOut}
onPress={() => setIsVisible(false)}>
<View style={[a.flex_1, a.px_lg, {marginTop: 250}]}>
{/* Webkit needs this to have a zindex of 2? */}
<View style={[a.gap_md, {zIndex: 2}]}>
<Text
style={[a.font_bold, a.text_4xl, {lineHeight: 40, color: 'white'}]}>
Download Bluesky to get started!
</Text>
<Text style={[a.text_lg, {color: 'white'}]}>
We'll remember the starter pack you chose and use it when you create
an account in the app.
</Text>
</View>
</View>
</AnimatedPressable>
)
}