Browse Source

improve error management

Olivier Massot 5 months ago
parent
commit
1ab1e084a9

+ 2 - 1
composables/data/useAp2iRequestService.ts

@@ -9,7 +9,7 @@ import ApiRequestService from '~/services/data/apiRequestService'
  */
  */
 let apiRequestServiceClass: null | ApiRequestService = null
 let apiRequestServiceClass: null | ApiRequestService = null
 
 
-export const useAp2iRequestService = () => {
+export const useAp2iRequestService = (retry: number | false = 1) => {
   const runtimeConfig = useRuntimeConfig()
   const runtimeConfig = useRuntimeConfig()
 
 
   const baseURL = runtimeConfig.ap2iBaseUrl ?? runtimeConfig.public.ap2iBaseUrl
   const baseURL = runtimeConfig.ap2iBaseUrl ?? runtimeConfig.public.ap2iBaseUrl
@@ -48,6 +48,7 @@ export const useAp2iRequestService = () => {
     baseURL,
     baseURL,
     onRequest,
     onRequest,
     onResponse,
     onResponse,
+    retry,
   }
   }
 
 
   // Avoid memory leak
   // Avoid memory leak

+ 69 - 0
composables/utils/useAp2iErrorHandler.ts

@@ -0,0 +1,69 @@
+import { useNuxtApp } from '#app'
+
+/**
+ * Composable for handling API errors, especially for organization-related errors
+ */
+export const useAp2iErrorHandler = () => {
+  const { $i18n } = useNuxtApp()
+
+  /**
+   * Process API error and extract meaningful error message
+   * @param error - The error object from API response
+   * @returns Processed error message
+   */
+  const processApiError = (error: unknown): string => {
+    let errorMessage =
+      "Une erreur s'est produite. Veuillez réessayer plus tard ou nous contacter directement."
+
+    // Try to extract the specific error message from the API response
+    if (
+      error &&
+      typeof error === 'object' &&
+      'data' in error &&
+      error.data &&
+      typeof error.data === 'object'
+    ) {
+      const errorData = error.data as { detail?: string }
+      if (errorData.detail) {
+        // Check if it's the specific error about organization already existing
+        const organizationExistsRegex =
+          /Handling ".*" failed: An organization named '(.+)' already exists in (.+)/
+        const match = errorData.detail.match(organizationExistsRegex)
+
+        if (match) {
+          // Extract the organization name and city name and use the translation
+          const organizationName = match[1]
+          const cityName = match[2]
+          const translationKey =
+            "An organization named '%s' already exists in %s"
+
+          const translatedMessage = $i18n.t(translationKey, [
+            organizationName,
+            cityName,
+          ])
+          errorMessage = translatedMessage as string
+        } else {
+          // For other errors, use translation if available, otherwise use the original message
+          if (!errorData.detail) return errorData.detail
+
+          // Remove the "Handling ... failed:" part if present
+          let cleanedDetail = errorData.detail
+          const handlingFailedRegex = /Handling ".*" failed: (.*)/
+          const handlingMatch = cleanedDetail.match(handlingFailedRegex)
+
+          if (handlingMatch) {
+            cleanedDetail = handlingMatch[1]
+          }
+
+          errorMessage = ($i18n.t(cleanedDetail) || cleanedDetail) as string
+        }
+      }
+    }
+
+    return errorMessage
+  }
+
+  return {
+    processApiError,
+  }
+}

+ 4 - 1
lang/fr.json

@@ -7,5 +7,8 @@
   "TEMPORARY": "Vacataire",
   "TEMPORARY": "Vacataire",
   "INTERIM": "Intermittent",
   "INTERIM": "Intermittent",
   "OTHER": "Autre",
   "OTHER": "Autre",
-  "Invalid phone number": "Numéro de téléphone invalide"
+  "Invalid phone number": "Numéro de téléphone invalide",
+  "Request expired: submission date is more than 15 minutes old": "Requête expirée : la date de soumission date de plus de 15 minutes",
+  "An organization named '%s' already exists in %s": "Une organisation nommée '{0}' existe déjà à {1}",
+  "Invalid request status": "Statut de la requête invalide"
 }
 }

+ 381 - 387
pages/shop/try/artist-premium.vue

@@ -15,385 +15,389 @@
               Essayez gratuitement Opentalent Artist Premium pendant 30 jours !
               Essayez gratuitement Opentalent Artist Premium pendant 30 jours !
             </h1>
             </h1>
 
 
-            <div class="description mb-8">
-              <p>
-                Opentalent Artist Premium est une solution en ligne complète,
-                pensée pour les orchestres, chorales, compagnies de théâtre, de
-                danse ou de cirque. Elle vous aide à gagner du temps dans
-                l'organisation de vos activités, à mieux collaborer avec vos
-                équipes et à renforcer votre visibilité auprès de votre public.
-              </p>
-
-              <p>
-                Pendant 30 jours, profitez de toutes les fonctionnalités
-                d'Opentalent Artist Premium, gratuitement et sans engagement :
-              </p>
-
-              <ul class="benefits-list">
-                <li>
-                  <span class="mr-1">✔️</span> Gestion intuitive des membres et
-                  des événements
-                </li>
-                <li>
-                  <span class="mr-1">✔️</span> Planification avancée des
-                  répétitions, spectacles et tournées
-                </li>
-                <li>
-                  <span class="mr-1">✔️</span> Outils de communication intégrés
-                  (emails, publipostage, etc.)
-                </li>
-                <li>
-                  <span class="mr-1">✔️</span> Site web personnalisable pour
-                  présenter vos projets et votre structure
-                </li>
-                <li>
-                  <span class="mr-1">✔️</span> Accès collaboratif pour vos
-                  équipes, en temps réel
-                </li>
-              </ul>
-
-              <p>
-                Il vous suffit de remplir le formulaire ci-dessous pour activer
-                votre essai gratuit.
-              </p>
-
-              <p>
-                Lancez-vous dès aujourd'hui et découvrez comment Opentalent peut
-                transformer votre organisation artistique !
-              </p>
-            </div>
+            <div v-if="!trialRequestSent">
+              <div class="description mb-8">
+                <p>
+                  Opentalent Artist Premium est une solution en ligne complète,
+                  pensée pour les orchestres, chorales, compagnies de théâtre,
+                  de danse ou de cirque. Elle vous aide à gagner du temps dans
+                  l'organisation de vos activités, à mieux collaborer avec vos
+                  équipes et à renforcer votre visibilité auprès de votre
+                  public.
+                </p>
 
 
-            <v-form
-              v-if="!trialRequestSent"
-              ref="form"
-              validate-on="submit lazy"
-              @submit.prevent="submit"
-            >
-              <v-container>
-                <div v-if="isDevelopment" class="dev-tools-container">
-                  <v-btn
-                    color="info"
-                    size="small"
-                    prepend-icon="fa fa-magic"
-                    @click="fillWithDummyData"
-                  >
-                    Remplir avec des données de test
-                  </v-btn>
-                </div>
-                <i
-                  >Les champs dont le nom est suivi d'un astérisque (*) sont
-                  obligatoires.</i
-                >
-
-                <h2 class="section-title">Coordonnées de la structure</h2>
-
-                <!-- Structure name -->
-                <v-row>
-                  <v-col cols="12">
-                    <v-text-field
-                      v-model="trialRequest.structureName"
-                      :rules="[validateRequired]"
-                      label="Nom de la structure*"
-                      required
-                      @input="onStructureNameUpdated"
-                    />
-                  </v-col>
-                </v-row>
-
-                <!-- Structure address -->
-                <v-row>
-                  <v-col cols="12" md="6">
-                    <v-text-field
-                      v-model="trialRequest.address"
-                      :rules="[validateRequired]"
-                      label="Adresse siège social de la structure*"
-                      required
-                    />
-                  </v-col>
-                  <v-col cols="12" md="6">
-                    <v-text-field
-                      v-model="trialRequest.addressComplement"
-                      label="Adresse (suite)"
-                    />
-                  </v-col>
-                </v-row>
-
-                <v-row>
-                  <v-col cols="12" md="6">
-                    <v-text-field
-                      v-model="trialRequest.postalCode"
-                      :rules="[validateRequired, validatePostalCode]"
-                      label="Code postal*"
-                      required
-                    />
-                  </v-col>
-
-                  <v-col cols="12" md="6">
-                    <v-text-field
-                      v-model="trialRequest.city"
-                      :rules="[validateRequired]"
-                      label="Ville*"
-                      required
-                    />
-                  </v-col>
-                </v-row>
-
-                <!-- Structure email and SIREN -->
-                <v-row>
-                  <v-col cols="12" md="6">
-                    <v-text-field
-                      v-model="trialRequest.structureEmail"
-                      :rules="[validateRequired, validateEmail]"
-                      label="Adresse mail de la structure*"
-                      required
-                      type="email"
-                    />
-                  </v-col>
-                  <v-col cols="12" md="6">
-                    <v-text-field
-                      v-model="trialRequest.siren"
-                      :rules="[validateSiren]"
-                      label="SIREN (optionnel)"
-                      hint="Numéro à 9 chiffres"
-                    />
-                  </v-col>
-                </v-row>
-
-                <!-- Structure type and legal status -->
-                <v-row>
-                  <v-col cols="12" md="6">
-                    <v-select
-                      v-model="trialRequest.structureType"
-                      :rules="[validateRequired]"
-                      label="Type de la structure*"
-                      :items="structureTypes"
-                      item-value="value"
-                      item-title="title"
-                      required
-                    />
-                  </v-col>
-                  <v-col cols="12" md="6">
-                    <v-select
-                      v-model="trialRequest.legalStatus"
-                      :rules="[validateRequired]"
-                      label="Statut juridique*"
-                      :items="legalStatuses"
-                      item-value="value"
-                      item-title="title"
-                      required
-                    />
-                  </v-col>
-                </v-row>
-
-                <h2 class="section-title">Représentée par</h2>
-
-                <!-- Representative function -->
-                <v-row>
-                  <v-col cols="12">
-                    <v-text-field
-                      v-model="trialRequest.representativeFunction"
-                      :rules="[validateRequired]"
-                      label="Fonction*"
-                      required
-                    />
-                  </v-col>
-                </v-row>
-
-                <!-- Representative name -->
-                <v-row>
-                  <v-col cols="12" md="6">
-                    <v-text-field
-                      v-model="trialRequest.representativeFirstName"
-                      :rules="[validateRequired]"
-                      label="Prénom*"
-                      required
-                    />
-                  </v-col>
-
-                  <v-col cols="12" md="6">
-                    <v-text-field
-                      v-model="trialRequest.representativeLastName"
-                      :rules="[validateRequired]"
-                      label="Nom*"
-                      required
-                    />
-                  </v-col>
-                </v-row>
-
-                <!-- Representative contact -->
-                <v-row>
-                  <v-col cols="12" md="6">
-                    <v-text-field
-                      v-model="trialRequest.representativeEmail"
-                      :rules="[validateRequired, validateEmail]"
-                      label="Adresse mail*"
-                      required
-                      type="email"
-                    />
-                  </v-col>
-
-                  <v-col cols="12" md="6">
-                    <CommonPhoneInput
-                      ref="phoneInput"
-                      v-model="trialRequest.representativePhone"
-                      label="Téléphone*"
-                      required
-                    />
-                  </v-col>
-                </v-row>
-
-                <h2 class="section-title mb-6">Informations de connexion</h2>
-
-                <!-- Structure identifier -->
-                <v-row>
-                  <v-col cols="12" md="6" class="mx-auto">
-                    <v-text-field
-                      v-model="trialRequest.structureIdentifier"
-                      :rules="[
-                        validateRequired,
-                        validateSubdomain,
-                        validateSubdomainAvailability,
-                      ]"
-                      label="Identifiant de la structure*"
-                      required
-                      class="text-center"
-                      @input="onStructureIdentifierUpdated"
-                    />
-                    <div class="validationMessage">
-                      <span v-if="validationPending">
-                        <v-progress-circular size="16" indeterminate />
-                        <i class="ml-2">Vérification en cours</i>
-                      </span>
-                      <span
-                        v-else-if="subdomainAvailable === true"
-                        class="text-success"
-                      >
-                        <v-icon>fa fa-check</v-icon>
-                        <i class="ml-2"> Cet identifiant est disponible</i>
-                      </span>
-                      <span
-                        v-else-if="subdomainAvailable === false"
-                        class="text-error"
-                      >
-                        <v-icon>fa fa-x</v-icon>
-                        <i class="ml-2">Cet identifiant n'est pas disponible</i>
-                      </span>
-                    </div>
-                    <div class="mt-2">
-                      <i v-if="trialRequest.structureIdentifier">
-                        Le compte administrateur de la structure sera
-                        <strong>
-                          admin{{ trialRequest.structureIdentifier }}
-                        </strong>
-                      </i>
-                    </div>
-                    <div>
-                      <i>
-                        Veuillez renseigner un mot de passe pour ce compte :
-                      </i>
-                    </div>
-                  </v-col>
-                </v-row>
-
-                <!-- Password field -->
-                <v-row>
-                  <v-col cols="12" md="6" class="mx-auto">
-                    <v-text-field
-                      v-model="trialRequest.password"
-                      :rules="[validateRequired, validatePassword]"
-                      label="Mot de passe*"
-                      required
-                      :type="showPassword ? 'text' : 'password'"
-                      :append-inner-icon="
-                        showPassword ? 'fa fa-eye-slash' : 'fa fa-eye'
-                      "
-                      @click:append-inner="showPassword = !showPassword"
-                    />
-                    <div class="mt-1">
-                      <i>
-                        Le mot de passe doit contenir au moins 8 caractères, une
-                        minuscule, une majuscule, un chiffre et un caractère
-                        spécial.
-                      </i>
-                    </div>
-                  </v-col>
-                </v-row>
-
-                <h2 class="section-title">Accord de termes et conditions</h2>
-
-                <!-- Terms checkboxes -->
-                <v-checkbox
-                  v-model="trialRequest.termsAccepted"
-                  :rules="[validateCheckbox]"
-                  required
-                >
-                  <template #label>
-                    Mon organisme accepte les &nbsp;
-                    <a
-                      href="https://maestro.opentalent.fr/uploads/share/Documents_juridique/CGU.pdf"
-                      target="_blank"
+                <p>
+                  Pendant 30 jours, profitez de toutes les fonctionnalités
+                  d'Opentalent Artist Premium, gratuitement et sans engagement :
+                </p>
+
+                <ul class="benefits-list">
+                  <li>
+                    <span class="mr-1">✔️</span> Gestion intuitive des membres
+                    et des événements
+                  </li>
+                  <li>
+                    <span class="mr-1">✔️</span> Planification avancée des
+                    répétitions, spectacles et tournées
+                  </li>
+                  <li>
+                    <span class="mr-1">✔️</span> Outils de communication
+                    intégrés (emails, publipostage, etc.)
+                  </li>
+                  <li>
+                    <span class="mr-1">✔️</span> Site web personnalisable pour
+                    présenter vos projets et votre structure
+                  </li>
+                  <li>
+                    <span class="mr-1">✔️</span> Accès collaboratif pour vos
+                    équipes, en temps réel
+                  </li>
+                </ul>
+
+                <p>
+                  Il vous suffit de remplir le formulaire ci-dessous pour
+                  activer votre essai gratuit.
+                </p>
+
+                <p>
+                  Lancez-vous dès aujourd'hui et découvrez comment Opentalent
+                  peut transformer votre organisation artistique !
+                </p>
+              </div>
+
+              <v-form
+                ref="form"
+                validate-on="submit lazy"
+                @submit.prevent="submit"
+              >
+                <v-container>
+                  <div v-if="isDevelopment" class="dev-tools-container">
+                    <v-btn
+                      color="info"
+                      size="small"
+                      prepend-icon="fa fa-magic"
+                      @click="fillWithDummyData"
                     >
                     >
-                      conditions générales d'utilisation </a
-                    >.*
-                  </template>
-                </v-checkbox>
-
-                <v-checkbox
-                  v-model="trialRequest.legalRepresentative"
-                  :rules="[validateCheckbox]"
-                  label="J'agis en tant que représentant légal de l'association ou de la structure.*"
-                  required
-                />
-
-                <v-checkbox
-                  v-model="trialRequest.newsletterSubscription"
-                  label="J'accepte de recevoir la lettre d'information culturelle afin de découvrir des idées de sorties adaptées à ma région."
-                />
-
-                <div class="d-flex flex-row justify-center">
-                  <LayoutCaptcha />
-                </div>
+                      Remplir avec des données de test
+                    </v-btn>
+                  </div>
+                  <i
+                    >Les champs dont le nom est suivi d'un astérisque (*) sont
+                    obligatoires.</i
+                  >
 
 
-                <!-- Submit Button -->
-                <div class="d-flex flex-row justify-center my-10">
-                  <v-btn
-                    type="submit"
-                    color="secondary"
-                    size="large"
-                    class="submit-btn"
+                  <h2 class="section-title">Coordonnées de la structure</h2>
+
+                  <!-- Structure name -->
+                  <v-row>
+                    <v-col cols="12">
+                      <v-text-field
+                        v-model="trialRequest.structureName"
+                        :rules="[validateRequired]"
+                        label="Nom de la structure*"
+                        required
+                        @input="onStructureNameUpdated"
+                      />
+                    </v-col>
+                  </v-row>
+
+                  <!-- Structure address -->
+                  <v-row>
+                    <v-col cols="12" md="6">
+                      <v-text-field
+                        v-model="trialRequest.address"
+                        :rules="[validateRequired]"
+                        label="Adresse siège social de la structure*"
+                        required
+                      />
+                    </v-col>
+                    <v-col cols="12" md="6">
+                      <v-text-field
+                        v-model="trialRequest.addressComplement"
+                        label="Adresse (suite)"
+                      />
+                    </v-col>
+                  </v-row>
+
+                  <v-row>
+                    <v-col cols="12" md="6">
+                      <v-text-field
+                        v-model="trialRequest.postalCode"
+                        :rules="[validateRequired, validatePostalCode]"
+                        label="Code postal*"
+                        required
+                      />
+                    </v-col>
+
+                    <v-col cols="12" md="6">
+                      <v-text-field
+                        v-model="trialRequest.city"
+                        :rules="[validateRequired]"
+                        label="Ville*"
+                        required
+                      />
+                    </v-col>
+                  </v-row>
+
+                  <!-- Structure email and SIREN -->
+                  <v-row>
+                    <v-col cols="12" md="6">
+                      <v-text-field
+                        v-model="trialRequest.structureEmail"
+                        :rules="[validateRequired, validateEmail]"
+                        label="Adresse mail de la structure*"
+                        required
+                        type="email"
+                      />
+                    </v-col>
+                    <v-col cols="12" md="6">
+                      <v-text-field
+                        v-model="trialRequest.siren"
+                        :rules="[validateSiren]"
+                        label="SIREN (optionnel)"
+                        hint="Numéro à 9 chiffres"
+                      />
+                    </v-col>
+                  </v-row>
+
+                  <!-- Structure type and legal status -->
+                  <v-row>
+                    <v-col cols="12" md="6">
+                      <v-select
+                        v-model="trialRequest.structureType"
+                        :rules="[validateRequired]"
+                        label="Type de la structure*"
+                        :items="structureTypes"
+                        item-value="value"
+                        item-title="title"
+                        required
+                      />
+                    </v-col>
+                    <v-col cols="12" md="6">
+                      <v-select
+                        v-model="trialRequest.legalStatus"
+                        :rules="[validateRequired]"
+                        label="Statut juridique*"
+                        :items="legalStatuses"
+                        item-value="value"
+                        item-title="title"
+                        required
+                      />
+                    </v-col>
+                  </v-row>
+
+                  <h2 class="section-title">Représentée par</h2>
+
+                  <!-- Representative function -->
+                  <v-row>
+                    <v-col cols="12">
+                      <v-text-field
+                        v-model="trialRequest.representativeFunction"
+                        :rules="[validateRequired]"
+                        label="Fonction*"
+                        required
+                      />
+                    </v-col>
+                  </v-row>
+
+                  <!-- Representative name -->
+                  <v-row>
+                    <v-col cols="12" md="6">
+                      <v-text-field
+                        v-model="trialRequest.representativeFirstName"
+                        :rules="[validateRequired]"
+                        label="Prénom*"
+                        required
+                      />
+                    </v-col>
+
+                    <v-col cols="12" md="6">
+                      <v-text-field
+                        v-model="trialRequest.representativeLastName"
+                        :rules="[validateRequired]"
+                        label="Nom*"
+                        required
+                      />
+                    </v-col>
+                  </v-row>
+
+                  <!-- Representative contact -->
+                  <v-row>
+                    <v-col cols="12" md="6">
+                      <v-text-field
+                        v-model="trialRequest.representativeEmail"
+                        :rules="[validateRequired, validateEmail]"
+                        label="Adresse mail*"
+                        required
+                        type="email"
+                      />
+                    </v-col>
+
+                    <v-col cols="12" md="6">
+                      <CommonPhoneInput
+                        ref="phoneInput"
+                        v-model="trialRequest.representativePhone"
+                        label="Téléphone*"
+                        required
+                      />
+                    </v-col>
+                  </v-row>
+
+                  <h2 class="section-title mb-6">Informations de connexion</h2>
+
+                  <!-- Structure identifier -->
+                  <v-row>
+                    <v-col cols="12" md="6" class="mx-auto">
+                      <v-text-field
+                        v-model="trialRequest.structureIdentifier"
+                        :rules="[
+                          validateRequired,
+                          validateSubdomain,
+                          validateSubdomainAvailability,
+                        ]"
+                        label="Identifiant de la structure*"
+                        required
+                        class="text-center"
+                        @input="onStructureIdentifierUpdated"
+                      />
+                      <div class="validationMessage">
+                        <span v-if="validationPending">
+                          <v-progress-circular size="16" indeterminate />
+                          <i class="ml-2">Vérification en cours</i>
+                        </span>
+                        <span
+                          v-else-if="subdomainAvailable === true"
+                          class="text-success"
+                        >
+                          <v-icon>fa fa-check</v-icon>
+                          <i class="ml-2"> Cet identifiant est disponible</i>
+                        </span>
+                        <span
+                          v-else-if="subdomainAvailable === false"
+                          class="text-error"
+                        >
+                          <v-icon>fa fa-x</v-icon>
+                          <i class="ml-2"
+                            >Cet identifiant n'est pas disponible</i
+                          >
+                        </span>
+                      </div>
+                      <div class="mt-2">
+                        <i v-if="trialRequest.structureIdentifier">
+                          Le compte administrateur de la structure sera
+                          <strong>
+                            admin{{ trialRequest.structureIdentifier }}
+                          </strong>
+                        </i>
+                      </div>
+                      <div>
+                        <i>
+                          Veuillez renseigner un mot de passe pour ce compte :
+                        </i>
+                      </div>
+                    </v-col>
+                  </v-row>
+
+                  <!-- Password field -->
+                  <v-row>
+                    <v-col cols="12" md="6" class="mx-auto">
+                      <v-text-field
+                        v-model="trialRequest.password"
+                        :rules="[validateRequired, validatePassword]"
+                        label="Mot de passe*"
+                        required
+                        :type="showPassword ? 'text' : 'password'"
+                        :append-inner-icon="
+                          showPassword ? 'fa fa-eye-slash' : 'fa fa-eye'
+                        "
+                        @click:append-inner="showPassword = !showPassword"
+                      />
+                      <div class="mt-1">
+                        <i>
+                          Le mot de passe doit contenir au moins 8 caractères,
+                          une minuscule, une majuscule, un chiffre et un
+                          caractère spécial.
+                        </i>
+                      </div>
+                    </v-col>
+                  </v-row>
+
+                  <h2 class="section-title">Accord de termes et conditions</h2>
+
+                  <!-- Terms checkboxes -->
+                  <v-checkbox
+                    v-model="trialRequest.termsAccepted"
+                    :rules="[validateCheckbox]"
+                    required
                   >
                   >
-                    COMMENCER MON ESSAI DE 30 JOURS
-                  </v-btn>
-                </div>
+                    <template #label>
+                      Mon organisme accepte les &nbsp;
+                      <a
+                        href="https://maestro.opentalent.fr/uploads/share/Documents_juridique/CGU.pdf"
+                        target="_blank"
+                      >
+                        conditions générales d'utilisation </a
+                      >.*
+                    </template>
+                  </v-checkbox>
+
+                  <v-checkbox
+                    v-model="trialRequest.legalRepresentative"
+                    :rules="[validateCheckbox]"
+                    label="J'agis en tant que représentant légal de l'association ou de la structure.*"
+                    required
+                  />
 
 
-                <p class="text-center no-credit-card">
-                  Aucune carte de crédit requise. En cliquant sur "Commencer mon
-                  essai de 30 jours", vous acceptez de démarrer votre période
-                  d'essai gratuit.
-                </p>
+                  <v-checkbox
+                    v-model="trialRequest.newsletterSubscription"
+                    label="J'accepte de recevoir la lettre d'information culturelle afin de découvrir des idées de sorties adaptées à ma région."
+                  />
 
 
-                <div v-if="validationError" class="error">
-                  Des champs du formulaire sont invalides ou manquants.
-                </div>
+                  <div class="d-flex flex-row justify-center">
+                    <LayoutCaptcha />
+                  </div>
+
+                  <!-- Submit Button -->
+                  <div class="d-flex flex-row justify-center my-10">
+                    <v-btn
+                      type="submit"
+                      color="secondary"
+                      size="large"
+                      class="submit-btn"
+                    >
+                      COMMENCER MON ESSAI DE 30 JOURS
+                    </v-btn>
+                  </div>
+
+                  <p class="text-center no-credit-card">
+                    Aucune carte de crédit requise. En cliquant sur "Commencer
+                    mon essai de 30 jours", vous acceptez de démarrer votre
+                    période d'essai gratuit.
+                  </p>
 
 
-                <div v-if="errorMsg" class="error">
-                  {{ errorMsg }}
+                  <div v-if="validationError" class="error">
+                    Des champs du formulaire sont invalides ou manquants.
+                  </div>
+
+                  <div v-if="errorMsg" class="error">
+                    {{ errorMsg }}
+                  </div>
+                </v-container>
+
+                <div class="legal">
+                  Les données recueillies par Opentalent sont utilisées pour le
+                  traitement de votre demande et pour vous informer sur nos
+                  offres. Elles sont destinées aux services Opentalent et à ses
+                  sous-traitants pour l'exécution des contrats. Conformément à
+                  la loi "Informatique et Libertés du 6 Janvier 1978", vous
+                  disposez d'un droit d'accès, de modifications, de
+                  rectification et de suppression des données vous concernant.
+                  Pour toute demande, adressez-vous à : OPENTALENT, 265 rue de
+                  la Grange 74950 SCIONZIER - FRANCE, opentalent.fr s'engage à
+                  la confidentialité et à la protection de vos données."
                 </div>
                 </div>
-              </v-container>
-
-              <div class="legal">
-                Les données recueillies par Opentalent sont utilisées pour le
-                traitement de votre demande et pour vous informer sur nos
-                offres. Elles sont destinées aux services Opentalent et à ses
-                sous-traitants pour l'exécution des contrats. Conformément à la
-                loi "Informatique et Libertés du 6 Janvier 1978", vous disposez
-                d'un droit d'accès, de modifications, de rectification et de
-                suppression des données vous concernant. Pour toute demande,
-                adressez-vous à : OPENTALENT, 265 rue de la Grange 74950
-                SCIONZIER - FRANCE, opentalent.fr s'engage à la confidentialité
-                et à la protection de vos données."
-              </div>
-            </v-form>
+              </v-form>
+            </div>
 
 
             <div
             <div
               v-else
               v-else
@@ -442,10 +446,11 @@ import { useRouter } from 'vue-router'
 import type { Ref } from 'vue'
 import type { Ref } from 'vue'
 import { reactive } from 'vue'
 import { reactive } from 'vue'
 import _ from 'lodash'
 import _ from 'lodash'
-import { useRuntimeConfig, useAsyncData, useNuxtApp } from '#app'
+import { useRuntimeConfig, useAsyncData } from '#app'
 import type { TrialRequest } from '~/types/interface'
 import type { TrialRequest } from '~/types/interface'
 import { STRUCTURE_TYPE, LEGAL_STATUS } from '~/types/types'
 import { STRUCTURE_TYPE, LEGAL_STATUS } from '~/types/types'
 import { useAp2iRequestService } from '~/composables/data/useAp2iRequestService'
 import { useAp2iRequestService } from '~/composables/data/useAp2iRequestService'
+import { useAp2iErrorHandler } from '~/composables/utils/useAp2iErrorHandler'
 import {
 import {
   convertPhoneNumberToInternationalFormat,
   convertPhoneNumberToInternationalFormat,
   slugify,
   slugify,
@@ -472,6 +477,7 @@ const legalStatuses = Object.values(LEGAL_STATUS)
 
 
 // Get apiRequestService for subdomain availability check
 // Get apiRequestService for subdomain availability check
 const { ap2iRequestService } = useAp2iRequestService()
 const { ap2iRequestService } = useAp2iRequestService()
+const { processApiError } = useAp2iErrorHandler()
 
 
 // Check if we're in a development environment
 // Check if we're in a development environment
 const config = useRuntimeConfig()
 const config = useRuntimeConfig()
@@ -688,21 +694,9 @@ const submit = async (): Promise<void> => {
     errorMsg.value =
     errorMsg.value =
       "Une erreur s'est produite lors de l'activation de votre essai. Veuillez réessayer plus tard ou nous contacter directement."
       "Une erreur s'est produite lors de l'activation de votre essai. Veuillez réessayer plus tard ou nous contacter directement."
 
 
-    // Try to extract the specific error message from the API response
-    if (
-      e &&
-      typeof e === 'object' &&
-      'data' in e &&
-      e.data &&
-      typeof e.data === 'object'
-    ) {
-      const errorData = e.data as { detail?: string }
-      if (errorData.detail) {
-        const { $i18n } = useNuxtApp()
-        // Use translation if available, otherwise use the original message
-        errorMsg.value += '\n' + ($i18n.t(errorData.detail) || errorData.detail)
-      }
-    }
+    // Process the error message using the common error handler
+    const processedError = processApiError(e)
+    errorMsg.value += '\n' + processedError
   }
   }
 }
 }
 
 

+ 24 - 30
pages/shop/try/validation.vue

@@ -50,20 +50,22 @@
 
 
 <script setup lang="ts">
 <script setup lang="ts">
 import { useRoute } from 'vue-router'
 import { useRoute } from 'vue-router'
-import { useNuxtApp } from '#app'
 import { useAp2iRequestService } from '~/composables/data/useAp2iRequestService'
 import { useAp2iRequestService } from '~/composables/data/useAp2iRequestService'
+import { useAp2iErrorHandler } from '~/composables/utils/useAp2iErrorHandler'
+import UrlUtils from '~/services/utils/urlUtils'
 
 
 const route = useRoute()
 const route = useRoute()
-const { ap2iRequestService } = useAp2iRequestService()
+const { ap2iRequestService } = useAp2iRequestService(false)
+const runtimeConfig = useRuntimeConfig()
+const { processApiError } = useAp2iErrorHandler()
 
 
 const loading = ref(true)
 const loading = ref(true)
-const validationSuccess = ref(false)
-const errorMsg = ref(
-  "Une erreur s'est produite lors de la validation de votre demande."
-)
+const validationSuccess: Ref<boolean | null> = ref(null)
+const errorMsg: Ref<string | null> = ref(null)
 
 
 // UUID validation regex
 // UUID validation regex
-const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
+const uuidRegex =
+  /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
 
 
 // Get token from query parameters
 // Get token from query parameters
 const token = computed(() => {
 const token = computed(() => {
@@ -71,50 +73,42 @@ const token = computed(() => {
   return typeof queryToken === 'string' ? queryToken : ''
   return typeof queryToken === 'string' ? queryToken : ''
 })
 })
 
 
-// Validate token and make API request
-onMounted(async () => {
+const validate = async () => {
   try {
   try {
     // Check if token is present
     // Check if token is present
     if (!token.value) {
     if (!token.value) {
       errorMsg.value = "Aucun jeton de validation n'a été fourni."
       errorMsg.value = "Aucun jeton de validation n'a été fourni."
-      loading.value = false
       return
       return
     }
     }
 
 
     // Validate token format (UUID)
     // Validate token format (UUID)
     if (!uuidRegex.test(token.value)) {
     if (!uuidRegex.test(token.value)) {
       errorMsg.value = 'Le format du jeton de validation est invalide.'
       errorMsg.value = 'Le format du jeton de validation est invalide.'
-      loading.value = false
       return
       return
     }
     }
 
 
     // Make API request to validate token
     // Make API request to validate token
-    await ap2iRequestService.get('/api/public/shop/validate/' + token.value)
+    await ap2iRequestService.get(
+      UrlUtils.join(
+        runtimeConfig.public.ap2iBaseUrl,
+        '/api/public/shop/validate/',
+        token.value
+      )
+    )
 
 
     // If we get here, the validation was successful
     // If we get here, the validation was successful
     validationSuccess.value = true
     validationSuccess.value = true
-    loading.value = false
   } catch (error) {
   } catch (error) {
     console.error('Error validating token:', error)
     console.error('Error validating token:', error)
+    validationSuccess.value = false
 
 
-    // Try to extract the specific error message from the API response
-    if (
-      error &&
-      typeof error === 'object' &&
-      'data' in error &&
-      error.data &&
-      typeof error.data === 'object'
-    ) {
-      const errorData = error.data as { detail?: string }
-      if (errorData.detail) {
-        const { $i18n } = useNuxtApp()
-        // Use translation if available, otherwise use the original message
-        errorMsg.value = $i18n.t(errorData.detail) || errorData.detail
-      }
-    }
-
-    loading.value = false
+    errorMsg.value = processApiError(error)
   }
   }
+}
+
+onMounted(async () => {
+  await validate()
+  loading.value = false
 })
 })
 </script>
 </script>