diff --git a/android/app/BUCK b/android/app/BUCK index 12e92184..932ee17e 100644 --- a/android/app/BUCK +++ b/android/app/BUCK @@ -45,12 +45,12 @@ android_library( android_build_config( name = "build_config", - package = "com.wddclient", + package = "com.woodongdang.client.android", ) android_resource( name = "res", - package = "com.wddclient", + package = "com.woodongdang.client.android", res = "src/main/res", ) diff --git a/android/app/build.gradle b/android/app/build.gradle index 4d9dc1a9..503482e6 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -98,11 +98,11 @@ android { buildToolsVersion '28.0.3' defaultConfig { - applicationId "com.wddclient" + applicationId "com.woodongdang.client.android" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 1 - versionName "1.0" + versionCode 2 + versionName "0.0.2" ndk { abiFilters "armeabi-v7a", "x86" } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 8469451f..f5f882f5 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,5 +1,5 @@ + package="com.woodongdang.client.android"> @@ -15,12 +15,19 @@ + android:windowSoftInputMode="adjustNothing"> + + + + + + diff --git a/android/app/src/main/java/com/wddclient/MainActivity.java b/android/app/src/main/java/com/woodongdang/client/android/MainActivity.java similarity index 95% rename from android/app/src/main/java/com/wddclient/MainActivity.java rename to android/app/src/main/java/com/woodongdang/client/android/MainActivity.java index 235ecda2..268a85f4 100644 --- a/android/app/src/main/java/com/wddclient/MainActivity.java +++ b/android/app/src/main/java/com/woodongdang/client/android/MainActivity.java @@ -1,4 +1,4 @@ -package com.wddclient; +package com.woodongdang.client.android; import com.facebook.react.ReactActivity; import com.facebook.react.ReactActivityDelegate; diff --git a/android/app/src/main/java/com/wddclient/MainApplication.java b/android/app/src/main/java/com/woodongdang/client/android/MainApplication.java similarity index 97% rename from android/app/src/main/java/com/wddclient/MainApplication.java rename to android/app/src/main/java/com/woodongdang/client/android/MainApplication.java index 55c0409c..620df886 100644 --- a/android/app/src/main/java/com/wddclient/MainApplication.java +++ b/android/app/src/main/java/com/woodongdang/client/android/MainApplication.java @@ -1,4 +1,4 @@ -package com.wddclient; +package com.woodongdang.client.android; import android.app.Application; diff --git a/ios/WddClient/AppDelegate.m b/ios/WddClient/AppDelegate.m index 06fadd92..c48419ff 100644 --- a/ios/WddClient/AppDelegate.m +++ b/ios/WddClient/AppDelegate.m @@ -7,6 +7,7 @@ #import "AppDelegate.h" +#import #import #import @@ -36,4 +37,10 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( return YES; } +- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url + sourceApplication:(NSString *)sourceApplication annotation:(id)annotation +{ + return [RCTLinkingManager application:application openURL:url + sourceApplication:sourceApplication annotation:annotation]; +} @end diff --git a/ios/WddClient/Info.plist b/ios/WddClient/Info.plist index 338c3726..bff44cbf 100644 --- a/ios/WddClient/Info.plist +++ b/ios/WddClient/Info.plist @@ -17,11 +17,24 @@ CFBundlePackageType APPL CFBundleShortVersionString - 0.0.1 + 0.0.2 CFBundleSignature ???? + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLName + woodongdang + CFBundleURLSchemes + + woodongdang + + + CFBundleVersion - 1 + 2 LSRequiresIPhoneOS NSAppTransportSecurity diff --git a/package.json b/package.json index b3b96ba4..a1f12ca3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wdd-client", - "version": "0.0.1", + "version": "0.0.2", "private": true, "scripts": { "start": "node node_modules/react-native/local-cli/cli.js start", diff --git a/src/App.tsx b/src/App.tsx index 4ff56450..39f1a40f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -10,7 +10,7 @@ const App: React.FC<{}> = () => { return ( - + ); }; diff --git a/src/components/base/Core.tsx b/src/components/base/Core.tsx index f3bae9f1..4e9f8a67 100644 --- a/src/components/base/Core.tsx +++ b/src/components/base/Core.tsx @@ -18,7 +18,11 @@ class Core extends PureComponent { super(props); configAxios(); Amplify.configure(awsconfig); - props.autoSignIn(this.props.navigation); + } + + componentDidMount() { + const { autoSignIn, navigation } = this.props; + autoSignIn(navigation); } render() { diff --git a/src/components/module/PageContainer/PageContainer.styles.ts b/src/components/module/PageContainer/PageContainer.styles.ts index c793797c..de9ddd8d 100644 --- a/src/components/module/PageContainer/PageContainer.styles.ts +++ b/src/components/module/PageContainer/PageContainer.styles.ts @@ -4,13 +4,11 @@ import { color, font } from 'src/theme'; export const views = StyleSheet.create({ container: { flex: 1 }, - contentWrapper: { flex: 1 }, - wrapperNormal: { marginHorizontal: '10.7%' }, - wrapperNarrow: { marginHorizontal: '5.3%' }, + contentWrapper: { flex: 1, marginHorizontal: '5.3%' }, topWrapper: { - marginTop: 20, + marginTop: 15, marginHorizontal: '4%', - height: 26, + height: 21, flexDirection: 'row', alignItems: 'center', }, @@ -42,23 +40,29 @@ export const views = StyleSheet.create({ titleWrapper: { marginVertical: 40, }, + backIcon: { + width: 18, + height: 16, + resizeMode: 'contain', + }, }); export const texts = StyleSheet.create({ top: { color: color.blackOpacity, - fontSize: font.size.large, + fontSize: 16, }, center: { color: color.black, fontSize: font.size.large, }, bottomText: { - color: color.gary55, + color: `${color.gary55}CC`, fontSize: font.size.small, + textDecorationLine: 'underline', }, bottomDiff: { - color: color.black33, + color: `${color.black33}7F`, fontSize: font.size.small, }, bottomBox: { diff --git a/src/components/module/PageContainer/PageContainer.tsx b/src/components/module/PageContainer/PageContainer.tsx index 0df7fdac..3dd5696b 100644 --- a/src/components/module/PageContainer/PageContainer.tsx +++ b/src/components/module/PageContainer/PageContainer.tsx @@ -1,10 +1,12 @@ import React, { ReactNode } from 'react'; +import { NavigationScreenProp } from 'react-navigation'; import { SafeAreaView, TouchableOpacity, Text, ScrollView, View, + Image, } from 'react-native'; import { views, texts } from './PageContainer.styles'; @@ -16,8 +18,7 @@ interface Props { subtitle?: string; // top left?: { - text: string; - handlePress: () => void; + navigation: NavigationScreenProp; }; right?: { text: string; @@ -34,10 +35,23 @@ interface Props { disable?: boolean; }; // options - narrow?: boolean; [x: string]: any; } +const NavbarLeft: React.FC = ({ navigation }) => { + function navBack() { + navigation.goBack(null); + } + return ( + + + + ); +}; + const PageContainer: React.FC = ({ children, title, @@ -46,7 +60,6 @@ const PageContainer: React.FC = ({ right, center, bottom, - narrow, ...scrollOptions }) => ( @@ -56,11 +69,7 @@ const PageContainer: React.FC = ({ {center} )} - {left && ( - - {left.text} - - )} + {left && } {right && ( = ({ )} - + {title && ( {title} @@ -104,19 +108,19 @@ const PageContainer: React.FC = ({ ) : ( - - {bottom.text} - {bottom.diffText && bottom.handleDiffPress && ( - {bottom.diffText} + {bottom.diffText} )} + + {bottom.text} + ))} diff --git a/src/components/module/TextAutocomplete/TextAutocomplete.tsx b/src/components/module/TextAutocomplete/TextAutocomplete.tsx index 959fd58d..9bf7da9b 100644 --- a/src/components/module/TextAutocomplete/TextAutocomplete.tsx +++ b/src/components/module/TextAutocomplete/TextAutocomplete.tsx @@ -69,9 +69,8 @@ class Search extends Component { visible={this.state.showModal} onRequestClose={this.toggleModal}> + right={{ text: '닫기', handlePress: this.toggleModal }} + scrollEnabled={false}> ; +} + interface Props { label: string; name: string; value?: string; alert?: string; + inputs?: InputsInterface; handleChange: (data: HandleChangeText) => void; + returnKeyType?: 'next' | 'done' | 'send' | 'search'; [x: string]: any; } @@ -24,7 +30,7 @@ interface State { isFocus: boolean; } -class TextInput extends Component { +class TextInput extends PureComponent { state: State = { isFocus: false }; handleFocus = (e: NativeSyntheticEvent) => { @@ -40,8 +46,31 @@ class TextInput extends Component { handleChange({ name, value }); }; + handleFocusNext = () => { + const { inputs, name } = this.props; + if (inputs) { + const keyArray = Object.keys(inputs); + keyArray.map((key, index) => { + if (key === name) { + // select next input + const { current } = inputs[keyArray[index + 1]]; + if (current) current.focus(); + } + }); + } + }; + render() { - const { label, value, alert, ...options } = this.props; + const { + name, + label, + value, + alert, + inputs, + returnKeyType, + ...options + } = this.props; + return ( { onChangeText={this.handleChangeWithName} onFocus={options.handleFocus || this.handleFocus} onBlur={this.handleBlur} + returnKeyType={returnKeyType || 'default'} autoCapitalize="none" autoCorrect={false} style={[ - inputs.text, - inputs[this.state.isFocus ? 'focused' : 'unFocused'], + inputStyle.text, + inputStyle[this.state.isFocus ? 'focused' : 'unFocused'], ]} {...options} + {...inputs && { ref: inputs[name] }} + {...(returnKeyType === 'next' + ? { + onSubmitEditing: this.handleFocusNext, + } + : {})} /> {alert && {alert}} diff --git a/src/components/module/withLoading.tsx b/src/components/module/withLoading.tsx index bfb7b145..32184ef0 100644 --- a/src/components/module/withLoading.tsx +++ b/src/components/module/withLoading.tsx @@ -58,6 +58,7 @@ export default function withLoading

( style={{ width: 100, height: 100, + resizeMode: 'contain', transform: [{ rotate: spin }], }} /> diff --git a/src/lib/icons/ic_agree.png b/src/lib/icons/ic_agree.png new file mode 100644 index 00000000..c8e400d9 Binary files /dev/null and b/src/lib/icons/ic_agree.png differ diff --git a/src/lib/icons/ic_agree@2x.png b/src/lib/icons/ic_agree@2x.png new file mode 100644 index 00000000..c32f13b6 Binary files /dev/null and b/src/lib/icons/ic_agree@2x.png differ diff --git a/src/lib/icons/ic_agree@3x.png b/src/lib/icons/ic_agree@3x.png new file mode 100644 index 00000000..22c1f8f2 Binary files /dev/null and b/src/lib/icons/ic_agree@3x.png differ diff --git a/src/lib/icons/ic_back.png b/src/lib/icons/ic_back.png new file mode 100644 index 00000000..eb4586dc Binary files /dev/null and b/src/lib/icons/ic_back.png differ diff --git a/src/lib/icons/ic_back@2x.png b/src/lib/icons/ic_back@2x.png new file mode 100644 index 00000000..17dd8a0c Binary files /dev/null and b/src/lib/icons/ic_back@2x.png differ diff --git a/src/lib/icons/ic_back@3x.png b/src/lib/icons/ic_back@3x.png new file mode 100644 index 00000000..5cda79a9 Binary files /dev/null and b/src/lib/icons/ic_back@3x.png differ diff --git a/src/pages/Router.ts b/src/pages/Router.ts index 9febbf5c..5f5822fd 100644 --- a/src/pages/Router.ts +++ b/src/pages/Router.ts @@ -9,7 +9,10 @@ export default createAppContainer( { core: CoreScreen, app: AppNavigator, - session: SessionNavigator, + session: { + screen: SessionNavigator, + path: 'session', + }, }, { initialRouteName: 'core', diff --git a/src/pages/Session/Agreement/Agreement.styles.ts b/src/pages/Session/Agreement/Agreement.styles.ts index c3ad4cea..427338d9 100644 --- a/src/pages/Session/Agreement/Agreement.styles.ts +++ b/src/pages/Session/Agreement/Agreement.styles.ts @@ -1,5 +1,5 @@ import { StyleSheet } from 'react-native'; -import { font, color } from 'src/theme'; +import { color } from 'src/theme'; export const views = StyleSheet.create({ agreeAll: { @@ -18,6 +18,14 @@ export const views = StyleSheet.create({ agreeAllInactive: { borderColor: color.grayDA, }, + agreeIcon: { + position: 'absolute', + top: -50, + right: '7.3%', + width: 58, + height: 54, + resizeMode: 'contain', + }, }); export const texts = StyleSheet.create({ diff --git a/src/pages/Session/Agreement/Agreement.tsx b/src/pages/Session/Agreement/Agreement.tsx index 42c2f750..6f3b0403 100644 --- a/src/pages/Session/Agreement/Agreement.tsx +++ b/src/pages/Session/Agreement/Agreement.tsx @@ -1,6 +1,6 @@ import React, { Component } from 'react'; -import produce, { isDraft } from 'immer'; -import { TouchableOpacity, Text } from 'react-native'; +import produce from 'immer'; +import { TouchableOpacity, Text, Image } from 'react-native'; import { NavigationScreenProp } from 'react-navigation'; import PageContainer from 'src/components/module/PageContainer'; @@ -56,6 +56,10 @@ class Agreement extends Component { ]}> 아래 약관에 모두 동의합니다. + ); diff --git a/src/pages/Session/ChangePassword/ChangePassword.tsx b/src/pages/Session/ChangePassword/ChangePassword.tsx index 733d69a7..60ff798b 100644 --- a/src/pages/Session/ChangePassword/ChangePassword.tsx +++ b/src/pages/Session/ChangePassword/ChangePassword.tsx @@ -1,10 +1,14 @@ import React, { Component } from 'react'; -import { Alert } from 'react-native'; +import { TextInput as Input } from 'react-native'; import { NavigationScreenProp } from 'react-navigation'; - +// Redux +import { connect } from 'react-redux'; +import * as actions from 'src/store/actions/user'; +// Components import PageContainer from 'src/components/module/PageContainer'; import TextInput, { HandleChangeText } from 'src/components/module/TextInput'; import RoundButton from 'src/components/module/RoundButton'; +// Other import { validatePassword } from 'src/lib/validates/string'; interface ParamInterface { @@ -15,6 +19,7 @@ interface ParamInterface { interface Props { navigation: NavigationScreenProp; + changePassword: typeof actions.changePassword; } interface State { @@ -23,6 +28,11 @@ interface State { } class ChangePassword extends Component { + private inputs = { + password: React.createRef(), + passwordCheck: React.createRef(), + }; + state: State = { password: { value: '', @@ -49,20 +59,12 @@ class ChangePassword extends Component { }; handleSubmit = () => { - // TODO: HANDLE CHANGE PASSWORD ON API - const { navigation } = this.props; - Alert.alert( - '추후에 비밀번호 변경을 추가할 계획입니다.', - '확인을 눌러 진행하세요.', - [ - { text: '예', onPress: () => navigation.navigate('signIn') }, - { - text: '아니오', - onPress: () => {}, - style: 'cancel', - }, - ] - ); + const { changePassword, navigation } = this.props; + const token = navigation.getParam('token', null); + if (token) { + const { password } = this.state; + changePassword({ password: password.value, token }, navigation); + } }; render() { @@ -73,16 +75,15 @@ class ChangePassword extends Component { navigation.navigate('signIn'), - }} + left={{ navigation }} scrollEnabled={false}> { name="passwordCheck" value={passwordCheck.value} secureTextEntry={true} + returnKeyType="send" + inputs={this.inputs} handleChange={this.handleChange} + onSubmitEditing={this.handleSubmit} /> { } } -export default ChangePassword; +export default connect( + null, + { changePassword: actions.changePassword } +)(ChangePassword); diff --git a/src/pages/Session/CreateDog/CreateDog.tsx b/src/pages/Session/CreateDog/CreateDog.tsx index 922845a0..5f058a20 100644 --- a/src/pages/Session/CreateDog/CreateDog.tsx +++ b/src/pages/Session/CreateDog/CreateDog.tsx @@ -14,24 +14,18 @@ import TextInput, { HandleChangeText } from 'src/components/module/TextInput'; import TextAutocomplete from 'src/components/module/TextAutocomplete'; import Selector, { HandleChangeSelector } from 'src/components/module/Selector'; // Other -import { Storage } from 'aws-amplify'; import ImagePicker from 'react-native-image-picker'; import produce from 'immer'; +import { uploadImage } from 'src/services/aws/s3'; import breeds from 'src/lib/consts/breeds.json'; interface Props extends LoadingProps { navigation: NavigationScreenProp; createDog: typeof actions.createDog; - user: ReducerState['user']; + email: ReducerState['user']['email']; } -interface State { - // ShortenDogInterface - name: string; - thumbnail: string; - breed: string; - gender: 'M' | 'F' | 'N' | ''; - // Options +interface State extends actions.ShortenDogInterface { thumbnailFile?: any; } @@ -77,21 +71,17 @@ class CreateDog extends Component { }; handleSubmit = async () => { - const { createDog, navigation, user, toggleLoading } = this.props; - const imgFile = this.state.thumbnailFile; - if (imgFile) { - await toggleLoading(); - const result = (await Storage.put( - `${user.email}/dogs/${this.state.name}/thumbnail.png`, - imgFile, - { contentType: 'image/png' } - )) as { key: string }; - await this.setState({ thumbnail: result.key }); - } - const { thumbnailFile, ...state } = this.state; + const { createDog, navigation, email, toggleLoading } = this.props; + const thumbnail = await uploadImage({ + table: 'dogs', + email, + name: this.state.name, + type: 'thumbnail', + file: this.state.thumbnailFile, + })(toggleLoading); - await toggleLoading(); - createDog(state, navigation); + const { name, breed, gender } = this.state; + await createDog({ name, breed, gender, thumbnail }, navigation); }; render() { @@ -102,7 +92,7 @@ class CreateDog extends Component { <> navigation.goBack(null) }} + left={{ navigation }} right={{ text: '취소', handlePress: () => navigation.popToTop() }} bottom={{ text: '다음', @@ -160,7 +150,7 @@ class CreateDog extends Component { export default connect( (state: ReducerState) => ({ - user: state.user, + email: state.user.email, }), { createDog: actions.createDog } )(withLoading(CreateDog)); diff --git a/src/pages/Session/CreateMeta/CreateMeta.tsx b/src/pages/Session/CreateMeta/CreateMeta.tsx index 81662a3e..43337a44 100644 --- a/src/pages/Session/CreateMeta/CreateMeta.tsx +++ b/src/pages/Session/CreateMeta/CreateMeta.tsx @@ -28,14 +28,12 @@ class CreateMeta extends Component { handleSubmit = () => { const { createMeta, navigation } = this.props; - const { gender, birth } = this.state; - createMeta( - { - gender, - birth: moment(birth).format('YYYY.MM.DD'), - }, - navigation - ); + const payload = { + gender: this.state.gender, + birth: moment(this.state.birth).format('YYYY.MM.DD'), + }; + + createMeta(payload, navigation); }; handleGenderChange = ({ name, value }: HandleChangeSelector) => { @@ -53,7 +51,7 @@ class CreateMeta extends Component { navigation.goBack(null) }} + left={{ navigation }} right={{ text: '취소', handlePress: () => navigation.popToTop() }} bottom={{ text: '다음', diff --git a/src/pages/Session/ForgotPassword/ForgotPassword.tsx b/src/pages/Session/ForgotPassword/ForgotPassword.tsx index 2cf5418c..607a4876 100644 --- a/src/pages/Session/ForgotPassword/ForgotPassword.tsx +++ b/src/pages/Session/ForgotPassword/ForgotPassword.tsx @@ -1,8 +1,10 @@ import React, { Component } from 'react'; -import { Alert } from 'react-native'; import produce from 'immer'; +import { connect } from 'react-redux'; import { NavigationScreenProp } from 'react-navigation'; +import * as userActions from 'src/store/actions/user'; +import { ReducerState } from 'src/store/reducers'; import PageContainer from 'src/components/module/PageContainer'; import TextInput, { HandleChangeText } from 'src/components/module/TextInput'; import RoundButton from 'src/components/module/RoundButton'; @@ -16,6 +18,8 @@ interface ParamInterface { interface Props { navigation: NavigationScreenProp; + forgotPassword: typeof userActions.forgotPassword; + user: ReducerState['user']; } interface State { @@ -30,6 +34,29 @@ class ForgotPassword extends Component { }, }; + getSnapshotBeforeUpdate(prevProps: Props) { + const { error } = this.props.user; + const prevError = prevProps.user.error; + if (error && (!prevError || error.status !== prevError.status)) + return error; + return null; + } + + componentDidUpdate( + props: Props, + state: State, + snapshot: Props['user']['error'] | null + ) { + if (snapshot) + this.setState(state => + produce(state, draft => { + delete draft.email.alert; + if (snapshot.status === 404) + draft.email.alert = snapshot.data.message; + }) + ); + } + mapEventToState = ({ value }: HandleChangeText) => { const valid = validateEmail(value); return { value, valid } as ParamInterface; @@ -43,23 +70,10 @@ class ForgotPassword extends Component { }; handleSendEmail = () => { - // TODO: REPLACE TO EMAIL VALIDATION - const { navigation } = this.props; + const { forgotPassword, navigation } = this.props; const { email } = this.state; - if (email.valid) - Alert.alert( - '추후에 이메일 인증을 추가할 계획입니다.', - '확인을 눌러 진행하세요.', - [ - { text: '예', onPress: () => navigation.navigate('changePassword') }, - { - text: '아니오', - onPress: () => {}, - style: 'cancel', - }, - ] - ); + if (email.valid) forgotPassword({ email: email.value }, navigation); else this.setState(state => produce(state, draft => { @@ -76,7 +90,7 @@ class ForgotPassword extends Component { navigation.goBack(null) }} + left={{ navigation }} scrollEnabled={false}> { } } -export default ForgotPassword; +export default connect( + (state: ReducerState) => ({ + user: state.user, + }), + { forgotPassword: userActions.forgotPassword } +)(ForgotPassword); diff --git a/src/pages/Session/SendEmail/SendEmail.tsx b/src/pages/Session/SendEmail/SendEmail.tsx new file mode 100644 index 00000000..6b544f21 --- /dev/null +++ b/src/pages/Session/SendEmail/SendEmail.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { View } from 'react-native'; +import { NavigationScreenProp } from 'react-navigation'; + +import PageContainer from 'src/components/module/PageContainer'; + +interface Props { + navigation: NavigationScreenProp; +} + +const SendEmail: React.FC = ({ navigation }) => ( + navigation.popToTop() }}> + + +); + +export default SendEmail; diff --git a/src/pages/Session/SendEmail/index.ts b/src/pages/Session/SendEmail/index.ts new file mode 100644 index 00000000..61a03c17 --- /dev/null +++ b/src/pages/Session/SendEmail/index.ts @@ -0,0 +1 @@ +export { default } from './SendEmail'; diff --git a/src/pages/Session/SignIn/SignIn.tsx b/src/pages/Session/SignIn/SignIn.tsx index 05951551..28bd2771 100644 --- a/src/pages/Session/SignIn/SignIn.tsx +++ b/src/pages/Session/SignIn/SignIn.tsx @@ -1,6 +1,6 @@ import React, { Component } from 'react'; import produce from 'immer'; -import { Image } from 'react-native'; +import { Image, TextInput as Input } from 'react-native'; import { connect } from 'react-redux'; import { NavigationScreenProp } from 'react-navigation'; @@ -30,6 +30,11 @@ interface State { } class SignIn extends Component { + private inputs = { + email: React.createRef(), + password: React.createRef(), + }; + state: State = { email: { value: '', valid: false }, password: { value: '', valid: false }, @@ -104,10 +109,10 @@ class SignIn extends Component { return ( navigation.navigate('forgotPassword'), - diffText: '회원가입 하기', - handleDiffPress: () => navigation.navigate('signUp'), + text: '회원가입', + handlePress: () => navigation.navigate('signUp'), + diffText: '비밀번호를 잊으셨나요?', + handleDiffPress: () => navigation.navigate('forgotPassword'), }} right={{ text: '건너뛰기', @@ -124,6 +129,8 @@ class SignIn extends Component { value={email.value} alert={email.alert} keyboardType="email-address" + returnKeyType="next" + inputs={this.inputs} handleChange={this.handleChange} /> { alert={password.alert} secureTextEntry={true} returnKeyType="send" + inputs={this.inputs} handleChange={this.handleChange} onSubmitEditing={this.handleSignIn} /> diff --git a/src/pages/Session/SignUp/SignUp.tsx b/src/pages/Session/SignUp/SignUp.tsx index 6b40c3d3..a27de85c 100644 --- a/src/pages/Session/SignUp/SignUp.tsx +++ b/src/pages/Session/SignUp/SignUp.tsx @@ -1,4 +1,5 @@ import React, { Component } from 'react'; +import { TextInput as Input } from 'react-native'; import { connect } from 'react-redux'; import produce from 'immer'; import { NavigationScreenProp } from 'react-navigation'; @@ -28,6 +29,13 @@ interface State { } class SignUp extends Component { + private inputs = { + name: React.createRef(), + email: React.createRef(), + password: React.createRef(), + passwordCheck: React.createRef(), + }; + state: State = { name: { value: '', valid: false }, email: { value: '', valid: false }, @@ -98,6 +106,8 @@ class SignUp extends Component { name="name" value={name.value} alert={name.alert} + returnKeyType="next" + inputs={this.inputs} handleChange={this.handleChange} /> { value={email.value} alert={email.alert} keyboardType="email-address" + returnKeyType="next" + inputs={this.inputs} handleChange={this.handleChange} /> { value={password.value} alert={password.alert} secureTextEntry={true} + returnKeyType="next" + inputs={this.inputs} handleChange={this.handleChange} /> { alert={passwordCheck.alert} secureTextEntry={true} returnKeyType="send" + inputs={this.inputs} handleChange={this.handleChange} onSubmitEditing={this.handleSignUp} /> diff --git a/src/pages/Session/index.ts b/src/pages/Session/index.ts index 5d6a01a1..7a744234 100644 --- a/src/pages/Session/index.ts +++ b/src/pages/Session/index.ts @@ -12,30 +12,16 @@ import CreateDogScreen from './CreateDog'; import TutorialScreen from './Tutorial'; import ForgotPasswordScreen from './ForgotPassword'; +import SendEmailScreen from './SendEmail'; import ChangePasswordScreen from './ChangePassword'; const SignUpNavigator = createMaterialTopTabNavigator( { - agreement: { - screen: AgreementScreen, - path: 'sign-up/agreement', - }, - signUpUser: { - screen: SignUpScreen, - path: 'sign-up/user', - }, - createMeta: { - screen: CreateMetaScreen, - path: 'sign-up/meta', - }, - createDog: { - screen: CreateDogScreen, - path: 'sign-up/dog', - }, - tutorial: { - screen: TutorialScreen, - path: 'sign-up/complete', - }, + agreement: AgreementScreen, + signUpUser: SignUpScreen, + createMeta: CreateMetaScreen, + createDog: CreateDogScreen, + tutorial: TutorialScreen, }, { initialRouteName: 'agreement', @@ -49,17 +35,21 @@ const SignUpNavigator = createMaterialTopTabNavigator( const ForgotPasswordNavigator = createMaterialTopTabNavigator( { - enterEmail: { + init: { screen: ForgotPasswordScreen, - path: 'forgot-password/email', + path: 'check-email', + }, + sendEmail: { + screen: SendEmailScreen, + path: 'send-email', }, changePassword: { screen: ChangePasswordScreen, - path: 'forgot-password/change', + path: 'change-password/:token', }, }, { - initialRouteName: 'enterEmail', + initialRouteName: 'init', animationEnabled: true, swipeEnabled: false, tabBarOptions: { @@ -70,12 +60,12 @@ const ForgotPasswordNavigator = createMaterialTopTabNavigator( export default createStackNavigator( { - signIn: { - screen: SignInScreen, - path: 'sign-in', - }, + signIn: SignInScreen, signUp: SignUpNavigator, - forgotPassword: ForgotPasswordNavigator, + forgotPassword: { + screen: ForgotPasswordNavigator, + path: 'forgot-password', + }, }, { initialRouteName: 'signIn', diff --git a/src/services/api/axios.ts b/src/services/api/axios.ts index 87fb0c35..79399b89 100644 --- a/src/services/api/axios.ts +++ b/src/services/api/axios.ts @@ -11,5 +11,5 @@ export function removeHeader() { export default function configAxios() { axios.defaults.baseURL = __DEV__ ? 'http://localhost:8080' - : 'http://ec2-13-209-98-100.ap-northeast-2.compute.amazonaws.com'; + : 'http://api.woodongdang.com'; } diff --git a/src/services/api/user.ts b/src/services/api/user.ts index 8cb9c6ad..25c08a04 100644 --- a/src/services/api/user.ts +++ b/src/services/api/user.ts @@ -1,28 +1,29 @@ import axios from 'axios'; -import { - UserInterface, - SignInInterface, - SignUpInterface, - CreateMetaInterface, -} from 'src/store/actions/user'; +import * as actions from 'src/store/actions/user'; export const getUser = async () => { // *** Set token on headers before call const response = await axios.get('/user'); - return response.data as UserInterface; + return response.data as actions.UserInterface; }; -export const signIn = async (body: SignInInterface) => { +export const signIn = async (body: actions.SignInInterface) => { const response = await axios.post('/signin', body); - return response.data as UserInterface; + return response.data as actions.SignInInterface; }; -export const signUp = async (body: SignUpInterface) => { +export const signUp = async (body: actions.SignUpInterface) => { const response = await axios.post('/signup', body); - return response.data as UserInterface; + return response.data as actions.UserInterface; }; -export const createMeta = async (body: CreateMetaInterface) => { +export const updateUser = async (body: actions.UpdateInterface) => { + // *** Set token on headers before call const response = await axios.patch('/user', body); - return response.data as UserInterface; + return response.data as actions.UserInterface; +}; + +export const forgotPassword = async (body: { email: string }) => { + const response = await axios.post('/forgot-password', body); + return response.data as { message: string }; }; diff --git a/src/services/aws/s3.ts b/src/services/aws/s3.ts new file mode 100644 index 00000000..cd134ae8 --- /dev/null +++ b/src/services/aws/s3.ts @@ -0,0 +1,37 @@ +import { Storage } from 'aws-amplify'; + +/** + * FILE STRUCTURE + * PRODUCTION : {table}/{email}/{name}/{type}.png + * DEVELOPMENT : __DEV__/{table}/{email}/{name}/{type}.png + */ +interface UploadImage { + table: 'dogs' | 'users' | 'places'; + email: string; + name: string; + type: 'thumbnail' | string; + file: any; +} + +type S3ResponseType = { key: string }; +type ToggleLoading = () => void; + +export const uploadImage = ({ + email, + table, + name, + type, + file, +}: UploadImage) => async (toggleLoading: ToggleLoading) => { + if (!file) return ''; + + await toggleLoading(); + const markDev = __DEV__ ? '__DEV__/' : ''; + const S3Response = (await Storage.put( + `${markDev}${table}/${email}/${name}/${type}.png`, + file, + { contentType: 'image/png' } + )) as S3ResponseType; + await toggleLoading(); + return S3Response.key; +}; diff --git a/src/store/actions/dog.ts b/src/store/actions/dog.ts index b4a756b1..e8789de3 100644 --- a/src/store/actions/dog.ts +++ b/src/store/actions/dog.ts @@ -6,7 +6,6 @@ export interface DogInterface extends ShortenDogInterface { user: string; // FK feeds: string[]; getLikes: string[]; - gender: 'M' | 'F' | 'N'; birth?: string; // YYYY.MM.DD weight?: number; info?: string; @@ -16,6 +15,7 @@ export interface ShortenDogInterface { name: string; thumbnail: string; breed: string; + gender: 'M' | 'F' | 'N' | ''; } // *** CONSTS diff --git a/src/store/actions/user.ts b/src/store/actions/user.ts index 89f232d5..6709b9f7 100644 --- a/src/store/actions/user.ts +++ b/src/store/actions/user.ts @@ -7,8 +7,8 @@ export interface UserInterface { readonly lastLogin: string; name: string; birth: string; - gender: 'M' | 'F' | ''; - status: 'ACTIVE' | 'PAUSED' | 'TERMINATED' | ''; + gender: string; // 'M' | 'F' + status: 'ACTIVE' | 'PAUSED' | 'TERMINATED'; dogs: { [id: string]: ShortenDogInterface | DogInterface; }; @@ -17,26 +17,36 @@ export interface UserInterface { }; } +export interface UpdateInterface { + token?: string; + password?: string; + name?: string; + birth?: string; + gender?: string; // 'M' | 'F' + dogs?: { + [id: string]: ShortenDogInterface | DogInterface; + }; + places?: { + [id: string]: ShortenDogInterface; + }; +} + export interface SignInInterface { email: string; password: string; } - export interface SignUpInterface extends SignInInterface { name: string; } -export interface CreateMetaInterface { - birth: string; - gender: string; -} - // *** CONSTS export const AUTO_SIGNIN = 'user/AUTO_SIGNIN'; export const SIGNIN = 'user/SIGNIN'; export const SIGNUP = 'user/SIGNUP'; export const SIGNOUT = 'user/SIGNOUT'; -export const UPDATE_META = 'user/UPDATE_META'; +export const CREATE_META = 'user/CREATE_META'; +export const FORGOT_PASSWORD = 'user/FORGOT_PASSWORD'; +export const CHANGE_PASSWORD = 'user/CHANGE_PASSWORD'; export const SET_USER_REQUEST = 'user/SET_USER_REQUEST'; export const SET_USER_SUCCESS = 'user/SET_USER_SUCCESS'; @@ -66,13 +76,21 @@ export const signOut = (navigation: Navigation) => ({ navigation, }); export const createMeta = ( - payload: CreateMetaInterface, + payload: { birth: string; gender: string }, + navigation: Navigation +) => ({ type: CREATE_META, payload, navigation }); +export const forgotPassword = ( + payload: { email: string }, navigation: Navigation ) => ({ - type: UPDATE_META, + type: FORGOT_PASSWORD, payload, navigation, }); +export const changePassword = ( + payload: { password: string; token: string }, + navigation: Navigation +) => ({ type: CHANGE_PASSWORD, payload, navigation }); export const setUserRequest = () => ({ type: SET_USER_REQUEST }); export const setUserSuccess = (payload: UserInterface) => ({ diff --git a/src/store/reducers/user.ts b/src/store/reducers/user.ts index 444b4606..806af716 100644 --- a/src/store/reducers/user.ts +++ b/src/store/reducers/user.ts @@ -20,7 +20,7 @@ const initialState: UserState = { name: '', birth: '', gender: '', - status: '', + status: 'TERMINATED', dogs: {}, places: {}, }; diff --git a/src/store/sagas/user.ts b/src/store/sagas/user.ts index a77f3666..12c32d15 100644 --- a/src/store/sagas/user.ts +++ b/src/store/sagas/user.ts @@ -14,11 +14,11 @@ import * as api from 'src/services/api/user'; function* autoSignIn(action: ReturnType) { try { - yield put(actions.setUserRequest()); // *** GET TOKEN FROM STORAGE const { token, nextStep } = yield call(getUserStorage); yield call(setHeader, token); - // *** GET DATA FROM API + // *** API + yield put(actions.setUserRequest()); const data = yield call(api.getUser); yield put(actions.setUserSuccess(data)); // *** NAVIGATE @@ -51,6 +51,7 @@ function* autoSignIn(action: ReturnType) { function* signIn(action: ReturnType) { try { + // *** API yield put(actions.setUserRequest()); const data = yield call(api.signIn, action.payload); yield put(actions.setUserSuccess(data)); @@ -67,13 +68,15 @@ function* signIn(action: ReturnType) { function* signUp(action: ReturnType) { try { + // *** API yield put(actions.setUserRequest()); const data = yield call(api.signUp, action.payload); yield put(actions.setUserSuccess(data)); // *** SET TOKEN const { token } = data; - const nextStep = 'createMeta'; yield call(setHeader, token); + // *** SAVE STEP ON STORAGE + const nextStep = 'createMeta'; yield call(setUserStorage, { token, nextStep }); // *** NAVIGATE yield call(action.navigation.navigate, nextStep); @@ -92,9 +95,11 @@ function* signOut(action: ReturnType) { function* createMeta(action: ReturnType) { try { + // *** API yield put(actions.setUserRequest()); - const data = yield call(api.createMeta, action.payload); + const data = yield call(api.updateUser, action.payload); yield put(actions.setUserSuccess(data)); + // *** SAVE STEP ON STORAGE const nextStep = 'createDog'; yield call(updateUserStorage, { nextStep }); // *** NAVIGATE @@ -104,10 +109,40 @@ function* createMeta(action: ReturnType) { } } +function* forgotPassword(action: ReturnType) { + try { + // *** API + yield call(api.forgotPassword, action.payload); + // *** NAVIGATE + yield call(action.navigation.navigate, 'sendEmail'); + } catch (e) { + yield put(actions.setUserFailure(e.response)); + } +} + +function* changePassword(action: ReturnType) { + try { + // *** SET TOKEN + const { token } = action.payload; + yield call(setHeader, token); + yield call(setUserStorage, { token }); + // *** API + yield put(actions.setUserRequest()); + const data = yield call(api.updateUser, action.payload); + yield put(actions.setUserSuccess(data)); + // *** NAVIGATE + yield call(action.navigation.navigate, 'app'); + } catch (e) { + yield put(actions.setUserFailure(e.response)); + } +} + export default function* root() { yield takeEvery(actions.AUTO_SIGNIN, autoSignIn); yield takeEvery(actions.SIGNIN, signIn); yield takeEvery(actions.SIGNUP, signUp); yield takeEvery(actions.SIGNOUT, signOut); - yield takeEvery(actions.UPDATE_META, createMeta); + yield takeEvery(actions.CREATE_META, createMeta); + yield takeEvery(actions.FORGOT_PASSWORD, forgotPassword); + yield takeEvery(actions.CHANGE_PASSWORD, changePassword); }