1
0
mirror of synced 2025-01-31 03:53:47 +01:00

build: change package name to dev.nulldori.eamemu

This commit is contained in:
Juchan Roh 2023-02-06 19:46:04 +09:00
parent 2fc95d18c7
commit eadd41f1d7
15 changed files with 321 additions and 680 deletions

View File

@ -98,14 +98,13 @@ android {
ndkVersion rootProject.ext.ndkVersion ndkVersion rootProject.ext.ndkVersion
compileSdkVersion rootProject.ext.compileSdkVersion compileSdkVersion rootProject.ext.compileSdkVersion
namespace "tk.nulldori.eamemu" namespace "dev.nulldori.eamemu"
defaultConfig { defaultConfig {
applicationId "tk.nulldori.eamemu" applicationId "dev.nulldori.eamemu"
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 220 versionCode 300
versionName "2.2.0" versionName "3.0.0"
vectorDrawables.useSupportLibrary = true
} }
splits { splits {
abi { abi {
@ -172,8 +171,6 @@ dependencies {
implementation jscFlavor implementation jscFlavor
} }
// for react-native-fs
implementation project(':react-native-fs')
implementation("org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion") implementation("org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion")
} }

View File

@ -4,7 +4,7 @@
* <p>This source code is licensed under the MIT license found in the LICENSE file in the root * <p>This source code is licensed under the MIT license found in the LICENSE file in the root
* directory of this source tree. * directory of this source tree.
*/ */
package tk.nulldori.eamemu; package dev.nulldori.eamemu;
import android.content.Context; import android.content.Context;
import com.facebook.flipper.android.AndroidFlipperClient; import com.facebook.flipper.android.AndroidFlipperClient;
import com.facebook.flipper.android.utils.FlipperUtils; import com.facebook.flipper.android.utils.FlipperUtils;

View File

@ -1,4 +1,4 @@
package tk.nulldori.eamemu package dev.nulldori.eamemu
import java.io.UnsupportedEncodingException import java.io.UnsupportedEncodingException
import kotlin.experimental.xor import kotlin.experimental.xor

View File

@ -1,4 +1,4 @@
package tk.nulldori.eamemu package dev.nulldori.eamemu
class B(arg4: ByteArray) { class B(arg4: ByteArray) {
private val k: IntArray private val k: IntArray

View File

@ -1,20 +1,10 @@
package tk.nulldori.eamemu; package dev.nulldori.eamemu;
import android.widget.Toast;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.LifecycleEventListener;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.Promise;
import java.util.Map;
import java.util.HashMap;
import tk.nulldori.eamemu.A;
public class CardConvModule extends ReactContextBaseJavaModule { public class CardConvModule extends ReactContextBaseJavaModule {
private static ReactApplicationContext reactContext; private static ReactApplicationContext reactContext;
private static A converter; private static A converter;

View File

@ -1,4 +1,4 @@
package tk.nulldori.eamemu; package dev.nulldori.eamemu;
import com.facebook.react.ReactPackage; import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule; import com.facebook.react.bridge.NativeModule;

View File

@ -1,4 +1,4 @@
package tk.nulldori.eamemu package dev.nulldori.eamemu
object E { object E {
fun a(arg4: CharSequence): ByteArray { fun a(arg4: CharSequence): ByteArray {

View File

@ -1,4 +1,4 @@
package tk.nulldori.eamemu; package dev.nulldori.eamemu;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
@ -50,9 +50,9 @@ public class HcefModule extends ReactContextBaseJavaModule implements LifecycleE
isHceFSupport = true; isHceFSupport = true;
nfcAdapter = NfcAdapter.getDefaultAdapter(getReactApplicationContext()); nfcAdapter = NfcAdapter.getDefaultAdapter(getReactApplicationContext());
if(nfcAdapter != null){ if(nfcAdapter != null && nfcAdapter.isEnabled()){
nfcFCardEmulation = NfcFCardEmulation.getInstance(nfcAdapter); nfcFCardEmulation = NfcFCardEmulation.getInstance(nfcAdapter);
componentName = new ComponentName("tk.nulldori.eamemu","tk.nulldori.eamemu.eAMEMuService"); componentName = new ComponentName("dev.nulldori.eamemu","dev.nulldori.eamemu.eAMEMuService");
if(nfcFCardEmulation != null){ if(nfcFCardEmulation != null){
nfcFCardEmulation.registerSystemCodeForService(componentName, "4000"); nfcFCardEmulation.registerSystemCodeForService(componentName, "4000");
isHceFEnabled = true; isHceFEnabled = true;
@ -74,52 +74,23 @@ public class HcefModule extends ReactContextBaseJavaModule implements LifecycleE
} }
@ReactMethod @ReactMethod
void setSID(String sid, Promise promise){ void enableService(String sid, Promise promise){
if(nfcFCardEmulation == null || componentName == null){ if(nfcFCardEmulation == null || componentName == null){
promise.reject("NULL_ERROR", "nfcFCardEmulation or componentName is null"); promise.reject("NULL_ERROR", "nfcFCardEmulation or componentName is null");
return ;
} }
sid = sid.toUpperCase(); if (!nfcFCardEmulation.setNfcid2ForService(componentName, sid)) {
promise.reject("SET_NFCID2_FAIL", "setNfcid2ForService returned false");
if(sid.length() != 16) return ;
promise.reject("LENGTH_ERROR", "The length of sid must be 16");
if(sid.matches("[0-9a-fA-F]+") == false)
promise.reject("HEX_ERROR", "SID must be 16-digit hex number");
if(sid.substring(0,4).contentEquals("02FE") == false)
promise.reject("PREFIX_ERROR", "SID must be start with 02FE");
boolean result = nfcFCardEmulation.setNfcid2ForService(componentName, sid);
if (result) {
promise.resolve(true);
} else {
promise.reject("FAIL", "setNfcid2ForService returned false");
}
} }
@ReactMethod if (!nfcFCardEmulation.enableService(getCurrentActivity(), componentName)) {
void enableService(Promise promise){
if(nfcFCardEmulation == null || componentName == null){
promise.reject("NULL_ERROR", "nfcFCardEmulation or componentName is null");
}
String cardId = nfcFCardEmulation.getNfcid2ForService(componentName);
if(cardId.length() != 16)
promise.reject("LENGTH_ERROR", "The length of sid must be 16");
if(cardId.matches("[0-9a-fA-F]+") == false)
promise.reject("HEX_ERROR", "SID must be 16-digit hex number");
if(cardId.substring(0,4).contentEquals("02FE") == false)
promise.reject("PREFIX_ERROR", "SID must be start with 02FE");
boolean result = nfcFCardEmulation.enableService(getCurrentActivity(), componentName);
if (result) {
nowUsing = true;
promise.resolve(true);
} else {
promise.reject("FAIL", "enableService returned false"); promise.reject("FAIL", "enableService returned false");
} }
nowUsing = true;
promise.resolve(true);
} }
@ReactMethod @ReactMethod
@ -128,14 +99,12 @@ public class HcefModule extends ReactContextBaseJavaModule implements LifecycleE
promise.reject("NULL_ERROR", "nfcFCardEmulation or componentName is null"); promise.reject("NULL_ERROR", "nfcFCardEmulation or componentName is null");
} }
boolean result = nfcFCardEmulation.disableService(getCurrentActivity()); if (!nfcFCardEmulation.disableService(getCurrentActivity())) {
if (result) {
nowUsing = false;
promise.resolve(true);
} else {
promise.reject("FAIL", "disableService returned false"); promise.reject("FAIL", "disableService returned false");
} }
nowUsing = false;
promise.resolve(true);
} }
@Override @Override

View File

@ -1,4 +1,4 @@
package tk.nulldori.eamemu; package dev.nulldori.eamemu;
import com.facebook.react.ReactPackage; import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule; import com.facebook.react.bridge.NativeModule;

View File

@ -1,4 +1,4 @@
package tk.nulldori.eamemu; package dev.nulldori.eamemu;
import android.os.Bundle; import android.os.Bundle;
import com.facebook.react.ReactActivity; import com.facebook.react.ReactActivity;

View File

@ -1,4 +1,4 @@
package tk.nulldori.eamemu; package dev.nulldori.eamemu;
import android.app.Application; import android.app.Application;
import com.facebook.react.PackageList; import com.facebook.react.PackageList;
@ -8,7 +8,7 @@ import com.facebook.react.ReactPackage;
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint; import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint;
import com.facebook.react.defaults.DefaultReactNativeHost; import com.facebook.react.defaults.DefaultReactNativeHost;
import com.facebook.soloader.SoLoader; import com.facebook.soloader.SoLoader;
import com.rnfs.RNFSPackage;
import java.util.List; import java.util.List;
public class MainApplication extends Application implements ReactApplication { public class MainApplication extends Application implements ReactApplication {

View File

@ -1,4 +1,4 @@
package tk.nulldori.eamemu; package dev.nulldori.eamemu;
import android.nfc.cardemulation.HostNfcFService; import android.nfc.cardemulation.HostNfcFService;
import android.os.Bundle; import android.os.Bundle;

View File

@ -4,7 +4,7 @@
* <p>This source code is licensed under the MIT license found in the LICENSE file in the root * <p>This source code is licensed under the MIT license found in the LICENSE file in the root
* directory of this source tree. * directory of this source tree.
*/ */
package com.rndiffapp; package dev.nulldori.eamemu;
import android.content.Context; import android.content.Context;
import com.facebook.react.ReactInstanceManager; import com.facebook.react.ReactInstanceManager;
/** /**

View File

@ -1,604 +0,0 @@
import React from 'react';
import {
View,
Text,
StatusBar,
SafeAreaView,
ScrollView,
TouchableOpacity,
Dimensions,
ImageBackground,
findNodeHandle,
Alert,
StyleSheet,
Image,
KeyboardAvoidingView,
TextInput,
} from 'react-native';
import update from 'react-addons-update';
import Icon from 'react-native-vector-icons/MaterialIcons';
import ImagePicker from 'react-native-image-crop-picker';
import CardConv from '../modules/CardConv';
import i18n from 'i18n-js';
class CardPreview extends React.Component {
render() {
let cardContent = (
<View
style={{
flex: 1,
backgroundColor: 'rgba(0,0,0,0.3)',
borderRadius: 8,
}}
>
<View style={{ flex: 1 }}>
<TouchableOpacity style={{ flex: 1 }}>
<Text
style={{
position: 'absolute',
top: 20,
left: 20,
fontSize: 17,
fontWeight: 'bold',
color: '#ffffff',
}}
>
{this.props.name}
</Text>
<View style={{ flex: 1, justifyContent: 'center', paddingTop: 20 }}>
<Text
style={{
paddingTop: 0,
textAlign: 'center',
alignSelf: 'center',
color: '#E0E0E0',
fontSize: 14,
}}
>
{this.props.uid
? this.props.uid.substr(0, 4) +
'-' +
this.props.uid.substr(4, 4) +
'-' +
this.props.uid.substr(8, 4) +
'-' +
this.props.uid.substr(12, 4)
: ''}
</Text>
<Text
style={{
paddingTop: 8,
textAlign: 'center',
alignSelf: 'center',
fontSize: 24,
color: '#FAFAFA',
fontWeight: '500',
letterSpacing: -0.5,
}}
>
{i18n.t('card_touch_to_enable')}
</Text>
</View>
</TouchableOpacity>
</View>
<View style={{ height: 1, backgroundColor: '#FAFAFA' }} />
<View style={{ height: 48, flexDirection: 'row' }}>
<TouchableOpacity
style={{
flex: 1,
alignItems: 'center',
justifyContent: 'center',
}}
>
<Text style={{ fontSize: 14, color: '#FAFAFA' }}>
{i18n.t('card_edit')}
</Text>
</TouchableOpacity>
<View style={{ width: 1, backgroundColor: '#FAFAFA' }} />
<TouchableOpacity
style={{
flex: 1,
alignItems: 'center',
justifyContent: 'center',
}}
>
<Text style={{ fontSize: 14, color: '#ffffff' }}>
{i18n.t('card_delete')}
</Text>
</TouchableOpacity>
</View>
</View>
);
return (
<View
style={[
{
borderRadius: 8,
height: this.props.cardHeight,
marginTop: 16,
justifyContent: 'center',
},
]}
>
{this.props.image ? (
<ImageBackground
source={{ uri: this.props.image }}
style={{
flex: 1,
resizeMode: 'contain',
}}
blurRadius={2}
borderRadius={8}
>
{cardContent}
</ImageBackground>
) : (
<View
style={{
flex: 1,
resizeMode: 'contain',
backgroundColor: '#03A9F4',
}}
blurRadius={2}
borderRadius={8}
>
{cardContent}
</View>
)}
</View>
);
}
}
class ETextInput extends React.Component {
state = {
focused: false,
value: this.props.value ?? '',
};
onFocusCallback() {
this.setState({ focused: true });
if (typeof this.props.onFocus === 'function') {
this.props.onFocus();
}
}
onBlurCallback() {
this.setState({ focused: false });
if (typeof this.props.onBlur === 'function') {
this.props.onBlur();
}
}
onChangeTextCallback(text) {
if (typeof this.props.filter === 'function') {
text = this.props.filter(text);
}
if (typeof this.props.onChangeText === 'function') {
this.props.onChangeText(text);
}
}
focus() {
this.textInput.focus();
}
render() {
return (
<View style={this.props.style}>
<Text
style={[
{
fontSize: 14,
color: this.props.error
? '#F44336'
: this.state.focused
? '#03A9F4'
: '#9E9E9E',
fontWeight: 'bold',
},
this.props.titleStyle,
]}
>
{this.props.title}
</Text>
<TextInput
style={[{ fontSize: 17, paddingTop: 4 }, this.props.textStyle]}
value={this.props.value}
onFocus={() => this.onFocusCallback()}
onBlur={() => this.onBlurCallback()}
onChangeText={text => this.onChangeTextCallback(text)}
editable={this.props.editable}
maxLength={this.props.maxLength}
autoCapitalize={this.props.autoCapitalize}
ref={ref => (this.textInput = ref)}
/>
<View
style={{
paddingTop: 2,
height: this.state.focused ? 2 : 1,
backgroundColor: this.props.error
? '#F44336'
: this.state.focused
? '#03A9F4'
: '#9E9E9E',
opacity: this.props.editable ? 1 : 0.5,
}}
/>
</View>
);
}
}
class CardEditScreen extends React.Component {
state = {
name: this.props.navigation.getParam('name', ''),
sidAll: this.props.navigation.getParam('sid', '02FE'),
sid2: '',
sid3: '',
sid4: '',
uid: '',
sidError: false,
image: this.props.navigation.getParam('image', ''),
index: this.props.navigation.getParam('index', null), // null 이면 카드 새로 생성
mode: '',
update: this.props.navigation.getParam('update', null),
cardFocused: 'false',
};
componentDidMount(): void {
let { height, width } = Dimensions.get('window');
function randomHex4Byte() {
let hexString = Math.min(
Math.floor(Math.random() * 65536),
65535,
).toString(16);
if (hexString.length < 4) {
hexString = '0'.repeat(4 - hexString.length) + hexString;
}
return hexString.toUpperCase();
}
if (this.state.index === null) {
this.setState(
{
mode: 'add',
sid2: randomHex4Byte(),
sid3: randomHex4Byte(),
sid4: randomHex4Byte(),
cardHeight: ((width - 48) * 53.98) / 85.6,
},
() => this.updateSID(),
);
} else {
this.setState(
{
mode: 'edit',
sid2: this.state.sidAll.substr(4, 4),
sid3: this.state.sidAll.substr(8, 4),
sid4: this.state.sidAll.substr(12, 4),
cardHeight: ((width - 48) * 53.98) / 85.6,
},
() => {
this.updateSID();
},
);
}
this.nameInput.focus();
}
makeSid() {
let sid = '02FE' + this.state.sid2 + this.state.sid3 + this.state.sid4;
sid = sid.toUpperCase();
if (sid.length !== 16) {
return '';
}
if (sid.replace(/[^0-9A-F]/g, '') !== sid) {
return '';
}
return sid;
}
async updateSID() {
if (this.makeSid() !== '') {
let uid = await CardConv.convertSID(this.makeSid());
this.setState({ uid: uid });
}
}
saveCard() {
let sid = this.makeSid();
if (sid === '') {
this.setState({ sidError: true });
return;
}
if (typeof this.state.update === 'function') {
this.state.update(
this.state.name,
sid,
this.state.index,
this.state.image,
this.props.navigation,
);
}
}
setRandomSid() {
function randomHex4Byte() {
let hexString = Math.min(
Math.floor(Math.random() * 65536),
65535,
).toString(16);
if (hexString.length < 4) {
hexString = '0'.repeat(4 - hexString.length) + hexString;
}
return hexString.toUpperCase();
}
this.setState(
{
sid2: randomHex4Byte(),
sid3: randomHex4Byte(),
sid4: randomHex4Byte(),
},
() => this.updateSID(),
);
}
selectPhoto() {
ImagePicker.openPicker({
cropping: true,
width: 856,
height: 540,
mediaType: 'photo',
}).then(res => {
if (res && res.path) {
this.setState({ image: res.path });
}
});
}
render() {
return (
<SafeAreaView style={{ flex: 1, paddingTop: StatusBar.currentHeight }}>
<StatusBar
barStyle="dark-content"
translucent={true}
backgroundColor={'#ffffff'}
/>
<KeyboardAvoidingView style={{ flex: 1 }}>
<View
style={{
height: 48,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#ffffff',
}}
>
<View
style={{
flex: 1,
alignItems: 'center',
justifyContent: 'center',
}}
>
<Text
style={{
fontSize: 17,
fontWeight: 'bold',
textAlignVertical: 'center',
}}
>
{this.state.mode === 'add'
? i18n.t('header_add')
: i18n.t('header_edit')}
</Text>
<TouchableOpacity
style={{
position: 'absolute',
top: 0,
bottom: 0,
right: 16,
alignItems: 'center',
justifyContent: 'center',
}}
onPress={() => this.saveCard()}
>
<Icon name="save" size={26} color={'rgba(0,0,0,0.7)'} />
</TouchableOpacity>
</View>
</View>
<ScrollView
style={{ flex: 1, paddingHorizontal: 24, paddingTop: 16 }}
>
<ETextInput
title={i18n.t('edit_name')}
value={this.state.name}
onChangeText={text => this.setState({ name: text })}
ref={ref => (this.nameInput = ref)}
/>
<TouchableOpacity onPress={() => this.selectPhoto()}>
<ETextInput
style={{ marginTop: 24 }}
title={i18n.t('edit_background')}
value={
this.state.image
? i18n.t('edit_background_selected')
: i18n.t('edit_background_empty')
}
editable={false}
/>
</TouchableOpacity>
<View style={{ marginTop: 24, flexDirection: 'row' }}>
<ETextInput
title={'SID'}
value={'02FE'}
onChangeText={text => this.setState({ sid: text })}
style={{ flex: 1 }}
editable={false}
error={this.state.sidError}
textStyle={{ textAlign: 'center' }}
titleStyle={
this.state.sidError !== true &&
this.state.sidFocus && { color: '#03A9F4' }
}
/>
<Text
style={{
width: 20,
marginTop: 24,
alignSelf: 'center',
textAlign: 'center',
color: '#9E9E9E',
fontSize: 14,
}}
>
-
</Text>
<ETextInput
textStyle={{ textAlign: 'center' }}
value={this.state.sid2}
onChangeText={text => {
this.setState({ sid2: text }, () => this.updateSID());
if (text.length === 4) {
this.sid3Input.focus();
}
}}
style={{ flex: 1 }}
maxLength={4}
error={this.state.sidError}
onFocus={() => this.setState({ sidFocus: true })}
onBlur={() => this.setState({ sidFocus: false })}
autoCapitalize={'characters'}
ref={ref => (this.sid2Input = ref)}
editable={false}
/>
<Text
style={{
width: 20,
marginTop: 24,
alignSelf: 'center',
textAlign: 'center',
color: '#9E9E9E',
fontSize: 14,
}}
>
-
</Text>
<ETextInput
textStyle={{ textAlign: 'center' }}
value={this.state.sid3}
onChangeText={text => {
this.setState({ sid3: text }, () => this.updateSID());
if (text.length === 4) {
this.sid4Input.focus();
}
}}
style={{ flex: 1 }}
maxLength={4}
error={this.state.sidError}
onFocus={() => this.setState({ sidFocus: true })}
onBlur={() => this.setState({ sidFocus: false })}
autoCapitalize={'characters'}
ref={ref => (this.sid3Input = ref)}
editable={false}
/>
<Text
style={{
width: 20,
marginTop: 24,
alignSelf: 'center',
textAlign: 'center',
color: '#9E9E9E',
fontSize: 14,
}}
>
-
</Text>
<ETextInput
textStyle={{ textAlign: 'center' }}
value={this.state.sid4}
onChangeText={text => {
this.setState({ sid4: text }, () => this.updateSID());
this.updateSID();
}}
style={{ flex: 1 }}
error={this.state.sidError}
maxLength={4}
onFocus={() => this.setState({ sidFocus: true })}
onBlur={() => this.setState({ sidFocus: false })}
autoCapitalize={'characters'}
ref={ref => (this.sid4Input = ref)}
editable={false}
/>
</View>
<Text
style={{
marginTop: 8,
fontSize: 14,
letterSpacing: -0.4,
color: this.state.sidError ? '#F44336' : '#9E9E9E',
}}
>
{i18n.t('edit_sid_notice')}
</Text>
<TouchableOpacity
style={{
marginTop: 24,
height: 48,
backgroundColor: '#03A9F4',
borderRadius: 8,
alignItems: 'center',
justifyContent: 'center',
}}
onPress={() => this.setRandomSid()}
>
<Text
style={{ fontSize: 17, color: '#ffffff', fontWeight: '500' }}
>
{i18n.t('edit_random')}
</Text>
</TouchableOpacity>
<Text
style={{
fontSize: 14,
color: '#9E9E9E',
marginTop: 20,
fontWeight: 'bold',
}}
>
{i18n.t('edit_preview')}
</Text>
<CardPreview
name={this.state.name}
uid={this.state.uid}
image={this.state.image}
cardHeight={this.state.cardHeight ?? 0}
/>
<View style={{ height: 50 }} />
</ScrollView>
</KeyboardAvoidingView>
</SafeAreaView>
);
}
}
export default CardEditScreen;

View File

@ -0,0 +1,289 @@
import React, { useCallback, useMemo, useState } from 'react';
import {
View,
Text,
ScrollView,
TouchableOpacity,
StyleSheet,
KeyboardAvoidingView,
TextInput,
TextInputProps,
ViewStyle,
TextInputFocusEventData,
NativeSyntheticEvent,
TextStyle,
} from 'react-native';
import CardConv from '../modules/CardConv';
import { NativeStackScreenProps } from '@react-navigation/native-stack';
import { RootStackParams } from '../../App';
import { Shadow } from 'react-native-shadow-2';
import CardView from '../components/Card';
import { addCard, updateCard } from '../data/cards';
import { useMutation, useQuery, useQueryClient } from 'react-query';
import { Card } from '../types';
type TextFieldProps = TextInputProps & {
title: string;
containerStyle?: ViewStyle;
};
const generateRandomCardNumber = () => {
const getRandom4Byte = () => {
return Math.trunc(Math.random() * 65536)
.toString(16)
.toUpperCase()
.padStart(4, '0');
};
return `02FE${getRandom4Byte()}${getRandom4Byte()}${getRandom4Byte()}`;
};
const FieldTitle = (props: { title: string; style?: TextStyle }) => {
return (
<Text style={[styles.textInputTitle, props.style]}>{props.title}</Text>
);
};
const TextField = (props: TextFieldProps) => {
const { onFocus, onBlur, title, containerStyle, ...textInputProps } = props;
const [isFocused, setIsFocused] = useState<boolean>(false);
const onFocusCallback = useCallback(
(event: NativeSyntheticEvent<TextInputFocusEventData>) => {
setIsFocused(true);
onFocus?.(event);
},
[onFocus],
);
const onBlurCallback = useCallback(
(event: NativeSyntheticEvent<TextInputFocusEventData>) => {
setIsFocused(false);
onBlur?.(event);
},
[onBlur],
);
return (
<View style={containerStyle}>
<FieldTitle
title={title}
style={isFocused ? styles.textInputTitleFocused : {}}
/>
<TextInput
style={styles.textInput}
onFocus={onFocusCallback}
onBlur={onBlurCallback}
{...textInputProps}
/>
<View
style={[
styles.textInputBottomBorder,
isFocused ? styles.textInputBottomBorderFocused : {},
]}
/>
</View>
);
};
type CardAddScreenProps = NativeStackScreenProps<RootStackParams, 'Add'>;
type CardEditScreenProps = NativeStackScreenProps<RootStackParams, 'Edit'>;
const CardEditScreen = (props: CardAddScreenProps | CardEditScreenProps) => {
const initialData = props.route.params?.card ?? undefined;
const [mode] = useState<'add' | 'edit'>(() => {
return initialData ? 'edit' : 'add';
});
const [cardName, setCardName] = useState<string>(initialData?.name ?? 'eAM');
const [cardNumber, setCardNumber] = useState<string>(() => {
return initialData?.sid ?? generateRandomCardNumber();
});
const uid = useQuery(['uid', cardNumber], () =>
CardConv.convertSID(cardNumber),
);
const styledUid = useMemo(() => {
if (!uid.isSuccess) {
return '카드번호를 불러오는 중...';
}
return (
uid.data.match(/[A-Za-z0-9]{4}/g)?.join(' - ') ??
'잘못된 카드 번호입니다.'
);
}, [uid]);
const onChangeCardName = useCallback((s: string) => {
setCardName(s);
}, []);
const changeCardNumber = useCallback(() => {
setCardNumber(generateRandomCardNumber());
}, []);
const queryClient = useQueryClient();
const addMutation = useMutation(
(card: Card) => {
return addCard(card);
},
{
onSuccess: async () => {
await queryClient.invalidateQueries('cards');
props.navigation.goBack();
},
},
);
const editMutation = useMutation(
({ index, card }: { index: number; card: Card }) => {
return updateCard(index, card);
},
{
onSuccess: async () => {
await queryClient.invalidateQueries('cards');
props.navigation.goBack();
},
},
);
const save = useCallback(() => {
const card = {
sid: cardNumber,
name: cardName,
};
if (mode === 'add') {
addMutation.mutate(card);
} else {
editMutation.mutate({ index: props.route.params!.index, card: card });
}
}, [
addMutation,
cardName,
cardNumber,
editMutation,
mode,
props.route.params,
]);
return (
<KeyboardAvoidingView style={styles.screen}>
<ScrollView
contentContainerStyle={styles.scrollView}
keyboardShouldPersistTaps="handled"
>
<View style={styles.cardPreviewContainer}>
<CardView
card={{
sid: cardNumber,
name: cardName,
}}
mainText={'카드 미리보기'}
index={0 /* dummy index */}
disabledMainButton={true}
hideBottomMenu={true}
/>
</View>
<View style={[styles.fieldItemContainer, { paddingTop: 0 }]}>
<TextField
title={'카드 이름'}
value={cardName}
onChangeText={onChangeCardName}
/>
</View>
<View style={styles.fieldItemContainer}>
<TextField title={'카드 번호'} value={styledUid} editable={false} />
<Shadow
style={styles.buttonShadowStyle}
containerStyle={styles.cardNumberChangeButton}
distance={4}
>
<TouchableOpacity
style={[styles.button]}
onPress={changeCardNumber}
disabled={!uid.isSuccess}
>
<Text style={styles.buttonText}> </Text>
</TouchableOpacity>
</Shadow>
</View>
<Shadow
style={styles.buttonShadowStyle}
containerStyle={[
styles.cardNumberChangeButton,
styles.saveButtonContainerStyle,
]}
distance={4}
>
<TouchableOpacity style={[styles.button]} onPress={save}>
<Text style={styles.buttonText}></Text>
</TouchableOpacity>
</Shadow>
</ScrollView>
</KeyboardAvoidingView>
);
};
const styles = StyleSheet.create({
screen: {
flex: 1,
},
scrollView: {
padding: 16,
},
cardPreviewContainer: {
paddingBottom: 32,
},
fieldItemContainer: {
paddingTop: 24,
},
textInput: {
fontSize: 16,
paddingTop: 4,
},
textInputTitle: {
fontSize: 14,
fontWeight: 'bold',
color: '#9e9e9e',
},
textInputTitleFocused: {
color: 'skyblue',
},
textInputBottomBorder: {
paddingTop: 2,
backgroundColor: '#9e9e9e',
height: 1,
},
textInputBottomBorderFocused: {
backgroundColor: 'skyblue',
height: 2,
},
button: {
height: 48,
borderRadius: 8,
backgroundColor: 'skyblue',
justifyContent: 'center',
alignItems: 'center',
},
buttonShadowStyle: {
width: '100%',
},
saveButtonContainerStyle: {
marginTop: 32,
},
cardNumberChangeButton: {
marginTop: 16,
},
cardImageSelectButton: {
marginTop: 8,
},
buttonText: {
color: 'white',
},
});
export default CardEditScreen;