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' => false // 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.             $newData['relevantText'] = $notificationMessage;
  323.             $newData['passVersion'] = time();
  324.             $newData['relevantDate'] = (new \DateTime())->format('c');
  325.             $this->writeLog('Définition des données du pass');
  326.             $pass->setData($newData);
  327.             $this->writeLog('Ajout des fichiers au pass');
  328.             $this->addFilesToPass($pass);
  329.             $this->writeLog('Création du contenu du pass');
  330.             $newPassContent $pass->create();
  331.             $this->writeLog('Comparaison du contenu du pass:');
  332.             $oldPassContent $this->getExistingPassContent($user);
  333.             if ($oldPassContent === $newPassContent) {
  334.                 $this->writeLog('ATTENTION: Le contenu du pass n\'a pas changé!');
  335.             } else {
  336.                 $this->writeLog('Le contenu du pass a été modifié');
  337.             }
  338.             $this->writeLog('Sauvegarde du contenu du pass');
  339.             $this->savePassContent($user$newPassContent);
  340.             // Mise à jour de la date de modification de la carte
  341.             $now = new \DateTime();
  342.             $card->setUpdatedAt($now);
  343.             $entityManager->persist($card);
  344.             $entityManager->flush();
  345.             $pushToken $card->getAppleWalletPushToken();
  346.             if ($pushToken) {
  347.                 // Envoi de la notification silencieuse
  348.                 $silentResult $this->sendSilentPushNotification($pushToken$newData['passTypeIdentifier']);
  349.                 $this->writeLog('Résultat de l\'envoi de la notification silencieuse: ' json_encode($silentResult));
  350.                 // Envoi de la notification visible
  351.                 $visibleResult $this->sendVisiblePushNotification($pushToken$notificationMessage$newData['passTypeIdentifier']);
  352.                 $this->writeLog('Résultat de l\'envoi de la notification visible: ' json_encode($visibleResult));
  353.             } else {
  354.                 $this->writeLog('ATTENTION: Aucun pushToken trouvé pour l\'envoi de la notification');
  355.             }
  356.             $this->writeLog('Fin de updatePass avec succès');
  357.             return new JsonResponse([
  358.                 'status' => 'Pass updated and notification sent',
  359.                 'lastUpdated' => $now->format(DateTimeInterface::ISO8601)
  360.             ], Response::HTTP_OK);
  361.         } catch (\Exception $e) {
  362.             $this->writeLog('Erreur dans updatePass: ' $e->getMessage());
  363.             $this->writeLog('Trace complète: ' $e->getTraceAsString());
  364.             return new JsonResponse(['error' => $e->getMessage()], Response::HTTP_INTERNAL_SERVER_ERROR);
  365.         }
  366.     }
  367.     private function logDataDifferences(array $oldData, array $newData): void
  368.     {
  369.         $differences $this->arrayRecursiveDiff($oldData$newData);
  370.         $this->writeLog('Différences dans les données: ' json_encode($differencesJSON_PRETTY_PRINT));
  371.     }
  372.     private function arrayRecursiveDiff($arr1$arr2) {
  373.         $result = array();
  374.         foreach ($arr1 as $key => $value) {
  375.             if (array_key_exists($key$arr2)) {
  376.                 if (is_array($value)) {
  377.                     $recursiveDiff $this->arrayRecursiveDiff($value$arr2[$key]);
  378.                     if (count($recursiveDiff)) {
  379.                         $result[$key] = $recursiveDiff;
  380.                     }
  381.                 } else {
  382.                     if ($value != $arr2[$key]) {
  383.                         $result[$key] = array('old' => $value'new' => $arr2[$key]);
  384.                     }
  385.                 }
  386.             } else {
  387.                 $result[$key] = array('old' => $value'new' => 'N/A');
  388.             }
  389.         }
  390.         return $result;
  391.     }
  392.     private function generatePassData(User $userCard $cardstring $notificationMessage): array
  393.     {
  394.         $dernierPassage $this->cardTransactionRepository->getDernierPassage($user)?->getTransactionDate();
  395.         $lastTransaction $card->getLastTransactionDate();
  396.         $lastTransactionFormatted $lastTransaction $lastTransaction->format('d/m/Y') : 'Aucun passage';
  397.         $styleUpdateMessage "Style mis à jour le " . (new \DateTime())->format('d/m/Y H:i');
  398. //        $newColors = [
  399. //            $this->getRandomColor(),
  400. //            $this->getRandomColor(),
  401. //            $this->getRandomColor()
  402. //        ];
  403. //
  404. //        $colorUpdateMessage = "Couleurs mises à jour (" . implode(', ', $newColors) . ")";
  405.         return [
  406.             'formatVersion' => 1,
  407.             'passTypeIdentifier' => 'pass.com.immybeauty.pkpass',
  408.             'teamIdentifier' => 'V9N5857WGW',
  409.             'serialNumber' => $user->getSlug(),
  410.             'webServiceURL' => 'https://immybeauty.fr/api/v1/passes',
  411.             'authenticationToken' => $card->getAppleWalletToken(),
  412.             'pushToken' => ''// Ce champ sera rempli par Apple lors de l'enregistrement du pass
  413.             'allowsUpdates' => true,
  414.             'sharingProhibited' => false,
  415.             "barcodes" => [
  416.                 [
  417.                     "message" => $this->generateQRCodeURLProfil($user),
  418.                     "format" => "PKBarcodeFormatQR",
  419.                     "messageEncoding" => "iso-8859-1"
  420.                 ]
  421.             ],
  422.             "organizationName" => "IMMY BEAUTY",
  423.             "foregroundColor" => "rgb(255, 255, 255)"// BLANC
  424.             "backgroundColor" => "rgb(20, 20, 20)"// NOIR
  425.             "labelColor" => "rgb(174, 136, 90)"// DORÉ
  426. //            "foregroundColor" => $newColors[0],
  427. //            "backgroundColor" => $newColors[1],
  428. //            "labelColor" => $newColors[2],
  429.             "description" => "IMMY BEAUTY",
  430.             "storeCard" => [
  431.                 'primaryFields' => [
  432.                     [
  433.                         'key' => 'notification',
  434.                         'label' => 'Notification',
  435.                         'value' => $notificationMessage,
  436.                         'changeMessage' => 'Nouvelle notification : %@'
  437.                     ]
  438.                 ],
  439. //                'primaryFields' => [
  440. //                    [
  441. //                        'key' => 'notification',
  442. //                        'label' => 'Notification',
  443. //                        'value' => $notificationMessage,
  444. //                        'changeMessage' => 'Nouvelle notification : %@'
  445. //                    ]
  446. //                ],
  447. //                [
  448. //                    'key' => 'style_update',
  449. //                    'label' => 'Mise à jour du style',
  450. //                    'value' => $styleUpdateMessage,
  451. //                    'changeMessage' => 'Nouveau style appliqué : %@'
  452. //                ],
  453.                 'headerFields' => [
  454.                     [
  455.                         'key' => 'header-name',
  456.                         'textAlignment' => 'PKTextAlignmentRight',
  457.                         'label' => 'CLIENT',
  458.                         'value' => $user->getFullName(),
  459.                         'changeMessage' => 'Nom du client mis à jour : %@'
  460.                     ]
  461.                 ],
  462.                 'auxiliaryFields' => [
  463. //                    [
  464. //                        'key' => 'color_update',
  465. //                        'label' => 'MISE À JOUR',
  466. //                        'value' => $colorUpdateMessage,
  467. //                        'changeMessage' => 'Nouvelles couleurs appliquées : %@'
  468. //                    ],
  469.                     [
  470.                         'key' => 'dernier-passage',
  471.                         'label' => 'DERNIER PASSAGE',
  472.                         'value' => $dernierPassage $dernierPassage->format('d/m/Y') : '-',
  473.                         'textAlignment' => 'PKTextAlignmentLeft',
  474.                         'changeMessage' => 'Nouveau dernier passage : %@'
  475.                     ],
  476.                     [
  477.                         'key' => 'points',
  478.                         'label' => 'POINTS',
  479.                         'value' => $card->getPoints(),
  480.                         'textAlignment' => 'PKTextAlignmentRight',
  481.                         'changeMessage' => 'Nouveau solde de points : %@'
  482.                     ],
  483.                     [
  484.                         'key' => 'notification',
  485.                         'label' => 'Message',
  486.                         'value' => $notificationMessage,
  487.                         'changeMessage' => 'Nouveau message : %@'
  488.                     ]
  489.                 ],
  490.                 "backFields" => [
  491.                     [
  492.                         "key" => "back-name",
  493.                         "label" => "SALON",
  494.                         "value" => "IMMY BEAUTY"
  495.                     ],
  496.                     [
  497.                         "key" => "back-horaires",
  498.                         "label" => "🕒 HORAIRES",
  499.                         "value" => "Mar - Dim\n10h00 - 19h30"
  500.                     ],
  501.                     [
  502.                         "key" => "back-tel",
  503.                         "label" => "📞 NUMERO TÉL",
  504.                         "value" => "0745264451",
  505.                         "dataDetectorTypes" => ["PKDataDetectorTypePhoneNumber"]
  506.                     ],
  507.                     [
  508.                         "key" => "back-planity",
  509.                         "label" => "📅 PRISE DE RDV",
  510.                         "value" => "https://www.planity.com/immy-beauty-93410-vaujours",
  511.                         "dataDetectorTypes" => ["PKDataDetectorTypeLink"]
  512.                     ],
  513.                     [
  514.                         "key" => "back-instagram",
  515.                         "label" => "📷 INSTAGRAM",
  516.                         "value" => "@immybeauty.fr",
  517.                         "link" => "https://www.instagram.com/immybeauty.fr",
  518.                         "dataDetectorTypes" => ["PKDataDetectorTypeLink"]
  519.                     ],
  520.                     [
  521.                         "key" => "back-tiktok",
  522.                         "label" => "🎵 TIKTOK",
  523.                         "value" => "@immybeauty.fr",
  524.                         "link" => "https://www.tiktok.com/@immybeauty.fr",
  525.                         "dataDetectorTypes" => ["PKDataDetectorTypeLink"]
  526.                     ],
  527.                     [
  528.                         "key" => "back-email",
  529.                         "label" => "✉️ EMAIL",
  530.                         "value" => "contact@immybeauty.fr",
  531.                         "link" => "mailto:contact@immybeauty.fr",
  532.                         "dataDetectorTypes" => ["PKDataDetectorTypeLink"]
  533.                     ],
  534.                     [
  535.                         "key" => "back-website",
  536.                         "label" => "🌐 SITE INTERNET",
  537.                         "value" => "www.immybeauty.fr",
  538.                         "link" => "https://immybeauty.fr",
  539.                         "dataDetectorTypes" => ["PKDataDetectorTypeLink"]
  540.                     ],
  541. //                    [
  542. //                        "key" => "hidden_update_trigger",
  543. //                        "label" => "Dernière mise à jour",
  544. //                        "value" => (new \DateTime())->format('Y-m-d H:i:s'),
  545. //                        'changeMessage' => '%@'
  546. //                    ],
  547.                     [
  548.                         "key" => "notification_center",
  549.                         "label" => "📬 Centre de messages",
  550.                         "value" => $notificationMessage,
  551. //                        "value" => "PUTIN DE MERDE " . (new \DateTime())->format('Y-m-d H:i:s'),
  552.                         "changeMessage" => "%@",
  553.                         'hidden' => false // Ajoutez cette ligne pour cacher le champ
  554.                     ],
  555.                 ],
  556.             ]
  557.         ];
  558.     }
  559.     private function generateRandomMessage(): string
  560.     {
  561.         $messages = [
  562.             "QUEL BAIL VOUS ?",
  563.             "QUOI DE NEUF ?",
  564.             "ÇA ROULE ?",
  565.             "COMMENT ÇA VA ?",
  566.             "QUOI DE BON ?",
  567.             "QUELLES NOUVELLES ?",
  568.             "ON DIT QUOI ?",
  569.             "ALORS, CONTENT(E) ?",
  570.             "TOUT BAIGNE ?",
  571.             "LA FORME ?"
  572.         ];
  573.         return $messages[array_rand($messages)];
  574.     }
  575.     private function sendSilentPushNotification(string $pushTokenstring $passTypeIdentifier): array
  576.     {
  577.         $url "https://api.push.apple.com/3/device/$pushToken";
  578.         $this->writeLog("Début de sendSilentPushNotification");
  579.         $this->writeLog("PushToken: $pushToken");
  580.         $this->writeLog("PassTypeIdentifier: $passTypeIdentifier");
  581.         try {
  582.             $jwt $this->getJWT();
  583.             $this->writeLog("JWT généré avec succès. Longueur du JWT: " strlen($jwt));
  584.             // Payload vide pour une notification silencieuse
  585.             $payload = [
  586.                 'aps' => [
  587.                     'content-available' => 1
  588.                 ]
  589.             ];
  590.             $this->writeLog("Payload de la notification: " json_encode($payload));
  591.             $headers = [
  592.                 'apns-topic' => $passTypeIdentifier,
  593.                 'apns-push-type' => 'background',
  594.                 'authorization' => 'bearer ' $jwt,
  595.             ];
  596.             $this->writeLog("Headers de la requête: " json_encode($headers));
  597.             $response $this->httpClient->request('POST'$url, [
  598.                 'headers' => $headers,
  599.                 'json' => $payload,
  600.                 'http_version' => '2.0',
  601.             ]);
  602.             $statusCode $response->getStatusCode();
  603.             $responseContent $response->getContent(false);
  604.             $responseHeaders $response->getHeaders();
  605.             $this->writeLog("Réponse reçue. Statut: $statusCode");
  606.             $this->writeLog("Contenu de la réponse: $responseContent");
  607.             $this->writeLog("Headers de la réponse: " json_encode($responseHeaders));
  608.             if ($statusCode !== 200) {
  609.                 $this->logger->error('Erreur lors de l\'envoi de la notification push', [
  610.                     'status_code' => $statusCode,
  611.                     'response' => $responseContent,
  612.                     'headers' => $responseHeaders,
  613.                 ]);
  614.                 return ['success' => false'status' => $statusCode'message' => $responseContent];
  615.             } else {
  616.                 $this->writeLog("Notification push silencieuse envoyée avec succès");
  617.                 return ['success' => true'status' => $statusCode];
  618.             }
  619.         } catch (\Exception $e) {
  620.             $this->writeLog("Exception lors de l'envoi de la notification push: " $e->getMessage());
  621.             $this->writeLog("Trace complète: " $e->getTraceAsString());
  622.             $this->logger->error('Exception lors de l\'envoi de la notification push', [
  623.                 'error' => $e->getMessage(),
  624.                 'trace' => $e->getTraceAsString(),
  625.             ]);
  626.             return ['success' => false'status' => 500'message' => $e->getMessage()];
  627.         }
  628.     }
  629.     private function getJWT(): string
  630.     {
  631.         $privateKeyPath $this->getParameter('apple_push_private_key_path');
  632.         $privateKey file_get_contents($privateKeyPath);
  633.         $keyId $this->getParameter('apple_push_key_id');
  634.         $teamId $this->getParameter('apple_team_id');
  635.         $payload = [
  636.             'iss' => $teamId,
  637.             'iat' => time(),
  638.             'exp' => time() + 3600// Expire dans 1 heure
  639.         ];
  640.         return JWT::encode($payload$privateKey'ES256'$keyId);
  641.     }
  642.     private function savePassContent(User $userstring $passContent): void
  643.     {
  644.         $passDirectory $this->getParameter('kernel.project_dir') . '/var/passes';
  645.         $filename sprintf('%s/%s.pkpass'$passDirectory$user->getSlug());
  646.         try {
  647.             $this->fileSystem->dumpFile($filename$passContent);
  648.         } catch (\Exception $e) {
  649.             $this->logger->error('Erreur lors de la sauvegarde du contenu du pass', [
  650.                 'user' => $user->getId(),
  651.                 'error' => $e->getMessage(),
  652.             ]);
  653.             throw $e;
  654.         }
  655.     }
  656.     /**
  657.      * @throws \Exception
  658.      */
  659.     #[Route('/api/v1/passes/v1/devices/{deviceLibraryIdentifier}/registrations/{passTypeIdentifier}'name'check_updates'methods: ['GET'])]
  660.     public function checkUpdates(string $deviceLibraryIdentifierstring $passTypeIdentifierRequest $request): JsonResponse
  661.     {
  662.         $this->writeLog("Début de checkUpdates pour device: $deviceLibraryIdentifier, passType: $passTypeIdentifier");
  663.         $this->writeLog("Headers de la requête: " json_encode($request->headers->all()));
  664.         try {
  665. //            // Vérification de l'authentification
  666. //            $authenticationToken = $request->headers->get('Authorization');
  667. //            $this->writeLog("Token d'authentification reçu: " . ($authenticationToken ? substr($authenticationToken, 0, 10) . '...' : 'non défini'));
  668. //            if (!$this->isValidAuthenticationToken($authenticationToken)) {
  669. //                $this->writeLog("Authentification invalide pour checkUpdates");
  670. //                return new JsonResponse(['error' => 'Invalid authentication token'], Response::HTTP_UNAUTHORIZED);
  671. //            }
  672.             // Récupération du paramètre passesUpdatedSince
  673.             $passesUpdatedSince $request->query->get('passesUpdatedSince');
  674.             $this->writeLog("passesUpdatedSince reçu: " . ($passesUpdatedSince ?? 'non défini'));
  675.             try {
  676.                 // Supprimer l'espace et le '00:00' à la fin si présent
  677.                 $passesUpdatedSince preg_replace('/\s+00:00$/'''$passesUpdatedSince);
  678.                 $updatedSince $passesUpdatedSince ? new \DateTime($passesUpdatedSince) : null;
  679.                 $this->writeLog("Date parsée avec succès: " . ($updatedSince $updatedSince->format(\DateTime::ATOM) : 'null'));
  680.             } catch (\Exception $e) {
  681.                 $this->writeLog("Erreur de parsing de la date: " $e->getMessage());
  682.                 $updatedSince null;
  683.             }
  684.             // Récupération des cartes mises à jour
  685.             $updatedCards $this->cardRepository->findUpdatedCards($deviceLibraryIdentifier$updatedSince);
  686.             $this->writeLog("Nombre de cartes trouvées pour ce device: " count($updatedCards));
  687.             $serialNumbers = [];
  688.             $lastUpdated null;
  689.             foreach ($updatedCards as $card) {
  690.                 $this->writeLog("Carte ID: " $card->getId() . ", UpdatedAt: " $card->getUpdatedAt()->format(\DateTime::ATOM));
  691.                 $serialNumbers[] = $card->getUser()->getSlug();
  692.                 if ($lastUpdated === null || $card->getUpdatedAt() > $lastUpdated) {
  693.                     $lastUpdated $card->getUpdatedAt();
  694.                 }
  695.             }
  696.             $this->writeLog("Nombre de passes à mettre à jour : " count($serialNumbers));
  697.             $response = [
  698.                 'serialNumbers' => $serialNumbers,
  699.                 'lastUpdated' => $lastUpdated $lastUpdated->format(\DateTime::ATOM) : null
  700.             ];
  701.             $this->writeLog("Fin de checkUpdates. Réponse : " json_encode($response));
  702.             return new JsonResponse($response);
  703.         } catch (\Exception $e) {
  704.             $this->writeLog("Erreur dans checkUpdates: " $e->getMessage());
  705.             $this->writeLog("Trace complète : " $e->getTraceAsString());
  706.             return new JsonResponse(['error' => 'An error occurred during update check'], Response::HTTP_INTERNAL_SERVER_ERROR);
  707.         }
  708.     }
  709.     private function isValidAuthenticationToken(?string $token): bool
  710.     {
  711.         // Implémentez ici la logique de vérification du token
  712.         // Pour le test, vous pouvez simplement retourner true
  713.         return true;
  714.     }
  715.     /**
  716.      * @throws PKPassException
  717.      */
  718.     #[Route('/api/v1/passes/{passTypeIdentifier}/{serialNumber}'name'get_updated_pass'methods: ['GET'])]
  719.     public function getUpdatedPass(
  720.         string $passTypeIdentifier,
  721.         string $serialNumber,
  722.         Request $request
  723.     ): Response
  724.     {
  725.         // Vérification de l'authentification
  726.         $authenticationToken $request->headers->get('Authorization');
  727.         if (!$this->isValidAuthenticationToken($authenticationToken)) {
  728.             return new JsonResponse(['error' => 'Invalid authentication token'], Response::HTTP_UNAUTHORIZED);
  729.         }
  730.         // Trouver l'utilisateur correspondant au serialNumber
  731.         $user $this->userRepository->findOneBy(['slug' => $serialNumber]);
  732.         if (!$user) {
  733.             return new JsonResponse(['error' => 'User not found'], Response::HTTP_NOT_FOUND);
  734.         }
  735.         $card $user->getCard();
  736.         if (!$card) {
  737.             return new JsonResponse(['error' => 'Card not found'], Response::HTTP_NOT_FOUND);
  738.         }
  739.         // Vérifier si le passTypeIdentifier correspond
  740.         if ($passTypeIdentifier !== 'pass.com.immybeauty.pkpass') {
  741.             return new JsonResponse(['error' => 'Invalid pass type identifier'], Response::HTTP_BAD_REQUEST);
  742.         }
  743.         // Générer le contenu mis à jour du pass
  744.         $certificatePath $this->getParameter('pass_certificate_path');
  745.         $certificatePassword $this->getParameter('pass_certificate_password');
  746.         $pass = new PKPass($certificatePath$certificatePassword);
  747.         $data $this->generatePassData($user$card'');
  748.         $pass->setData($data);
  749.         // Ajouter les fichiers nécessaires au pass (images, etc.)
  750.         $this->addFilesToPass($pass);
  751.         // Créer le contenu du pass
  752.         $passContent $pass->create();
  753.         // Envoyer le contenu du pass comme réponse
  754.         $response = new Response($passContent);
  755.         $response->headers->set('Content-Type''application/vnd.apple.pkpass');
  756.         $response->headers->set('Content-Disposition'$response->headers->makeDisposition(
  757.             ResponseHeaderBag::DISPOSITION_ATTACHMENT,
  758.             'updated_pass.pkpass'
  759.         ));
  760.         return $response;
  761.     }
  762.     /**
  763.      * @throws PKPassException
  764.      */
  765.     private function addFilesToPass(PKPass $pass): void
  766.     {
  767.         $imagesDir $this->getParameter('kernel.project_dir') . '/public/images/apple-wallet/';
  768.         $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'];
  769.         foreach ($requiredImages as $image) {
  770.             $imagePath $imagesDir $image;
  771.             if (file_exists($imagePath)) {
  772.                 try {
  773.                     $pass->addFile($imagePath);
  774.                 } catch (\Exception $e) {
  775.                     throw $e;
  776.                 }
  777.             } else {
  778.                 throw new \Exception("Required image file not found: $image");
  779.             }
  780.         }
  781.     }
  782.     private function writeLog(string $message): void
  783.     {
  784.         $logFile $this->getParameter('kernel.project_dir') . '/apple_wallet.log';
  785.         $timestamp date('Y-m-d H:i:s');
  786.         $logMessage "[$timestamp$messagePHP_EOL;
  787.         file_put_contents($logFile$logMessageFILE_APPEND);
  788.     }
  789.     #[Route('/api/v1/passes/v1/passes/{passTypeIdentifier}/{serialNumber}'name'get_pass'methods: ['GET'])]
  790.     public function getPass(string $passTypeIdentifierstring $serialNumberRequest $request): Response
  791.     {
  792.         $this->writeLog("Début de getPass pour passTypeIdentifier: $passTypeIdentifier, serialNumber: $serialNumber");
  793.         $this->writeLog("Headers de la requête: " json_encode($request->headers->all()));
  794.         // Vérification de l'authentification
  795.         $authenticationToken $request->headers->get('Authorization');
  796.         $this->writeLog("Token d'authentification reçu: " . ($authenticationToken substr($authenticationToken010) . '...' 'non défini'));
  797.         if (!$this->isValidAuthenticationToken($authenticationToken)) {
  798.             $this->writeLog("Authentification invalide pour getPass");
  799.             return new JsonResponse(['error' => 'Invalid authentication token'], Response::HTTP_UNAUTHORIZED);
  800.         }
  801.         // Vérifier si le passTypeIdentifier correspond
  802.         if ($passTypeIdentifier !== 'pass.com.immybeauty.pkpass') {
  803.             $this->writeLog("PassTypeIdentifier invalide: $passTypeIdentifier");
  804.             return new JsonResponse(['error' => 'Invalid pass type identifier'], Response::HTTP_BAD_REQUEST);
  805.         }
  806.         // Trouver l'utilisateur correspondant au serialNumber
  807.         $user $this->userRepository->findOneBy(['slug' => $serialNumber]);
  808.         if (!$user) {
  809.             $this->writeLog("Utilisateur non trouvé pour serialNumber: $serialNumber");
  810.             return new JsonResponse(['error' => 'User not found'], Response::HTTP_NOT_FOUND);
  811.         }
  812.         $this->writeLog("Utilisateur trouvé: ID=" $user->getId() . ", Nom=" $user->getFullName());
  813.         $card $user->getCard();
  814.         if (!$card) {
  815.             $this->writeLog("Carte non trouvée pour l'utilisateur: " $user->getId());
  816.             return new JsonResponse(['error' => 'Card not found'], Response::HTTP_NOT_FOUND);
  817.         }
  818.         $this->writeLog("Carte trouvée: ID=" $card->getId() . ", Points=" $card->getPoints());
  819.         // Vérifier si le pass a été modifié depuis la dernière requête
  820.         $ifModifiedSince $request->headers->get('If-Modified-Since');
  821.         $lastModified $card->getUpdatedAt();
  822.         if ($ifModifiedSince && $lastModified) {
  823.             $ifModifiedSinceDate \DateTime::createFromFormat(\DateTime::RFC7231$ifModifiedSince);
  824.             $lastModifiedFormatted $lastModified->format(\DateTime::RFC7231);
  825.             $this->writeLog("If-Modified-Since: " $ifModifiedSince);
  826.             $this->writeLog("Last-Modified: " $lastModifiedFormatted);
  827.             if ($ifModifiedSinceDate && $lastModified <= $ifModifiedSinceDate) {
  828.                 $this->writeLog("Pass non modifié, renvoi 304 Not Modified");
  829.                 return new Response(nullResponse::HTTP_NOT_MODIFIED);
  830.             }
  831.         }
  832.         $this->writeLog("Pass modifié depuis la dernière requête ou première requête");
  833.         try {
  834.             $this->writeLog("Génération du pass pour l'utilisateur: " $user->getId());
  835.             $certificatePath $this->getParameter('pass_certificate_path');
  836.             $certificatePassword $this->getParameter('pass_certificate_password');
  837.             $this->writeLog("Chemin du certificat: $certificatePath");
  838.             $pass = new PKPass($certificatePath$certificatePassword);
  839.             $lastNotification $card->getLastNotification() ?? '';
  840.             $data $this->generatePassData($user$card$lastNotification);
  841.             $this->writeLog("Données du pass générées: " json_encode($data));
  842.             $pass->setData($data);
  843.             $this->writeLog("Début de l'ajout des fichiers au pass");
  844.             $this->addFilesToPass($pass);
  845.             $this->writeLog("Fin de l'ajout des fichiers au pass");
  846.             $this->writeLog("Début de la création du contenu du pass");
  847.             $newPassContent $pass->create();
  848.             $this->writeLog("Fin de la création du contenu du pass");
  849.             if ($newPassContent === null) {
  850.                 $this->writeLog("Erreur : Impossible de créer le contenu du nouveau pass");
  851.                 return new JsonResponse(['error' => 'Failed to create pass content'], Response::HTTP_INTERNAL_SERVER_ERROR);
  852.             }
  853.             $this->writeLog("Nouveau pass créé avec succès. Taille du contenu : " strlen($newPassContent) . " octets");
  854.             // Comparaison avec l'ancien contenu du pass
  855.             $oldPassContent $this->getExistingPassContent($user);
  856.             if ($oldPassContent === $newPassContent) {
  857.                 $this->writeLog("ATTENTION: Le contenu du pass n'a pas changé!");
  858.             } else {
  859.                 $this->writeLog("Le contenu du pass a été modifié");
  860.                 $this->logPassDifferences($oldPassContent$newPassContent);
  861.             }
  862.             // Mettre à jour le contenu du pass et la date de modification
  863.             $card->setUpdatedAt(new \DateTime('now', new \DateTimeZone('UTC')));
  864.             $this->entityManager->flush();
  865.             $this->writeLog("Pass mis à jour dans la base de données avec la nouvelle date : " $lastModified->format(\DateTime::RFC7231));
  866.             $response = new Response($newPassContent);
  867.             $response->headers->set('Last-Modified'$lastModified->format(\DateTime::RFC7231));
  868.             $response->headers->set('Content-Type''application/vnd.apple.pkpass');
  869.             $response->headers->set('Content-Disposition'$response->headers->makeDisposition(
  870.                 ResponseHeaderBag::DISPOSITION_ATTACHMENT,
  871.                 'updated_pass.pkpass'
  872.             ));
  873.             $this->writeLog("Pass généré et envoyé avec Last-Modified: " $lastModified->format(\DateTime::RFC7231));
  874.             $this->writeLog("Taille du contenu envoyé : " strlen($newPassContent) . " octets");
  875.             $this->writeLog("Headers de la réponse : " json_encode($response->headers->all()));
  876.             return $response;
  877.         } catch (\Exception $e) {
  878.             $this->writeLog("Erreur lors de la génération du pass: " $e->getMessage());
  879.             $this->writeLog("Trace complète : " $e->getTraceAsString());
  880.             return new JsonResponse(['error' => 'An error occurred during pass generation: ' $e->getMessage()], Response::HTTP_INTERNAL_SERVER_ERROR);
  881.         }
  882.     }
  883.     private function logPassDifferences($oldContent$newContent): void
  884.     {
  885.         $this->writeLog("Début de la comparaison des passes");
  886.         $this->writeLog("Taille de l'ancien contenu : " strlen($oldContent) . " octets");
  887.         $this->writeLog("Taille du nouveau contenu : " strlen($newContent) . " octets");
  888.         // Comparer les 100 premiers octets
  889.         $this->writeLog("Premiers 100 octets de l'ancien contenu : " bin2hex(substr($oldContent0100)));
  890.         $this->writeLog("Premiers 100 octets du nouveau contenu : " bin2hex(substr($newContent0100)));
  891.         // Comparer les 100 derniers octets
  892.         $this->writeLog("Derniers 100 octets de l'ancien contenu : " bin2hex(substr($oldContent, -100)));
  893.         $this->writeLog("Derniers 100 octets du nouveau contenu : " bin2hex(substr($newContent, -100)));
  894.         // Comparer les métadonnées du pass
  895.         $oldMetadata $this->extractPassMetadata($oldContent);
  896.         $newMetadata $this->extractPassMetadata($newContent);
  897.         $this->writeLog("Anciennes métadonnées : " json_encode($oldMetadataJSON_PRETTY_PRINT));
  898.         $this->writeLog("Nouvelles métadonnées : " json_encode($newMetadataJSON_PRETTY_PRINT));
  899.         $diff $this->arrayRecursiveDiff($oldMetadata$newMetadata);
  900.         if (!empty($diff)) {
  901.             $this->writeLog("Différences dans les métadonnées : " json_encode($diffJSON_PRETTY_PRINT));
  902.         } else {
  903.             $this->writeLog("Aucune différence dans les métadonnées");
  904.         }
  905.         $this->writeLog("Fin de la comparaison des passes");
  906.     }
  907.     private function extractPassMetadata($content): array
  908.     {
  909.         $metadata = [];
  910.         $tempFile tempnam(sys_get_temp_dir(), 'pass_');
  911.         if ($tempFile === false) {
  912.             $this->writeLog("Erreur : Impossible de créer un fichier temporaire");
  913.             return $metadata;
  914.         }
  915.         try {
  916.             file_put_contents($tempFile$content);
  917.             $zip = new \ZipArchive();
  918.             if ($zip->open($tempFile) === TRUE) {
  919.                 if (($jsonData $zip->getFromName('pass.json')) !== false) {
  920.                     $metadata json_decode($jsonDatatrue);
  921.                     if (json_last_error() !== JSON_ERROR_NONE) {
  922.                         $this->writeLog("Erreur de décodage JSON : " json_last_error_msg());
  923.                     }
  924.                 } else {
  925.                     $this->writeLog("Fichier pass.json non trouvé dans l'archive");
  926.                 }
  927.                 $zip->close();
  928.             } else {
  929.                 $this->writeLog("Impossible d'ouvrir l'archive ZIP");
  930.             }
  931.         } catch (\Exception $e) {
  932.             $this->writeLog("Erreur lors de l'extraction des métadonnées : " $e->getMessage());
  933.         } finally {
  934.             if (file_exists($tempFile)) {
  935.                 unlink($tempFile);
  936.             }
  937.         }
  938.         return $metadata;
  939.     }
  940.     private function getExistingPassContent(User $user): ?string
  941.     {
  942.         $passPath $this->getParameter('kernel.project_dir') . '/var/passes/' $user->getSlug() . '.pkpass';
  943.         if (file_exists($passPath)) {
  944.             $content file_get_contents($passPath);
  945.             return $content !== false $content null;
  946.         }
  947.         $this->writeLog("Pas de pass existant trouvé pour l'utilisateur: " $user->getId());
  948.         return null;
  949.     }
  950.     #[Route('/api/v1/passes/v1/log'name'pass_log'methods: ['POST'])]
  951.     public function logPass(Request $request): JsonResponse
  952.     {
  953.         $content $request->getContent();
  954.         $this->writeLog('Pass log received: ' $content);
  955.         return new JsonResponse(nullResponse::HTTP_OK);
  956.     }
  957.     #[Route('/api/v1/passes/v1/devices/{deviceLibraryIdentifier}/registrations/{passTypeIdentifier}/{serialNumber}'name'unregister_device'methods: ['DELETE'])]
  958.     public function unregisterDevice(
  959.         string $deviceLibraryIdentifier,
  960.         string $passTypeIdentifier,
  961.         string $serialNumber,
  962.         Request $request,
  963.         EntityManagerInterface $entityManager
  964.     ): JsonResponse
  965.     {
  966.         $this->writeLog("Début de unregisterDevice pour device: $deviceLibraryIdentifier, passType: $passTypeIdentifier, serialNumber: $serialNumber");
  967.         // Vérification de l'authentification
  968.         $authToken $request->headers->get('Authorization');
  969.         if (!$this->isValidAuthenticationToken($authToken)) {
  970.             $this->writeLog("Authentification invalide pour unregisterDevice");
  971.             return new JsonResponse(['error' => 'Invalid authentication token'], Response::HTTP_UNAUTHORIZED);
  972.         }
  973.         try {
  974.             // Trouver l'utilisateur correspondant au serialNumber
  975.             $user $this->userRepository->findOneBy(['slug' => $serialNumber]);
  976.             if (!$user) {
  977.                 $this->writeLog("Utilisateur non trouvé pour serialNumber: $serialNumber");
  978.                 return new JsonResponse(['error' => 'User not found'], Response::HTTP_NOT_FOUND);
  979.             }
  980.             $card $user->getCard();
  981.             if (!$card) {
  982.                 $this->writeLog("Carte non trouvée pour l'utilisateur: " $user->getId());
  983.                 return new JsonResponse(['error' => 'Card not found'], Response::HTTP_NOT_FOUND);
  984.             }
  985.             // Vérifier si le deviceLibraryIdentifier correspond
  986.             if ($card->getDeviceLibraryIdentifier() !== $deviceLibraryIdentifier) {
  987.                 $this->writeLog("DeviceLibraryIdentifier ne correspond pas pour l'utilisateur: " $user->getId());
  988.                 return new JsonResponse(['error' => 'Device mismatch'], Response::HTTP_BAD_REQUEST);
  989.             }
  990.             // Supprimer les informations d'enregistrement
  991.             $card->setAppleWalletToken(null);
  992.             $card->setAppleWalletPushToken(null);
  993.             $card->setDeviceLibraryIdentifier(null);
  994.             // Mettre à jour la date de modification
  995.             $card->setUpdatedAt(new \DateTime());
  996.             $entityManager->persist($card);
  997.             $entityManager->flush();
  998.             $this->writeLog("Désinscription réussie pour l'utilisateur: " $user->getId());
  999.             return new JsonResponse(nullResponse::HTTP_OK);
  1000.         } catch (\Exception $e) {
  1001.             $this->writeLog("Erreur lors de la désinscription: " $e->getMessage());
  1002.             return new JsonResponse(['error' => 'An error occurred during unregistration'], Response::HTTP_INTERNAL_SERVER_ERROR);
  1003.         }
  1004.     }
  1005.     /**
  1006.      * @throws PKPassException
  1007.      */
  1008.     #[Route('/test-apple-wallet-flow/{userSlug}'name'test_apple_wallet_flow')]
  1009.     public function testAppleWalletFlow(string $userSlug): JsonResponse
  1010.     {
  1011.         $this->log('Début du test du flux Apple Wallet', ['userSlug' => $userSlug]);
  1012.         // Créer une requête avec un jeton CSRF valide
  1013.         $csrfToken $this->csrfTokenManager->getToken('add-to-apple-wallet');
  1014.         $request = new Request(['_token' => $csrfToken->getValue()]);
  1015.         // Simuler l'ajout d'un pass
  1016.         $addResponse $this->addToAppleWallet($userSlug$request);
  1017.         $this->log('Réponse de addToAppleWallet', ['status' => $addResponse->getStatusCode()]);
  1018.         // Simuler l'enregistrement d'un appareil
  1019.         $registerRequest = new Request([], [], [], [], [], [], json_encode(['pushToken' => 'test_push_token']));
  1020.         $registerResponse $this->registerDevice('test_device''pass.com.immybeauty.pkpass'$userSlug$registerRequest$this->entityManager);
  1021.         $this->log('Réponse de registerDevice', ['status' => $registerResponse->getStatusCode()]);
  1022.         // Simuler une vérification des mises à jour
  1023.         $checkUpdatesResponse $this->checkUpdates('test_device''pass.com.immybeauty.pkpass', new Request());
  1024.         $this->log('Réponse de checkUpdates', ['status' => $checkUpdatesResponse->getStatusCode(), 'content' => $checkUpdatesResponse->getContent()]);
  1025.         // Simuler une mise à jour du pass
  1026.         $updateResponse $this->updatePass($userSlug, new Request(), $this->entityManager);
  1027.         $this->log('Réponse de updatePass', ['status' => $updateResponse->getStatusCode(), 'content' => $updateResponse->getContent()]);
  1028.         // Simuler la récupération d'un pass mis à jour
  1029.         $getPassResponse $this->getPass('pass.com.immybeauty.pkpass'$userSlug, new Request());
  1030.         $this->log('Réponse de getPass', ['status' => $getPassResponse->getStatusCode()]);
  1031.         // Simuler la désinscription d'un appareil
  1032.         $unregisterResponse $this->unregisterDevice('test_device''pass.com.immybeauty.pkpass'$userSlug, new Request(), $this->entityManager);
  1033.         $this->log('Réponse de unregisterDevice', ['status' => $unregisterResponse->getStatusCode()]);
  1034.         $this->log('Fin du test du flux Apple Wallet');
  1035.         return new JsonResponse(['message' => 'Test du flux Apple Wallet terminé'], Response::HTTP_OK);
  1036.     }
  1037.     private function log(string $message, array $context = []): void
  1038.     {
  1039.         $this->logger->info($message$context);
  1040.         $this->writeLog($message ' ' json_encode($context));
  1041.     }
  1042.     private function generateQRCodeURLProfil(User $user): string
  1043.     {
  1044.         return $this->getParameter('APP_URL') . '/fidelite/mon-profil/' $user->getSlug();
  1045.     }
  1046.     // Créer une nouvelle méthode
  1047.     private function sendVisiblePushNotification(string $pushTokenstring $messagestring $passTypeIdentifier): array
  1048.     {
  1049.         $url "https://api.push.apple.com/3/device/$pushToken";
  1050.         try {
  1051.             $jwt $this->getJWT();
  1052.             $payload = [
  1053.                 'aps' => [
  1054.                     'alert' => [
  1055.                         'title' => 'IMMY BEAUTY',
  1056.                         'body' => $message
  1057.                     ],
  1058.                     'sound' => 'default'
  1059.                 ],
  1060.                 'passTypeIdentifier' => $passTypeIdentifier
  1061.             ];
  1062.             $headers = [
  1063.                 'apns-topic' => $passTypeIdentifier,
  1064.                 'apns-push-type' => 'alert',
  1065.                 'authorization' => 'bearer ' $jwt,
  1066.             ];
  1067.             $response $this->httpClient->request('POST'$url, [
  1068.                 'headers' => $headers,
  1069.                 'json' => $payload,
  1070.                 'http_version' => '2.0',
  1071.             ]);
  1072.             return [
  1073.                 'success' => $response->getStatusCode() === 200,
  1074.                 'status' => $response->getStatusCode(),
  1075.                 'content' => $response->getContent(false)
  1076.             ];
  1077.         } catch (\Exception $e) {
  1078.             return ['success' => false'error' => $e->getMessage()];
  1079.         }
  1080.     }
  1081. }