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 {
|
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||||
self.window = UIWindow()
|
self.window = UIWindow()
|
||||||
|
|
||||||
self.controller = ViewController()
|
self.controller = ViewController(window: self.window!)
|
||||||
|
|
||||||
self.window?.rootViewController = self.controller
|
self.window?.rootViewController = self.controller
|
||||||
self.window?.makeKeyAndVisible()
|
self.window?.makeKeyAndVisible()
|
||||||
|
|||||||
@@ -1,26 +1,95 @@
|
|||||||
import UIKit
|
import UIKit
|
||||||
import WebKit
|
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 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() {
|
override func viewDidLoad() {
|
||||||
super.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.translatesAutoresizingMaskIntoConstraints = false
|
||||||
webView.contentMode = .scaleToFill
|
webView.contentMode = .scaleToFill
|
||||||
|
|
||||||
self.view.addSubview(webView)
|
self.view.addSubview(webView)
|
||||||
|
|
||||||
self.webView = 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) {
|
func handleURL(url: URL) {
|
||||||
|
self.starterPackUrl = url
|
||||||
let urlString = "\(url.absoluteString)?clip=true"
|
let urlString = "\(url.absoluteString)?clip=true"
|
||||||
|
|
||||||
if let url = URL(string: urlString) {
|
if let url = URL(string: urlString) {
|
||||||
self.webView?.load(URLRequest(url: url))
|
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 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 {AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
@@ -23,6 +24,8 @@ import {ListMaybePlaceholder} from '#/components/Lists'
|
|||||||
import {Default as ProfileCardInner} from '#/components/ProfileCard'
|
import {Default as ProfileCardInner} from '#/components/ProfileCard'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
|
||||||
|
const AnimatedPressable = Animated.createAnimatedComponent(Pressable)
|
||||||
|
|
||||||
function parseStarterPackHttpUri(uri: string): {name?: string; rkey?: string} {
|
function parseStarterPackHttpUri(uri: string): {name?: string; rkey?: string} {
|
||||||
const parsed = new URL(uri)
|
const parsed = new URL(uri)
|
||||||
const [_, _path, name, rkey] = parsed.pathname.split('/')
|
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({
|
export function LandingScreen({
|
||||||
setScreenState,
|
setScreenState,
|
||||||
}: {
|
}: {
|
||||||
@@ -92,8 +105,26 @@ function LandingScreenInner({
|
|||||||
const usedStarterPack = useUsedStarterPack()
|
const usedStarterPack = useUsedStarterPack()
|
||||||
const {isTabletOrDesktop} = useWebMediaQueries()
|
const {isTabletOrDesktop} = useWebMediaQueries()
|
||||||
|
|
||||||
|
const [appClipOverlayVisible, setAppClipOverlayVisible] =
|
||||||
|
React.useState(false)
|
||||||
|
|
||||||
const listItemsCount = starterPack.list?.listItemCount ?? 0
|
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)) {
|
if (!AppBskyGraphStarterpack.isRecord(record)) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -142,13 +173,7 @@ function LandingScreenInner({
|
|||||||
) : null}
|
) : null}
|
||||||
<Button
|
<Button
|
||||||
label={_(msg`Join the conversation now!`)}
|
label={_(msg`Join the conversation now!`)}
|
||||||
onPress={() => {
|
onPress={onJoinPress}
|
||||||
setUsedStarterPack({
|
|
||||||
uri: starterPack.uri,
|
|
||||||
cid: starterPack.cid,
|
|
||||||
})
|
|
||||||
setScreenState(LoggedOutScreenState.S_CreateAccount)
|
|
||||||
}}
|
|
||||||
variant="solid"
|
variant="solid"
|
||||||
color="primary"
|
color="primary"
|
||||||
size="large">
|
size="large">
|
||||||
@@ -246,6 +271,53 @@ function LandingScreenInner({
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
<AppClipOverlay
|
||||||
|
visible={appClipOverlayVisible}
|
||||||
|
setIsVisible={setAppClipOverlayVisible}
|
||||||
|
/>
|
||||||
</CenteredView>
|
</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