Files
social-media/frontend/src/views/profile/ProfilePage.vue

954 lines
36 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup>
import { computed, markRaw, onMounted, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useCreatorProfileStore } from '@/stores/creatorProfileStore.js';
import { useUserProfileStore } from '@/stores/userProfileStore.js';
import { useClient } from '@/plugins/api.js';
import SocialsDialog from './creators/SocialsDialog.vue';
import AliasDialog from '@/views/profile/account/AliasDialog.vue';
import FullnameDialog from '@/views/profile/account/FullnameDialog.vue';
import EmailDialog from '@/views/profile/account/EmailDialog.vue';
import ChangePasswordDialog from '@/views/profile/account/ChangePasswordDialog.vue';
import ChangeStripeIdDialog from '@/views/profile/creators/ChangeStripeIdDialog.vue';
import ChangeNameDialog from '@/views/profile/creators/ChangeNameDialog.vue';
import ChangeSlugDialog from '@/views/profile/creators/ChangeSlugDialog.vue';
import ChangeTitleDialog from '@/views/profile/creators/ChangeTitleDialog.vue';
import ChangePhoneDialog from '@/views/profile/creators/ChangePhoneDialog.vue';
import ChangeEmailDialog from '@/views/profile/creators/ChangeEmailDialog.vue';
import Youtube from '@/views/svg/Youtube.vue';
import Web from '@/views/svg/Web.vue';
import Reddit from '@/views/svg/Reddit.vue';
import X from '@/views/svg/X.vue';
import Linkedin from '@/views/svg/Linkedin.vue';
import Tiktok from '@/views/svg/Tiktok.vue';
import Instagram from '@/views/svg/Instagram.vue';
import Facebook from '@/views/svg/Facebook.vue';
import { useI18n } from 'vue-i18n';
import QRCodeVue from 'qrcode.vue';
import { mdiCheck, mdiChevronRight, mdiContentCopy, mdiCreditCard, mdiCreditCardOff, mdiDownload } from '@mdi/js';
import { useToast } from 'vue-toastification';
import hutopyLogo from '@/assets/hutopy-icon-white-circle.png';
const { t } = useI18n();
const userProfileStore = useUserProfileStore();
const creatorProfileStore = useCreatorProfileStore();
const baseURL = window.location.origin;
const client = useClient();
const route = useRoute();
const router = useRouter();
const toast = useToast();
// Copy URL state
const copySuccess = ref(false);
const copyButtonRef = ref(null);
// Computed properties for Stripe status
const stripeReady = computed(() => {
return stripeStatus.value === 'fully_configured';
});
const stripeStatus = computed(() => {
console.log('stripeStatus');
const creator = creatorProfileStore.creator;
if (!creator.isStripeAccountPresent) {
return 'not_configured';
}
if (!creator.isStripeDetailsSubmitted) {
return 'needs_more_info';
}
if (!creator.isStripeChargesEnabled || !creator.isStripePayoutReady) {
return 'pending_verification';
}
return 'fully_configured';
});
const stripeStatusText = computed(() => {
switch (stripeStatus.value) {
case 'not_configured':
return t('notConfigured');
case 'needs_more_info':
return t('needsMoreInfo');
case 'pending_verification':
return t('pendingVerification');
default:
return t('configured');
}
});
const stripeButtonText = computed(() => {
switch (stripeStatus.value) {
case 'not_configured':
return t('configureStripe');
case 'needs_more_info':
case 'pending_verification':
return t('continueStripeSetup');
case 'fully_configured':
return t('removeStripe');
default:
return t('removeStripe');
}
});
const imageSettings = ref({
src: hutopyLogo,
x: undefined,
y: undefined,
width: 64,
height: 64,
excavate: false,
});
async function checkStripeAccountStatus() {
try {
const response = await client.post('/api/stripe/check-status');
if (response.data && response.data.stripeAccount) {
creatorProfileStore.creator.isStripeAccountPresent = response.data.isStripeAccountPresent;
creatorProfileStore.creator.isStripeDetailsSubmitted = response.data.isStripeDetailsSubmitted;
creatorProfileStore.creator.isStripePayoutReady = response.data.isStripePayoutReady;
creatorProfileStore.creator.isStripeChargesEnabled = response.data.isStripeChargesEnabled;
toast.success('Your Stripe account is connected and ready for payouts.');
} else {
toast.success('Your Stripe account is connected.');
}
} catch (error) {
toast.error('We couldnt verify your Stripe connection. Please try again.');
}
}
onMounted(async () => {
const { stripe } = route.query;
if (stripe === 'complete') {
await checkStripeAccountStatus();
}
if (stripe === 'refresh') {
toast.warning('You didnt finish connecting your Stripe account. Please try again.');
}
if (stripe) {
await router.replace({ query: { ...route.query, stripe: undefined } });
}
});
async function copyCreatorUrl() {
try {
const url = `${baseURL}/@${creatorProfileStore.creator.slug}`;
await navigator.clipboard.writeText(url);
copySuccess.value = true;
setTimeout(() => {
copySuccess.value = false;
}, 2000);
} catch (err) {
console.error('Failed to copy:', err);
}
}
// ### Fullname
const dialogEditFullnameShown = ref(false);
function openEditFullname() {
dialogEditFullnameShown.value = true;
}
function handleCloseEditFullname() {
dialogEditFullnameShown.value = false;
}
function handleSaveEditFullname(firstname, lastname) {
userProfileStore.changeFullname(firstname, lastname);
dialogEditFullnameShown.value = false;
}
// ### Alias
const dialogEditAliasShown = ref(false);
function openEditAlias() {
dialogEditAliasShown.value = true;
}
function handleCloseEditAlias() {
dialogEditAliasShown.value = false;
}
function handleSaveEditAlias(alias) {
userProfileStore.changeAlias(alias);
dialogEditAliasShown.value = false;
}
const dialogShown = ref(false);
const currentComponent = ref('');
const restoreDialogShown = ref(false);
const deleteDialogShown = ref(false);
const componentsMap = {
EmailDialog: markRaw(EmailDialog),
ChangePasswordDialog: markRaw(ChangePasswordDialog),
SocialsDialog: markRaw(SocialsDialog),
ChangeSlugDialog: markRaw(ChangeSlugDialog),
ChangeNameDialog: markRaw(ChangeNameDialog),
ChangeTitleDialog: markRaw(ChangeTitleDialog),
ChangeStripeIdDialog: markRaw(ChangeStripeIdDialog),
ChangePhoneDialog: markRaw(ChangePhoneDialog),
ChangeEmailDialog: markRaw(ChangeEmailDialog),
};
const stripeButtonBusy = ref(false);
async function connectStripe() {
try {
stripeButtonBusy.value = true;
const res = await client.post('/api/stripe/connect');
window.location.href = res.data.url;
} catch (error) {
toast.error('We couldnt connect your Stripe account. Please try again.');
stripeButtonBusy.value = false;
}
}
async function removeStripe() {
try {
stripeButtonBusy.value = true;
await client.delete('/api/stripe');
creatorProfileStore.creator.isStripeAccountPresent = false;
creatorProfileStore.creator.isStripeDetailsSubmitted = false;
creatorProfileStore.creator.isStripePayoutReady = false;
creatorProfileStore.creator.isStripeChargesEnabled = false;
} catch (error) {
toast.error('We couldnt connect your Stripe account. Please try again.');
} finally {
stripeButtonBusy.value = false;
}
}
const openDialog = component => {
currentComponent.value = componentsMap[component];
dialogShown.value = true;
};
const closeDialog = () => {
currentComponent.value = null;
dialogShown.value = false;
};
function handleRestore() {
creatorProfileStore.restoreCreatorPage();
restoreDialogShown.value = false;
}
function handleDelete() {
creatorProfileStore.removeCreatorPage();
deleteDialogShown.value = false;
}
function downloadQRCode() {
try {
// Get the SVG element more specifically
const qrContainer = document.querySelector('.qr-code');
const canvasElement = qrContainer?.querySelector('canvas');
if (!canvasElement) {
console.error('QR code canvas element not found');
return;
}
const padding = 20;
const newCanvas = document.createElement('canvas');
const ctx = newCanvas.getContext('2d');
// Set canvas size to include padding
newCanvas.width = canvasElement.width + padding * 2;
newCanvas.height = canvasElement.height + padding * 2;
// Fill white background
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, newCanvas.width, newCanvas.height);
// Draw the original QR code canvas with padding
ctx.drawImage(canvasElement, padding, padding);
// Convert to PNG and download
const pngUrl = newCanvas.toDataURL('image/png');
const downloadLink = document.createElement('a');
downloadLink.href = pngUrl;
downloadLink.download = `hutopy-qr-${creatorProfileStore.creator.slug}.png`;
document.body.appendChild(downloadLink);
downloadLink.click();
document.body.removeChild(downloadLink);
} catch (error) {
console.error('Error in downloadQRCode:', error);
}
}
async function deconfigureStripe() {
try {
await client.post(`/api/membership/stripe-account`, {
stripeAccountId: '',
});
await creatorProfileStore.fetchCreatorProfile();
} catch (error) {
console.error('Error deconfiguring stripe:', error);
}
}
</script>
<template>
<div class="min-h-screen w-full">
<div class="m-4 flex flex-col items-center gap-4">
<div class="card">
<div class="card-title">
{{ t('personalInfo') }}
</div>
<div class="content">
<button
class="action"
@click="openEditFullname"
>
<span class="label">{{ t('fullName') }}</span>
<span class="value">{{ userProfileStore.fullname }}</span>
<span class="chevron">
<v-icon :icon="mdiChevronRight" />
</span>
</button>
<button
class="action"
@click="openEditAlias"
>
<span class="label">{{ t('alias') }}</span>
<span class="value">{{ userProfileStore.user.alias }}</span>
<span class="chevron">
<v-icon :icon="mdiChevronRight" />
</span>
</button>
</div>
<div class="content">
<button
class="action"
@click="openDialog('EmailDialog')"
>
<span class="label">{{ t('email') }}</span>
<span class="value">{{ userProfileStore.user.email }}</span>
<span class="chevron">
<v-icon :icon="mdiChevronRight" />
</span>
</button>
</div>
<div class="content">
<button
class="action"
@click="openDialog('ChangePasswordDialog')"
>
<span class="label">{{ t('changePassword') }}</span>
<span class="value"></span>
<span class="chevron">
<v-icon :icon="mdiChevronRight" />
</span>
</button>
</div>
</div>
<template v-if="creatorProfileStore.hasCreator">
<div class="card">
<div class="card-title">
{{ t('creatorInfo') }}
</div>
<div class="content">
<div
class="action"
@click="openDialog('ChangeSlugDialog')"
>
<span class="label">{{ t('handle') }}</span>
<span class="value">{{ baseURL }}/@{{ creatorProfileStore.creator.slug }}</span>
<button
ref="copyButtonRef"
:class="{ success: copySuccess }"
class="copy-button"
@click.stop="copyCreatorUrl"
>
<v-icon :icon="copySuccess ? mdiCheck : mdiContentCopy" />
</button>
<span class="chevron">
<v-icon :icon="mdiChevronRight" />
</span>
</div>
<!-- NAME -->
<button
class="action"
@click="openDialog('ChangeNameDialog')"
>
<span class="label">{{ t('name') }}</span>
<span class="value">{{ creatorProfileStore.creator.name }}</span>
<span class="chevron">
<v-icon :icon="mdiChevronRight" />
</span>
</button>
<!-- TITLE -->
<button
class="action"
@click="openDialog('ChangeTitleDialog')"
>
<span class="label">{{ t('title') }}</span>
<span class="value">{{ creatorProfileStore.creator.title }}</span>
<span class="chevron">
<v-icon :icon="mdiChevronRight" />
</span>
</button>
<!-- PHONE NUMBER -->
<button
class="action"
@click="openDialog('ChangePhoneDialog')"
>
<span class="label">{{ t('phoneNumber') }}</span>
<span
:class="{ 'not-set': !creatorProfileStore.creator.presentation?.phoneNumber }"
class="value"
>
{{ creatorProfileStore.creator.presentation?.phoneNumber || t('notSet') }}
</span>
<span class="chevron">
<v-icon :icon="mdiChevronRight" />
</span>
</button>
<!-- EMAIL -->
<button
class="action"
@click="openDialog('ChangeEmailDialog')"
>
<span class="label">{{ t('email') }}</span>
<span
:class="{ 'not-set': !creatorProfileStore.creator.presentation?.email }"
class="value"
>
{{ creatorProfileStore.creator.presentation?.email || t('notSet') }}
</span>
<span class="chevron">
<v-icon :icon="mdiChevronRight" />
</span>
</button>
</div>
</div>
<div class="card">
<div class="card-title">
{{ t('payment-information') }}
</div>
<div class="content">
<div class="stripe-status">
<span class="label">{{ t('stripeStatus') }}</span>
<span
:class="stripeStatus"
class="value"
>
{{ stripeStatusText }}
</span>
<div class="stripe-actions">
<button
:class="stripeReady ? 'deconfigure-stripe-button' : 'configure-stripe-button'"
:disabled="stripeButtonBusy"
@click="() => (stripeReady ? removeStripe() : connectStripe())"
>
<v-icon
v-if="!stripeButtonBusy"
:icon="stripeReady ? mdiCreditCardOff : mdiCreditCard"
/>
<v-progress-circular
v-else
class="mr-2"
color="text-hTextOnPrimary"
indeterminate
size="20"
width="2"
/>
{{ stripeButtonText }}
</button>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-title">
{{ t('socialNetworks') }}
</div>
<div class="content">
<button
class="action"
@click="openDialog('SocialsDialog')"
>
<span class="label">
<facebook class="social-icon"></facebook>
</span>
<span class="value">{{ creatorProfileStore.creator.socials?.facebookUrl }}</span>
<span class="chevron">
<v-icon :icon="mdiChevronRight" />
</span>
</button>
<button
class="action"
@click="openDialog('SocialsDialog')"
>
<span class="label">
<instagram class="social-icon"></instagram>
</span>
<span class="value">{{ creatorProfileStore.creator.socials?.instagramUrl }}</span>
<span class="chevron">
<v-icon :icon="mdiChevronRight" />
</span>
</button>
<button
class="action"
@click="openDialog('SocialsDialog')"
>
<span class="label">
<x class="social-icon"></x>
</span>
<span class="value">{{ creatorProfileStore.creator.socials?.xUrl }}</span>
<span class="chevron">
<v-icon :icon="mdiChevronRight" />
</span>
</button>
<button
class="action"
@click="openDialog('SocialsDialog')"
>
<span class="label">
<linkedin class="social-icon"></linkedin>
</span>
<span class="value">{{ creatorProfileStore.creator.socials?.linkedInUrl }}</span>
<span class="chevron">
<v-icon :icon="mdiChevronRight" />
</span>
</button>
<button
class="action"
@click="openDialog('SocialsDialog')"
>
<span class="label">
<tiktok class="social-icon"></tiktok>
</span>
<span class="value">{{ creatorProfileStore.creator.socials?.tikTokUrl }}</span>
<span class="chevron">
<v-icon :icon="mdiChevronRight" />
</span>
</button>
<button
class="action"
@click="openDialog('SocialsDialog')"
>
<span class="label">
<youtube class="social-icon"></youtube>
</span>
<span class="value">{{ creatorProfileStore.creator.socials?.youtubeUrl }}</span>
<span class="chevron">
<v-icon :icon="mdiChevronRight" />
</span>
</button>
<button
class="action"
@click="openDialog('SocialsDialog')"
>
<span class="label">
<reddit class="social-icon"></reddit>
</span>
<span class="value">{{ creatorProfileStore.creator.socials?.redditUrl }}</span>
<span class="chevron">
<v-icon :icon="mdiChevronRight" />
</span>
</button>
<button
class="action"
@click="openDialog('SocialsDialog')"
>
<span class="label">
<web class="social-icon"></web>
</span>
<span class="value">{{ creatorProfileStore.creator.socials?.websiteUrl }}</span>
<span class="chevron">
<v-icon :icon="mdiChevronRight" />
</span>
</button>
</div>
</div>
<div class="card">
<div class="card-title">
{{ t('qrCode') }}
</div>
<div class="content">
<div class="qr-container">
<p class="qr-text">{{ t('qrCodeDescription') }}</p>
<div class="qr-code">
<QRCodeVue
v-if="creatorProfileStore.creator"
:image-settings="imageSettings"
:margin="0"
:size="200"
:value="baseURL + '/@' + creatorProfileStore.creator.slug"
foreground="#6B0065"
level="H"
render-as="canvas"
/>
</div>
<button
v-if="creatorProfileStore.creator"
class="download-button"
@click="downloadQRCode"
>
<v-icon :icon="mdiDownload" />
{{ t('downloadQRCode') }}
</button>
</div>
</div>
</div>
<div class="card">
<div class="card-title">
{{ t('dangerZone') }}
</div>
<div class="content">
<span class="p-2">
{{ t('dangerZoneWarning') }}
</span>
<div class="p-2 m-2 w-auto flex justify-center">
<div class="w-1/3">
<button
v-if="!creatorProfileStore.creator.isDeleted"
class="primary danger-action"
@click="deleteDialogShown = true"
>
{{ t('deleteCreatorPage') }}
</button>
<button
v-else
class="primary safe-action"
@click="restoreDialogShown = true"
>
{{ t('restoreCreatorPage') }}
</button>
</div>
</div>
</div>
</div>
</template>
</div>
</div>
<v-dialog
v-model="dialogEditFullnameShown"
persistent
>
<FullnameDialog
:firstname="userProfileStore.user.firstname"
:lastname="userProfileStore.user.lastname"
@close="handleCloseEditFullname"
@save="handleSaveEditFullname"
/>
</v-dialog>
<v-dialog
v-model="dialogEditAliasShown"
persistent
>
<alias-dialog
:alias="userProfileStore.user.alias"
@close="handleCloseEditAlias"
@save="handleSaveEditAlias"
></alias-dialog>
</v-dialog>
<v-dialog
v-model="dialogShown"
persistent
>
<component
:is="currentComponent"
:creator="creatorProfileStore.creator"
:email="userProfileStore.user.email"
@closeRequested="closeDialog"
></component>
</v-dialog>
<v-dialog v-model="restoreDialogShown">
<div class="card dialog">
<div class="card-title">{{ t('restoreCreatorPage') }}</div>
<div class="card-content">
<p>{{ t('restoreWarning') }}</p>
<div class="card-actions">
<button
class="secondary"
@click="restoreDialogShown = false"
>
{{ t('cancel') }}
</button>
<button
class="primary"
@click="handleRestore"
>
{{ t('accept') }}
</button>
</div>
</div>
</div>
</v-dialog>
<v-dialog v-model="deleteDialogShown">
<div class="card dialog">
<div class="card-title">{{ t('deleteCreatorPage') }}</div>
<div class="card-content">
<p>{{ t('deleteWarning') }}</p>
<div class="card-actions">
<button
class="secondary"
@click="deleteDialogShown = false"
>
{{ t('cancel') }}
</button>
<button
class="primary danger-action"
@click="handleDelete"
>
{{ t('accept') }}
</button>
</div>
</div>
</div>
</v-dialog>
</template>
<style scoped>
.card {
@apply rounded-lg p-4 w-full;
}
.card-title {
@apply text-hOnBackground text-lg font-bold;
}
.content {
@apply flex flex-col gap-2;
}
.action {
@apply flex flex-row items-center w-full p-3 rounded-lg;
@apply hover:bg-hSurface;
@apply transition-colors duration-500;
}
.label {
@apply text-hOnBackground w-[200px] text-left;
@apply flex items-center justify-start;
}
.copy-button {
@apply ml-2 p-1 rounded-full;
@apply transition-all duration-300;
@apply relative overflow-hidden;
@apply opacity-60;
}
.copy-button:hover {
@apply opacity-100;
@apply bg-hSurface;
}
.copy-button::after {
content: '';
@apply absolute inset-0 bg-white/20;
@apply scale-0 rounded-full;
@apply transition-transform duration-300;
}
.copy-button:active::after {
@apply scale-150;
@apply opacity-0;
}
.copy-button.success {
@apply bg-green-500/20;
@apply opacity-100;
}
.value {
@apply text-hOnBackground flex-1 text-center;
@apply flex items-center justify-center;
}
.value.not-set {
@apply text-gray-400;
}
.chevron {
@apply text-hOnBackground w-[40px] text-right;
@apply flex items-center justify-end;
}
.social-icon {
@apply fill-current w-6 h-6;
@apply text-hOnBackground;
@apply mr-2;
}
.danger-action {
@apply bg-red-800 hover:bg-red-700 active:bg-red-600;
}
.safe-action {
@apply bg-green-800 hover:bg-green-700 active:bg-green-600;
}
.qr-container {
@apply flex flex-col items-center gap-4 p-4;
}
.qr-code {
@apply bg-white p-4 rounded-2xl;
}
.qr-text {
@apply text-hOnBackground text-center;
}
.download-button {
@apply flex items-center gap-2 px-4 py-2 rounded-lg;
@apply bg-hutopyPrimary text-hOnPrimary;
@apply hover:bg-hutopySecondary;
@apply transition-colors duration-300;
}
.stripe-status {
@apply flex flex-row items-center w-full p-3 rounded-lg;
@apply bg-hSurface;
@apply cursor-default;
@apply transition-colors duration-300;
}
.stripe-status .value {
@apply text-hOnBackground flex-1 text-center;
@apply flex items-center justify-center;
}
.stripe-status .value.fully_configured {
@apply text-green-500;
}
.stripe-status .value.configured {
@apply text-green-500;
}
.stripe-status .value.pending_verification {
@apply text-yellow-500;
}
.stripe-status .value.needs_more_info {
@apply text-orange-500;
}
.stripe-actions {
@apply flex items-center gap-2 ml-4;
}
.configure-stripe-button {
@apply flex items-center justify-center gap-2 px-4 py-2 rounded-lg;
@apply bg-hutopyPrimary text-hOnPrimary;
@apply hover:bg-hutopySecondary;
@apply transition-colors duration-300;
}
.deconfigure-stripe-button {
@apply flex items-center justify-center gap-2 px-4 py-2 rounded-lg;
@apply bg-red-600 text-white;
@apply hover:bg-red-700;
@apply transition-colors duration-300;
}
</style>
<i18n>
{
"en": {
"personalInfo": "Personal Information",
"fullName": "Full Name",
"alias": "Alias",
"email": "Email",
"changePassword": "Update Password",
"creatorInfo": "Creator Information",
"dangerZone": "Danger Zone",
"dangerZoneWarning": "The actions below can have significant impacts on your creator page. Please proceed with caution.",
"deleteWarning": "Are you sure you want to delete your creator page? This action cannot be undone.",
"restoreWarning": "Are you sure you want to restore your creator page? This will make your page visible again.",
"deleteCreatorPage": "Delete Creator Page",
"restoreCreatorPage": "Restore Creator Page",
"stripeAccountId": "Stripe Account ID",
"socialNetworks": "Social Networks",
"handle": "Creator Handle",
"qrCode": "QR Code",
"qrCodeDescription": "Print this QR code to share your Hutopy with the world! Perfect for business cards, social media, and promotional materials.",
"downloadQRCode": "Download QR Code",
"payment-information": "Payment Information",
"stripeStatus": "Stripe Status",
"configured": "Configured",
"notConfigured": "Not Configured",
"needsMoreInfo": "Requires More Information",
"pendingVerification": "Pending Verification",
"continueStripeSetup": "Continue Stripe Setup",
"reviewStripe": "Review Stripe",
"notSet": "Not Set",
"configureStripe": "Connect Stripe",
"phoneNumber": "Phone Number",
"title": "Title",
"removeStripe": "Remove Stripe"
},
"fr": {
"personalInfo": "Informations Personnelles",
"fullName": "Nom Complet",
"alias": "Alias",
"email": "Email",
"changePassword": "Modifier le mot de passe",
"creatorInfo": "Informations du Créateur",
"dangerZone": "Zone de Danger",
"dangerZoneWarning": "Les actions ci-dessous peuvent avoir des impacts significatifs sur votre page de créateur. Veuillez procéder avec précaution.",
"deleteWarning": "Êtes-vous sûr de vouloir supprimer votre page de créateur ? Cette action est irréversible.",
"restoreWarning": "Êtes-vous sûr de vouloir restaurer votre page de créateur ? Cela rendra votre page à nouveau visible.",
"deleteCreatorPage": "Supprimer la Page Créateur",
"restoreCreatorPage": "Restaurer la Page Créateur",
"stripeAccountId": "ID de Compte Stripe",
"socialNetworks": "Réseaux Sociaux",
"handle": "Identifiant du créateur",
"qrCode": "Code QR",
"qrCodeDescription": "Imprimez ce code QR pour partager votre Hutopy avec le monde ! Parfait pour les cartes de visite, les réseaux sociaux et les supports promotionnels.",
"downloadQRCode": "Télécharger le Code QR",
"payment-information": "Informations de Paiement",
"stripeStatus": "État de Stripe",
"configured": "Configuré",
"notConfigured": "Non Configuré",
"needsMoreInfo": "Demande plus d'informations",
"pendingVerification": "Vérification en Cours",
"continueStripeSetup": "Continuer Configuration Stripe",
"reviewStripe": "Reviser Stripe",
"notSet": "Non Défini",
"configureStripe": "Connecter Stripe",
"phoneNumber": "Numéro de téléphone",
"title": "Titre",
"removeStripe": "Retirer Stripe"
}
}
</i18n>