Visualización de los pedidos de LemonSqueezy en la página de cuenta
Keep on Learning!
If you liked what you've learned so far, dive in! Subscribe to get access to this tutorial plus video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeÚltimamente hemos pedido un montón de limonada digital, pero no tenemos una forma cómoda de ver esos pedidos. ¿No sería genial poder ver una lista de ellos en la página de nuestra cuenta? Ahora que hemos establecido una relación entre la entidad User
y el cliente LemonSqueezy, ¡podemos hacerlo!
API de LemonSqueezy para obtener pedidos
Empieza abriendo src/Store/LemonSqueezyApi.php
. Añade un nuevo método -public function listOrders()
- y devuelve un array
.
// ... lines 1 - 11 | |
final readonly class LemonSqueezyApi | |
{ | |
// ... lines 14 - 96 | |
public function listOrders(): array | |
{ | |
} | |
} |
Esta función obtendrá los pedidos de la API de LemonSqueezy. Si nos dirigimos a la documentación de LemonSqueezy, en "Listar todos los pedidos", podemos ver que necesitamos utilizar una petición GET
a la ruta/orders
.
De vuelta a nuestro nuevo método, añade $response = $this->client->request()
, y dentro,Request::METHOD_GET
a la ruta orders
. Ahora, devuelve $response->toArray()
.
// ... lines 1 - 96 | |
public function listOrders(): array | |
{ | |
$response = $this->client->request(Request::METHOD_GET, 'orders'); | |
return $response->toArray(); | |
} | |
// ... lines 103 - 104 |
Pero espera... no queremos mostrar todos los pedidos -sólo los de nuestra tienda y el usuario actual-, así que tenemos que añadir algunos parámetros de consulta adicionales para filtrar esta lista.
Añadir parámetros de consulta de filtrado
Añadamos un array vacío como tercer argumento al método request()
, y dentro escribamos 'query' => []
, 'filter' => []
, 'store_id' => $this->storeId
, y 'user_email' => $user->getEmail()
. También tenemos que añadir User $user
al métodolistOrders()
anterior. ¡Perfecto!
// ... lines 1 - 96 | |
public function listOrders(User $user): array | |
{ | |
$response = $this->client->request(Request::METHOD_GET, 'orders', [ | |
'query' => [ | |
'filter' => [ | |
'store_id' => $this->storeId, | |
'user_email' => $user->getEmail(), | |
], | |
], | |
]); | |
// ... lines 107 - 108 | |
} | |
// ... lines 110 - 111 |
A continuación, abre UserController.php
. Aquí abajo, en account()
, inyectaLemonSqueezyApi $api
. También necesitamos el usuario actual, así que añade#[CurrentUser] User $user
. Abajo, crea la variable $orders
y ponla en$api->listOrders()
. Por último, en return
, pasa 'orders' => $orders
.
// ... lines 1 - 7 | |
use App\Store\LemonSqueezyApi; | |
// ... lines 9 - 15 | |
use Symfony\Component\Security\Http\Attribute\CurrentUser; | |
// ... line 17 | |
class UserController extends AbstractController | |
{ | |
// ... lines 20 - 53 | |
public function account(LemonSqueezyApi $api, #[CurrentUser] User $user): Response | |
{ | |
$orders = $api->listOrders($user); | |
return $this->render('user/account.html.twig', [ | |
'orders' => $orders, | |
]); | |
} | |
} |
Representación de pedidos y estilo CSS de Tailwind
Ahora tenemos que procesar esos pedidos Abre la plantilla account.html.twig
... y en algún lugar debajo de {{ app.user.email }}
, pega este código boilerplate con algo de estilo CSS de Tailwind. Puedes copiarlo de los bloques de código que hay debajo del vídeo.
// ... lines 1 - 4 | |
{% block content %} | |
// ... lines 6 - 9 | |
{% if app.user.lsCustomerId %} | |
<div class="absolute"></div> | |
<div class="relative overflow-x-auto rounded-2xl border-2 border-[#4F272B] mt-7"> | |
<table class="table-fixed w-full text-sm text-left rtl:text-right"> | |
<thead class="poppins-bold uppercase"> | |
<tr> | |
<th scope="col" class="px-6 py-3 border-b border-[#4F272B]"> | |
Order | |
</th> | |
<th scope="col" class="px-6 py-3 border-b border-[#4F272B]"> | |
Date | |
</th> | |
<th scope="col" class="px-6 py-3 border-b border-[#4F272B]"> | |
Amount | |
</th> | |
<th scope="col" class="px-6 py-3 border-b border-[#4F272B]"> | |
Action | |
</th> | |
</tr> | |
</thead> | |
<tbody> | |
{% for order in orders.data %} | |
<tr class="hover:bg-gray-50 text-lg poppins-regular"> | |
<td class="px-6 py-4"> | |
Order | |
</td> | |
<td class="px-6 py-4"> | |
Date | |
</td> | |
<td class="px-6 py-4"> | |
Amount | |
</td> | |
<td class="flex items-center px-6 py-4 text-sm"> | |
<a href="#view" target="_blank" class="uppercase rounded-[60px] border-2 border-[#50272B] bg-white hover:bg-slate-100 shadow-inner poppins-bold py-1 px-4">View</a> | |
</td> | |
</tr> | |
{% endfor %} | |
</tbody> | |
</table> | |
</div> | |
{% else %} | |
<div class="rounded-2xl py-7 px-7 border-2 border-[#4F272B] w-[450px] mt-7 flex flex-col justify-center"> | |
<p class="text-center mb-5 poppins-regular text-lg">No orders yet</p> | |
<div class="mt-3 mb-3 mx-auto"><a class="rounded-[60px] border-2 border-[#50272B] bg-[#FBD509] hover:bg-[#E2BC00] shadow-inner py-3 px-5 w-full poppins-bold uppercase" href="{{ path('app_homepage') }}">View Lemonade Options</a></div> | |
</div> | |
{% endif %} | |
// ... lines 56 - 57 | |
{% endblock %} |
Como no queremos que todo el mundo vea nuestros pedidos, tenemos que mostrar la lista sólo si app.user.lsCustomerId
está activado. Si lo está, se mostrará una tabla de pedidos. Si no, sólo mostraremos un mensaje "Todavía no hay pedidos".
Veamos lo que tenemos hasta ahora Volvemos a nuestro sitio, actualizamos la página y... ¡voilá! ¡Nuestra lista de pedidos llena de datos ficticios es visible! Pronto tendremos que sustituir estos datos ficticios por pedidos reales, pero antes, ve aUserController::account()
y dd()
la variable $orders
. Actualiza de nuevo, y... ahí están nuestros "datos" con una matriz de pedidos.
Utilizar datos dinámicos en la tabla de pedidos
Si hacemos clic en "atributos", veremos un montón de campos que podemos utilizar para nuestra tabla de pedidos. El primero que cogeré es order_number
. En nuestro código, sustituye Order
por #{{ order.attributes.order_number }}
. Para Date
, sustitúyelo por{{ order.attributes.created_at|date('d M Y, H:i') }}
. Aquí estamos utilizando el filtro de fecha para que la fecha sea más fácil de leer. Tenemos varias opciones para elegir el campo Amount
. Yo utilizaré total_formatted
porque está preformateado por LemonSqueezy. Por último, para nuestro enlace, escribiremos{{ order.attributes.urls.receipt }}
.
// ... lines 1 - 4 | |
{% block content %} | |
// ... lines 6 - 9 | |
{% if app.user.lsCustomerId %} | |
// ... lines 11 - 12 | |
<table class="table-fixed w-full text-sm text-left rtl:text-right"> | |
// ... lines 14 - 29 | |
<tbody> | |
{% for order in orders.data %} | |
<tr class="hover:bg-gray-50 text-lg poppins-regular"> | |
<td class="px-6 py-4"> | |
#{{ order.attributes.order_number }} | |
</td> | |
<td class="px-6 py-4"> | |
{{ order.attributes.created_at|date('d M Y, H:i') }} | |
</td> | |
<td class="px-6 py-4"> | |
{{ order.attributes.total_formatted }} | |
</td> | |
<td class="flex items-center px-6 py-4 text-sm"> | |
<a href="{{ order.attributes.urls.receipt }}" target="_blank" class="uppercase rounded-[60px] border-2 border-[#50272B] bg-white hover:bg-slate-100 shadow-inner poppins-bold py-1 px-4">View</a> | |
</td> | |
</tr> | |
{% endfor %} | |
</tbody> | |
</table> | |
// ... lines 49 - 54 | |
{% endif %} | |
// ... lines 56 - 57 | |
{% endblock %} |
Antes de probarlo, vuelve a UserController.php
y elimina el dd()
que añadimos antes.
Paginar los pedidos
Lemon Squeezy devuelve 10 pedidos por defecto. Si quieres ver menos de diez a la vez, podemos paginar la lista añadiendo 'page' => ['size' => 5]
a la consulta. Si volvemos atrás y actualizamos... ¡ahora sólo se muestran los 5 últimos pedidos! ¡Funciona!
// ... lines 1 - 11 | |
final readonly class LemonSqueezyApi | |
{ | |
// ... lines 14 - 96 | |
public function listOrders(User $user): array | |
{ | |
$response = $this->client->request(Request::METHOD_GET, 'orders', [ | |
'query' => [ | |
// ... lines 101 - 104 | |
'page' => [ | |
'size' => 5, // @see https://docs.lemonsqueezy.com/api/getting-started/requests#pagination | |
], | |
], | |
]); | |
// ... lines 110 - 111 | |
} | |
} |
Lo ideal sería añadir una paginación real a continuación para que los usuarios puedan navegar por todos los pedidos sin salir de nuestro sitio, pero por ahora, vamos a añadir un enlace a la lista completa de pedidos en LemonSqueezy.
En la plantilla, añade un enlace, con el href:https://app.lemonsqueezy.com/my-orders/{{
(orders.data|first).attributes.identifier|default('') }}, target: _blank
, y text: More Orders
.
Ese enlace llevará al usuario a LemonSqueezy y mostrará todos sus pedidos, con el primer pedido preseleccionado.
Pero no necesitamos ver este enlace todo el tiempo: sólo si el usuario tiene más de cinco pedidos. Volvamos a dd($orders)
, actualicemos e inspeccionemos los datos. En meta
, page
, vemos total
y perPage
, así que volvamos atrás, eliminemos dd()
, y envolvamos el enlace con{% if
orders.meta.page.total > orders.meta.page.perPage %}. Arreglaré este espaciado y añadiré {% endif %}
al final.
// ... lines 1 - 4 | |
{% block content %} | |
// ... lines 6 - 9 | |
{% if app.user.lsCustomerId %} | |
// ... lines 11 - 49 | |
{% if orders.meta.page.total > orders.meta.page.perPage %} | |
<p> | |
<a href="https://app.lemonsqueezy.com/my-orders/{{ (orders.data|first).attributes.identifier|default('') }}" target="_blank">More Orders</a> | |
</p> | |
{% endif %} | |
// ... lines 55 - 59 | |
{% endif %} | |
// ... lines 61 - 62 | |
{% endblock %} |
Bien, si actualizamos de nuevo y hacemos clic en el enlace... vemos los detalles del último pedido, pero... ¿dónde están los demás? Esto parece ser actualmente una limitación de LemonSqueezy cuando está en modo de prueba. En producción, también se mostrarían todos los pedidos del cliente.
Evitar filtraciones
Bien, ahora vamos a centrar nuestra atención en un pequeño problema de seguridad. De momento, estamos filtrando los pedidos por el correo electrónico que los usuarios han registrado en nuestro sitio. Pero, en teoría, los usuarios podrían cambiar su correo electrónico por otro que no posean. Para evitarlo, tenemos que utilizar el correo electrónico establecido en el cliente LemonSqueezy, no en nuestra entidad User
. Dentro de LemonSqueezyApi.php
, añade un nuevopublic function
y llámalo retrieveCustomer()
con string $customerId
, devuelve array
. Dentro, escribe$response = $this->client->request(Request::METHOD_GET, 'customers/'
. $customerId). Debajo, return
$response->toArray().
// ... lines 1 - 11 | |
final readonly class LemonSqueezyApi | |
{ | |
// ... lines 14 - 113 | |
public function retrieveCustomer(string $customerId): array | |
{ | |
$response = $this->client->request(Request::METHOD_GET, 'customers/' . $customerId); | |
return $response->toArray(); | |
} | |
} |
Arriba, en listOrders()
, añade $lsCustomerId = $user->getLsCustomerId()
. Luego, if (!$lsCustomerId)
, return []
. Esto garantiza que no haya forma de que un usuario pueda listar pedidos si no tiene un ID de cliente de LemonSqueezy.
// ... lines 1 - 96 | |
public function listOrders(User $user): array | |
{ | |
$userEmail = $user->getEmail(); | |
if ($user->getLsCustomerId()) { | |
$lsCustomer = $this->retrieveCustomer($user->getLsCustomerId()); | |
$userEmail = $lsCustomer['data']['attributes']['email']; | |
} | |
// ... lines 104 - 117 | |
} | |
// ... lines 119 - 127 |
A continuación, escribe $lsCustomer = $this->retrieveCustomer($lsCustomerId)
. Por último, cambia el filtro user_email
por $lsCustomer['data']['attributes']['email']
.
// ... lines 1 - 96 | |
public function listOrders(User $user): array | |
{ | |
// ... lines 99 - 104 | |
$response = $this->client->request(Request::METHOD_GET, 'orders', [ | |
'query' => [ | |
'filter' => [ | |
// ... line 108 | |
'user_email' => $userEmail, | |
], | |
// ... lines 111 - 113 | |
], | |
]); | |
// ... lines 116 - 117 | |
} | |
// ... lines 119 - 127 |
¡Seguridad reforzada!
¡Y eso es todo! Has renderizado con éxito una lista de pedidos en la página de la cuenta utilizando la API de LemonSqueezy.
A continuación: Hagamos algunas mejoras en la gestión de errores de nuestra API, porque se está volviendo molesto depurar manualmente los errores.