mirror of
https://gitea.tendokyu.moe/ppc/amnet.git
synced 2026-09-25 15:47:59 +03:00
new "card add" flow
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, {useEffect} from "react";
|
||||
import React from "react";
|
||||
import {StatusBar} from "expo-status-bar";
|
||||
import {useColorScheme, View} from "react-native";
|
||||
import {Server, WalletCards} from "lucide-react-native";
|
||||
@@ -7,8 +7,8 @@ import {SafeAreaProvider} from "react-native-safe-area-context";
|
||||
import {MD3Colors} from "react-native-paper/lib/typescript/types";
|
||||
import {createBottomTabNavigator} from "@react-navigation/bottom-tabs";
|
||||
import {createNativeStackNavigator} from "@react-navigation/native-stack";
|
||||
import {DefaultTheme as NavigationLightTheme, DarkTheme as NavigationDarkTheme} from '@react-navigation/native';
|
||||
import {JetBrainsMono_400Regular, useFonts} from "@expo-google-fonts/jetbrains-mono";
|
||||
import {DefaultTheme as NavigationLightTheme, DarkTheme as NavigationDarkTheme} from '@react-navigation/native';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
adaptNavigationTheme,
|
||||
@@ -19,12 +19,15 @@ import {
|
||||
Paragraph
|
||||
} from 'react-native-paper';
|
||||
|
||||
import RealmContext from "./models/RealmContext";
|
||||
import linking from "./Linking";
|
||||
import RealmContext from "./models/RealmContext";
|
||||
|
||||
import {AMUITabBar} from "./components/AMUITabBar";
|
||||
import CardAddModal from "./app/CardAddModal";
|
||||
|
||||
import CardEditorModal from "./app/card/CardEditorModal";
|
||||
import CardAddModal from "./app/card/CardAddModal";
|
||||
import ServerAddModal from "./app/ServerAddModal";
|
||||
|
||||
import CardManagerStack from "./app/CardManagerStack";
|
||||
import ServerManagerStack from "./app/ServerManagerStack";
|
||||
|
||||
@@ -118,7 +121,8 @@ export default function AppRoot() {
|
||||
|
||||
<Stack.Group screenOptions={{presentation: "modal"}}>
|
||||
<Stack.Screen name="add-server" component={ServerAddModal} options={{title: "Add Server"}}/>
|
||||
<Stack.Screen name="add-card" component={CardAddModal} options={{title: "Add Card"}}/>
|
||||
<Stack.Screen name="add-card" component={CardAddModal} options={{title: "Add Card", headerShown: false}}/>
|
||||
<Stack.Screen name="edit-card" component={CardEditorModal} options={{title: "Edit Card"}}/>
|
||||
</Stack.Group>
|
||||
</Stack.Navigator>
|
||||
</NavigationContainer>
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
import {generateCardNumber} from "amnet-shared";
|
||||
import {useModalStyles} from "../utils/ModalStyles";
|
||||
import {useCallback, useLayoutEffect, useState} from "react";
|
||||
import {Platform, TextInput, TouchableOpacity, View} from "react-native";
|
||||
import {Button, Card, Divider, Paragraph, useTheme} from "react-native-paper";
|
||||
|
||||
import {AMCard} from "../models/AMCard";
|
||||
import RealmContext from "../models/RealmContext";
|
||||
|
||||
const {useRealm} = RealmContext;
|
||||
|
||||
export default function CardAddModal({navigation, route}) {
|
||||
const realm = useRealm();
|
||||
const theme = useTheme();
|
||||
const modalStyles = useModalStyles();
|
||||
|
||||
// state values
|
||||
const [cardName, setCardName] = useState('');
|
||||
const [cardNumber, setCardNumber] = useState('');
|
||||
const [cardNumberReadOnly, setCardNumberReadOnly] = useState(false);
|
||||
|
||||
const cardAddCallback = useCallback((cardId: string, cardName: string) => {
|
||||
const duplicateCard = realm.objects<AMCard>("card").find(x => x.code === cardId);
|
||||
|
||||
if (duplicateCard) {
|
||||
realm.write(() => {
|
||||
duplicateCard.name = cardName;
|
||||
});
|
||||
} else {
|
||||
realm.write(() => {
|
||||
realm.create("card", AMCard.generate(cardId, cardName));
|
||||
});
|
||||
}
|
||||
|
||||
navigation.goBack();
|
||||
}, [realm]);
|
||||
|
||||
// header effects
|
||||
if (Platform.OS === 'ios') {
|
||||
useLayoutEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerLeft: () => <TouchableOpacity onPress={() => navigation.goBack()}>
|
||||
<Paragraph style={modalStyles.headerText}>Cancel</Paragraph>
|
||||
</TouchableOpacity>,
|
||||
});
|
||||
}, [navigation, modalStyles]);
|
||||
}
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const canSave = cardName.length && cardNumber.length === 20;
|
||||
const themeOptions = canSave ? modalStyles.headerText : {color: theme.colors.outline};
|
||||
|
||||
navigation.setOptions({
|
||||
headerRight: () => <TouchableOpacity disabled={!canSave}
|
||||
onPress={() => cardAddCallback(cardNumber, cardName)}>
|
||||
<Paragraph style={{...themeOptions, fontWeight: "600"}}>Save</Paragraph>
|
||||
</TouchableOpacity>
|
||||
})
|
||||
}, [navigation, modalStyles, cardName, cardNumber]);
|
||||
|
||||
// enable renaming by passing cardId to modal
|
||||
useLayoutEffect(() => {
|
||||
if (!route.params?.cardId) {
|
||||
setCardNumberReadOnly(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setCardNumber(route.params.cardId);
|
||||
setCardNumberReadOnly(true);
|
||||
|
||||
if (route.params.cardName) {
|
||||
setCardName(route.params.cardName);
|
||||
}
|
||||
|
||||
navigation.setOptions({
|
||||
title: "Edit Card"
|
||||
});
|
||||
}, [route]);
|
||||
|
||||
return (<View style={{display: "flex", gap: 10, justifyContent: "center", paddingHorizontal: 15, paddingTop: 25}}>
|
||||
<Card mode="contained">
|
||||
<Card.Content>
|
||||
<View style={modalStyles.formItemRow}>
|
||||
<Paragraph style={modalStyles.formItemName}>
|
||||
Card Name
|
||||
</Paragraph>
|
||||
<TextInput style={modalStyles.formItemInput}
|
||||
numberOfLines={1} autoFocus
|
||||
value={cardName}
|
||||
onChangeText={setCardName}
|
||||
placeholder="Primary Card"
|
||||
placeholderTextColor={theme.colors.outline}/>
|
||||
</View>
|
||||
|
||||
<Divider style={{marginVertical: 15}}/>
|
||||
|
||||
<View style={modalStyles.formItemRow}>
|
||||
<Paragraph style={modalStyles.formItemName}>
|
||||
Matrix Code
|
||||
</Paragraph>
|
||||
<TextInput numberOfLines={1}
|
||||
keyboardType="numeric"
|
||||
readOnly={cardNumberReadOnly}
|
||||
placeholder="0000 0000 0000 0000 0000"
|
||||
placeholderTextColor={theme.colors.outline}
|
||||
value={cardNumber.match(/.{1,4}/g)?.join(" ")}
|
||||
onChangeText={val => setCardNumber(val?.replaceAll(/\D/g, '').substring(0, 20) ?? '')}
|
||||
style={{...modalStyles.formItemInput, color: cardNumberReadOnly ? theme.colors.onSurfaceDisabled : theme.colors.onSurface}}/>
|
||||
</View>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
|
||||
{/*only show card entry info when in create mode*/}
|
||||
{!cardNumberReadOnly && (
|
||||
<>
|
||||
<Paragraph style={modalStyles.formFootnote}>
|
||||
If you have a physical card, you can enter the 20-digit code on the back.
|
||||
</Paragraph>
|
||||
|
||||
<Button mode="text" onPress={() => setCardNumber(generateCardNumber())}>Generate Matrix Code</Button>
|
||||
</>
|
||||
)}
|
||||
</View>)
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import React from "react";
|
||||
import {Platform, TouchableOpacity, View} from "react-native";
|
||||
|
||||
import {useNavigation} from "@react-navigation/native";
|
||||
import {createNativeStackNavigator} from "@react-navigation/native-stack";
|
||||
|
||||
import {Card, Paragraph, Text, Title, useTheme} from "react-native-paper";
|
||||
import {SmartphoneNfc, SquareAsterisk, SquarePlus} from "lucide-react-native";
|
||||
|
||||
import ManualCardEntry from "./ManualCardEntry";
|
||||
import PhysicalCardScan from "./PhyscialCardScan";
|
||||
|
||||
const Stack = createNativeStackNavigator();
|
||||
|
||||
function CardAddModal() {
|
||||
return (
|
||||
<Stack.Navigator initialRouteName="Select">
|
||||
<Stack.Screen name="Select" component={CardAddMethodSelector} options={{headerShown: false}}/>
|
||||
|
||||
<Stack.Screen name="Scan" component={PhysicalCardScan} options={{title: "Add Physical Card"}}/>
|
||||
<Stack.Screen name="Manual" component={ManualCardEntry} options={{title: "Add Card Manually"}}/>
|
||||
</Stack.Navigator>
|
||||
)
|
||||
}
|
||||
|
||||
function CardAddMethodSelector() {
|
||||
const theme = useTheme();
|
||||
const navigation: any = useNavigation();
|
||||
|
||||
return (
|
||||
<View style={{paddingHorizontal: 15, paddingTop: 90, gap: 8}}>
|
||||
<View style={{paddingBottom: 25, gap: 5}}>
|
||||
<Title style={{textAlign: "center", fontSize: 32, fontWeight: "bold"}}>Add new Card</Title>
|
||||
<Paragraph style={{textAlign: "center"}}>AMNet supports scanning physical cards, entering details
|
||||
manually or generating new ones.</Paragraph>
|
||||
</View>
|
||||
|
||||
<MethodCard color={theme.colors.primary}
|
||||
icon={<SmartphoneNfc/>}
|
||||
title="Scan Physical Card"
|
||||
description="Amusement IC or Transport cards supported"
|
||||
onPress={() => navigation.navigate('Scan')}/>
|
||||
|
||||
<View></View>
|
||||
|
||||
<MethodCard color={theme.colors.secondary}
|
||||
icon={<SquareAsterisk/>}
|
||||
title="Enter Details Manually"
|
||||
//description="Manually enter card details (or generate a new one)"
|
||||
onPress={() => navigation.navigate('Manual')}/>
|
||||
|
||||
<MethodCard color="cornflowerblue"
|
||||
icon={<SquarePlus/>}
|
||||
title="Generate New Card"
|
||||
//description="Create a card from scratch"
|
||||
onPress={() => navigation.navigate('Manual', {autoGenerate: true})}/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function MethodCard(props: {
|
||||
title: string,
|
||||
description?: string,
|
||||
color: string,
|
||||
icon: React.ReactElement,
|
||||
onPress: () => void
|
||||
}) {
|
||||
const theme = useTheme();
|
||||
const large = props.description?.length > 0;
|
||||
|
||||
return (
|
||||
<TouchableOpacity onPress={() => props.onPress()}>
|
||||
<Card mode={theme.dark ? "elevated" : "contained"}>
|
||||
<Card.Content style={{display: "flex", flexDirection: "row", alignItems: "center", gap: 15}}>
|
||||
<View style={{
|
||||
borderRadius: large ? 10 : 5,
|
||||
height: large ? 50 : 30,
|
||||
width: large ? 50 : 30,
|
||||
backgroundColor: props.color,
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
}}>
|
||||
{React.cloneElement(props.icon, {size: large ? 24 : 16, color: theme.colors.surfaceVariant})}
|
||||
</View>
|
||||
<View style={{flexShrink: 1}}>
|
||||
<Text style={{fontSize: large ? 16 : 14}}>{props.title}</Text>
|
||||
{props.description && (<Paragraph style={{fontSize: 12}}>{props.description}</Paragraph>)}
|
||||
</View>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
</TouchableOpacity>
|
||||
)
|
||||
}
|
||||
|
||||
export default Platform.OS === 'ios' ? CardAddModal : ManualCardEntry;
|
||||
@@ -0,0 +1,97 @@
|
||||
import {BSON} from "realm";
|
||||
import {Card, Divider, Paragraph, useTheme} from "react-native-paper";
|
||||
import {useCallback, useEffect, useLayoutEffect, useState} from "react";
|
||||
import {Platform, TextInput, TouchableOpacity, View} from "react-native";
|
||||
|
||||
import {useModalStyles} from "../../utils/ModalStyles";
|
||||
|
||||
import {AMCard} from "../../models/AMCard";
|
||||
import RealmContext from "../../models/RealmContext";
|
||||
|
||||
const {useObject, useRealm} = RealmContext;
|
||||
|
||||
export default function CardEditorModal({navigation, route}) {
|
||||
if (!route.params || !route.params.cardId) {
|
||||
navigation.goBack();
|
||||
return;
|
||||
}
|
||||
|
||||
const theme = useTheme();
|
||||
const modalStyles = useModalStyles();
|
||||
|
||||
const realm = useRealm();
|
||||
const card = useObject(AMCard, BSON.ObjectId.createFromHexString(route.params.cardId));
|
||||
|
||||
const [cardName, setCardName] = useState<string>('');
|
||||
|
||||
const updateCardCallback = useCallback((newName: string) => {
|
||||
realm.write(() => {
|
||||
card.name = newName;
|
||||
});
|
||||
|
||||
navigation.goBack();
|
||||
}, [realm, navigation]);
|
||||
|
||||
useEffect(() => {
|
||||
if (card?.isValid() !== true) {
|
||||
navigation.goBack();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!cardName.length) {
|
||||
setCardName(card.name);
|
||||
}
|
||||
}, [card]);
|
||||
|
||||
// header effects
|
||||
if (Platform.OS === 'ios') {
|
||||
useLayoutEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerLeft: () => <TouchableOpacity onPress={() => navigation.goBack()}>
|
||||
<Paragraph style={modalStyles.headerText}>Cancel</Paragraph>
|
||||
</TouchableOpacity>,
|
||||
});
|
||||
}, [navigation, modalStyles]);
|
||||
}
|
||||
|
||||
useLayoutEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerRight: () => <TouchableOpacity onPress={() => updateCardCallback(cardName)} disabled={!cardName.length}>
|
||||
<Paragraph style={modalStyles.headerText}>Save</Paragraph>
|
||||
</TouchableOpacity>
|
||||
});
|
||||
}, [cardName]);
|
||||
|
||||
return (
|
||||
<View style={modalStyles.rootLayout}>
|
||||
<Card mode="contained">
|
||||
<Card.Content>
|
||||
<View style={modalStyles.formItemRow}>
|
||||
<Paragraph style={modalStyles.formItemName}>
|
||||
Card Name
|
||||
</Paragraph>
|
||||
<TextInput style={modalStyles.formItemInput}
|
||||
autoFocus
|
||||
numberOfLines={1}
|
||||
value={cardName}
|
||||
onChangeText={setCardName}
|
||||
placeholder="Primary Card"
|
||||
placeholderTextColor={theme.colors.outline}/>
|
||||
</View>
|
||||
|
||||
<Divider style={{marginVertical: 15}}/>
|
||||
|
||||
<View style={modalStyles.formItemRow}>
|
||||
<Paragraph style={modalStyles.formItemName}>
|
||||
Access Code
|
||||
</Paragraph>
|
||||
<TextInput readOnly
|
||||
numberOfLines={1}
|
||||
value={card?.code.match(/.{1,4}/g)?.join(" ")}
|
||||
style={{...modalStyles.formItemInput, color: theme.colors.onSurfaceDisabled}}/>
|
||||
</View>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import {generateCardNumber} from "amnet-shared";
|
||||
import {useCallback, useEffect, useLayoutEffect, useState} from "react";
|
||||
import {Platform, TextInput, TouchableOpacity, View} from "react-native";
|
||||
import {Button, Card, Divider, Paragraph, useTheme} from "react-native-paper";
|
||||
|
||||
import {AMCard} from "../../models/AMCard";
|
||||
import RealmContext from "../../models/RealmContext";
|
||||
import {useModalStyles} from "../../utils/ModalStyles";
|
||||
|
||||
const {useRealm} = RealmContext;
|
||||
|
||||
export default function CardAddModal({navigation, route}) {
|
||||
const realm = useRealm();
|
||||
const theme = useTheme();
|
||||
const modalStyles = useModalStyles();
|
||||
|
||||
// state values
|
||||
const [cardName, setCardName] = useState('');
|
||||
const [cardNumber, setCardNumber] = useState('');
|
||||
|
||||
const cardAddCallback = useCallback((cardId: string, cardName: string) => {
|
||||
const duplicateCard = realm.objects<AMCard>(AMCard.schema.name).find(x => x.code === cardId);
|
||||
|
||||
if (duplicateCard) {
|
||||
realm.write(() => {
|
||||
duplicateCard.name = cardName;
|
||||
});
|
||||
} else {
|
||||
realm.write(() => {
|
||||
realm.create<AMCard>(AMCard.schema.name, AMCard.generate(cardId, cardName));
|
||||
});
|
||||
}
|
||||
|
||||
if (Platform.OS === 'ios') {
|
||||
navigation.getParent()?.goBack();
|
||||
} else {
|
||||
navigation.goBack();
|
||||
}
|
||||
}, [realm]);
|
||||
|
||||
// header effects
|
||||
if (Platform.OS === 'ios') {
|
||||
useLayoutEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerLeft: () => <TouchableOpacity onPress={() => navigation.goBack()}>
|
||||
<Paragraph style={modalStyles.headerText}>Back</Paragraph>
|
||||
</TouchableOpacity>,
|
||||
});
|
||||
}, [navigation, modalStyles]);
|
||||
}
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const canSave = cardName.length && cardNumber.length === 20;
|
||||
const themeOptions = canSave ? modalStyles.headerText : {color: theme.colors.outline};
|
||||
|
||||
navigation.setOptions({
|
||||
headerRight: () => <TouchableOpacity disabled={!canSave} onPress={() => cardAddCallback(cardNumber, cardName)}>
|
||||
<Paragraph style={{...themeOptions, fontWeight: "600"}}>Save</Paragraph>
|
||||
</TouchableOpacity>
|
||||
})
|
||||
}, [navigation, modalStyles, cardName, cardNumber]);
|
||||
|
||||
// check params for autoGenerate request
|
||||
useEffect(() => {
|
||||
if (route.params?.autoGenerate) {
|
||||
setCardNumber(generateCardNumber());
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<View style={modalStyles.rootLayout}>
|
||||
<Card mode="contained">
|
||||
<Card.Content>
|
||||
<View style={modalStyles.formItemRow}>
|
||||
<Paragraph style={modalStyles.formItemName}>
|
||||
Card Name
|
||||
</Paragraph>
|
||||
<TextInput style={modalStyles.formItemInput}
|
||||
autoFocus
|
||||
numberOfLines={1}
|
||||
value={cardName}
|
||||
onChangeText={setCardName}
|
||||
placeholder="Primary Card"
|
||||
placeholderTextColor={theme.colors.outline}/>
|
||||
</View>
|
||||
|
||||
<Divider style={{marginVertical: 15}}/>
|
||||
|
||||
<View style={modalStyles.formItemRow}>
|
||||
<Paragraph style={modalStyles.formItemName}>
|
||||
Access Code
|
||||
</Paragraph>
|
||||
<TextInput numberOfLines={1}
|
||||
keyboardType="numeric"
|
||||
placeholder="0000 0000 0000 0000 0000"
|
||||
placeholderTextColor={theme.colors.outline}
|
||||
value={cardNumber.match(/.{1,4}/g)?.join(" ")}
|
||||
onChangeText={val => setCardNumber(val?.replaceAll(/\D/g, '').substring(0, 20) ?? '')}
|
||||
style={{...modalStyles.formItemInput, color: theme.colors.onSurface}}/>
|
||||
</View>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
|
||||
{/* only show card entry info when in create mode */}
|
||||
<Button mode="text" onPress={() => setCardNumber(generateCardNumber())}>Generate Access Code</Button>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import {useNavigation} from "@react-navigation/native";
|
||||
import {Card, Paragraph, useTheme} from "react-native-paper";
|
||||
import {useCallback, useLayoutEffect, useState} from "react";
|
||||
import {TextInput, TouchableOpacity, View} from "react-native";
|
||||
|
||||
import {useModalStyles} from "../../utils/ModalStyles";
|
||||
|
||||
import RealmContext from "../../models/RealmContext";
|
||||
import {AMCard, AMPhysicalCardData} from "../../models/AMCard";
|
||||
import {felicaCardReadAsync, icCard0008Number, ICType} from "../../lib/iccard";
|
||||
|
||||
const {useRealm} = RealmContext;
|
||||
|
||||
export default function PhysicalCardScan() {
|
||||
const realm = useRealm();
|
||||
const theme = useTheme();
|
||||
const navigation = useNavigation();
|
||||
const modalStyles = useModalStyles();
|
||||
|
||||
const [cardName, setCardName] = useState<string>('');
|
||||
|
||||
const handleScan = useCallback(async () => {
|
||||
const cardInfo = await felicaCardReadAsync();
|
||||
|
||||
if (!cardInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
const accessCode = cardInfo.type === ICType.Amusement ? cardInfo.accessCode : icCard0008Number(cardInfo.idm);
|
||||
|
||||
realm.write(() => {
|
||||
let existingCard = realm.objects<AMCard>(AMCard.schema.name).find(x => x.code === accessCode);
|
||||
const physicalCardInfo = realm.create<AMPhysicalCardData>(AMPhysicalCardData.schema.name, {
|
||||
cardIDm: cardInfo.idm,
|
||||
cardType: cardInfo.type,
|
||||
cardVendor: cardInfo.type === ICType.Amusement ? cardInfo.vendor : null
|
||||
});
|
||||
|
||||
if (!existingCard) {
|
||||
existingCard = realm.create<AMCard>(AMCard.schema.name, AMCard.generate(accessCode, cardName));
|
||||
} else {
|
||||
existingCard.name = cardName;
|
||||
}
|
||||
|
||||
// set physical card props
|
||||
existingCard.added = new Date();
|
||||
existingCard.physicalCardData = physicalCardInfo;
|
||||
});
|
||||
|
||||
// dismiss the parent screen (this screen can only be accessed from modal -> stack)
|
||||
navigation.getParent()?.goBack();
|
||||
}, [realm, navigation, cardName]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const canSave = cardName.length;
|
||||
const themeOptions = canSave ? modalStyles.headerText : {color: theme.colors.outline};
|
||||
|
||||
navigation.setOptions({
|
||||
headerRight: () => <TouchableOpacity disabled={!canSave} onPress={() => handleScan()}>
|
||||
<Paragraph style={{...themeOptions, fontWeight: "600"}}>Scan</Paragraph>
|
||||
</TouchableOpacity>
|
||||
})
|
||||
}, [navigation, modalStyles, cardName]);
|
||||
|
||||
return (
|
||||
<View style={modalStyles.rootLayout}>
|
||||
<Card mode="contained">
|
||||
<Card.Content>
|
||||
<View style={modalStyles.formItemRow}>
|
||||
<Paragraph style={modalStyles.formItemName}>
|
||||
Card Name
|
||||
</Paragraph>
|
||||
<TextInput style={modalStyles.formItemInput}
|
||||
autoFocus
|
||||
numberOfLines={1}
|
||||
value={cardName}
|
||||
onChangeText={setCardName}
|
||||
placeholder="Primary Card"
|
||||
placeholderTextColor={theme.colors.outline}/>
|
||||
</View>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
|
||||
<Paragraph>
|
||||
Ensure you have either an Amusement IC or Transport card ready to scan.
|
||||
After the scan has completed, the card will be saved to your device.
|
||||
</Paragraph>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -13,7 +13,14 @@ const modalStyles = StyleSheet.create({
|
||||
},
|
||||
formItemDivider: {
|
||||
marginVertical: 15
|
||||
}
|
||||
},
|
||||
rootLayout: {
|
||||
display: "flex",
|
||||
gap: 10,
|
||||
justifyContent: "center",
|
||||
paddingHorizontal: 15,
|
||||
paddingTop: 25
|
||||
},
|
||||
});
|
||||
|
||||
export function useModalStyles() {
|
||||
|
||||
Reference in New Issue
Block a user