update
This commit is contained in:
@@ -0,0 +1,277 @@
|
||||
import React, { useGlobal } from "reactn";
|
||||
import { Text, Pressable } from "react-native";
|
||||
import * as ImagePicker from "expo-image-picker";
|
||||
import useMinuit from "react-native-minuit/src/hooks/useMinuit.js";
|
||||
|
||||
import { responsiveWidth } from "../actions/responsiveSizes.js";
|
||||
import Page from "../layouts/Page";
|
||||
|
||||
import Avatar from "../components/Avatar";
|
||||
import InputRow from "../components/InputRow";
|
||||
import ItemRowList from "../components/ItemRowList.js";
|
||||
import alert from "../components/Alert.js";
|
||||
|
||||
import firebase, { usersRef } from "../config/firebase";
|
||||
|
||||
import { Fonts, gutters, Palette, Style } from "../styles";
|
||||
import { uploadFileToFirebase } from "../helpers/uploadToFirebase";
|
||||
import { Routes } from "../navigation/Routes.js";
|
||||
|
||||
import { useUserData } from "../providers/UserDataProvider.js";
|
||||
|
||||
export default ({ navigation }) => {
|
||||
const { setIsLoading, setTooltip } = useMinuit();
|
||||
const { onSignOut } = useUserData();
|
||||
|
||||
const [currentUID] = useGlobal("currentUID");
|
||||
const [currentUserData] = useGlobal("currentUserData");
|
||||
|
||||
const settingsList = [
|
||||
{
|
||||
title: "Nom du compte",
|
||||
key: "name",
|
||||
value: currentUserData?.name || "",
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
title: "Adresse email",
|
||||
key: "email",
|
||||
value: currentUserData?.email || "",
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
title: "Mot de passe",
|
||||
key: "password",
|
||||
value: "",
|
||||
type: "string",
|
||||
},
|
||||
];
|
||||
|
||||
const destructiveSettingsList = [
|
||||
{
|
||||
title: "Supprimer mon compte",
|
||||
action: () => onDeleteUserAccount(),
|
||||
textStyle: {
|
||||
color: Palette.red,
|
||||
},
|
||||
},
|
||||
].filter(({ condition = true }) => condition);
|
||||
|
||||
const onChangePicture = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
let result = await ImagePicker.launchImageLibraryAsync({
|
||||
mediaTypes: ImagePicker.MediaTypeOptions.Images,
|
||||
allowsEditing: true,
|
||||
aspect: [4, 4],
|
||||
quality: 1,
|
||||
});
|
||||
|
||||
if (result?.assets?.[0]?.uri) {
|
||||
const { resultURI = null } = await uploadFileToFirebase({
|
||||
uri: result?.assets?.[0]?.uri,
|
||||
path: `users/${currentUID}/profilePicture.png`,
|
||||
});
|
||||
|
||||
if (resultURI) {
|
||||
await usersRef.doc(currentUID).update({
|
||||
profilePictureURL: resultURI,
|
||||
});
|
||||
|
||||
await firebase.auth().currentUser.updateProfile({
|
||||
photoURL: resultURI,
|
||||
});
|
||||
|
||||
setTooltip({
|
||||
type: "success",
|
||||
text: "Photo de profil mise à jour",
|
||||
});
|
||||
} else {
|
||||
throw new Error("Erreur changement photo de profil");
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
|
||||
setTooltip({
|
||||
type: "error",
|
||||
text: error.message,
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onUpdateValue = async ({
|
||||
key,
|
||||
value = null,
|
||||
currentPassword = null,
|
||||
}) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const user = firebase.auth().currentUser;
|
||||
|
||||
const reauthenticateUser = async () => {
|
||||
try {
|
||||
const credential = firebase.auth.EmailAuthProvider.credential(
|
||||
user?.email,
|
||||
currentPassword
|
||||
);
|
||||
|
||||
await user.reauthenticateWithCredential(credential);
|
||||
} catch (error) {
|
||||
throw new Error("Erreur authentification");
|
||||
}
|
||||
};
|
||||
|
||||
if (!value) {
|
||||
throw new Error("Vérifiez les informations saisies");
|
||||
}
|
||||
|
||||
if (key === "name") {
|
||||
await user.updateProfile({
|
||||
displayName: value,
|
||||
});
|
||||
}
|
||||
|
||||
if (["email", "password"].includes(key)) {
|
||||
await reauthenticateUser();
|
||||
|
||||
if (key === "email") {
|
||||
try {
|
||||
await user.updateEmail(value);
|
||||
} catch (e) {
|
||||
throw new Error("Erreur changement email");
|
||||
}
|
||||
}
|
||||
|
||||
if (key === "password") {
|
||||
try {
|
||||
await user.updatePassword(value);
|
||||
} catch (e) {
|
||||
throw new Error("Erreur changement mot de passe");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (key !== "password") {
|
||||
await usersRef.doc(currentUID).update({
|
||||
[key]: value,
|
||||
});
|
||||
}
|
||||
|
||||
setTooltip({
|
||||
type: "success",
|
||||
text: "Modifications enregistrées",
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
|
||||
setTooltip({
|
||||
type: "error",
|
||||
text: error.message,
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onDeleteUserAccount = async () => {
|
||||
alert(
|
||||
"Êtes-vous sûr?",
|
||||
"La suppression de votre compte entraînera la suppression de toutes les données liées.",
|
||||
[
|
||||
{
|
||||
text: "Annuler",
|
||||
onPress: () => {},
|
||||
style: "cancel",
|
||||
},
|
||||
{
|
||||
text: "Confirmer",
|
||||
onPress: async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
await firebase
|
||||
.functions()
|
||||
.httpsCallable("users-deleteUserAccount")();
|
||||
|
||||
await onSignOut();
|
||||
|
||||
setTooltip({
|
||||
type: "success",
|
||||
text: "Compte supprimé avec succès!",
|
||||
});
|
||||
|
||||
navigation.reset({
|
||||
index: 0,
|
||||
routes: [
|
||||
{
|
||||
name: Routes.Login,
|
||||
},
|
||||
],
|
||||
});
|
||||
} catch (error) {
|
||||
setTooltip({
|
||||
type: "error",
|
||||
text: error.message,
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
style: "confirm",
|
||||
},
|
||||
],
|
||||
{ cancelable: false }
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Page headerType="NAVIGATE" title={"Paramètres du compte"} scrollEnabled>
|
||||
<Pressable
|
||||
onPress={onChangePicture}
|
||||
style={[Style.containerCenter, { marginBottom: gutters }]}
|
||||
>
|
||||
<Avatar
|
||||
name={currentUserData?.name}
|
||||
url={currentUserData?.profilePictureURL || null}
|
||||
forceRawImage
|
||||
size={responsiveWidth(30)}
|
||||
containerStyle={{
|
||||
marginBottom: gutters / 2,
|
||||
...Style.defaultShadows,
|
||||
}}
|
||||
/>
|
||||
<Text
|
||||
style={Fonts({
|
||||
type: "section",
|
||||
style: { color: Palette.primary, textDecorationLine: "underline" },
|
||||
})}
|
||||
>
|
||||
Modifier
|
||||
</Text>
|
||||
</Pressable>
|
||||
|
||||
{settingsList.map((setting) => {
|
||||
const { title, key, value, type } = setting;
|
||||
|
||||
return (
|
||||
<InputRow
|
||||
key={key}
|
||||
itemKey={key}
|
||||
value={value}
|
||||
type={type}
|
||||
title={title}
|
||||
onUpdateValue={onUpdateValue}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{destructiveSettingsList.map((setting, index) => (
|
||||
<ItemRowList key={index} {...setting} containerStyle={{}} />
|
||||
))}
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
import React, { useState, useGlobal } from "reactn";
|
||||
import { useKeyboard } from "@react-native-community/hooks";
|
||||
|
||||
import { responsiveHeight } from "../actions/responsiveSizes.js";
|
||||
|
||||
import Page from "../layouts/Page";
|
||||
|
||||
import ChatInterface, { onSendMessage } from "../components/ChatInterface";
|
||||
import ChatInput from "../components/ChatInput.js";
|
||||
import useLayoutType from "../hooks/useLayoutType.js";
|
||||
|
||||
import { gutters } from "../styles/Style.js";
|
||||
|
||||
export default ({ navigation }) => {
|
||||
const [currentUID] = useGlobal("currentUID");
|
||||
const [currentProjectID] = useGlobal("currentProjectID");
|
||||
|
||||
const [message, setMessage] = useState("");
|
||||
|
||||
const chatID = `TEST_CHAT_ID`;
|
||||
|
||||
const { isDesktop } = useLayoutType();
|
||||
const { keyboardShown = false, keyboardHeight = 0 } = useKeyboard();
|
||||
|
||||
return (
|
||||
<Page headerType={isDesktop ? "NONE" : "BASE"}>
|
||||
<ChatInterface
|
||||
hasChatbot
|
||||
chatID={chatID}
|
||||
projectID={currentProjectID}
|
||||
inputStyle={{ bottom: responsiveHeight(15) }}
|
||||
messageListContainerStyle={{ flex: 1 }}
|
||||
/>
|
||||
|
||||
<ChatInput
|
||||
chatID={chatID}
|
||||
message={message}
|
||||
setMessage={setMessage}
|
||||
keyboardVerticalOffset={responsiveHeight(15)}
|
||||
onSendMessage={async ({ customPayload = {} } = {}) => {
|
||||
onSendMessage({
|
||||
message,
|
||||
setMessage,
|
||||
chatID,
|
||||
customPayload: { ...customPayload },
|
||||
});
|
||||
}}
|
||||
placeholder={"Laissez un commentaire..."}
|
||||
containerStyle={{
|
||||
position: "absolute",
|
||||
bottom: gutters * 6 + (keyboardShown ? keyboardHeight : 0),
|
||||
right: 0,
|
||||
left: 0,
|
||||
width: "auto",
|
||||
}}
|
||||
/>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,86 @@
|
||||
import React, { useState, useRef, useGlobal } from "reactn";
|
||||
import { Text, KeyboardAvoidingView } from "react-native";
|
||||
import { responsiveHeight } from "../actions/responsiveSizes.js";
|
||||
|
||||
import firebase from "../config/firebase";
|
||||
|
||||
import Button from "../components/Button";
|
||||
import { Input } from "../components/Input";
|
||||
|
||||
import { Fonts, Style } from "../styles";
|
||||
import Page from "../layouts/Page";
|
||||
|
||||
export default ({ navigation }) => {
|
||||
const [, setTooltip] = useGlobal("_tooltip");
|
||||
const [, setIsLoading] = useGlobal("_isLoading");
|
||||
|
||||
const [email, setEmail] = useState(__DEV__ ? "hello@minuit.agency" : "");
|
||||
|
||||
const onResetPassword = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
await firebase.auth().sendPasswordResetEmail(email);
|
||||
|
||||
setTooltip({
|
||||
text: "Email de réinitialisation envoyé!",
|
||||
type: "success",
|
||||
});
|
||||
|
||||
navigation.goBack();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
setTooltip({
|
||||
text: "Une erreur est survenue",
|
||||
type: "error",
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Page
|
||||
headerType="NAVIGATE"
|
||||
title={"Nouveau mot de passe"}
|
||||
scrollEnabled
|
||||
bottomStickyContent={() => (
|
||||
<Button
|
||||
isAbsoluteBottom
|
||||
text="Renvoyer le mot de passe"
|
||||
onPress={onResetPassword}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
<KeyboardAvoidingView
|
||||
style={{ ...Style.containerCenter, width: "100%" }}
|
||||
behavior="padding"
|
||||
keyboardVerticalOffset={responsiveHeight(10)}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
...Fonts({ type: "default" }),
|
||||
marginBottom: responsiveHeight(10),
|
||||
}}
|
||||
>
|
||||
Saisissez votre adresse mail, nous vous enverrons un lien pour
|
||||
réinitialiser votre mot de passe.
|
||||
</Text>
|
||||
|
||||
<Input
|
||||
label="Adresse email"
|
||||
placeholder="Votre adresse email"
|
||||
value={email}
|
||||
setValue={setEmail}
|
||||
containerStyle={{ marginBottom: 20 }}
|
||||
textInputProps={{
|
||||
keyboardType: "email-address",
|
||||
autoCapitalize: "none",
|
||||
autoCompleteType: "email",
|
||||
textContentType: "emailAddress",
|
||||
}}
|
||||
/>
|
||||
</KeyboardAvoidingView>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,224 @@
|
||||
import { useState } from "react";
|
||||
import React from "reactn";
|
||||
import { responsiveHeight } from "react-native-responsive-dimensions";
|
||||
import useMinuit from "react-native-minuit/src/hooks/useMinuit";
|
||||
|
||||
import Page from "../layouts/Page";
|
||||
|
||||
import { Palette, gutters } from "../styles";
|
||||
|
||||
import SectionTextHeader from "../components/SectionTextHeader";
|
||||
import OptionSelector from "../components/OptionSelector";
|
||||
import { Input } from "../components/Input";
|
||||
import Button from "../components/Button";
|
||||
import DocumentDropZone from "../components/DocumentDropZone";
|
||||
import FilesPreview from "../components/FilesPreview";
|
||||
|
||||
import { documentsRef } from "../config/firebase";
|
||||
|
||||
const accountTypeList = {
|
||||
INDIVIDUAL: "Particulier",
|
||||
COMPANY: "Société",
|
||||
};
|
||||
|
||||
const accountTypeColorMap = {
|
||||
INDIVIDUAL: {
|
||||
primary: Palette.primary,
|
||||
secondary: Palette.transparentPrimary,
|
||||
},
|
||||
COMPANY: {
|
||||
primary: Palette.radioactivGreen,
|
||||
secondary: Palette.transparentRadioactivGreen,
|
||||
},
|
||||
};
|
||||
|
||||
export default ({ containerStyle = {} }) => {
|
||||
const { setIsLoading, setTooltip } = useMinuit();
|
||||
|
||||
const documentID = "TEST_DOCUMENT_ID";
|
||||
|
||||
const [billingDetails, setBillingDetails] = useState({
|
||||
type: "INDIVIDUAL",
|
||||
name: "",
|
||||
address: "",
|
||||
city: "",
|
||||
zipCode: "",
|
||||
countryCode: "",
|
||||
vatNumber: "",
|
||||
companyIdentifier: "",
|
||||
files: [],
|
||||
});
|
||||
|
||||
const {
|
||||
type = "INDIVIDUAL",
|
||||
name = "",
|
||||
address = "",
|
||||
city = "",
|
||||
zipCode = "",
|
||||
countryCode = "FR",
|
||||
vatNumber = "",
|
||||
companyIdentifier = "",
|
||||
files = [],
|
||||
} = billingDetails || {};
|
||||
|
||||
const updateBillingDetails = (key, value) => {
|
||||
setBillingDetails({
|
||||
...billingDetails,
|
||||
[key]: value,
|
||||
});
|
||||
};
|
||||
|
||||
const fieldList = [
|
||||
{
|
||||
key: "name",
|
||||
label: type === "COMPANY" ? "Raison sociale" : "Nom",
|
||||
value: name,
|
||||
inputProps: {
|
||||
autoCapitalize: type === "COMPANY" ? "none" : "words",
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "address",
|
||||
label: "Adresse",
|
||||
value: address,
|
||||
inputProps: {
|
||||
type: "autoCompleteAddress",
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "city",
|
||||
label: "Ville",
|
||||
value: city,
|
||||
},
|
||||
{
|
||||
key: "zipCode",
|
||||
label: "Code postal",
|
||||
value: zipCode,
|
||||
inputProps: {
|
||||
isNumeric: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "countryCode",
|
||||
label: "Pays",
|
||||
value: countryCode,
|
||||
type: "countryPicker",
|
||||
},
|
||||
{
|
||||
key: "vatNumber",
|
||||
label: "Numéro de TVA intracommunautaire (facultatif)",
|
||||
value: vatNumber,
|
||||
condition: type === "COMPANY",
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
key: "companyIdentifier",
|
||||
label: "Numéro SIRET",
|
||||
value: companyIdentifier,
|
||||
condition: type === "COMPANY",
|
||||
},
|
||||
].filter(({ condition = true }) => condition);
|
||||
|
||||
const onSubmit = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
let cleanObject = {};
|
||||
|
||||
fieldList.forEach(({ key, value, label, required = true }) => {
|
||||
if (value) {
|
||||
cleanObject[key] = value;
|
||||
} else if (required) {
|
||||
throw new Error(`Le champ ${label.toLowerCase()} est obligatoire`);
|
||||
}
|
||||
});
|
||||
|
||||
await documentsRef.doc(documentID).set(
|
||||
{
|
||||
...cleanObject,
|
||||
},
|
||||
{
|
||||
merge: true,
|
||||
}
|
||||
);
|
||||
|
||||
setTooltip({
|
||||
type: "sucess",
|
||||
text: "Document publié!",
|
||||
});
|
||||
} catch (error) {
|
||||
setTooltip({
|
||||
type: "error",
|
||||
text: error.message,
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Page
|
||||
title="Accueil"
|
||||
scrollEnabled
|
||||
containerStyle={containerStyle}
|
||||
bottomStickyContent={() => (
|
||||
<Button
|
||||
isAbsoluteBottom
|
||||
text="Enregistrer"
|
||||
contentContainerStyle={{ bottom: responsiveHeight(15) }}
|
||||
onPress={onSubmit}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
<>
|
||||
<SectionTextHeader title="Type de compte" />
|
||||
|
||||
<OptionSelector
|
||||
optionTypeList={accountTypeList}
|
||||
selected={type}
|
||||
setSelected={(value) => updateBillingDetails("type", value)}
|
||||
containerStyle={{ marginBottom: gutters }}
|
||||
colorMap={accountTypeColorMap}
|
||||
/>
|
||||
|
||||
{fieldList.map(
|
||||
({ key, label, value, type = "default", inputProps = {} }) => (
|
||||
<React.Fragment key={key}>
|
||||
<SectionTextHeader title={label} />
|
||||
|
||||
<Input
|
||||
key={key}
|
||||
placeholder={label}
|
||||
value={value}
|
||||
setValue={(value) => updateBillingDetails(key, value)}
|
||||
containerStyle={{ marginBottom: gutters }}
|
||||
type={type}
|
||||
{...inputProps}
|
||||
/>
|
||||
</React.Fragment>
|
||||
)
|
||||
)}
|
||||
|
||||
<DocumentDropZone
|
||||
documentExists={!!documentID}
|
||||
documentID={documentID}
|
||||
collectionRef={documentsRef}
|
||||
setFiles={(files) => updateBillingDetails("files", files)}
|
||||
containerStyle={{
|
||||
alignSelf: "center",
|
||||
height: 150,
|
||||
width: "100%",
|
||||
marginBottom: gutters,
|
||||
}}
|
||||
/>
|
||||
|
||||
<FilesPreview
|
||||
files={files}
|
||||
setFiles={(files) => updateBillingDetails("files", files)}
|
||||
documentID={documentID}
|
||||
containerStyle={{}}
|
||||
/>
|
||||
</>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,344 @@
|
||||
import React, { useState, useRef, useEffect, useGlobal } from "reactn";
|
||||
import {
|
||||
Text,
|
||||
View,
|
||||
KeyboardAvoidingView,
|
||||
Image,
|
||||
Pressable,
|
||||
} from "react-native";
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
|
||||
import {
|
||||
responsiveHeight,
|
||||
responsiveWidth,
|
||||
} from "../actions/responsiveSizes.js";
|
||||
|
||||
import firebase from "../config/firebase";
|
||||
|
||||
import Button from "../components/Button";
|
||||
import { Input } from "../components/Input";
|
||||
|
||||
import { Fonts, gutters, Palette, Style } from "../styles";
|
||||
import { Routes } from "../navigation";
|
||||
import { art, mockups } from "../assets";
|
||||
import {
|
||||
checkIfEmailIsValid,
|
||||
checkIfPasswordIsStrongEnough,
|
||||
handleFirebaseError,
|
||||
} from "../actions/signupActions";
|
||||
|
||||
import useLayoutType from "../hooks/useLayoutType.js";
|
||||
import { capitalize } from "../helpers/index.js";
|
||||
|
||||
export default ({ navigation }) => {
|
||||
console.log("LOGINSCREEN");
|
||||
const [, setTooltip] = useGlobal("_tooltip");
|
||||
const [, setIsLoading] = useGlobal("_isLoading");
|
||||
|
||||
const [mode, setMode] = useState("LOGIN");
|
||||
|
||||
const [emailIsStatic, setEmailIsStatic] = useState(false);
|
||||
|
||||
const [email, setEmail] = useState("login@minuit.com");
|
||||
const [password, setPassword] = useState("password");
|
||||
|
||||
const [firstName, setFirstName] = useState("");
|
||||
const [lastName, setLastName] = useState("");
|
||||
|
||||
const passwordInputRef = useRef();
|
||||
|
||||
const { isDesktop } = useLayoutType();
|
||||
|
||||
useEffect(() => {
|
||||
const checkIfUserIsLoggedIn = async () => {
|
||||
const userEmail = await AsyncStorage.getItem("userEmail");
|
||||
|
||||
if (userEmail) {
|
||||
setEmail(userEmail);
|
||||
setEmailIsStatic(true);
|
||||
}
|
||||
};
|
||||
|
||||
checkIfUserIsLoggedIn();
|
||||
}, []);
|
||||
|
||||
const onLogin = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
let trimmedEmail = email.trim();
|
||||
|
||||
await firebase.auth().signInWithEmailAndPassword(trimmedEmail, password);
|
||||
await AsyncStorage.setItem("userEmail", trimmedEmail);
|
||||
|
||||
onAuthSuccess();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
setTooltip({
|
||||
text: handleFirebaseError(error.code),
|
||||
type: "error",
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onSignup = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
if (!checkIfPasswordIsStrongEnough({ password })) {
|
||||
throw new Error(
|
||||
"Votre mot de passe doit contenir au moins 6 caractères, une majuscule et un chiffre."
|
||||
);
|
||||
}
|
||||
|
||||
if (!checkIfEmailIsValid({ email })) {
|
||||
throw new Error("Veuillez renseigner une adresse email valide.");
|
||||
}
|
||||
|
||||
const { user = null } = await firebase
|
||||
.auth()
|
||||
.createUserWithEmailAndPassword(email, password);
|
||||
|
||||
if (!user) {
|
||||
throw new Error("Erreur lors de la création du compte");
|
||||
}
|
||||
|
||||
let userObject = {
|
||||
firstName: capitalize(firstName.trim()),
|
||||
lastName: capitalize(lastName.trim()),
|
||||
};
|
||||
|
||||
// push user data to firestore
|
||||
|
||||
onAuthSuccess();
|
||||
|
||||
setTooltip({
|
||||
text: "Votre compte a été créé avec succès!",
|
||||
type: "success",
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
setTooltip({
|
||||
text: error?.code?.startsWith("auth/")
|
||||
? handleFirebaseError(error.code)
|
||||
: error.message,
|
||||
type: "error",
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onAuthSuccess = async () => {
|
||||
navigation.reset({
|
||||
index: 0,
|
||||
routes: [{ name: Routes.Splash }],
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<View
|
||||
headerType="NONE"
|
||||
style={{
|
||||
flex: 1,
|
||||
backgroundColor: Palette.lightPurple,
|
||||
...Style.containerRow,
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
position: "relative",
|
||||
flex: 1,
|
||||
padding: 2 * gutters,
|
||||
backgroundColor: Palette.darkPurple,
|
||||
height: "100%",
|
||||
...Style.containerCenter,
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
resizeMode="contain"
|
||||
source={art.gradientTriangle}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: responsiveHeight(20),
|
||||
right: responsiveHeight(5),
|
||||
width: responsiveWidth(140),
|
||||
height: responsiveWidth(140),
|
||||
}}
|
||||
/>
|
||||
|
||||
<KeyboardAvoidingView
|
||||
style={{ ...Style.containerCenter, width: "100%" }}
|
||||
behavior="padding"
|
||||
keyboardVerticalOffset={responsiveHeight(10)}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
...Fonts({ type: "megaTitle" }),
|
||||
marginBottom: responsiveHeight(10),
|
||||
}}
|
||||
>
|
||||
minuit.starter
|
||||
</Text>
|
||||
|
||||
{emailIsStatic && mode === "LOGIN" ? (
|
||||
<>
|
||||
<Text
|
||||
style={{
|
||||
...Fonts({
|
||||
type: "default",
|
||||
color: Palette.white,
|
||||
style: { marginBottom: 20, textAlign: "center" },
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{`Vous êtes connecté(e) avec l'adresse ${email},`}
|
||||
</Text>
|
||||
|
||||
<Pressable
|
||||
onPress={async () => {
|
||||
await AsyncStorage.removeItem("userEmail");
|
||||
|
||||
setEmailIsStatic(false);
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
...Fonts({
|
||||
type: "default",
|
||||
color: Palette.primary,
|
||||
style: {
|
||||
marginBottom: 20,
|
||||
textAlign: "center",
|
||||
textDecorationLine: "underline",
|
||||
},
|
||||
}),
|
||||
}}
|
||||
>
|
||||
Modifier
|
||||
</Text>
|
||||
</Pressable>
|
||||
</>
|
||||
) : (
|
||||
<Input
|
||||
layout="line"
|
||||
type="email"
|
||||
label="Adresse email"
|
||||
placeholder="Votre adresse email"
|
||||
value={email}
|
||||
setValue={setEmail}
|
||||
containerStyle={{ marginBottom: 20 }}
|
||||
textInputProps={{
|
||||
returnKeyType: "next",
|
||||
onSubmitEditing: () => passwordInputRef.current.focus(),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Input
|
||||
layout="line"
|
||||
inputRef={passwordInputRef}
|
||||
label="Mot de passe"
|
||||
placeholder="Votre mot de passe"
|
||||
value={password}
|
||||
setValue={setPassword}
|
||||
type="password"
|
||||
containerStyle={{ marginBottom: gutters }}
|
||||
textInputProps={{
|
||||
returnKeyType: "done",
|
||||
onSubmitEditing: onLogin,
|
||||
}}
|
||||
/>
|
||||
|
||||
{mode === "LOGIN" ? (
|
||||
<Pressable
|
||||
onPress={() => navigation.navigate("ForgotPassword")}
|
||||
style={{ marginBottom: gutters }}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
...Fonts({
|
||||
type: "default",
|
||||
style: { textDecorationLine: "underline" },
|
||||
}),
|
||||
}}
|
||||
>
|
||||
Mot de passe oublié?
|
||||
</Text>
|
||||
</Pressable>
|
||||
) : (
|
||||
<>
|
||||
<Input
|
||||
layout="line"
|
||||
label="Prénom"
|
||||
placeholder="Votre prénom"
|
||||
value={firstName}
|
||||
setValue={setFirstName}
|
||||
containerStyle={{ marginBottom: gutters }}
|
||||
/>
|
||||
|
||||
<Input
|
||||
layout="line"
|
||||
label="Nom"
|
||||
placeholder="Votre nom"
|
||||
value={lastName}
|
||||
setValue={setLastName}
|
||||
containerStyle={{ marginBottom: gutters }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Button
|
||||
{...(mode === "LOGIN"
|
||||
? { text: "Connexion", onPress: onLogin }
|
||||
: {
|
||||
text: "Inscription",
|
||||
onPress: onSignup,
|
||||
})}
|
||||
containerStyle={{ marginBottom: gutters }}
|
||||
/>
|
||||
|
||||
<Pressable onPress={() => navigation.navigate("TermsOfUse")}>
|
||||
<Text
|
||||
style={Fonts({
|
||||
type: "default",
|
||||
style: { textAlign: "center", opacity: 1 },
|
||||
})}
|
||||
>
|
||||
En vous {mode === "LOGIN" ? "connectant" : "inscrivant"}, vous
|
||||
acceptez sans réserve les{" "}
|
||||
<Text
|
||||
style={{
|
||||
color: Palette.primary,
|
||||
textDecorationLine: "underline",
|
||||
}}
|
||||
>
|
||||
conditions générales d'utilisation
|
||||
</Text>{" "}
|
||||
de minuit.starter.
|
||||
</Text>
|
||||
</Pressable>
|
||||
</KeyboardAvoidingView>
|
||||
</View>
|
||||
|
||||
{isDesktop && (
|
||||
<View style={{ flex: 2, height: "100%", overflow: "hidden" }}>
|
||||
<Image
|
||||
resizeMode="contain"
|
||||
source={mockups.loginAppDashboard}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,92 @@
|
||||
import React, { useEffect } from "reactn";
|
||||
import { Text, View, FlatList } from "react-native";
|
||||
import { useDataFromRef } from "react-native-minuit/src/hooks";
|
||||
import moment from "moment";
|
||||
import { Motion } from "@legendapp/motion";
|
||||
|
||||
import Page from "../layouts/Page";
|
||||
|
||||
import { Fonts, Style } from "../styles";
|
||||
import { notificationsRef } from "../config/firebase";
|
||||
|
||||
import { LoaderIndicator } from "../providers/LoadingProvider";
|
||||
|
||||
export default (props) => {
|
||||
const baseNotificationRef = notificationsRef;
|
||||
|
||||
useEffect(() => {
|
||||
markAsRead();
|
||||
}, []);
|
||||
|
||||
const markAsRead = async () => {
|
||||
try {
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const {
|
||||
data: notificationList,
|
||||
loading,
|
||||
loadMore,
|
||||
} = useDataFromRef({
|
||||
ref: baseNotificationRef.orderBy("timestamp", "desc"),
|
||||
usePagination: true,
|
||||
batchSize: 40,
|
||||
documentID: "notificationID",
|
||||
});
|
||||
|
||||
return (
|
||||
<Page headerType="NAVIGATE" title="Notifications">
|
||||
<FlatList
|
||||
removeClippedSubviews={true}
|
||||
estimatedItemSize={100}
|
||||
showsVerticalScrollIndicator={false}
|
||||
data={notificationList}
|
||||
renderItem={({ item, index }) => {
|
||||
const { title = "", timestamp } = item;
|
||||
|
||||
const randomDuration = Math.floor(Math.random() * 1000) + 500;
|
||||
|
||||
return (
|
||||
<Motion.Pressable
|
||||
key={index}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{
|
||||
opacity: {
|
||||
type: "tween",
|
||||
duration: randomDuration,
|
||||
},
|
||||
}}
|
||||
style={Style.containerSpaceBetween}
|
||||
onPress={() => {}}
|
||||
>
|
||||
<View style={Style.containerRow}>
|
||||
<Text
|
||||
numberOfLines={2}
|
||||
style={Fonts({ style: { width: "100%" } })}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<Text style={Fonts({ style: { opacity: 0.5 } })}>
|
||||
{moment(timestamp.toDate()).format("[Le] DD/MM [à] HH:mm")}
|
||||
</Text>
|
||||
</Motion.Pressable>
|
||||
);
|
||||
}}
|
||||
keyExtractor={(item) =>
|
||||
`Notifications_${item.notificationID}_${item.projectID}`
|
||||
}
|
||||
ItemSeparatorComponent={() => (
|
||||
<View style={{ ...Style.separatorHorizontal }} />
|
||||
)}
|
||||
ListFooterComponent={loading ? <LoaderIndicator /> : null}
|
||||
onEndReached={loadMore}
|
||||
onEndReachedThreshold={0.5}
|
||||
/>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from "react";
|
||||
import { Text, View } from "react-native";
|
||||
import Button from "../components/Button";
|
||||
import { gutters } from "../styles";
|
||||
import { navigate } from "../navigation/NavigationService";
|
||||
import { Routes } from "../navigation";
|
||||
|
||||
export default ({ navigation }) => {
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
justifyContent: "flex-end",
|
||||
padding: gutters,
|
||||
paddingBottom: gutters * 2,
|
||||
}}
|
||||
>
|
||||
<Button text="WRITING" onPress={() => navigate(Routes.Writing)} />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
import React from "reactn";
|
||||
import { Text } from "react-native";
|
||||
|
||||
import Page from "../layouts/Page";
|
||||
|
||||
import { Fonts } from "../styles";
|
||||
|
||||
const privacyPolicy = `Politique de Confidentialité de minuit.starter
|
||||
|
||||
1. Introduction
|
||||
|
||||
La présente Politique de Confidentialité s'applique à l'application "minuit.starter" éditée par MINUIT.AGENCY. Elle a pour objet d'informer les utilisateurs de la manière dont leurs informations personnelles sont collectées, utilisées et protégées.
|
||||
|
||||
2. Collecte des données
|
||||
|
||||
Nous collectons les informations que vous nous fournissez directement lorsque vous utilisez notre application. Cela peut inclure votre nom, adresse e-mail, contenu du projet, et tout autre information que vous choisissez de partager.
|
||||
|
||||
3. Utilisation des données
|
||||
|
||||
Les informations collectées sont utilisées pour :
|
||||
|
||||
Fournir, maintenir et améliorer notre application.
|
||||
Traiter les transactions et envoyer des notifications relatives à vos transactions.
|
||||
Envoyer des communications techniques, mises à jour, alertes de sécurité et messages d'assistance.
|
||||
4. Partage des informations
|
||||
Nous ne vendons, ni louons vos informations personnelles à des tiers à des fins marketing. Nous pouvons partager vos informations avec des tiers en relation avec la prestation de services pour notre compte (par exemple, le traitement des paiements).
|
||||
|
||||
5. Sécurité
|
||||
|
||||
MINUIT.AGENCY s'engage à protéger la sécurité de vos informations et utilise pour ce faire des mesures techniques et organisationnelles appropriées.
|
||||
|
||||
6. Conservation des données
|
||||
|
||||
Nous conservons vos informations aussi longtemps que votre compte est actif ou aussi longtemps que nécessaire pour vous fournir des services.
|
||||
|
||||
7. Vos droits
|
||||
|
||||
Conformément à la réglementation en vigueur, vous disposez d'un droit d'accès, de rectification et de suppression de vos données. Vous pouvez également vous opposer à l'utilisation de vos données.
|
||||
|
||||
8. Mises à jour de la Politique de Confidentialité
|
||||
|
||||
Nous pouvons occasionnellement mettre à jour cette politique. Si des modifications majeures sont apportées, nous vous en informerons via notre application ou par d'autres moyens.
|
||||
|
||||
9. Contact
|
||||
|
||||
Si vous avez des questions concernant cette Politique de Confidentialité, contactez-nous à : hello@minuit.agency.
|
||||
|
||||
Dernière mise à jour : 06/08/2023.`;
|
||||
|
||||
export default ({} = {}) => {
|
||||
return (
|
||||
<Page
|
||||
headerType="NAVIGATE"
|
||||
title={"Politique de confidentialité"}
|
||||
scrollEnabled
|
||||
>
|
||||
<Text style={{ ...Fonts({ type: "default" }) }}>{privacyPolicy}</Text>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,93 @@
|
||||
import React, { useGlobal } from "reactn";
|
||||
|
||||
import Page from "../layouts/Page";
|
||||
|
||||
import InputRow from "../components/InputRow.js";
|
||||
|
||||
import { projectsRef } from "../config/firebase";
|
||||
import {
|
||||
checkIfFigmaLinkIsValid,
|
||||
getValueFromKeyState,
|
||||
} from "../helpers/index.js";
|
||||
|
||||
export default (props) => {
|
||||
const [, setIsLoading] = useGlobal("_isLoading");
|
||||
const [, setTooltip] = useGlobal("_tooltip");
|
||||
|
||||
const [currentProjectData] = useGlobal("currentProjectData");
|
||||
|
||||
const settingsList = [
|
||||
{
|
||||
title: "Lien du Figma",
|
||||
key: "links.figma",
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
title: "Lien de test",
|
||||
key: "links.test",
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
title: "Lien de production",
|
||||
key: "links.production",
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
title: "Lien du dashboard",
|
||||
key: "dashboardURL",
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
title: "Lien du git",
|
||||
key: "links.git",
|
||||
type: "string",
|
||||
},
|
||||
];
|
||||
|
||||
const onUpdateSetting = async ({ key, value }) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
if (key === "links.figma") {
|
||||
if (!checkIfFigmaLinkIsValid({ figmaLink: value })) {
|
||||
throw new Error("Le lien Figma n'est pas valide");
|
||||
}
|
||||
}
|
||||
|
||||
await projectsRef.doc(currentProjectData.id).update({
|
||||
[key]: value,
|
||||
});
|
||||
|
||||
setTooltip({
|
||||
type: "success",
|
||||
text: "Paramètre mis à jour",
|
||||
});
|
||||
} catch (error) {
|
||||
setTooltip({
|
||||
type: "error",
|
||||
text: error.message,
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Page headerType="NAVIGATE" title={"Liens du projet"} scrollEnabled>
|
||||
{settingsList.map((setting) => {
|
||||
const { title, key, type } = setting;
|
||||
|
||||
return (
|
||||
<InputRow
|
||||
key={key}
|
||||
itemKey={key}
|
||||
value={getValueFromKeyState({ key, state: currentProjectData })}
|
||||
type={type}
|
||||
title={title}
|
||||
onUpdateValue={onUpdateSetting}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,226 @@
|
||||
import React, { useState } from "reactn";
|
||||
import { Text, Pressable } from "react-native";
|
||||
import * as ImagePicker from "expo-image-picker";
|
||||
import useMinuit from "react-native-minuit/src/hooks/useMinuit.js";
|
||||
|
||||
import Page from "../layouts/Page";
|
||||
import { responsiveWidth } from "../actions/responsiveSizes.js";
|
||||
|
||||
import Avatar from "../components/Avatar";
|
||||
import InputRow from "../components/InputRow.js";
|
||||
import ItemRowList from "../components/ItemRowList.js";
|
||||
import alert from "../components/Alert.js";
|
||||
|
||||
import { Fonts, gutters, Palette, Style } from "../styles";
|
||||
|
||||
import { projectsRef } from "../config/firebase";
|
||||
|
||||
import { uploadFileToFirebase } from "../helpers/uploadToFirebase";
|
||||
import { getValueFromKeyState } from "../helpers/index.js";
|
||||
|
||||
import { useUserData } from "../providers/UserDataProvider.js";
|
||||
import { Routes } from "../navigation/Routes.js";
|
||||
|
||||
export default ({ navigation }) => {
|
||||
const { setIsLoading, setTooltip } = useMinuit();
|
||||
|
||||
const [stateToEdit] = useState({});
|
||||
|
||||
const { isSuperAdmin } = useUserData();
|
||||
|
||||
const settingsList = [
|
||||
{
|
||||
title: "Activer le 'shake'",
|
||||
key: "enableShake",
|
||||
type: "boolean",
|
||||
},
|
||||
{
|
||||
title: "Activer les copies de sauvegarde",
|
||||
key: "enableBackup",
|
||||
type: "boolean",
|
||||
},
|
||||
{
|
||||
title: "Passer en mode 'maintenance'",
|
||||
key: "enableMaintenanceMode",
|
||||
type: "boolean",
|
||||
},
|
||||
{
|
||||
title: "Identifiant Firebase",
|
||||
key: "firebaseConfig.projectId",
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
title: "Identifiant App Store",
|
||||
key: "storeConfig.apple.appID",
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
title: "Identifiant Algolia",
|
||||
key: "algoliaConfig.appId",
|
||||
type: "string",
|
||||
},
|
||||
];
|
||||
|
||||
const destructiveSettingsList = [
|
||||
{
|
||||
title: "Archiver le projet",
|
||||
action: () => onArchiveProject(),
|
||||
textStyle: {
|
||||
color: Palette.red,
|
||||
},
|
||||
condition: isSuperAdmin,
|
||||
},
|
||||
].filter(({ condition = true }) => condition);
|
||||
|
||||
const onChangePicture = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
let result = await ImagePicker.launchImageLibraryAsync({
|
||||
mediaTypes: ImagePicker.MediaTypeOptions.All,
|
||||
allowsEditing: true,
|
||||
aspect: [4, 4],
|
||||
quality: 1,
|
||||
});
|
||||
|
||||
if (result?.assets?.[0]?.uri) {
|
||||
const { resultURI = null } = await uploadFileToFirebase({
|
||||
uri: result?.assets?.[0]?.uri,
|
||||
path: `projects/${Math.random()}/icon.png`,
|
||||
});
|
||||
|
||||
if (resultURI) {
|
||||
await projectsRef.doc(Math.random()).update({
|
||||
iconURL: resultURI,
|
||||
});
|
||||
|
||||
setTooltip({
|
||||
type: "success",
|
||||
text: "Image changée avec succès",
|
||||
});
|
||||
} else {
|
||||
throw new Error("Erreur changement d'image");
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
|
||||
setTooltip({
|
||||
type: "error",
|
||||
text: error.message,
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onUpdateSetting = async ({ key, value }) => {
|
||||
try {
|
||||
await projectsRef.doc(Math.random()).update({
|
||||
[key]: value,
|
||||
});
|
||||
|
||||
setTooltip({
|
||||
type: "success",
|
||||
text: "Paramètre mis à jour",
|
||||
});
|
||||
} catch (error) {
|
||||
setTooltip({
|
||||
type: "error",
|
||||
text: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onArchiveProject = async () => {
|
||||
alert(
|
||||
"Êtes-vous sûr?",
|
||||
"Pour annuler l'archivage du projet, il faudra contacter le support.",
|
||||
[
|
||||
{
|
||||
text: "Annuler",
|
||||
onPress: () => {},
|
||||
style: "cancel",
|
||||
},
|
||||
{
|
||||
text: "Confirmer",
|
||||
onPress: async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
setTooltip({
|
||||
type: "success",
|
||||
text: "Projet archivé avec succès!",
|
||||
});
|
||||
|
||||
navigation.reset({
|
||||
index: 0,
|
||||
routes: [
|
||||
{
|
||||
name: Routes.BottomTab,
|
||||
},
|
||||
],
|
||||
});
|
||||
} catch (error) {
|
||||
setTooltip({
|
||||
type: "error",
|
||||
text: error.message,
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
style: "confirm",
|
||||
},
|
||||
],
|
||||
{ cancelable: false }
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Page headerType="NAVIGATE" title={"Paramètres du projet"} scrollEnabled>
|
||||
<Pressable
|
||||
onPress={onChangePicture}
|
||||
style={[Style.containerCenter, { marginBottom: gutters }]}
|
||||
>
|
||||
<Avatar
|
||||
name={"random"}
|
||||
url={null}
|
||||
forceRawImage
|
||||
size={responsiveWidth(30)}
|
||||
containerStyle={{
|
||||
marginBottom: gutters / 2,
|
||||
...Style.defaultShadows,
|
||||
}}
|
||||
/>
|
||||
<Text
|
||||
style={Fonts({
|
||||
type: "section",
|
||||
style: { color: Palette.primary, textDecorationLine: "underline" },
|
||||
})}
|
||||
>
|
||||
Modifier
|
||||
</Text>
|
||||
</Pressable>
|
||||
|
||||
{settingsList.map((setting) => {
|
||||
const { title, key, type } = setting;
|
||||
|
||||
return (
|
||||
<InputRow
|
||||
key={key}
|
||||
itemKey={key}
|
||||
value={getValueFromKeyState({ key, state: stateToEdit })}
|
||||
type={type}
|
||||
title={title}
|
||||
onUpdateValue={onUpdateSetting}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{destructiveSettingsList.map((setting, index) => (
|
||||
<ItemRowList key={index} {...setting} containerStyle={{}} />
|
||||
))}
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,191 @@
|
||||
import React, { useRef, useContext, useGlobal } from "reactn";
|
||||
import { Text, View } from "react-native";
|
||||
|
||||
import { WebViewContext } from "../providers/WebViewProvider";
|
||||
import { useUserData } from "../providers/UserDataProvider";
|
||||
|
||||
import Page from "../layouts/Page";
|
||||
|
||||
import { Routes } from "../navigation";
|
||||
import { responsiveHeight } from "../actions/responsiveSizes.js";
|
||||
|
||||
import ItemRowList from "../components/ItemRowList";
|
||||
import BottomSheetContainer from "../components/BottomSheetContainer.js";
|
||||
import IconSelector from "../components/IconSelector.js";
|
||||
|
||||
import useLayoutType from "../hooks/useLayoutType";
|
||||
|
||||
import { Fonts, Palette, Style, gutters } from "../styles";
|
||||
import { icons } from "../assets";
|
||||
|
||||
import { onStoreReview, openLinkInBrowser } from "../helpers";
|
||||
import {
|
||||
agencyWhatsAppNumber,
|
||||
calendlyUrl,
|
||||
instagramUrl,
|
||||
termsOfSalesUrl,
|
||||
} from "../data";
|
||||
import IconContainer from "../components/IconContainer.js";
|
||||
|
||||
export default ({ navigation }) => {
|
||||
const { isDesktop, isWeb, isNative } = useLayoutType();
|
||||
const { onSignOut } = useUserData();
|
||||
|
||||
const [currentProjectID] = useGlobal("currentProjectID");
|
||||
|
||||
const { setWebViewUrl } = useContext(WebViewContext);
|
||||
|
||||
const appIconBottomSheetRef = useRef(null);
|
||||
|
||||
const settingsList = [
|
||||
{
|
||||
sectionTitle: "Projet",
|
||||
icon: icons.tools,
|
||||
optionList: [
|
||||
{
|
||||
title: "Paramètres",
|
||||
action: () => navigation.navigate(Routes.ProjectSettings),
|
||||
},
|
||||
],
|
||||
condition: currentProjectID,
|
||||
},
|
||||
{
|
||||
sectionTitle: "Compte",
|
||||
icon: icons.user,
|
||||
optionList: [
|
||||
{
|
||||
title: "Paramètres du compte",
|
||||
action: () => navigation.navigate(Routes.AccountSettings),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
sectionTitle: "Support",
|
||||
icon: icons.support,
|
||||
optionList: [
|
||||
{
|
||||
title: "Prendre rendez-vous",
|
||||
action: () => setWebViewUrl(calendlyUrl),
|
||||
},
|
||||
{
|
||||
title: "Nous suivre sur Instagram",
|
||||
action: () => setWebViewUrl(instagramUrl),
|
||||
},
|
||||
{
|
||||
title: "Nous contacter sur WhatsApp",
|
||||
action: () => {
|
||||
openLinkInBrowser({
|
||||
url: `http://api.whatsapp.com/send/?phone=${agencyWhatsAppNumber}&text&type=phone_number`,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Changer l'icône de l'application",
|
||||
action: () => {
|
||||
appIconBottomSheetRef.current?.expand();
|
||||
},
|
||||
condition: isNative,
|
||||
},
|
||||
{
|
||||
title: "Noter l'application",
|
||||
action: () => {
|
||||
onStoreReview();
|
||||
},
|
||||
condition: !isWeb,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
sectionTitle: "Légal",
|
||||
icon: icons.law,
|
||||
optionList: [
|
||||
{
|
||||
title: "Conditions générales d'utilisation",
|
||||
action: () => navigation.navigate(Routes.TermsOfUse),
|
||||
},
|
||||
{
|
||||
title: "Conditions générales de vente",
|
||||
action: () => setWebViewUrl(termsOfSalesUrl),
|
||||
},
|
||||
{
|
||||
title: "Politique de confidentialité",
|
||||
action: () => navigation.navigate(Routes.PrivacyPolicy),
|
||||
},
|
||||
{
|
||||
title: "Se déconnecter",
|
||||
action: async () => {
|
||||
await onSignOut();
|
||||
},
|
||||
textStyle: { color: Palette.red },
|
||||
addMarginTopFromPrevious: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
.filter(({ condition = true }) => condition)
|
||||
.filter(
|
||||
({ optionList }) =>
|
||||
optionList.filter(({ condition }) => !condition || condition).length > 0
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Page
|
||||
headerType={isDesktop ? "NONE" : "BASE"}
|
||||
scrollEnabled
|
||||
pageTitle="Paramètres"
|
||||
contentContainerStyle={{
|
||||
paddingTop: gutters / 2,
|
||||
paddingBottom: responsiveHeight(50),
|
||||
}}
|
||||
>
|
||||
{settingsList.map(
|
||||
({ icon, sectionTitle = "", optionList = [] }, index) => (
|
||||
<View
|
||||
key={index}
|
||||
style={{ ...Style.containerItem, marginBottom: gutters / 2 }}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
...Style.containerRow,
|
||||
}}
|
||||
>
|
||||
<IconContainer icon={icon} />
|
||||
|
||||
<Text
|
||||
style={Fonts({
|
||||
type: "title",
|
||||
color: Palette.white,
|
||||
style: {},
|
||||
})}
|
||||
>
|
||||
{sectionTitle}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{optionList
|
||||
.filter(({ condition = true }) => condition)
|
||||
.map((item, index) => (
|
||||
<ItemRowList
|
||||
key={index}
|
||||
{...item}
|
||||
containerStyle={{}}
|
||||
separatorPosition="top"
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
)
|
||||
)}
|
||||
</Page>
|
||||
|
||||
<BottomSheetContainer
|
||||
bottomSheetRef={appIconBottomSheetRef}
|
||||
index={-1}
|
||||
enablePanDownToClose
|
||||
snapPoints={["80%"]}
|
||||
>
|
||||
<IconSelector onClose={() => appIconBottomSheetRef.current?.close()} />
|
||||
</BottomSheetContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
import React, { useEffect, useContext } from "reactn";
|
||||
import { View } from "react-native";
|
||||
|
||||
import firebase, { usersRef } from "../config/firebase";
|
||||
import { Routes } from "../navigation";
|
||||
|
||||
import { SplashAnimationContext } from "../providers/SplashAnimationProvider";
|
||||
import { UserDataContext } from "../providers/UserDataProvider";
|
||||
|
||||
import { isDesktop, isWeb } from "../hooks/useLayoutType";
|
||||
|
||||
export default ({ navigation }) => {
|
||||
const { setCurrentUserData } = useContext(UserDataContext);
|
||||
const { setIsFullyLoaded } = useContext(SplashAnimationContext);
|
||||
|
||||
useEffect(() => {
|
||||
userRouting();
|
||||
}, []);
|
||||
|
||||
const userRouting = async () => {
|
||||
try {
|
||||
if (!firebase.auth().currentUser?.uid) {
|
||||
throw new Error("No user");
|
||||
}
|
||||
|
||||
const userDoc = await usersRef
|
||||
.doc(firebase.auth().currentUser?.uid)
|
||||
.get();
|
||||
|
||||
if (userDoc?.data()) {
|
||||
setCurrentUserData(userDoc.data());
|
||||
|
||||
navigation.reset({
|
||||
index: 0,
|
||||
routes: [
|
||||
{
|
||||
name: Routes.BottomTab,
|
||||
params: {
|
||||
screen: Routes.Home,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
} else {
|
||||
throw new Error("No data");
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
|
||||
navigation.reset({
|
||||
index: 0,
|
||||
routes: [
|
||||
{
|
||||
name: isDesktop || isWeb ? Routes.Login : Routes.Onboarding,
|
||||
},
|
||||
],
|
||||
});
|
||||
} finally {
|
||||
setIsFullyLoaded(true);
|
||||
}
|
||||
};
|
||||
|
||||
return <View style={{ flex: 1 }}></View>;
|
||||
};
|
||||
@@ -0,0 +1,77 @@
|
||||
import React from "reactn";
|
||||
import { Text } from "react-native";
|
||||
|
||||
import Page from "../layouts/Page";
|
||||
|
||||
import { Fonts } from "../styles";
|
||||
|
||||
const terms = `Conditions Générales d'Utilisation (CGU) de minuit.starter
|
||||
|
||||
Édition par MINUIT.AGENCY,
|
||||
44 RUE MINVIELLE, 33000 BORDEAUX,
|
||||
RCS Bordeaux 919 289 496,
|
||||
Capital : 1000 €.
|
||||
Représenté par minuit.holdings, Président.
|
||||
|
||||
Définitions :
|
||||
|
||||
Utilisateur : toute personne physique ou morale qui utilise l'application minuit.starter.
|
||||
Contenu Utilisateur : toute information, texte, image, ou autre matériel que les utilisateurs publient ou partagent sur minuit.starter.
|
||||
Minuit.boost : service prioritaire commandé par les utilisateurs sur minuit.starter pour accélérer le processus de réalisation de leurs tâches.
|
||||
Tâche Supplémentaire : toute tâche additionnelle commandée par les utilisateurs sur minuit.starter.
|
||||
Minuit.coin : unité de monnaie virtuelle utilisée sur minuit.starter pour acheter des services ou des tâches supplémentaires.
|
||||
Acceptation des CGU et CGV
|
||||
L'utilisation de "minuit.starter" et/ou le téléchargement de l'application sur l'App Store iOS implique l'acceptation inconditionnelle de ces CGU. Tout achat de services ou de tâches supplémentaires sur la plateforme implique également l'acceptation des Conditions Générales de Vente (CGV). Si vous n'êtes pas d'accord avec l'une de ces conditions, vous devez immédiatement cesser toute utilisation de l'application.
|
||||
|
||||
Objet
|
||||
"minuit.starter" fournit des outils pour aider les utilisateurs à publier des tâches, suivre l'évolution de leurs projets, commander des Minuit.boosts pour un service prioritaire, commander des tâches supplémentaires, et acheter des minuit.coins pour utiliser les services payants proposés sur la plateforme.
|
||||
|
||||
Fonctionnement AI de l'Application
|
||||
Toutes les informations, notamment les délais, fournies par l'application sont basées sur des algorithmes d'intelligence artificielle. Elles sont fournies "telles quelles", sans garantie d'exactitude, d'exhaustivité ou de pertinence pour des situations spécifiques. Les réponses du chat AI sont à titre informatif et ne doivent pas remplacer un avis professionnel.
|
||||
|
||||
Accès & Utilisation
|
||||
Seuls les individus majeurs et capables juridiquement peuvent utiliser l'application. Toute utilisation illégale, abusive ou en violation des présentes CGU est interdite.
|
||||
|
||||
Disponibilité & Interruption
|
||||
L'accès à minuit.starter peut être interrompu, suspendu ou modifié à tout moment, pour n'importe quelle raison, sans préavis ni obligation. MINUIT.AGENCY ne sera pas responsable des pertes de données ou des interruptions de service.
|
||||
|
||||
Confidentialité
|
||||
Nous traitons les données personnelles conformément à notre politique de confidentialité, que les utilisateurs doivent consulter séparément.
|
||||
|
||||
Responsabilité des Informations et Contenu Utilisateur
|
||||
MINUIT.AGENCY ne garantit pas la véracité, la précision, ou la fiabilité des informations fournies par l'application. minuit.starter est une plateforme sur laquelle les utilisateurs peuvent publier du contenu. Malgré nos meilleurs efforts pour maintenir un environnement sûr et respectueux, MINUIT.AGENCY n'est pas responsable du contenu publié par les utilisateurs. Tout contenu publié reflète uniquement l'opinion de l'utilisateur qui le publie.
|
||||
|
||||
Propriété Intellectuelle
|
||||
Toute violation des droits de propriété intellectuelle de MINUIT.AGENCY pourra donner lieu à des poursuites judiciaires.
|
||||
|
||||
Limitation de Responsabilité
|
||||
MINUIT.AGENCY décline toute responsabilité pour tout dommage, direct ou indirect, résultant de l'utilisation de l'application.
|
||||
|
||||
Modifications des CGU
|
||||
Les CGU peuvent être modifiées à tout moment. Il appartient à l'utilisateur de les consulter régulièrement.
|
||||
|
||||
Résiliation & Suspension
|
||||
MINUIT.AGENCY peut suspendre ou résilier l'accès d'un utilisateur sans préavis en cas de non-respect des CGU ou pour tout autre motif à sa discrétion.
|
||||
|
||||
Droit Applicable & Juridiction
|
||||
Les CGU sont soumises au droit français. Tout litige sera porté devant les tribunaux compétents de Bordeaux.
|
||||
|
||||
Divers
|
||||
Si une disposition des présentes CGU est jugée inapplicable, les autres resteront en vigueur.
|
||||
|
||||
Contact
|
||||
Pour toute question : hello@minuit.agency.
|
||||
|
||||
Dernière mise à jour : 22/10/2023.`;
|
||||
|
||||
export default ({ navigation, route }) => {
|
||||
return (
|
||||
<Page
|
||||
headerType="NAVIGATE"
|
||||
title={"Conditions Générales d'Utilisation"}
|
||||
scrollEnabled
|
||||
>
|
||||
<Text style={{ ...Fonts({ type: "default" }) }}>{terms}</Text>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
import { View } from "react-native";
|
||||
import React from "react";
|
||||
import Page from "../../layouts/Page";
|
||||
import GradientButton from "../../components/GradientButton";
|
||||
import { gutters } from "../../styles";
|
||||
import { navigate } from "../../navigation/NavigationService";
|
||||
import { Routes } from "../../navigation";
|
||||
|
||||
const Writing = () => {
|
||||
return (
|
||||
<Page
|
||||
headerType="NONE"
|
||||
containerStyle={{ paddingHorizontal: 0 }}
|
||||
contentContainerStyle={{
|
||||
padding: gutters * 2,
|
||||
}}
|
||||
>
|
||||
<View style={{ flex: 1, justifyContent: "flex-end" }}>
|
||||
<GradientButton
|
||||
title="Commencer"
|
||||
onPress={() => navigate(Routes.WritingLyrics)}
|
||||
/>
|
||||
</View>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export default Writing;
|
||||
@@ -0,0 +1,44 @@
|
||||
import { View, Text, Image, StyleSheet } from "react-native";
|
||||
import React from "react";
|
||||
import Page from "../../layouts/Page";
|
||||
import { ai } from "../../assets";
|
||||
import GradientButton from "../../components/GradientButton";
|
||||
import { gutters } from "../../styles";
|
||||
import BorderGradientButton from "../../components/BorderGradientButton";
|
||||
import MusicLandHeader from "../../components/MusicLandHeader";
|
||||
import { goBack } from "../../navigation/NavigationService";
|
||||
|
||||
const WritingLyrics = () => {
|
||||
return (
|
||||
<Page headerType="NONE">
|
||||
<Image source={ai.nathalie} style={styles.img} resizeMode="contain" />
|
||||
<MusicLandHeader showSkip onPressBack={goBack} />
|
||||
<View
|
||||
style={{ flex: 1, position: "relative", justifyContent: "flex-end" }}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
paddingBottom: gutters * 2,
|
||||
paddingHorizontal: gutters,
|
||||
gap: 12,
|
||||
}}
|
||||
>
|
||||
<BorderGradientButton />
|
||||
<GradientButton title="Écrire des paroles avec une IA" />
|
||||
</View>
|
||||
</View>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export default WritingLyrics;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
img: {
|
||||
width: "100%",
|
||||
height: "70%",
|
||||
position: "absolute",
|
||||
bottom: -40,
|
||||
right: -30,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user