Fakultas Ilmu Komputer UI

Commit e2bcf02b authored by Doan Andreas Nathanael's avatar Doan Andreas Nathanael
Browse files

Merge branch 'PBI-5-add_signup_from_login' into 'staging'

Signup from Login Page (after choose plan)

See merge request !57
parents d401fdd6 7bbf1304
Pipeline #78675 failed with stages
in 60 minutes and 1 second
......@@ -132,10 +132,11 @@ android {
defaultConfig {
applicationId "com.dietela_mobile"
minSdkVersion rootProject.ext.minSdkVersion
minSdkVersion 21
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.0"
multiDexEnabled true
}
splits {
abi {
......@@ -166,6 +167,15 @@ android {
}
}
packagingOptions {
pickFirst 'lib/x86/libc++_shared.so'
pickFirst 'lib/x86_64/libjsc.so'
pickFirst 'lib/arm64-v8a/libjsc.so'
pickFirst 'lib/arm64-v8a/libc++_shared.so'
pickFirst 'lib/x86_64/libc++_shared.so'
pickFirst 'lib/armeabi-v7a/libc++_shared.so'
}
// applicationVariants are e.g. debug, release
applicationVariants.all { variant ->
variant.outputs.each { output ->
......
......@@ -37,6 +37,12 @@
F1FC0F9DF7AD4E16B1413E97 /* Zocial.ttf in Resources */ = {isa = PBXBuildFile; fileRef = FFFB1F491FAF45A6AB0B18CE /* Zocial.ttf */; };
4F26060B007F432094C404C8 /* Roboto-Medium.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 18B35410CBB148C2BEFEA318 /* Roboto-Medium.ttf */; };
4335541BBE1046F882A88C90 /* Montserrat-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = C58717AF5FB64985811F9CD9 /* Montserrat-Bold.ttf */; };
909A2149528E4648AED622D9 /* banner_girl_eating.png in Resources */ = {isa = PBXBuildFile; fileRef = D1B0F0D6AE7B4495817F18EA /* banner_girl_eating.png */; };
DD79CB122C184FF997FC65E2 /* default_nutritionist.png in Resources */ = {isa = PBXBuildFile; fileRef = 54601BBD3934484E94422884 /* default_nutritionist.png */; };
1921908D710F465996653989 /* dietela_logo.png in Resources */ = {isa = PBXBuildFile; fileRef = A965AFB71D8B40FF90D4EB8A /* dietela_logo.png */; };
D0C06F312DB04A01A8A1ACB6 /* google_logo.png in Resources */ = {isa = PBXBuildFile; fileRef = 26E7567AB61D423C993A42FD /* google_logo.png */; };
13885185E9B9435A92FC8860 /* logo_white_small.png in Resources */ = {isa = PBXBuildFile; fileRef = 2C9CB6BEC9DC42798CD6164C /* logo_white_small.png */; };
4A5F5589A0C04BC1BFA7F88F /* status_berat_badan.png in Resources */ = {isa = PBXBuildFile; fileRef = 2FA25590FB5F4BC785039908 /* status_berat_badan.png */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
......@@ -93,6 +99,12 @@
FFFB1F491FAF45A6AB0B18CE /* Zocial.ttf */ = {isa = PBXFileReference; name = "Zocial.ttf"; path = "../node_modules/react-native-vector-icons/Fonts/Zocial.ttf"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; };
18B35410CBB148C2BEFEA318 /* Roboto-Medium.ttf */ = {isa = PBXFileReference; name = "Roboto-Medium.ttf"; path = "../assets/fonts/Roboto-Medium.ttf"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; };
C58717AF5FB64985811F9CD9 /* Montserrat-Bold.ttf */ = {isa = PBXFileReference; name = "Montserrat-Bold.ttf"; path = "../assets/fonts/Montserrat-Bold.ttf"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; };
D1B0F0D6AE7B4495817F18EA /* banner_girl_eating.png */ = {isa = PBXFileReference; name = "banner_girl_eating.png"; path = "../assets/images/banner_girl_eating.png"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; };
54601BBD3934484E94422884 /* default_nutritionist.png */ = {isa = PBXFileReference; name = "default_nutritionist.png"; path = "../assets/images/default_nutritionist.png"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; };
A965AFB71D8B40FF90D4EB8A /* dietela_logo.png */ = {isa = PBXFileReference; name = "dietela_logo.png"; path = "../assets/images/dietela_logo.png"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; };
26E7567AB61D423C993A42FD /* google_logo.png */ = {isa = PBXFileReference; name = "google_logo.png"; path = "../assets/images/google_logo.png"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; };
2C9CB6BEC9DC42798CD6164C /* logo_white_small.png */ = {isa = PBXFileReference; name = "logo_white_small.png"; path = "../assets/images/logo_white_small.png"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; };
2FA25590FB5F4BC785039908 /* status_berat_badan.png */ = {isa = PBXFileReference; name = "status_berat_badan.png"; path = "../assets/images/status_berat_badan.png"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
......@@ -224,6 +236,12 @@
FFFB1F491FAF45A6AB0B18CE /* Zocial.ttf */,
18B35410CBB148C2BEFEA318 /* Roboto-Medium.ttf */,
C58717AF5FB64985811F9CD9 /* Montserrat-Bold.ttf */,
D1B0F0D6AE7B4495817F18EA /* banner_girl_eating.png */,
54601BBD3934484E94422884 /* default_nutritionist.png */,
A965AFB71D8B40FF90D4EB8A /* dietela_logo.png */,
26E7567AB61D423C993A42FD /* google_logo.png */,
2C9CB6BEC9DC42798CD6164C /* logo_white_small.png */,
2FA25590FB5F4BC785039908 /* status_berat_badan.png */,
);
name = Resources;
sourceTree = "<group>";
......@@ -388,6 +406,12 @@
F1FC0F9DF7AD4E16B1413E97 /* Zocial.ttf in Resources */,
4F26060B007F432094C404C8 /* Roboto-Medium.ttf in Resources */,
4335541BBE1046F882A88C90 /* Montserrat-Bold.ttf in Resources */,
909A2149528E4648AED622D9 /* banner_girl_eating.png in Resources */,
DD79CB122C184FF997FC65E2 /* default_nutritionist.png in Resources */,
1921908D710F465996653989 /* dietela_logo.png in Resources */,
D0C06F312DB04A01A8A1ACB6 /* google_logo.png in Resources */,
13885185E9B9435A92FC8860 /* logo_white_small.png in Resources */,
4A5F5589A0C04BC1BFA7F88F /* status_berat_badan.png in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
......
import { UserRole } from 'services/auth/models';
import { Client } from 'services/profiles/models';
export const mockClientList: Client[] = [
{
user: {
id: 1,
name: 'Doan Di Dinding',
email: 'doan@dinding.com',
role: UserRole.CLIENT,
},
diet_profile_id: 1,
diet_questionnaire_id: 1,
diet_recommendation_id: 1,
},
];
......@@ -4,6 +4,8 @@ import { render } from '@testing-library/react-native';
import { ErrorToast } from './styles';
import App from '.';
jest.useFakeTimers();
describe('Application', () => {
it('renders correctly', () => {
render(<App />);
......
......@@ -3,6 +3,7 @@ import {
publicNavigation,
clientNavigation,
nutritionistNavigation,
adminNavigation,
} from 'constants/navigation';
import { UserRole, AuthUserResponse } from 'services/auth/models';
import { TransactionStatus } from 'services/payment/models';
......@@ -33,6 +34,12 @@ export const getNavigation = (
navigation: nutritionistNavigation,
};
}
if (user.role === UserRole.ADMIN) {
return {
initialRoute: ROUTES.clientListForAdmin,
navigation: adminNavigation,
};
}
}
return {
......
......@@ -29,6 +29,9 @@ import {
ClientProfile,
ClientProfileForAdmin,
PaymentWebView,
ProfileDietRecommendation,
ClientListAdmin,
LoginChoosePlan,
} from 'scenes';
import { FC } from 'react';
......@@ -82,6 +85,11 @@ export const publicNavigation: NavRoute[] = [
component: LoginPage,
header: 'Login',
},
{
name: ROUTES.loginChoosePlan,
component: LoginChoosePlan,
header: 'Login',
},
{
name: ROUTES.nutritionistAdminLogin,
component: NutritionistAdminLogin,
......@@ -167,19 +175,40 @@ export const nutritionistNavigation: NavRoute[] = [
component: ReadOnlyDietProfile,
header: 'Profil Klien',
},
{
name: ROUTES.profileDietRecommendation,
component: ProfileDietRecommendation,
header: 'Rekomendasi Profil Diet',
},
];
export const adminNavigation: NavRoute[] = [
{
name: ROUTES.clientProfile,
name: ROUTES.clientListForAdmin,
component: ClientListAdmin,
header: 'List Klien',
},
{
name: ROUTES.clientProfileAdmin,
component: ClientProfileForAdmin,
header: 'Profil Klien',
},
{
name: ROUTES.clientDietReportAdmin,
component: ComingSoonPage,
header: 'Profil Klien',
},
{
name: ROUTES.clientChatAdmin,
component: ComingSoonPage,
header: 'Profil Klien',
},
];
export const testNavigation: NavRoute[] = [
...clientNavigation,
...nutritionistNavigation,
...adminNavigation,
{
name: ROUTES.initial,
component: InitialPage,
......@@ -194,6 +223,11 @@ export const testNavigation: NavRoute[] = [
component: LoginPage,
header: 'Login',
},
{
name: ROUTES.loginChoosePlan,
component: LoginChoosePlan,
header: 'Login',
},
{
name: ROUTES.nutritionistAdminLogin,
component: NutritionistAdminLogin,
......
......@@ -16,6 +16,7 @@ export const nutritionistDetail = `${checkout}/nutritionist`;
export const registration = 'registration';
export const login = 'login';
export const loginChoosePlan = 'login-choose-plan';
export const nutritionistAdminLogin = 'nutritionist-admin-login';
const profile = 'profile';
......@@ -29,3 +30,11 @@ export const clientListForNutritionist = `${nutritionist}/client-list`;
export const clientProfileNutritionist = `${nutritionist}/client-profile`;
export const clientDietReportNutritionist = `${nutritionist}/client-diet-report`;
export const clientChatNutritionist = `${nutritionist}/client-chat`;
export const profileDietRecommendation = `${clientProfileNutritionist}/recommendation`;
const admin = 'admin';
export const clientListForAdmin = `${admin}/client-list`;
export const clientProfileAdmin = `${admin}/client-profile`;
export const clientDietReportAdmin = `${admin}/client-diet-report`;
export const clientChatAdmin = `${admin}/client-chat`;
import React from 'react';
import { render, waitFor } from 'utils/testing';
import * as ROUTES from 'constants/routes';
import axios from 'axios';
import ClientListAdmin from '.';
import { mockClientList } from '__mocks__/clientList';
jest.mock('axios');
const mockAxios = axios as jest.Mocked<typeof axios>;
describe('ClientListAdmin', () => {
it('renders correctly', async () => {
mockAxios.request.mockImplementationOnce(() =>
Promise.resolve({
status: 200,
data: [],
}),
);
render(<ClientListAdmin />, ROUTES.clientListForAdmin);
});
it('shows correct client list', async () => {
mockAxios.request.mockImplementationOnce(() =>
Promise.resolve({
status: 200,
data: mockClientList,
}),
);
const { queryByText, queryAllByText } = render(
<ClientListAdmin />,
ROUTES.clientListForAdmin,
);
await waitFor(() => expect(queryAllByText(/List Klien/i)).toBeTruthy());
expect(queryByText(mockClientList[0].user.name)).toBeTruthy();
expect(queryByText(/Download Csv/i)).toBeTruthy();
});
});
import React, { FC } from 'react';
import { ScrollView } from 'react-native-gesture-handler';
import { BigButton, Loader } from 'components/core';
import { useNavigation } from '@react-navigation/core';
import { useApi, useDownloadFiles } from 'hooks';
import { ClientCardNutritionist } from 'scenes/nutritionist/ClientListNutritionist/components';
import { retrieveClientListApi } from 'services/profiles';
import * as ROUTES from 'constants/routes';
import { layoutStyles } from 'styles';
import { Dimensions, StyleSheet, View } from 'react-native';
import { Section } from 'components/layout';
import { getAbsoluteUrl } from 'utils/format';
const ClientListAdmin: FC = () => {
const navigation = useNavigation();
const { isLoading, data: clients = [] } = useApi(retrieveClientListApi);
const { download } = useDownloadFiles(getAbsoluteUrl('exportcsv'));
console.log(clients);
if (isLoading) {
return <Loader />;
}
return (
<View style={layoutStyles}>
<ScrollView style={styles.container}>
{clients.map((client, idx) => (
<ClientCardNutritionist
key={idx}
clientName={client.user.name}
onPressClientProfile={() => {
navigation.navigate(ROUTES.clientProfileAdmin, {
id: client.diet_questionnaire_id,
});
}}
onPressClientDietReport={() => {
navigation.navigate(ROUTES.clientDietReportAdmin, {});
}}
onPressClientChat={() => {
navigation.navigate(ROUTES.clientChatAdmin, {});
}}
/>
))}
</ScrollView>
<Section>
<BigButton title="Download CSV" onPress={download} />
</Section>
</View>
);
};
const styles = StyleSheet.create({
container: { height: Dimensions.get('window').height * 0.83 },
});
export default ClientListAdmin;
import React from 'react';
import { render, fireEvent, waitFor } from 'utils/testing';
import * as ROUTES from 'constants/routes';
import LoginChoosePlan from '.';
describe('LoginChoosePlan', () => {
it('renders correctly', () => {
render(<LoginChoosePlan />, ROUTES.loginChoosePlan);
});
test('has link button that navigates to Registration Page', async () => {
const { getByText, queryByText, queryAllByText } = render(
<LoginChoosePlan />,
ROUTES.loginChoosePlan,
);
expect(queryByText(/Kembali ke Registrasi/i)).toBeTruthy();
await waitFor(() => fireEvent.press(getByText(/Kembali ke Registrasi/i)));
expect(queryAllByText(/Registrasi/i)).toBeTruthy();
});
});
import { useNavigation } from '@react-navigation/core';
import { Link } from 'components/core';
import React, { FC } from 'react';
import Login from '../Login';
const LoginChoosePlan: FC = () => {
const navigation = useNavigation();
return (
<>
<Login />
<Link title="Kembali ke Registrasi" onPress={() => navigation.goBack()} />
</>
);
};
export default LoginChoosePlan;
import React, { FC, useContext } from 'react';
import { useForm, useSignupEffect } from 'hooks';
import { ScrollView } from 'react-native-gesture-handler';
import { useNavigation } from '@react-navigation/core';
import { useNavigation, useRoute } from '@react-navigation/core';
import { BigButton, Link, Toast } from 'components/core';
import { Section } from 'components/layout';
......@@ -20,8 +20,13 @@ const isPasswordField = (name: string) =>
const ManualRegistrationPage: FC = () => {
const { signup, loginWithGoogle, isLoading } = useContext(UserContext);
const routes = useRoute();
const navigation = useNavigation();
const loginRoute = routes.params?.from_choose_plan
? ROUTES.login
: ROUTES.loginChoosePlan;
const {
getTextInputProps,
handleSubmit,
......@@ -76,7 +81,7 @@ const ManualRegistrationPage: FC = () => {
<Section>
<Link
title="Sudah punya akun? Login disini"
onPress={() => navigation.navigate(ROUTES.login)}
onPress={() => navigation.navigate(loginRoute)}
/>
</Section>
</ScrollView>
......
......@@ -47,7 +47,9 @@ const ChoosePlan: FC = () => {
} else {
navigation.reset({
index: 0,
routes: [{ name: ROUTES.registration }],
routes: [
{ name: ROUTES.registration, params: { fromChoosePlan: true } },
],
});
}
} else {
......
export { default as LoginPage } from './auth/Login';
export { default as LoginChoosePlan } from './auth/LoginChoosePlan';
export { default as NutritionistAdminLogin } from './auth/NutritionistAdminLogin';
export { default as ManualRegistrationPage } from './auth/ManualRegistrationPage';
......@@ -8,6 +9,7 @@ export { default as ComingSoonPage } from './common/ComingSoonPage';
export { default as AllAccessQuestionnaire } from './questionnaire/AllAccessQuestionnaire';
export { default as DietelaQuizResult } from './questionnaire/DietelaQuizResult';
export { default as ExtendedQuestionnaire } from './questionnaire/ExtendedQuestionnaire';
export { default as ProfileDietRecommendation } from './questionnaire/ProfileDietRecommendation';
export { default as ReadOnlyDietProfile } from './questionnaire/ReadOnlyDietProfile';
export * from './questionnaire/ExtendedQuestionnaire/components';
......@@ -21,5 +23,7 @@ export { default as PaymentWebView } from './payment/PaymentWebView';
export { default as ClientListNutritionist } from './nutritionist/ClientListNutritionist';
export { default as ClientListAdmin } from './admin/ClientListAdmin';
export { default as ClientProfile } from './profile/ClientProfile';
export { default as ClientProfileForAdmin } from './profile/ClientProfileForAdmin';
import React from 'react';
import { render, waitFor } from 'utils/testing';
import * as ROUTES from 'constants/routes';
import ProfileDietRecommendation from '.';
jest.mock('react-native-toast-message');
jest.mock('axios');
jest.mock('react-native-document-picker');
describe('ProfileDietRecommendation', () => {
it('renders correctly', () => {
render(<ProfileDietRecommendation />, ROUTES.profileDietRecommendation, {
routeParams: {
name: 'Doan',
id: 1,
},
});
});
it('shows correct name', async () => {
const { queryByText } = render(
<ProfileDietRecommendation />,
ROUTES.profileDietRecommendation,
{
routeParams: {
name: 'Doan',
id: 1,
},
},
);
await waitFor(() => expect(queryByText(/Rekomendasi/i)).toBeTruthy());
});
afterAll(() => {
jest.clearAllMocks();
});
});
import React, { FC, useState } from 'react';
import { Dimensions, ScrollView, StyleSheet, Text } from 'react-native';
import { useNavigation, useRoute } from '@react-navigation/core';
import DocumentPicker, {
DocumentPickerResponse,
} from 'react-native-document-picker';
import { fieldValidation, initialValues, textField } from './schema';
import { Section } from 'components/layout';
import { BodyMedium, HeadingLarge } from 'components/typography';
import Pdf from 'react-native-pdf';
import { BigButton, Loader, Toast } from 'components/core';
import { layoutStyles } from 'styles';
import { useApi, useForm } from 'hooks';
import { generateValidationSchema } from 'utils/form';
import { TextField } from 'components/form';
import { retrieveClientListApi } from 'services/profiles';
import { Client } from 'services/profiles/models';
import { submitDietRecommendationByIdApi } from 'services/dietRecommendation';
import * as ROUTES from 'constants/routes';
interface Params {
name: string;
id: number;
}
const ProfileDietRecommendation: FC = () => {
const route = useRoute();
const navigation = useNavigation();
const [fileError, setFileError] = useState(false);
const [file, setFile] = useState<DocumentPickerResponse>();
const { name: userName, id } = route.params as Params;
const { isLoading, data: clients = [] } = useApi(retrieveClientListApi);
const {
getTextInputProps,
setFieldError,
handleSubmit,
isSubmitting,
} = useForm({
initialValues,
validationSchema: generateValidationSchema(fieldValidation),
onSubmit: async (values) => {
const client = getClient();
setFileError(!file);
const payload = new FormData();
payload.append('client_plan_meal', {
uri: file?.uri,
type: file?.type,
name: file?.name,
});
payload.append('nutritional_advice', values.nutritional_advice);
payload.append('lifestyle_advice', values.lifestyle_advice);
const response = await submitDietRecommendationByIdApi(
client.diet_recommendation_id,
payload,
);
if (!response.success) {
const error = response.error;