139 lines
4.7 KiB
TypeScript
139 lines
4.7 KiB
TypeScript
import type { MessageBlock } from "@rakazo/contracts";
|
|
import { abortableDelay } from "@rakazo/core";
|
|
import { useEffect, useRef, useState } from "react";
|
|
import { ActivityIndicator, Linking, Pressable, Text, View } from "react-native";
|
|
import { rpc } from "../lib/api";
|
|
import { appConnectPresentation } from "../lib/app-connect";
|
|
import { native } from "../lib/native";
|
|
|
|
export function AppConnectCard({
|
|
botId,
|
|
block,
|
|
}: {
|
|
botId: string;
|
|
block: Extract<MessageBlock, { kind: "app_connect" }>;
|
|
}) {
|
|
const [busy, setBusy] = useState(false);
|
|
const [localStatus, setLocalStatus] = useState<"pending" | "connected">(block.status);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const connectionAttempt = useRef<AbortController | null>(null);
|
|
const status = block.status === "connected" ? "connected" : localStatus;
|
|
const view = appConnectPresentation({ ...block, status }, busy);
|
|
|
|
useEffect(() => () => connectionAttempt.current?.abort(), []);
|
|
|
|
async function authorize() {
|
|
connectionAttempt.current?.abort();
|
|
const controller = new AbortController();
|
|
connectionAttempt.current = controller;
|
|
setBusy(true);
|
|
setError(null);
|
|
try {
|
|
const started = await rpc<{ connectionId: string; authorizationUrl: string | null }>(
|
|
"connections/begin",
|
|
{
|
|
provider: block.provider,
|
|
displayName: block.name,
|
|
},
|
|
{ signal: controller.signal },
|
|
);
|
|
if (controller.signal.aborted) return;
|
|
if (started.authorizationUrl) await Linking.openURL(started.authorizationUrl);
|
|
for (let attempt = 0; attempt < 60; attempt += 1) {
|
|
if (controller.signal.aborted) return;
|
|
const row = await rpc<{ status: string }>(
|
|
"connections/complete",
|
|
{ connectionId: started.connectionId },
|
|
{ signal: controller.signal },
|
|
).catch(() => undefined);
|
|
if (row?.status === "connected") {
|
|
if (controller.signal.aborted) return;
|
|
await rpc("onboarding/appConnected", { botId, provider: block.provider });
|
|
if (controller.signal.aborted) return;
|
|
setLocalStatus("connected");
|
|
return;
|
|
}
|
|
await abortableDelay(2_000, controller.signal);
|
|
}
|
|
if (!controller.signal.aborted) setError("Authorization timed out. Please try again.");
|
|
} catch (reason) {
|
|
if (!controller.signal.aborted) {
|
|
setError(reason instanceof Error ? reason.message : "Could not authorize this app");
|
|
}
|
|
} finally {
|
|
if (connectionAttempt.current === controller) {
|
|
connectionAttempt.current = null;
|
|
setBusy(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
return (
|
|
<View
|
|
accessibilityLabel={`${block.name} connection`}
|
|
style={{
|
|
width: "90%",
|
|
borderRadius: 18,
|
|
borderWidth: 1,
|
|
borderColor: "#232326",
|
|
backgroundColor: "#17171A",
|
|
paddingHorizontal: 16,
|
|
paddingVertical: 14,
|
|
gap: 8,
|
|
}}
|
|
>
|
|
<View style={{ flexDirection: "row", alignItems: "center", gap: 12 }}>
|
|
<View
|
|
style={{
|
|
width: 40,
|
|
height: 40,
|
|
borderRadius: 10,
|
|
backgroundColor: "#30356A",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
}}
|
|
>
|
|
<Text style={{ color: "#E2E4FF", fontSize: 15, fontWeight: "600" }}>
|
|
{block.name.slice(0, 1).toUpperCase()}
|
|
</Text>
|
|
</View>
|
|
<View style={{ flex: 1, gap: 2 }}>
|
|
<Text style={{ color: "#ECECEE", fontSize: 15, fontWeight: "600" }}>{view.title}</Text>
|
|
<Text style={{ color: "#85858A", fontSize: 13.5 }} numberOfLines={2}>
|
|
{view.description}
|
|
</Text>
|
|
</View>
|
|
{view.showAuthorize ? (
|
|
<Pressable
|
|
accessibilityRole="button"
|
|
accessibilityLabel={`Authorize ${block.name}`}
|
|
disabled={busy}
|
|
onPress={() => void authorize()}
|
|
style={{
|
|
minHeight: 36,
|
|
paddingHorizontal: 14,
|
|
borderRadius: 999,
|
|
backgroundColor: native.fillPressed,
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
}}
|
|
>
|
|
{busy ? (
|
|
<ActivityIndicator color={native.label} />
|
|
) : (
|
|
<Text style={{ color: native.label, fontSize: 14, fontWeight: "600" }}>
|
|
{view.actionLabel}
|
|
</Text>
|
|
)}
|
|
</Pressable>
|
|
) : (
|
|
<Text style={{ color: "#4ECB71", fontSize: 13.5, fontWeight: "600" }}>
|
|
{view.actionLabel}
|
|
</Text>
|
|
)}
|
|
</View>
|
|
{error ? <Text style={{ color: "#E96B6B", fontSize: 13 }}>{error}</Text> : null}
|
|
</View>
|
|
);
|
|
}
|