import {BSON} from "realm"; import NfcManager from "react-native-nfc-manager"; import React, {useCallback, useEffect, useLayoutEffect, useMemo, useState} from "react"; import {ActivityIndicator, Button, IconButton, Paragraph, Title, useTheme} from "react-native-paper"; import {Platform, RefreshControl, ScrollView, Share, StyleSheet, Text, TouchableOpacity, View} from "react-native"; import { CircleCheck, CircleDot, CreditCard, Plus, SmartphoneNfcIcon, Share as ShareIcon, WifiOff, TvMinimalPlay, Star, SmartphoneNfc, SquareArrowOutUpRight } from "lucide-react-native"; import {EmptyStatePlaceholder} from "../components/EmptyStatePlaceholder"; import {AMUIStatusAlert} from "../components/AMUIStatusAlert"; import {AMUICardDetail} from "../components/AMUICard"; import {ICCard} from "../components/ICCard"; import RealmContext from "../models/RealmContext"; import {AMServer} from "../models/AMServer"; import {AMCard} from "../models/AMCard"; import {felicaCardReadAsync, icCard0008Number, ICType} from "../lib/iccard"; import { AMCardInRequest, AMServerConnectionInfo, AMServerInstanceMetrics, fetchServerInfo, getGameName, performCardIn } from "amnet-shared"; import ObjectId = BSON.ObjectId; import {AMCardRecentUse} from "../models/AMCardRecentUse"; const {useRealm, useQuery, useObject} = RealmContext; export default function ServerCardIn({route, navigation}) { const realm = useRealm(); const theme = useTheme(); let {serverId} = route.params; const server = useObject(AMServer, ObjectId.createFromHexString(serverId)); const cards = useQuery({ type: AMCard, query: q => q.sorted("lastUsed", true) }); // nfc support const [nfcSupported, setNfcSupported] = useState(Platform.OS === 'android' ? false : null); useEffect(() => { if (nfcSupported !== null) { return; } NfcManager.isSupported().then(setNfcSupported); }, []); // server state const [serverRefreshing, setServerRefreshing] = useState(false); const [serverReloadCounter, setServerReloadCounter] = useState(0); const [serverConnectionSuccess, setServerConnectionSuccess] = useState(null); const [userSelectedCard, setUserSelectedCard] = useState(null); const [cardInCompleted, setCardInCompleted] = useState(null); const disableCardIn = useMemo(() => cardInCompleted !== null || !serverConnectionSuccess, [cardInCompleted, serverConnectionSuccess]); const serverDefaultCardQuery = useObject(AMCardRecentUse, server.gameId); const serverDefaultCard = serverDefaultCardQuery?.card ?? null; const selectedCard = useMemo(() => [userSelectedCard, serverDefaultCard, cards.at(0)].find(x => x?.isValid() == true) ?? null, [userSelectedCard, serverDefaultCard, cards]); const unselectedCards = useMemo(() => cards.filter(x => x.isValid() === true && x._id.toHexString() !== selectedCard?._id.toHexString()), [cards, selectedCard]); useLayoutEffect(() => { if (server?.isValid() !== true) { return; } navigation.setOptions({ title: server.serverName, headerRight: () => ( Share.share({url: server.shareUrl})}> ) }); }, [navigation, server]); // invalid card/server selection useEffect(() => { if (server?.isValid() !== true) { navigation.goBack(); return; } }, [server]); useEffect(() => { if (userSelectedCard?.isValid() === false) { setUserSelectedCard(null); } }, [userSelectedCard]); // handle auto-refresh useEffect(() => { if (!serverConnectionSuccess || server?.isValid() !== true) { return; } const metrics = server as AMServerInstanceMetrics; if (metrics.sessionUptime > 0 && metrics.timeSinceLastPoll === null) { const timeout = setTimeout(() => setServerReloadCounter(serverReloadCounter + 1), 5000); return () => clearTimeout(timeout); } }, [serverConnectionSuccess, server]); // server info update useEffect(() => { const abortController = new AbortController(); fetchServerInfo(server.serverAddress, abortController.signal).then(r => { if (!r) { setServerConnectionSuccess(false); return; } setServerConnectionSuccess(true); realm.write(() => { server.lastUsed = new Date(); Object.keys(r).forEach(x => server[x] = r[x]); }); }).catch(() => { setServerConnectionSuccess(false); }).finally(() => { setServerRefreshing(false); }); const timeout = setTimeout(() => abortController.abort(), 5000); return () => { clearTimeout(timeout); abortController.abort(); } }, [serverId, serverReloadCounter, realm]); const cardInCallback = useCallback((card: AMCard) => { setCardInCompleted(false); const abortController = new AbortController(); const request: AMCardInRequest = { cardId: card.code, physicalCardIDm: card.physicalCardData?.cardIDm ?? undefined }; performCardIn(server, request, abortController.signal).then(r => { const now = new Date(); realm.write(() => { server.lastUsed = now; card.lastUsed = now; // replace the "most recent card" record if (server.gameId) { if (serverDefaultCardQuery) { realm.delete(serverDefaultCardQuery); } realm.create(AMCardRecentUse.schema.name, AMCardRecentUse.generate(server.gameId, card)); } }); setCardInCompleted(r); }).catch(() => { setCardInCompleted(false); }).finally(() => { setTimeout(() => setCardInCompleted(null), 3000); }); setTimeout(() => abortController.abort(), 5000); return () => abortController.abort(); }, [server]); const performPhysicalCardIn = useCallback(async () => { const card = await felicaCardReadAsync(); if (!card) { return; } const savedCard = cards.find(x => x.physicalCardData?.cardIDm === card.idm); if (savedCard) { setUserSelectedCard(savedCard); cardInCallback(savedCard); return; } setCardInCompleted(false); const cardId = card.type !== ICType.Amusement ? icCard0008Number(card.idm) : card.accessCode; const abortController = new AbortController(); performCardIn(server, {cardId, physicalCardIDm: card.idm}, abortController.signal).then(r => { setCardInCompleted(r); }).catch(() => { setCardInCompleted(false); }).finally(() => { setTimeout(() => setCardInCompleted(null), 3000); }); setTimeout(() => abortController.abort(), 5000); return () => abortController.abort(); }, [server]); const performRefresh = () => { setServerRefreshing(true); setServerReloadCounter(serverReloadCounter + 1); }; if (server?.isValid() !== true) { return null; } if (serverConnectionSuccess === null) { return ( Connecting to server... ) } return ( }> {server.gameId ? `Play ${getGameName(server.gameId)}` : "Selected Card"} {selectedCard ? <> }> {serverDefaultCard?.code == selectedCard.code && } {nfcSupported === true && ( }/> )} : }/> } {cards.length > 0 && ( Additional Cards {unselectedCards.length ? unselectedCards.map((card: AMCard) => ( } onPress={() => setUserSelectedCard(card)}> {serverDefaultCard?.code == card.code && } )) : }/> } )} ) } function StatusAlert(props: { server: AMServerConnectionInfo & Partial, connectionEstablished: boolean }) { if (!props.connectionEstablished) { return ( There was an issue connecting to {props.server.serverAddress}. Check the network connection and try again. ) } // don't show if undefined (some older server versions) if (props.server.timeSinceLastPoll === null) { return ( Scanning a card below may not have any effect until the service has fully started up. ) } return null; } function DefaultCardDetail() { return ( <> {/* spacer */} Previously used with this game ) } const styles = StyleSheet.create({ section: { gap: 10 }, sectionTitle: { fontWeight: "bold" }, emptyPlaceholderView: { marginVertical: 30, gap: 15 }, container: { paddingTop: Platform.OS === 'android' ? 120 : 0, paddingHorizontal: 16, paddingBottom: 80, gap: 20 } });