src/Controller/AppleWalletController.php line 1073

Open in your IDE?
  1. <?php
  2. namespace App\Controller;
  3. use App\Entity\Card;
  4. use App\Entity\User;
  5. use App\Repository\CardRepository;
  6. use App\Repository\CardTransactionRepository;
  7. use App\Repository\UserRepository;
  8. use DateTimeInterface;
  9. use Doctrine\ORM\EntityManagerInterface;
  10. use Firebase\JWT\JWT;
  11. use PKPass\PKPassException;
  12. use Psr\Log\LoggerInterface;
  13. use Symfony\Bridge\Monolog\Logger;
  14. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  15. use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
  16. use Symfony\Component\Filesystem\Filesystem;
  17. use Symfony\Component\HttpFoundation\JsonResponse;
  18. use Symfony\Component\HttpFoundation\Request;
  19. use Symfony\Component\HttpFoundation\Response;
  20. use Symfony\Component\HttpFoundation\ResponseHeaderBag;
  21. use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException;
  22. use Symfony\Component\Routing\Annotation\Route;
  23. use PKPass\PKPass;
  24. use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
  25. use Symfony\Component\Security\Core\Exception\AccessDeniedException;
  26. use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
  27. use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
  28. use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
  29. use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
  30. use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
  31. use Symfony\Contracts\HttpClient\HttpClientInterface;
  32. class AppleWalletController extends AbstractController
  33. {
  34.     public function __construct(
  35.         private readonly UserRepository $userRepository,
  36.         private readonly EntityManagerInterface $entityManager,
  37.         private readonly CardRepository $cardRepository,
  38.         private readonly CardTransactionRepository $cardTransactionRepository,
  39.         private readonly LoggerInterface $logger,
  40.         private readonly HttpClientInterface $httpClient,
  41.         private readonly Filesystem $fileSystem,
  42.         private readonly CsrfTokenManagerInterface $csrfTokenManager
  43.     )
  44.     {
  45.     }
  46.     /**
  47.      * @throws PKPassException
  48.      * @throws \Exception
  49.      */
  50.     #[Route('/add-to-apple-wallet/{userSlug}'name'add_to_apple_wallet')]
  51.     public function addToAppleWallet(
  52.         string $userSlug,
  53.         Request $request
  54.     )
  55.     {
  56.         try {
  57.             if (!$this->getUser() || !$this->isGranted('ROLE_USER')) {
  58.                 throw new AccessDeniedException('Accès refusé. Vous devez être connecté.');
  59.             }
  60.             // Vérifier le jeton CSRF
  61.             $submittedToken $request->query->get('_token');
  62.             if (!$this->isCsrfTokenValid('add-to-apple-wallet'$submittedToken)) {
  63.                 throw new AccessDeniedException('Invalid CSRF token.');
  64.             }
  65.             $certificatePath $this->getParameter('pass_certificate_path');
  66.             $certificatePassword $this->getParameter('pass_certificate_password');
  67.             $pass = new PKPass($certificatePath$certificatePassword);
  68.             $imagesDir $this->getParameter('kernel.project_dir') . '/public/images/apple-wallet/';
  69.             $requiredImages = ['icon.png''icon@2x.png''icon@3x.png''logo.png''logo@2x.png''logo@3x.png''strip.png''strip@2x.png''strip@3x.png'];
  70.             foreach ($requiredImages as $image) {
  71.                 $imagePath $imagesDir $image;
  72.                 if (file_exists($imagePath)) {
  73.                     $pass->addFile($imagePath);
  74.                 } else {
  75.                     throw new \Exception("Required image file not found: $image");
  76.                 }
  77.             }
  78.             // Vérifiez que le chemin pointe vers un fichier
  79.             if (!is_file($imagePath)) {
  80.                 throw new \Exception("Le fichier $imagePath n'existe pas ou n'est pas un fichier.");
  81.             }
  82.             // Ajoutez l'image au package Pass
  83.             $user $this->userRepository->findOneBy(['slug' => $userSlug]);
  84.             if (!$user) {
  85.                 return new JsonResponse(['error' => 'User not found'], Response::HTTP_NOT_FOUND);
  86.             }
  87.             $card $user->getCard();
  88.             if (!$card) {
  89.                 return new JsonResponse(['error' => 'Card not found'], Response::HTTP_NOT_FOUND);
  90.             }
  91.             $slug $user->getSlug();
  92.             $dernierPassage $this->cardTransactionRepository->getDernierPassage($user)?->getTransactionDate();
  93.             if (!$card->getAppleWalletToken()) {
  94.                 $card->generateAppleWalletToken();
  95.                 $this->entityManager->persist($card);
  96.                 $this->entityManager->flush();
  97.             }
  98.             $data = [
  99.                 'formatVersion' => 1,
  100.                 'passTypeIdentifier' => 'pass.com.immybeauty.pkpass',
  101.                 'teamIdentifier' => 'V9N5857WGW',
  102.                 'serialNumber' => $slug,
  103.                 'webServiceURL' => 'https://immybeauty.fr/api/v1/passes',
  104.                 'authenticationToken' => $card->getAppleWalletToken(),
  105.                 'pushToken' => ''// Ce champ sera rempli par Apple lors de l'enregistrement du pass
  106.                 'allowsUpdates' => true,
  107.                 'sharingProhibited' => false,
  108.                 "barcodes" => [
  109.                     [
  110.                         "message" => $this->generateQRCodeURLProfil($user),
  111.                         "format" => "PKBarcodeFormatQR",
  112.                         "messageEncoding" => "iso-8859-1"
  113.                     ]
  114.                 ],
  115.                 "organizationName" => "IMMY BEAUTY",
  116.                 "description" => "IMMY BEAUTY",
  117.                 "foregroundColor" => "rgb(255, 255, 255)"// BLANC
  118.                 "backgroundColor" => "rgb(20, 20, 20)"// NOIR
  119.                 "labelColor" => "rgb(174, 136, 90)"// DORÉ
  120.                 "relevantDate" => (new \DateTime())->format('c'), // Ajoutez cette ligne
  121.                 "storeCard" => [
  122. //                    'primaryFields' => [
  123. //                        [
  124. //                            'key' => 'notification',
  125. //                            'label' => 'Notification',
  126. //                            'value' => 'Votre pass est à jour',
  127. //                            'changeMessage' => 'Nouvelle notification : %@'
  128. //                        ]
  129. //                    ],
  130.                     "headerFields" => [
  131.                         [
  132.                             "key" => "header-name",
  133.                             "textAlignment" => "PKTextAlignmentRight",
  134.                             "label" => "CLIENT",
  135.                             "value" => $user->getFullName()
  136.                         ]
  137.                     ],
  138.                     "auxiliaryFields" => [
  139.                         [
  140.                             "key" => "dernier-passage",
  141.                             "label" => "DERNIER PASSAGE",
  142.                             "value" => $dernierPassage $dernierPassage->format('d/m/Y') : '-',
  143.                             "textAlignment" => "PKTextAlignmentLeft"
  144.                         ],
  145.                         [
  146.                             "key" => "points",
  147.                             "label" => "POINTS",
  148.                             "value" => $card->getPoints(),
  149.                             "textAlignment" => "PKTextAlignmentRight"
  150.                         ]
  151.                     ],
  152.                     "backFields" => [
  153.                         [
  154.                             "key" => "back-name",
  155.                             "label" => "💇🏻‍♀️ SALON",
  156.                             "value" => "IMMY BEAUTY"
  157.                         ],
  158.                         [
  159.                             "key" => "back-horaires",
  160.                             "label" => "🕒 HORAIRES",
  161.                             "value" => "Mar - Dim\n10h00 - 19h30"
  162.                         ],
  163.                         [
  164.                             "key" => "back-tel",
  165.                             "label" => "📞 NUMERO TÉL",
  166.                             "value" => "0745264451",
  167.                             "dataDetectorTypes" => ["PKDataDetectorTypePhoneNumber"]
  168.                         ],
  169.                         [
  170.                             "key" => "back-planity",
  171.                             "label" => "📅 PRISE DE RDV",
  172.                             "value" => "https://www.planity.com/immy-beauty-93410-vaujours",
  173.                             "dataDetectorTypes" => ["PKDataDetectorTypeLink"]
  174.                         ],
  175.                         [
  176.                             "key" => "back-instagram",
  177.                             "label" => "📷 INSTAGRAM",
  178.                             "value" => "@immybeauty.fr",
  179.                             "link" => "https://www.instagram.com/immybeauty.fr",
  180.                             "dataDetectorTypes" => ["PKDataDetectorTypeLink"]
  181.                         ],
  182.                         [
  183.                             "key" => "back-tiktok",
  184.                             "label" => "🎵 TIKTOK",
  185.                             "value" => "@immybeauty.fr",
  186.                             "link" => "https://www.tiktok.com/@immybeauty.fr",
  187.                             "dataDetectorTypes" => ["PKDataDetectorTypeLink"]
  188.                         ],
  189.                         [
  190.                             "key" => "back-email",
  191.                             "label" => "✉️ EMAIL",
  192.                             "value" => "contact@immybeauty.fr",
  193.                             "link" => "mailto:contact@immybeauty.fr",
  194.                             "dataDetectorTypes" => ["PKDataDetectorTypeLink"]
  195.                         ],
  196.                         [
  197.                             "key" => "back-website",
  198.                             "label" => "🌐 SITE INTERNET",
  199.                             "value" => "www.immybeauty.fr",
  200.                             "link" => "https://immybeauty.fr",
  201.                             "dataDetectorTypes" => ["PKDataDetectorTypeLink"]
  202.                         ],
  203. //                        [
  204. //                            "key" => "hidden_update_trigger",
  205. //                            "label" => "Dernière mise à jour",
  206. //                            "value" => (new \DateTime())->format('Y-m-d H:i:s'),
  207. //                            'changeMessage' => '%@'
  208. //                        ],
  209.                         [
  210.                             "key" => "notification_center",
  211.                             "label" => "📬 Centre de messages",
  212.                             "value" => "",
  213.                             'changeMessage' => '%@',
  214.                             'hidden' => true // Ajoutez cette ligne pour cacher le champ
  215.                         ],
  216.                     ],
  217.                 ]
  218.             ];
  219.             $pass->setData($data);
  220.             $passContent $pass->create();
  221.             $response = new Response($passContent);
  222.             $response->headers->set('Content-Type''application/vnd.apple.pkpass');
  223.             $response->headers->set('Content-Disposition'$response->headers->makeDisposition(
  224.                 ResponseHeaderBag::DISPOSITION_ATTACHMENT,
  225.                 'pass.pkpass'
  226.             ));
  227.             return $response;
  228.         } catch (AccessDeniedException $e) {
  229.             return new JsonResponse(['error' => $e->getMessage()], Response::HTTP_FORBIDDEN);
  230.         } catch (\Exception $e) {
  231.             return new JsonResponse([
  232.                 'error' => 'Une erreur est survenue lors de la création du pass.',
  233.                 'message' => $e->getMessage()
  234.             ], Response::HTTP_INTERNAL_SERVER_ERROR);
  235.         }
  236.     }
  237.     #[Route('/api/v1/passes/v1/devices/{deviceLibraryIdentifier}/registrations/{passTypeIdentifier}/{serialNumber}'name'register_device'methods: ['POST'])]
  238.     public function registerDevice(
  239.         string $deviceLibraryIdentifier,
  240.         string $passTypeIdentifier,
  241.         string $serialNumber,
  242.         Request $request,
  243.         EntityManagerInterface $entityManager
  244.     ): JsonResponse
  245.     {
  246.         try {
  247.             $content json_decode($request->getContent(), true);
  248.             $pushToken $content['pushToken'] ?? null;
  249.             if (!$pushToken) {
  250.                 return new JsonResponse(['error' => 'Push token is required'], Response::HTTP_BAD_REQUEST);
  251.             }
  252.             $user $this->userRepository->findOneBy(['slug' => $serialNumber]);
  253.             if (!$user) {
  254.                 return new JsonResponse(['error' => 'User not found'], Response::HTTP_NOT_FOUND);
  255.             }
  256.             $card $user->getCard();
  257.             if (!$card) {
  258.                 return new JsonResponse(['error' => 'Card not found'], Response::HTTP_NOT_FOUND);
  259.             }
  260.             $card->setAppleWalletPushToken($pushToken);
  261.             $card->setDeviceLibraryIdentifier($deviceLibraryIdentifier);
  262.             $entityManager->persist($card);
  263.             $entityManager->flush();
  264.             return new JsonResponse(['status' => 'Push token registered successfully'], Response::HTTP_OK);
  265.         } catch (\Exception $e) {
  266.             return new JsonResponse(['error' => $e->getMessage()], Response::HTTP_INTERNAL_SERVER_ERROR);
  267.         }
  268.     }
  269.     #[Route('/test-notification/{userSlug}/{notificationMessage}'name'test_notification'methods: ['GET'])]
  270.     public function testNotification(string $userSlugstring $notificationMessage): JsonResponse
  271.     {
  272.         $this->writeLog('Début de testNotification pour userSlug: ' $userSlug);
  273.         $this->writeLog('Message de notification reçu: ' $notificationMessage);
  274.         // Décoder le message de notification (il peut contenir des caractères spéciaux encodés dans l'URL)
  275.         $decodedMessage urldecode($notificationMessage);
  276.         $request = new Request([], ['notificationMessage' => $decodedMessage]);
  277.         $this->writeLog('Appel de updatePass');
  278.         $result $this->updatePass($userSlug$request$this->entityManager);
  279.         $resultData json_decode($result->getContent(), true);
  280.         $this->writeLog('Nouvelle date de mise à jour : ' . ($resultData['lastUpdated'] ?? 'non disponible'));
  281.         $this->writeLog('Fin de testNotification. Résultat: ' json_encode($result->getContent()));
  282.         return $result;
  283.     }
  284.     public function updatePass(string $userSlugRequest $requestEntityManagerInterface $entityManager): JsonResponse
  285.     {
  286.         $this->writeLog('Début de updatePass pour userSlug: ' $userSlug);
  287.         try {
  288.             $user $this->userRepository->findOneBy(['slug' => $userSlug]);
  289.             if (!$user) {
  290.                 $this->writeLog('Utilisateur non trouvé pour le slug: ' $userSlug);
  291.                 return new JsonResponse(['error' => 'User not found'], Response::HTTP_NOT_FOUND);
  292.             }
  293.             $card $user->getCard();
  294.             if (!$card) {
  295.                 $this->writeLog('Carte non trouvée pour l\'utilisateur: ' $userSlug);
  296.                 return new JsonResponse(['error' => 'Card not found'], Response::HTTP_NOT_FOUND);
  297.             }
  298.             $this->writeLog('Création du pass avec le certificat');
  299.             $certificatePath $this->getParameter('pass_certificate_path');
  300.             $certificatePassword $this->getParameter('pass_certificate_password');
  301.             $pass = new PKPass($certificatePath$certificatePassword);
  302.             $this->writeLog('Génération des données du pass');
  303. //            $notificationMessage = $this->generateRandomMessage();
  304.             $notificationMessage $request->request->get('notificationMessage');
  305.             $oldNotification $card->getLastNotification();
  306.             $card->setLastNotification($notificationMessage);
  307.             $this->entityManager->persist($card);
  308.             $this->entityManager->flush();
  309.             $oldData $this->generatePassData($user$card$oldNotification);
  310.             $newData $this->generatePassData($user$card$notificationMessage);
  311.             $this->writeLog('Comparaison des anciennes et nouvelles données:');
  312.             $this->logDataDifferences($oldData$newData);
  313.             $oldNotification $oldData['storeCard']['backFields'][8]['value'] ?? '';
  314.             $newNotification $newData['storeCard']['backFields'][8]['value'] ?? '';
  315.             $this->writeLog("Ancienne notification : " $oldNotification);
  316.             $this->writeLog("Nouvelle notification : " $newNotification);
  317.             if ($oldNotification !== $newNotification) {
  318.                 $this->writeLog("Le message de notification a changé, mise à jour nécessaire");
  319.             } else {
  320.                 $this->writeLog("ATTENTION: Le message de notification n'a pas changé");
  321.             }
  322.             $this->writeLog('Définition des données du pass');
  323.             $pass->setData($newData);
  324.             $this->writeLog('Ajout des fichiers au pass');
  325.             $this->addFilesToPass($pass);
  326.             $this->writeLog('Création du contenu du pass');
  327.             $newPassContent $pass->create();
  328.             $this->writeLog('Comparaison du contenu du pass:');
  329.             $oldPassContent $this->getExistingPassContent($user);
  330.             if ($oldPassContent === $newPassContent) {
  331.                 $this->writeLog('ATTENTION: Le contenu du pass n\'a pas changé!');
  332.             } else {
  333.                 $this->writeLog('Le contenu du pass a été modifié');
  334.             }
  335.             $this->writeLog('Sauvegarde du contenu du pass');
  336.             $this->savePassContent($user$newPassContent);
  337.             // Mise à jour de la date de modification de la carte
  338.             $now = new \DateTime();
  339.             $card->setUpdatedAt($now);
  340.             $entityManager->persist($card);
  341.             $entityManager->flush();
  342.             $pushToken $card->getAppleWalletPushToken();
  343.             if ($pushToken) {
  344.                 $this->writeLog('Envoi de la notification push silencieuse');
  345.                 $result $this->sendSilentPushNotification($pushToken$newData['passTypeIdentifier']);
  346.                 $this->writeLog('Résultat de l\'envoi de la notification: ' json_encode($result));
  347.             } else {
  348.                 $this->writeLog('ATTENTION: Aucun pushToken trouvé pour l\'envoi de la notification');
  349.             }
  350.             $this->writeLog('Fin de updatePass avec succès');
  351.             return new JsonResponse([
  352.                 'status' => 'Pass updated and notification sent',
  353.                 'lastUpdated' => $now->format(DateTimeInterface::ISO8601)
  354.             ], Response::HTTP_OK);
  355.         } catch (\Exception $e) {
  356.             $this->writeLog('Erreur dans updatePass: ' $e->getMessage());
  357.             $this->writeLog('Trace complète: ' $e->getTraceAsString());
  358.             return new JsonResponse(['error' => $e->getMessage()], Response::HTTP_INTERNAL_SERVER_ERROR);
  359.         }
  360.     }
  361.     private function logDataDifferences(array $oldData, array $newData): void
  362.     {
  363.         $differences $this->arrayRecursiveDiff($oldData$newData);
  364.         $this->writeLog('Différences dans les données: ' json_encode($differencesJSON_PRETTY_PRINT));
  365.     }
  366.     private function arrayRecursiveDiff($arr1$arr2) {
  367.         $result = array();
  368.         foreach ($arr1 as $key => $value) {
  369.             if (array_key_exists($key$arr2)) {
  370.                 if (is_array($value)) {
  371.                     $recursiveDiff $this->arrayRecursiveDiff($value$arr2[$key]);
  372.                     if (count($recursiveDiff)) {
  373.                         $result[$key] = $recursiveDiff;
  374.                     }
  375.                 } else {
  376.                     if ($value != $arr2[$key]) {
  377.                         $result[$key] = array('old' => $value'new' => $arr2[$key]);
  378.                     }
  379.                 }
  380.             } else {
  381.                 $result[$key] = array('old' => $value'new' => 'N/A');
  382.             }
  383.         }
  384.         return $result;
  385.     }
  386.     private function generatePassData(User $userCard $cardstring $notificationMessage): array
  387.     {
  388.         $dernierPassage $this->cardTransactionRepository->getDernierPassage($user)?->getTransactionDate();
  389.         $lastTransaction $card->getLastTransactionDate();
  390.         $lastTransactionFormatted $lastTransaction $lastTransaction->format('d/m/Y') : 'Aucun passage';
  391.         $styleUpdateMessage "Style mis à jour le " . (new \DateTime())->format('d/m/Y H:i');
  392. //        $newColors = [
  393. //            $this->getRandomColor(),
  394. //            $this->getRandomColor(),
  395. //            $this->getRandomColor()
  396. //        ];
  397. //
  398. //        $colorUpdateMessage = "Couleurs mises à jour (" . implode(', ', $newColors) . ")";
  399.         return [
  400.             'formatVersion' => 1,
  401.             'passTypeIdentifier' => 'pass.com.immybeauty.pkpass',
  402.             'teamIdentifier' => 'V9N5857WGW',
  403.             'serialNumber' => $user->getSlug(),
  404.             'webServiceURL' => 'https://immybeauty.fr/api/v1/passes',
  405.             'authenticationToken' => $card->getAppleWalletToken(),
  406.             'pushToken' => ''// Ce champ sera rempli par Apple lors de l'enregistrement du pass
  407.             'allowsUpdates' => true,
  408.             'sharingProhibited' => false,
  409.             "barcodes" => [
  410.                 [
  411.                     "message" => $this->generateQRCodeURLProfil($user),
  412.                     "format" => "PKBarcodeFormatQR",
  413.                     "messageEncoding" => "iso-8859-1"
  414.                 ]
  415.             ],
  416.             "organizationName" => "IMMY BEAUTY",
  417.             "foregroundColor" => "rgb(255, 255, 255)"// BLANC
  418.             "backgroundColor" => "rgb(20, 20, 20)"// NOIR
  419.             "labelColor" => "rgb(174, 136, 90)"// DORÉ
  420. //            "foregroundColor" => $newColors[0],
  421. //            "backgroundColor" => $newColors[1],
  422. //            "labelColor" => $newColors[2],
  423.             "description" => "IMMY BEAUTY",
  424.             "storeCard" => [
  425. //                'primaryFields' => [
  426. //                    [
  427. //                        'key' => 'notification',
  428. //                        'label' => 'Notification',
  429. //                        'value' => $notificationMessage,
  430. //                        'changeMessage' => 'Nouvelle notification : %@'
  431. //                    ]
  432. //                ],
  433. //                [
  434. //                    'key' => 'style_update',
  435. //                    'label' => 'Mise à jour du style',
  436. //                    'value' => $styleUpdateMessage,
  437. //                    'changeMessage' => 'Nouveau style appliqué : %@'
  438. //                ],
  439.                 'headerFields' => [
  440.                     [
  441.                         'key' => 'header-name',
  442.                         'textAlignment' => 'PKTextAlignmentRight',
  443.                         'label' => 'CLIENT',
  444.                         'value' => $user->getFullName(),
  445.                         'changeMessage' => 'Nom du client mis à jour : %@'
  446.                     ]
  447.                 ],
  448.                 'auxiliaryFields' => [
  449. //                    [
  450. //                        'key' => 'color_update',
  451. //                        'label' => 'MISE À JOUR',
  452. //                        'value' => $colorUpdateMessage,
  453. //                        'changeMessage' => 'Nouvelles couleurs appliquées : %@'
  454. //                    ],
  455.                     [
  456.                         'key' => 'dernier-passage',
  457.                         'label' => 'DERNIER PASSAGE',
  458.                         'value' => $dernierPassage $dernierPassage->format('d/m/Y') : '-',
  459.                         'textAlignment' => 'PKTextAlignmentLeft',
  460.                         'changeMessage' => 'Nouveau dernier passage : %@'
  461.                     ],
  462.                     [
  463.                         'key' => 'points',
  464.                         'label' => 'POINTS',
  465.                         'value' => $card->getPoints(),
  466.                         'textAlignment' => 'PKTextAlignmentRight',
  467.                         'changeMessage' => 'Nouveau solde de points : %@'
  468.                     ]
  469.                 ],
  470.                 "backFields" => [
  471.                     [
  472.                         "key" => "back-name",
  473.                         "label" => "SALON",
  474.                         "value" => "IMMY BEAUTY"
  475.                     ],
  476.                     [
  477.                         "key" => "back-horaires",
  478.                         "label" => "🕒 HORAIRES",
  479.                         "value" => "Mar - Dim\n10h00 - 19h30"
  480.                     ],
  481.                     [
  482.                         "key" => "back-tel",
  483.                         "label" => "📞 NUMERO TÉL",
  484.                         "value" => "0745264451",
  485.                         "dataDetectorTypes" => ["PKDataDetectorTypePhoneNumber"]
  486.                     ],
  487.                     [
  488.                         "key" => "back-planity",
  489.                         "label" => "📅 PRISE DE RDV",
  490.                         "value" => "https://www.planity.com/immy-beauty-93410-vaujours",
  491.                         "dataDetectorTypes" => ["PKDataDetectorTypeLink"]
  492.                     ],
  493.                     [
  494.                         "key" => "back-instagram",
  495.                         "label" => "📷 INSTAGRAM",
  496.                         "value" => "@immybeauty.fr",
  497.                         "link" => "https://www.instagram.com/immybeauty.fr",
  498.                         "dataDetectorTypes" => ["PKDataDetectorTypeLink"]
  499.                     ],
  500.                     [
  501.                         "key" => "back-tiktok",
  502.                         "label" => "🎵 TIKTOK",
  503.                         "value" => "@immybeauty.fr",
  504.                         "link" => "https://www.tiktok.com/@immybeauty.fr",
  505.                         "dataDetectorTypes" => ["PKDataDetectorTypeLink"]
  506.                     ],
  507.                     [
  508.                         "key" => "back-email",
  509.                         "label" => "✉️ EMAIL",
  510.                         "value" => "contact@immybeauty.fr",
  511.                         "link" => "mailto:contact@immybeauty.fr",
  512.                         "dataDetectorTypes" => ["PKDataDetectorTypeLink"]
  513.                     ],
  514.                     [
  515.                         "key" => "back-website",
  516.                         "label" => "🌐 SITE INTERNET",
  517.                         "value" => "www.immybeauty.fr",
  518.                         "link" => "https://immybeauty.fr",
  519.                         "dataDetectorTypes" => ["PKDataDetectorTypeLink"]
  520.                     ],
  521. //                    [
  522. //                        "key" => "hidden_update_trigger",
  523. //                        "label" => "Dernière mise à jour",
  524. //                        "value" => (new \DateTime())->format('Y-m-d H:i:s'),
  525. //                        'changeMessage' => '%@'
  526. //                    ],
  527.                     [
  528.                         "key" => "notification_center",
  529.                         "label" => "📬 Centre de messages",
  530.                         "value" => $notificationMessage,
  531. //                        "value" => "PUTIN DE MERDE " . (new \DateTime())->format('Y-m-d H:i:s'),
  532.                         "changeMessage" => "%@",
  533.                         'hidden' => true // Ajoutez cette ligne pour cacher le champ
  534.                     ],
  535.                 ],
  536.             ]
  537.         ];
  538.     }
  539.     private function generateRandomMessage(): string
  540.     {
  541.         $messages = [
  542.             "QUEL BAIL VOUS ?",
  543.             "QUOI DE NEUF ?",
  544.             "ÇA ROULE ?",
  545.             "COMMENT ÇA VA ?",
  546.             "QUOI DE BON ?",
  547.             "QUELLES NOUVELLES ?",
  548.             "ON DIT QUOI ?",
  549.             "ALORS, CONTENT(E) ?",
  550.             "TOUT BAIGNE ?",
  551.             "LA FORME ?"
  552.         ];
  553.         return $messages[array_rand($messages)];
  554.     }
  555.     private function sendSilentPushNotification(string $pushTokenstring $passTypeIdentifier): array
  556.     {
  557.         $url "https://api.push.apple.com/3/device/$pushToken";
  558.         $this->writeLog("Début de sendSilentPushNotification");
  559.         $this->writeLog("PushToken: $pushToken");
  560.         $this->writeLog("PassTypeIdentifier: $passTypeIdentifier");
  561.         try {
  562.             $jwt $this->getJWT();
  563.             $this->writeLog("JWT généré avec succès. Longueur du JWT: " strlen($jwt));
  564.             // Payload vide pour une notification silencieuse
  565.             $payload = [
  566.                 'aps' => [
  567.                     'content-available' => 1
  568.                 ]
  569.             ];
  570.             $this->writeLog("Payload de la notification: " json_encode($payload));
  571.             $headers = [
  572.                 'apns-topic' => $passTypeIdentifier,
  573.                 'apns-push-type' => 'background',
  574.                 'authorization' => 'bearer ' $jwt,
  575.             ];
  576.             $this->writeLog("Headers de la requête: " json_encode($headers));
  577.             $response $this->httpClient->request('POST'$url, [
  578.                 'headers' => $headers,
  579.                 'json' => $payload,
  580.                 'http_version' => '2.0',
  581.             ]);
  582.             $statusCode $response->getStatusCode();
  583.             $responseContent $response->getContent(false);
  584.             $responseHeaders $response->getHeaders();
  585.             $this->writeLog("Réponse reçue. Statut: $statusCode");
  586.             $this->writeLog("Contenu de la réponse: $responseContent");
  587.             $this->writeLog("Headers de la réponse: " json_encode($responseHeaders));
  588.             if ($statusCode !== 200) {
  589.                 $this->logger->error('Erreur lors de l\'envoi de la notification push', [
  590.                     'status_code' => $statusCode,
  591.                     'response' => $responseContent,
  592.                     'headers' => $responseHeaders,
  593.                 ]);
  594.                 return ['success' => false'status' => $statusCode'message' => $responseContent];
  595.             } else {
  596.                 $this->writeLog("Notification push silencieuse envoyée avec succès");
  597.                 return ['success' => true'status' => $statusCode];
  598.             }
  599.         } catch (\Exception $e) {
  600.             $this->writeLog("Exception lors de l'envoi de la notification push: " $e->getMessage());
  601.             $this->writeLog("Trace complète: " $e->getTraceAsString());
  602.             $this->logger->error('Exception lors de l\'envoi de la notification push', [
  603.                 'error' => $e->getMessage(),
  604.                 'trace' => $e->getTraceAsString(),
  605.             ]);
  606.             return ['success' => false'status' => 500'message' => $e->getMessage()];
  607.         }
  608.     }
  609.     private function getJWT(): string
  610.     {
  611.         $privateKeyPath $this->getParameter('apple_push_private_key_path');
  612.         $privateKey file_get_contents($privateKeyPath);
  613.         $keyId $this->getParameter('apple_push_key_id');
  614.         $teamId $this->getParameter('apple_team_id');
  615.         $payload = [
  616.             'iss' => $teamId,
  617.             'iat' => time(),
  618.             'exp' => time() + 3600// Expire dans 1 heure
  619.         ];
  620.         return JWT::encode($payload$privateKey'ES256'$keyId);
  621.     }
  622.     private function savePassContent(User $userstring $passContent): void
  623.     {
  624.         $passDirectory $this->getParameter('kernel.project_dir') . '/var/passes';
  625.         $filename sprintf('%s/%s.pkpass'$passDirectory$user->getSlug());
  626.         try {
  627.             $this->fileSystem->dumpFile($filename$passContent);
  628.         } catch (\Exception $e) {
  629.             $this->logger->error('Erreur lors de la sauvegarde du contenu du pass', [
  630.                 'user' => $user->getId(),
  631.                 'error' => $e->getMessage(),
  632.             ]);
  633.             throw $e;
  634.         }
  635.     }
  636.     /**
  637.      * @throws \Exception
  638.      */
  639.     #[Route('/api/v1/passes/v1/devices/{deviceLibraryIdentifier}/registrations/{passTypeIdentifier}'name'check_updates'methods: ['GET'])]
  640.     public function checkUpdates(string $deviceLibraryIdentifierstring $passTypeIdentifierRequest $request): JsonResponse
  641.     {
  642.         $this->writeLog("Début de checkUpdates pour device: $deviceLibraryIdentifier, passType: $passTypeIdentifier");
  643.         $this->writeLog("Headers de la requête: " json_encode($request->headers->all()));
  644.         try {
  645. //            // Vérification de l'authentification
  646. //            $authenticationToken = $request->headers->get('Authorization');
  647. //            $this->writeLog("Token d'authentification reçu: " . ($authenticationToken ? substr($authenticationToken, 0, 10) . '...' : 'non défini'));
  648. //            if (!$this->isValidAuthenticationToken($authenticationToken)) {
  649. //                $this->writeLog("Authentification invalide pour checkUpdates");
  650. //                return new JsonResponse(['error' => 'Invalid authentication token'], Response::HTTP_UNAUTHORIZED);
  651. //            }
  652.             // Récupération du paramètre passesUpdatedSince
  653.             $passesUpdatedSince $request->query->get('passesUpdatedSince');
  654.             $this->writeLog("passesUpdatedSince reçu: " . ($passesUpdatedSince ?? 'non défini'));
  655.             try {
  656.                 // Supprimer l'espace et le '00:00' à la fin si présent
  657.                 $passesUpdatedSince preg_replace('/\s+00:00$/'''$passesUpdatedSince);
  658.                 $updatedSince $passesUpdatedSince ? new \DateTime($passesUpdatedSince) : null;
  659.                 $this->writeLog("Date parsée avec succès: " . ($updatedSince $updatedSince->format(\DateTime::ATOM) : 'null'));
  660.             } catch (\Exception $e) {
  661.                 $this->writeLog("Erreur de parsing de la date: " $e->getMessage());
  662.                 $updatedSince null;
  663.             }
  664.             // Récupération des cartes mises à jour
  665.             $updatedCards $this->cardRepository->findUpdatedCards($deviceLibraryIdentifier$updatedSince);
  666.             $this->writeLog("Nombre de cartes trouvées pour ce device: " count($updatedCards));
  667.             $serialNumbers = [];
  668.             $lastUpdated null;
  669.             foreach ($updatedCards as $card) {
  670.                 $this->writeLog("Carte ID: " $card->getId() . ", UpdatedAt: " $card->getUpdatedAt()->format(\DateTime::ATOM));
  671.                 $serialNumbers[] = $card->getUser()->getSlug();
  672.                 if ($lastUpdated === null || $card->getUpdatedAt() > $lastUpdated) {
  673.                     $lastUpdated $card->getUpdatedAt();
  674.                 }
  675.             }
  676.             $this->writeLog("Nombre de passes à mettre à jour : " count($serialNumbers));
  677.             $response = [
  678.                 'serialNumbers' => $serialNumbers,
  679.                 'lastUpdated' => $lastUpdated $lastUpdated->format(\DateTime::ATOM) : null
  680.             ];
  681.             $this->writeLog("Fin de checkUpdates. Réponse : " json_encode($response));
  682.             return new JsonResponse($response);
  683.         } catch (\Exception $e) {
  684.             $this->writeLog("Erreur dans checkUpdates: " $e->getMessage());
  685.             $this->writeLog("Trace complète : " $e->getTraceAsString());
  686.             return new JsonResponse(['error' => 'An error occurred during update check'], Response::HTTP_INTERNAL_SERVER_ERROR);
  687.         }
  688.     }
  689.     private function isValidAuthenticationToken(?string $token): bool
  690.     {
  691.         // Implémentez ici la logique de vérification du token
  692.         // Pour le test, vous pouvez simplement retourner true
  693.         return true;
  694.     }
  695.     /**
  696.      * @throws PKPassException
  697.      */
  698.     #[Route('/api/v1/passes/{passTypeIdentifier}/{serialNumber}'name'get_updated_pass'methods: ['GET'])]
  699.     public function getUpdatedPass(
  700.         string $passTypeIdentifier,
  701.         string $serialNumber,
  702.         Request $request
  703.     ): Response
  704.     {
  705.         // Vérification de l'authentification
  706.         $authenticationToken $request->headers->get('Authorization');
  707.         if (!$this->isValidAuthenticationToken($authenticationToken)) {
  708.             return new JsonResponse(['error' => 'Invalid authentication token'], Response::HTTP_UNAUTHORIZED);
  709.         }
  710.         // Trouver l'utilisateur correspondant au serialNumber
  711.         $user $this->userRepository->findOneBy(['slug' => $serialNumber]);
  712.         if (!$user) {
  713.             return new JsonResponse(['error' => 'User not found'], Response::HTTP_NOT_FOUND);
  714.         }
  715.         $card $user->getCard();
  716.         if (!$card) {
  717.             return new JsonResponse(['error' => 'Card not found'], Response::HTTP_NOT_FOUND);
  718.         }
  719.         // Vérifier si le passTypeIdentifier correspond
  720.         if ($passTypeIdentifier !== 'pass.com.immybeauty.pkpass') {
  721.             return new JsonResponse(['error' => 'Invalid pass type identifier'], Response::HTTP_BAD_REQUEST);
  722.         }
  723.         // Générer le contenu mis à jour du pass
  724.         $certificatePath $this->getParameter('pass_certificate_path');
  725.         $certificatePassword $this->getParameter('pass_certificate_password');
  726.         $pass = new PKPass($certificatePath$certificatePassword);
  727.         $data $this->generatePassData($user$card'');
  728.         $pass->setData($data);
  729.         // Ajouter les fichiers nécessaires au pass (images, etc.)
  730.         $this->addFilesToPass($pass);
  731.         // Créer le contenu du pass
  732.         $passContent $pass->create();
  733.         // Envoyer le contenu du pass comme réponse
  734.         $response = new Response($passContent);
  735.         $response->headers->set('Content-Type''application/vnd.apple.pkpass');
  736.         $response->headers->set('Content-Disposition'$response->headers->makeDisposition(
  737.             ResponseHeaderBag::DISPOSITION_ATTACHMENT,
  738.             'updated_pass.pkpass'
  739.         ));
  740.         return $response;
  741.     }
  742.     /**
  743.      * @throws PKPassException
  744.      */
  745.     private function addFilesToPass(PKPass $pass): void
  746.     {
  747.         $imagesDir $this->getParameter('kernel.project_dir') . '/public/images/apple-wallet/';
  748.         $requiredImages = ['icon.png''icon@2x.png''icon@3x.png''logo.png''logo@2x.png''logo@3x.png''strip.png''strip@2x.png''strip@3x.png'];
  749.         foreach ($requiredImages as $image) {
  750.             $imagePath $imagesDir $image;
  751.             if (file_exists($imagePath)) {
  752.                 try {
  753.                     $pass->addFile($imagePath);
  754.                 } catch (\Exception $e) {
  755.                     throw $e;
  756.                 }
  757.             } else {
  758.                 throw new \Exception("Required image file not found: $image");
  759.             }
  760.         }
  761.     }
  762.     private function writeLog(string $message): void
  763.     {
  764.         $logFile $this->getParameter('kernel.project_dir') . '/apple_wallet.log';
  765.         $timestamp date('Y-m-d H:i:s');
  766.         $logMessage "[$timestamp$messagePHP_EOL;
  767.         file_put_contents($logFile$logMessageFILE_APPEND);
  768.     }
  769.     #[Route('/api/v1/passes/v1/passes/{passTypeIdentifier}/{serialNumber}'name'get_pass'methods: ['GET'])]
  770.     public function getPass(string $passTypeIdentifierstring $serialNumberRequest $request): Response
  771.     {
  772.         $this->writeLog("Début de getPass pour passTypeIdentifier: $passTypeIdentifier, serialNumber: $serialNumber");
  773.         $this->writeLog("Headers de la requête: " json_encode($request->headers->all()));
  774.         // Vérification de l'authentification
  775.         $authenticationToken $request->headers->get('Authorization');
  776.         $this->writeLog("Token d'authentification reçu: " . ($authenticationToken substr($authenticationToken010) . '...' 'non défini'));
  777.         if (!$this->isValidAuthenticationToken($authenticationToken)) {
  778.             $this->writeLog("Authentification invalide pour getPass");
  779.             return new JsonResponse(['error' => 'Invalid authentication token'], Response::HTTP_UNAUTHORIZED);
  780.         }
  781.         // Vérifier si le passTypeIdentifier correspond
  782.         if ($passTypeIdentifier !== 'pass.com.immybeauty.pkpass') {
  783.             $this->writeLog("PassTypeIdentifier invalide: $passTypeIdentifier");
  784.             return new JsonResponse(['error' => 'Invalid pass type identifier'], Response::HTTP_BAD_REQUEST);
  785.         }
  786.         // Trouver l'utilisateur correspondant au serialNumber
  787.         $user $this->userRepository->findOneBy(['slug' => $serialNumber]);
  788.         if (!$user) {
  789.             $this->writeLog("Utilisateur non trouvé pour serialNumber: $serialNumber");
  790.             return new JsonResponse(['error' => 'User not found'], Response::HTTP_NOT_FOUND);
  791.         }
  792.         $this->writeLog("Utilisateur trouvé: ID=" $user->getId() . ", Nom=" $user->getFullName());
  793.         $card $user->getCard();
  794.         if (!$card) {
  795.             $this->writeLog("Carte non trouvée pour l'utilisateur: " $user->getId());
  796.             return new JsonResponse(['error' => 'Card not found'], Response::HTTP_NOT_FOUND);
  797.         }
  798.         $this->writeLog("Carte trouvée: ID=" $card->getId() . ", Points=" $card->getPoints());
  799.         // Vérifier si le pass a été modifié depuis la dernière requête
  800.         $ifModifiedSince $request->headers->get('If-Modified-Since');
  801.         $lastModified $card->getUpdatedAt();
  802.         if ($ifModifiedSince && $lastModified) {
  803.             $ifModifiedSinceDate \DateTime::createFromFormat(\DateTime::RFC7231$ifModifiedSince);
  804.             $lastModifiedFormatted $lastModified->format(\DateTime::RFC7231);
  805.             $this->writeLog("If-Modified-Since: " $ifModifiedSince);
  806.             $this->writeLog("Last-Modified: " $lastModifiedFormatted);
  807.             if ($ifModifiedSinceDate && $lastModified <= $ifModifiedSinceDate) {
  808.                 $this->writeLog("Pass non modifié, renvoi 304 Not Modified");
  809.                 return new Response(nullResponse::HTTP_NOT_MODIFIED);
  810.             }
  811.         }
  812.         $this->writeLog("Pass modifié depuis la dernière requête ou première requête");
  813.         try {
  814.             $this->writeLog("Génération du pass pour l'utilisateur: " $user->getId());
  815.             $certificatePath $this->getParameter('pass_certificate_path');
  816.             $certificatePassword $this->getParameter('pass_certificate_password');
  817.             $this->writeLog("Chemin du certificat: $certificatePath");
  818.             $pass = new PKPass($certificatePath$certificatePassword);
  819.             $lastNotification $card->getLastNotification() ?? '';
  820.             $data $this->generatePassData($user$card$lastNotification);
  821.             $this->writeLog("Données du pass générées: " json_encode($data));
  822.             $pass->setData($data);
  823.             $this->writeLog("Début de l'ajout des fichiers au pass");
  824.             $this->addFilesToPass($pass);
  825.             $this->writeLog("Fin de l'ajout des fichiers au pass");
  826.             $this->writeLog("Début de la création du contenu du pass");
  827.             $newPassContent $pass->create();
  828.             $this->writeLog("Fin de la création du contenu du pass");
  829.             if ($newPassContent === null) {
  830.                 $this->writeLog("Erreur : Impossible de créer le contenu du nouveau pass");
  831.                 return new JsonResponse(['error' => 'Failed to create pass content'], Response::HTTP_INTERNAL_SERVER_ERROR);
  832.             }
  833.             $this->writeLog("Nouveau pass créé avec succès. Taille du contenu : " strlen($newPassContent) . " octets");
  834.             // Comparaison avec l'ancien contenu du pass
  835.             $oldPassContent $this->getExistingPassContent($user);
  836.             if ($oldPassContent === $newPassContent) {
  837.                 $this->writeLog("ATTENTION: Le contenu du pass n'a pas changé!");
  838.             } else {
  839.                 $this->writeLog("Le contenu du pass a été modifié");
  840.                 $this->logPassDifferences($oldPassContent$newPassContent);
  841.             }
  842.             // Mettre à jour le contenu du pass et la date de modification
  843.             $card->setUpdatedAt(new \DateTime('now', new \DateTimeZone('UTC')));
  844.             $this->entityManager->flush();
  845.             $this->writeLog("Pass mis à jour dans la base de données avec la nouvelle date : " $lastModified->format(\DateTime::RFC7231));
  846.             $response = new Response($newPassContent);
  847.             $response->headers->set('Last-Modified'$lastModified->format(\DateTime::RFC7231));
  848.             $response->headers->set('Content-Type''application/vnd.apple.pkpass');
  849.             $response->headers->set('Content-Disposition'$response->headers->makeDisposition(
  850.                 ResponseHeaderBag::DISPOSITION_ATTACHMENT,
  851.                 'updated_pass.pkpass'
  852.             ));
  853.             $this->writeLog("Pass généré et envoyé avec Last-Modified: " $lastModified->format(\DateTime::RFC7231));
  854.             $this->writeLog("Taille du contenu envoyé : " strlen($newPassContent) . " octets");
  855.             $this->writeLog("Headers de la réponse : " json_encode($response->headers->all()));
  856.             return $response;
  857.         } catch (\Exception $e) {
  858.             $this->writeLog("Erreur lors de la génération du pass: " $e->getMessage());
  859.             $this->writeLog("Trace complète : " $e->getTraceAsString());
  860.             return new JsonResponse(['error' => 'An error occurred during pass generation: ' $e->getMessage()], Response::HTTP_INTERNAL_SERVER_ERROR);
  861.         }
  862.     }
  863.     private function logPassDifferences($oldContent$newContent): void
  864.     {
  865.         $this->writeLog("Début de la comparaison des passes");
  866.         $this->writeLog("Taille de l'ancien contenu : " strlen($oldContent) . " octets");
  867.         $this->writeLog("Taille du nouveau contenu : " strlen($newContent) . " octets");
  868.         // Comparer les 100 premiers octets
  869.         $this->writeLog("Premiers 100 octets de l'ancien contenu : " bin2hex(substr($oldContent0100)));
  870.         $this->writeLog("Premiers 100 octets du nouveau contenu : " bin2hex(substr($newContent0100)));
  871.         // Comparer les 100 derniers octets
  872.         $this->writeLog("Derniers 100 octets de l'ancien contenu : " bin2hex(substr($oldContent, -100)));
  873.         $this->writeLog("Derniers 100 octets du nouveau contenu : " bin2hex(substr($newContent, -100)));
  874.         // Comparer les métadonnées du pass
  875.         $oldMetadata $this->extractPassMetadata($oldContent);
  876.         $newMetadata $this->extractPassMetadata($newContent);
  877.         $this->writeLog("Anciennes métadonnées : " json_encode($oldMetadataJSON_PRETTY_PRINT));
  878.         $this->writeLog("Nouvelles métadonnées : " json_encode($newMetadataJSON_PRETTY_PRINT));
  879.         $diff $this->arrayRecursiveDiff($oldMetadata$newMetadata);
  880.         if (!empty($diff)) {
  881.             $this->writeLog("Différences dans les métadonnées : " json_encode($diffJSON_PRETTY_PRINT));
  882.         } else {
  883.             $this->writeLog("Aucune différence dans les métadonnées");
  884.         }
  885.         $this->writeLog("Fin de la comparaison des passes");
  886.     }
  887.     private function extractPassMetadata($content): array
  888.     {
  889.         $metadata = [];
  890.         $tempFile tempnam(sys_get_temp_dir(), 'pass_');
  891.         if ($tempFile === false) {
  892.             $this->writeLog("Erreur : Impossible de créer un fichier temporaire");
  893.             return $metadata;
  894.         }
  895.         try {
  896.             file_put_contents($tempFile$content);
  897.             $zip = new \ZipArchive();
  898.             if ($zip->open($tempFile) === TRUE) {
  899.                 if (($jsonData $zip->getFromName('pass.json')) !== false) {
  900.                     $metadata json_decode($jsonDatatrue);
  901.                     if (json_last_error() !== JSON_ERROR_NONE) {
  902.                         $this->writeLog("Erreur de décodage JSON : " json_last_error_msg());
  903.                     }
  904.                 } else {
  905.                     $this->writeLog("Fichier pass.json non trouvé dans l'archive");
  906.                 }
  907.                 $zip->close();
  908.             } else {
  909.                 $this->writeLog("Impossible d'ouvrir l'archive ZIP");
  910.             }
  911.         } catch (\Exception $e) {
  912.             $this->writeLog("Erreur lors de l'extraction des métadonnées : " $e->getMessage());
  913.         } finally {
  914.             if (file_exists($tempFile)) {
  915.                 unlink($tempFile);
  916.             }
  917.         }
  918.         return $metadata;
  919.     }
  920.     private function getExistingPassContent(User $user): ?string
  921.     {
  922.         $passPath $this->getParameter('kernel.project_dir') . '/var/passes/' $user->getSlug() . '.pkpass';
  923.         if (file_exists($passPath)) {
  924.             $content file_get_contents($passPath);
  925.             return $content !== false $content null;
  926.         }
  927.         $this->writeLog("Pas de pass existant trouvé pour l'utilisateur: " $user->getId());
  928.         return null;
  929.     }
  930.     #[Route('/api/v1/passes/v1/log'name'pass_log'methods: ['POST'])]
  931.     public function logPass(Request $request): JsonResponse
  932.     {
  933.         $content $request->getContent();
  934.         $this->writeLog('Pass log received: ' $content);
  935.         return new JsonResponse(nullResponse::HTTP_OK);
  936.     }
  937.     #[Route('/api/v1/passes/v1/devices/{deviceLibraryIdentifier}/registrations/{passTypeIdentifier}/{serialNumber}'name'unregister_device'methods: ['DELETE'])]
  938.     public function unregisterDevice(
  939.         string $deviceLibraryIdentifier,
  940.         string $passTypeIdentifier,
  941.         string $serialNumber,
  942.         Request $request,
  943.         EntityManagerInterface $entityManager
  944.     ): JsonResponse
  945.     {
  946.         $this->writeLog("Début de unregisterDevice pour device: $deviceLibraryIdentifier, passType: $passTypeIdentifier, serialNumber: $serialNumber");
  947.         // Vérification de l'authentification
  948.         $authToken $request->headers->get('Authorization');
  949.         if (!$this->isValidAuthenticationToken($authToken)) {
  950.             $this->writeLog("Authentification invalide pour unregisterDevice");
  951.             return new JsonResponse(['error' => 'Invalid authentication token'], Response::HTTP_UNAUTHORIZED);
  952.         }
  953.         try {
  954.             // Trouver l'utilisateur correspondant au serialNumber
  955.             $user $this->userRepository->findOneBy(['slug' => $serialNumber]);
  956.             if (!$user) {
  957.                 $this->writeLog("Utilisateur non trouvé pour serialNumber: $serialNumber");
  958.                 return new JsonResponse(['error' => 'User not found'], Response::HTTP_NOT_FOUND);
  959.             }
  960.             $card $user->getCard();
  961.             if (!$card) {
  962.                 $this->writeLog("Carte non trouvée pour l'utilisateur: " $user->getId());
  963.                 return new JsonResponse(['error' => 'Card not found'], Response::HTTP_NOT_FOUND);
  964.             }
  965.             // Vérifier si le deviceLibraryIdentifier correspond
  966.             if ($card->getDeviceLibraryIdentifier() !== $deviceLibraryIdentifier) {
  967.                 $this->writeLog("DeviceLibraryIdentifier ne correspond pas pour l'utilisateur: " $user->getId());
  968.                 return new JsonResponse(['error' => 'Device mismatch'], Response::HTTP_BAD_REQUEST);
  969.             }
  970.             // Supprimer les informations d'enregistrement
  971.             $card->setAppleWalletToken(null);
  972.             $card->setAppleWalletPushToken(null);
  973.             $card->setDeviceLibraryIdentifier(null);
  974.             // Mettre à jour la date de modification
  975.             $card->setUpdatedAt(new \DateTime());
  976.             $entityManager->persist($card);
  977.             $entityManager->flush();
  978.             $this->writeLog("Désinscription réussie pour l'utilisateur: " $user->getId());
  979.             return new JsonResponse(nullResponse::HTTP_OK);
  980.         } catch (\Exception $e) {
  981.             $this->writeLog("Erreur lors de la désinscription: " $e->getMessage());
  982.             return new JsonResponse(['error' => 'An error occurred during unregistration'], Response::HTTP_INTERNAL_SERVER_ERROR);
  983.         }
  984.     }
  985.     /**
  986.      * @throws PKPassException
  987.      */
  988.     #[Route('/test-apple-wallet-flow/{userSlug}'name'test_apple_wallet_flow')]
  989.     public function testAppleWalletFlow(string $userSlug): JsonResponse
  990.     {
  991.         $this->log('Début du test du flux Apple Wallet', ['userSlug' => $userSlug]);
  992.         // Créer une requête avec un jeton CSRF valide
  993.         $csrfToken $this->csrfTokenManager->getToken('add-to-apple-wallet');
  994.         $request = new Request(['_token' => $csrfToken->getValue()]);
  995.         // Simuler l'ajout d'un pass
  996.         $addResponse $this->addToAppleWallet($userSlug$request);
  997.         $this->log('Réponse de addToAppleWallet', ['status' => $addResponse->getStatusCode()]);
  998.         // Simuler l'enregistrement d'un appareil
  999.         $registerRequest = new Request([], [], [], [], [], [], json_encode(['pushToken' => 'test_push_token']));
  1000.         $registerResponse $this->registerDevice('test_device''pass.com.immybeauty.pkpass'$userSlug$registerRequest$this->entityManager);
  1001.         $this->log('Réponse de registerDevice', ['status' => $registerResponse->getStatusCode()]);
  1002.         // Simuler une vérification des mises à jour
  1003.         $checkUpdatesResponse $this->checkUpdates('test_device''pass.com.immybeauty.pkpass', new Request());
  1004.         $this->log('Réponse de checkUpdates', ['status' => $checkUpdatesResponse->getStatusCode(), 'content' => $checkUpdatesResponse->getContent()]);
  1005.         // Simuler une mise à jour du pass
  1006.         $updateResponse $this->updatePass($userSlug, new Request(), $this->entityManager);
  1007.         $this->log('Réponse de updatePass', ['status' => $updateResponse->getStatusCode(), 'content' => $updateResponse->getContent()]);
  1008.         // Simuler la récupération d'un pass mis à jour
  1009.         $getPassResponse $this->getPass('pass.com.immybeauty.pkpass'$userSlug, new Request());
  1010.         $this->log('Réponse de getPass', ['status' => $getPassResponse->getStatusCode()]);
  1011.         // Simuler la désinscription d'un appareil
  1012.         $unregisterResponse $this->unregisterDevice('test_device''pass.com.immybeauty.pkpass'$userSlug, new Request(), $this->entityManager);
  1013.         $this->log('Réponse de unregisterDevice', ['status' => $unregisterResponse->getStatusCode()]);
  1014.         $this->log('Fin du test du flux Apple Wallet');
  1015.         return new JsonResponse(['message' => 'Test du flux Apple Wallet terminé'], Response::HTTP_OK);
  1016.     }
  1017.     private function log(string $message, array $context = []): void
  1018.     {
  1019.         $this->logger->info($message$context);
  1020.         $this->writeLog($message ' ' json_encode($context));
  1021.     }
  1022.     private function generateQRCodeURLProfil(User $user): string
  1023.     {
  1024.         return $this->getParameter('APP_URL') . '/fidelite/mon-profil/' $user->getSlug();
  1025.     }
  1026. }