present app clip
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user