This commit is contained in:
Philip Cesar Garay
2025-07-31 21:25:33 +08:00
parent 5cfbc81e3a
commit 84fc719a10
307 changed files with 46470 additions and 0 deletions
+277
View File
@@ -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>
);
};
+59
View File
@@ -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>
);
};
+86
View File
@@ -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>
);
};
+224
View File
@@ -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>
);
};
+344
View File
@@ -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>
);
};
+92
View File
@@ -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>
);
};
+21
View File
@@ -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>
);
};
+60
View File
@@ -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>
);
};
+93
View File
@@ -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>
);
};
+226
View File
@@ -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>
);
};
+191
View File
@@ -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>
</>
);
};
+64
View File
@@ -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>;
};
+77
View File
@@ -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>
);
};
+28
View File
@@ -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;
+44
View File
@@ -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,
},
});