P & D
Research and Development가 아니라 Philosophy and Development의 P & D 입니다. 개발글에 있어서 새로운 시도를 하고 있습니다. 그저 개발기를 적어 내려가는 것이 아니라, 어떤 철학을 가지고 개발을 해나가고 있는지, 어떤 시도를 했는지, 개발은 어떻게 하는지를 적어 내려갑니다. 이번 시리즈는 도움을 주고 받는 방법 - 이타인클럽의 철학과 개발 과정을 다룰 것입니다.
이전글 - [철학과 개발] #17 - 도움은 세계 공통적이다!
철학
이전에 작성한 익명성에 대한 부분으로 대체합니다.
[철학과 개발] #16 익명성 - 인증방법
개발
지난 번 로그인 방법 설명하면서, 로그인 코드를 같이 설명하려다가 다국어 설정 부분이 있어서 별도로 설명합니다.
로그인을 처리하기 위해 기본적으로 다음과 같은 구조를 택했습니다.
- 로그인 화면은 SiginScreen에서 담당
https://github.com/EtainClub/helpus/blob/master/app/src/screens/SigninScreen.js - 실제 로그인 처리는 AuthContext에서 담당
https://github.com/EtainClub/helpus/blob/master/app/src/context/AuthContext.js
여기서는 국가 코드 설정 부분은 설명하지 않습니다. 관련 구현은 소스 코드를 참고해 주세요.
https://github.com/EtainClub/helpus
안드로이드
안드로이드는 전화번호 인증 방식이 좀 남다릅니다.
인증을 일반적으로 구현하다 보면 다음과 같은 에러 상황이 발생합니다.
Phone auth on android - error when confirming code 'auth/session-expired'
https://github.com/invertase/react-native-firebase/issues/530
구현은 다음을 참고합니다.
https://rnfirebase.io/docs/v5.x.x/auth/phone-auth
안드로이드 경우 전화 인증이 자동으로 됩니다. 수동으로 인증 코드를 입력하고 싶어도 잘 안되네요.
- firebase의 로그인 상태 감지
- 로그인 상태라면 signin 처리 수행
// SigninScreen.js
useEffect(() => {
// auth change listener for android only
let unsubscribe = null;
if (Platform.OS === 'android') {
unsubscribe = firebase.auth().onAuthStateChanged(user => {
if (user) { // user is verified and logged in
console.log('[onAuthStateChanged] user', user);
// sign in
signin({ user, navigation });
} else {
console.log('[onAuthStateChanged]');
}
});
}
return () => {
// unsubscribe the auth chnage
if (Platform.OS === 'android')
unsubscribe();
};
}, []);
- signin 함수: 실질적 처리 함수인 processSignin 호출
// AuthContext.js
const signin = dispatch => {
return async ({ user, navigation }) => {
processSignin({ dispatch, user, navigation });
}
};
- UI 구성 . 자동으로 인증되어 SMS 인증 코드 입력하는 부분이 없습니다.
- 참고로 아래 Spacer는 단순히 vertical 여백을 주기 위한 것입니다.
{
<Spacer>
<Button
buttonStyle={{borderRadius: 0, marginLeft: 0, marginRight: 0, marginBottom: 0}}
titleStyle={{ fontSize: 25, fontWeight: 'bold' }}
title={Platform.OS === 'android' ? t('SigninScreen.androidStart') : t('SigninScreen.verifyButton')}
loading={state.loading}
onPress={() => signinPhoneNumber({ phoneNumber: '+' + country.callingCode[0]+phoneNumber })}
/>
</Spacer>
- 입력한 전화 번호 오류 검사
// AuthContext.js
const validatePhoneNumber = (phoneNumber) => {
let regexp = /^\+[0-9]?()[0-9](\s|\S)(\d[0-9]{8,16})$/
return regexp.test(phoneNumber)
};
- firebase에 signin 요청
// AuthContext.js
// signin with phone number
const signinPhoneNumber = dispatch => {
const { t } = useTranslation();
return async ({ phoneNumber }) => {
console.log('[signinPhoneNumber] phoneNumber', phoneNumber);
// check validity of the phone number
let valid = validatePhoneNumber(phoneNumber);
if (!valid) {
console.log('[signinPhoneNumber] phone number is not valid', phoneNumber);
return;
}
// setup language
firebase.auth().languageCode = i18next.language;
// sign with the given phone number
firebase.auth().signInWithPhoneNumber(phoneNumber)
.then((confirmResult) => {
// start auth action
dispatch({
type: 'start_auth',
payload: confirmResult
});
})
.catch(error => {
console.log('[signinPhoneNumber] Error!', error);
dispatch({
type: 'add_error',
payload: t('AuthContext.SigninError')
});
});
};
}
- processSignin 함수
// AuthContext.js
// process signin
const processSignin = ({ dispatch, user, navigation }) => {
const avatarUrl = "https://firebasestorage.googleapis.com/v0/b/helpus-206eb.appspot.com/o/avatar%2F661220f6-2742-47f7-96df-c037c532ded5.jpg?alt=media&token=156d2af8-4433-4335-a32d-edf788196f74";
user.getIdToken(/* forceRefresh */ true)
.then(async idToken => {
// store id token for login/logout
await AsyncStorage.setItem('idToken', idToken);
console.log('sigin user id', user.uid);
//// get message push token
// request permission
firebase.messaging().requestPermission();
// get the device push token
firebase
.messaging()
.getToken()
.then(async pushToken => {
console.log('push token', pushToken);
// store push token
await AsyncStorage.setItem('fcmToken', pushToken);
//// create a new user doc or update push token if the user exists
const userRef = firebase.firestore().collection('users').doc(user.uid);
userRef.get()
.then(docSnapshot => {
console.log('[processSignin] doc snapshot', docSnapshot);
if (docSnapshot.exists) {
console.log('[processSignin] doc exist');
userRef.update({pushToken});
} else {
console.log('[processSignin] doc does not exist');
// create a user info
userRef.set({
pushToken,
name: 'unknown',
avatarUrl,
})
.then(() => {
// create initial profile on firebase
console.log('[processSignin] creating initial profile');
})
.catch(error => console.log('failed to create a user info', error));
}
// update the state with
dispatch({
type: 'signin',
payload: { idToken, pushToken },
});
// navigate to the main flow
navigation.navigate('mainFlow');
}) // end of get user doc
.catch(error => {
console.log('[processSignin] cannot get user doc', error);
});
}) // end of pushToken
.catch(error => {
console.log('getPushToken', error);
dispatch({
type: 'add_error',
payload: i18next.t('AuthContext.getPushTokenError')
});
});
}) // end of token
.catch(error => {
console.log('getToken', error);
dispatch({
type: 'add_error',
payload: i18next.t('AuthContext.SigninTokenError')
});
});
};
ios
ios의 경우는 전화번호를 입력하고 수신한 인증 코드를 입력할 수 있게 되어 있습니다.
앱 스토어 심사 받을 때, 전화번호 인증관련해서 계속 reject 되고 있습니다. 전혀 문제가 없는데도 말이죠. 매번 심사 요청하고, reject 먹고, 문제 없다는 것을 얘기하면 그제서야 심사 통과 시켜주는 것을 반복하고 있습니다. 이와 관련해서 앱 심사 알바설을 제기했죠!
애플 앱 심사 짜증 - 심사 알바설
먼저 screen ui 구성입니다. 안드로이드와 달리 SMS 인증 코드를 넣는 부분과 인증 버튼이 있습니다.
{
<Spacer>
<Input label={t('SigninScreen.verifyNumber')}
inputStyle={{ paddingLeft: 10, borderWidth: 2 }}
value={code}
onChangeText={setCode}
autoCapitalize="none"
autoCorrect={false}
keyboardType='numeric'
/>
</Spacer>
<Spacer>
<Button
buttonStyle={{borderRadius: 0, marginLeft: 0, marginRight: 0, marginBottom: 0}}
titleStyle={{ fontSize: 25, fontWeight: 'bold' }}
title={t('SigninScreen.start')}
loading={state.confirmLoading}
onPress={() => confirmVerificationCode({
phoneNumber,
code,
confirmResult: state.confirmResult,
navigation })}
/>
</Spacer>
전화번호 입력 처리 및 로그인 처리는 위 안드로이드 부분을 참고하세요.
ios용으로 SMS 코드 인증 체크 하는 부분입니다.
인증코드가 올바르다면 processSignin 함수를 호출
// verify phone number for ios
const confirmVerificationCode = dispatch => {
const { t } = useTranslation();
return async ({ phoneNumber, code, confirmResult, navigation }) => {
console.log('[confirmVerificationCode] code', code);
console.log('[confirmVerificationCode] confirmResult', confirmResult);
// start confirm action
dispatch({
type: 'start_confirm'
});
// verify the confirm result and code that your input exists
if (confirmResult && code.length) {
// confirm
confirmResult.confirm(code)
.then(user => {
processSignin({ dispatch, user, phoneNumber, navigation });
}) // end of confirm code
.catch(error => {
console.log('confirm code', error);
dispatch({
type: 'add_error',
payload: t('AuthContext.SigninConfirmError')
});
});
} // end of if (confirm && code.length)
};
};
로그인 유지 및 로그아웃 처리
로그인 상태를 유지하기 위해 로그인시에 사용자 id 토큰을 async storage에 저장합니다.
// process signin
const processSignin = ({ dispatch, user, navigation }) => {
user.getIdToken(/* forceRefresh */ true)
.then(async idToken => {
// save the phone number in storage
// store id token for login/logout
await AsyncStorage.setItem('idToken', idToken);
사용자 토큰이 스로리지에 있는 한 로그인 과정을 생략할 수 있습니다.
// AuthContext.js
// skip login if the token exists
const trySigninWithToken = dispatch => {
return async () => {
// get id token from storage
let idToken = await AsyncStorage.getItem('idToken');
if (idToken) {
// dispatch signin action
dispatch({ type: 'signin', payload: idToken });
// navigate to landing screen
NavigationService.navigate('mainFlow');
// @test
// NavigationService.navigate('ProfileContract');
} else {
NavigationService.navigate('loginFlow');
}
};
};
로그아웃 또는 signout도 firebase를 이용해서 처리합니다. 처리 로직은, 사용자가 로그아웃 버튼을 누르면 async 스토리지에서 사용자의 id 토큰을 삭제합니다. 추가로 firebase push 토큰도 삭제합니다.
// AuthContext.js
// sign out
const signout = dispatch => {
return async ({ userId, navigation }) => {
console.log('signout userId', userId);
// remove the id token in the storage
await AsyncStorage.removeItem('idToken');
//// remove the push token in the firestore
const userRef = firebase.firestore().doc(`users/${userId}`);
// remove push token
userRef.get()
.then(async docSnapshot => {
console.log('[signout] doc snapshot', docSnapshot);
if (docSnapshot.exists) {
console.log('[signout] doc exist');
await userRef.update({ pushToken: null });
// signout from firebase
firebase.auth().signOut()
.then(() => {
// dispatch signout action
dispatch({ type: 'signout' });
// navigate to loginFlow
navigation.navigate('loginFlow');
})
.catch(error => {
console.log('failed to signout from firebase', error);
});
}
})
.catch(error => {
console.log('failed to remove push token', error);
});
};
}
도움 주고 받는 앱 helpus
앱 사용자 중 다음과 같은 도움이 가능하 사용자들이 있습니다.
- 최면 상담, 치료
- 영어, 일본어 관련 문제
- 건강 상담, 심리 상담
물론, 저도 명상, 루시드 드림 관련 상담이 가능합니다.
구글 플레이: https://play.google.com/store/apps/details?id=club.etain.helpus
애플 앱 스토어: https://apps.apple.com/us/app/helpus-instant-help-in-town/id1496615309
APK 파일 다운로드설치: https://etain.club/download
소개 영상
사용법
보다 자세한 내용은 홈페이지를 참고해 주세요.
https://etain.club