Módulo bigcommerce-app-adapter

escrito por un humano, no por la IA clock10 min

En la primera parte de este artículo hablamos de los mecanismos de extensión de BigCommerce y de cómo podemos crear y configurar nuestras propias Apps. También presentamos el módulo bigcommerce-app-adapter que nos soluciona una buena parte de la complejidad de construir una App para BigCommerce.

En esta segunda parte vamos a crear una App muy simple para demostrar:

  • Cómo se integra bigcommerce-app-adapter en una App
  • Cómo se usa el proxy incluido en bigcommerce-app-adapter
  • Cómo se puede desarrollar la App en un entorno local
  • Cómo se integra la App con BigCommerce

Nuestra App tiene una sola página: la página de inicio o home page. En está página vamos a mostrar un logotipo de nuestra empresa y un texto con el nombre de la tienda. Así de sencillo.

A pesar de esta aparente simplicidad, para que la App funcione y el nombre de la tienda se muestre tendremos que ser capaces de hacer una llamada a la API de BigCommerce que nos da este dato en particular (/v2/store). Si esta llamada funciona, hacer uso de cualquier otro end-point de la API de BigCommerce será ya solo un asunto de tener los permisos adecuados.

Nuestra aplicación estará escrita en PHP y usaremos el framework Laravel para crearla.

Creando nuestra App paso a paso

Nuestra primera aproximación será crear una App operativa que funcione en nuestro entorno de trabajo local y con Docker.

El primer paso será crear una nueva aplicación Laravel que llamaremos ‘bc-sample-app’:

curl -s https://laravel.build/bc-sample-app | bash

A continuación vamos a añadir el módulo bigcommerce-app-adapter a nuestra App:

cd bc-sample-app/

composer config repositories.ebolution-bigcommerce-app-adapter git https://github.com/ebolution/bigcommerce-app-adapter.git

composer require ebolution/bigcommerce-app-adapter:dev-master

Como veremos nuestro módulo arrastra una dependencia mas que es ebolution/laravel-module-manager. Arrancamos nuestra aplicación con Docker y Sail:

vendor/bin/sail up

Abrimos una nueva ventana de shell en el mismo directorio de la App y ejecutamos los comandos de instalación del módulo:

vendor/bin/sail artisan migrate

vendor/bin/sail artisan vendor:publish --tag=bigcommerce-app-adapter --ansi

Ahora vamos a crear el código de nuestra App. Nuestra App es muy simple y consta de lo siguientes elementos:

  • Una ruta (routes/web.php)
  • Un controlador (app/Http/Controllers/SampleHomeController.php)
  • Una vista (resources/views/home.blade.php)
  • Un script en JS (public/js/home.js)
  • Una hoja de estilos (public/css/styles.css)
  • Una imagen (public/img/logo.png)

Para la ruta editamos el archivo routes/web.php dejando este contenido:

<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\SampleHomeController;

Route::get('/', SampleHomeController::class);

Para el controlador creamos un nuevo controlador (vendor/bin/sail artisan make:controller SampleHomeController) con este código:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\View\View;

class SampleHomeController extends Controller
{
    public function __invoke(Request $request): View
    {
        $token = $request->input('token');
        return view('home', compact('token'));
    }
}

Como vemos el controlador es muy sencillo y simplemente muestra la vista de la home pasándola un único dato que recibe como parámetro de entrada (token). Más adelante hablaremos de la importancia de este parámetro. De momento lo vamos a ignorar.

La vista de nuestra home es muy sencilla, muestra una imagen y un texto centrados en el view-port:

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">

        <title>{{ config('app.name') }}</title>

        <!-- Fonts -->
        <link rel="preconnect" href="https://fonts.bunny.net">
        <link href="https://fonts.bunny.net/css?family=figtree:400,600&display=swap" rel="stylesheet" />

        <!-- Styles -->
        <link href="{{ asset('css/styles.css') }}" rel="stylesheet" />
    </head>
    <body>
        <div class="container">
            <div class="data">
                <img class="image" src="{{ asset('img/logo.png') }}" alt="centered-image">
                <div id="store-name"></div>
            </div>
        </div>
        <script>
            var auth_token = '{{ $token }}'
        </script>
        <script defer src="{{ asset('js/home.js') }}"></script>
    </body>
</html>

El token que se le pasa desde el controlador se publica como una variable JS (auth_token). De momento solo es necesario tener en cuenta que este dato vendrá con valor null.

Nuestro script es el que va a hacer la llamada al proxy incluido dentro de bigcommerce-app-adapter para obtener le nombre de la tienda. Si la llamada tiene éxito, se mostrará el dato recibido en el contenedor store-name.

window.onload = function () {
    fetch('/api/bc-api/v2/store', {
        headers: {
            'X-Auth-Token': auth_token
        }
    }).then(function (response) {
        return response.json();
    }).then(function (data) {
        let container = document.getElementById("store-name");
        container.innerHTML = data.name;
    }).catch(function (err) {
        console.warn('Something went wrong.', err);
    });
};

Vemos que hacemos una llamada a la ruta /api/bc-api/v2/store que representa una llamada a https://api.bigcommerce.com/stores/{{STORE_HASH}}/v2/store a través del proxy, dado que esta llamada a Get Store Information nos devuelve el nombre de la tienda, entre otros detalles.

Nuestra hoja de estilos simplemente se encarga de que las cosas estén centradas:

html, body {
    height: 100%;
    margin: 0;
    padding: 0;
}
.container {
    height: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
}
.data {
    display: block;
    text-align: center;
}
.image {
    max-width: 100%;
    max-height: 100%;
}

Nos faltaría la imagen (el logo de la App) que sería un archivo PNG cualquiera, con una resolución lo suficientemente pequeña para que entre en nuestro view-port (ej. aprovechamos el logo de la App para BigCommerce que es de 350×130 px).

Bien, ya tenemos nuestra App. Si tratamos de cargar la página de inicio (http://localhost), veremos esto:

Nuestra App se muestra, pero el nombre de la tienda no aparece. Si miramos los mensaje de la consola veremos un error:

Failed to load resource: the server responded with a status of 500 (Internal Server Error) /api/bc-api/v2/store:1

Básicamente nuestro problema es que no tenemos credenciales válidas para hacer llamadas autorizadas a BigCommerce y tampoco hemos configurado ningún store_hash para hacer la llamada sobre una tienda en concreto.

Desarrollo local

En nuestra primera aproximación queremos poder desarrollar nuestra App sin necesidad de que sea realmente una App de BigCommerce instalada en una tienda. Sí necesitamos, no obstante, que nuestro proxy de BigCommerce haga llamadas a una tienda real de BigCommerce (seguramente un Sandbox) para trabajar con datos reales.

El módulo bigcommerce-app-adapter tiene un modo de desarrollo local en el que no es necesario que se invoque como una App de BigCommerce. Para activarlo hay que fijar un parámetro en nuestro .env:

APP_ENV=local

Cuando la aplicación trabaja en modo local, hay otra serie de variables del .env que nuestro módulo va a usar para comunicarse con BigCommerce:

BC_LOCAL_CLIENT_ID=<api-account-client-id>
BC_LOCAL_SECRET=<api-account-client-secret>
BC_LOCAL_ACCESS_TOKEN=<api-account-access-token>
BC_LOCAL_STORE_HASH=<api-account-store-hash>

Para poder asignar un valor a estas variables, debemos ir a nuestra tienda Sandbox y crear una clave de API para desarrolladores: Settings > Store-level API accounts > Create API account

Los permisos (OAuth scopes) que le demos a la clave dependerán de las llamadas que queramos hacer a la API y ya describimos en la primera parte del artículo como deducir que permisos necesitamos. Para nuestra sencilla App solo necesitamos acceso de lectura a la información y configuración básica de la tienda:

Al guardar esta clave BigCommerce descargará un fichero con un contenido similar a este:

ACCESS TOKEN: zoxzovn4u2m9bf1qidspe2aph5rhyhw
CLIENT NAME: bc-sample-app
CLIENT ID: hajgwgcb6co3176l6iyslswpk34oh6m
CLIENT SECRET: 46e123dc2435c13a969097468f8207a3ebdbee1c09c40aaea29028d8dd82c032
NAME: bc-sample-app
API PATH: https://api.bigcommerce.com/stores/23uu8ke7g0/v3/

Esta información es la necesaria para configurar nuestras variables de entorno:

BC_LOCAL_CLIENT_ID=hajgwgcb6co3176l6iyslswpk34oh6m
BC_LOCAL_SECRET=46e123dc2435c13a969097468f8207a3ebdbee1c09c40aaea29028d8dd82c032
BC_LOCAL_ACCESS_TOKEN=zoxzovn4u2m9bf1qidspe2aph5rhyhw
BC_LOCAL_STORE_HASH=23uu8ke7g0

Nótese que el store_hash es una de las partes del API PATH.

Una vez configuradas estas variables, si recargamos la página deberíamos ver ya el nombre de nuestra tienda:

Pruebas de integración

Aunque el recurso anterior es útil para desarrollar en local y desentendernos de la capa de complejidad adicional que supone trabajar como una verdadera App de BigCommerce, lo cierto es que de esta forma tenemos acceso a un único store de BigCommerce.

Recuerda que una App es multi-tenant por naturaleza. Eso significa que estará alojada en un servidor y dando servicio a un número indeterminado de tiendas. Cada una de las tiendas requiere de unos credenciales de acceso diferentes, y además tendrá que ser BigCommerce el que proporcione estos credenciales durante el dialogo de instalación de la App en la tienda.

En algún momento tendremos que pasar de nuestro ambiente de desarrollo puramente local a una prueba integrada con BigCommerce.

En la primera parte de este artículo se describió como crear y configurar nuestro borrador de aplicación en BigCommerce. Vamos a ver ahora como podemos hacer una prueba de nuestra App integrada con BigCommerce sin renunciar a trabajar en un entorno de desarrollo local basado en Docker.

Para ello la clave será una herramienta que se denomina ngrok. Esta utilidad crea un túnel desde Internet a un puerto local en nuestra computadora. Eso permite que un puerto como localhost:80 sea accesible a través de una URL pública de Internet.

Si no dispones de la herramienta, revisa la documentación para la mejor forma de instalarla en tu equipo. Cuando ya la tengas disponible ejecútala con el siguiente comando:

ngrok http 80

Esto hace que tanto https://0f06-185-248-78-225.ngrok-free.app como http://0f06-185-248-78-225.ngrok-free.app estén redirigidas a nuestro puerto local 80, que es donde lanzaremos nuestro contenedor Docker con la aplicación Laravel.

Ahora tenemos que hacer un pequeño cambio en el archivo de configuración de entorno (.env) para indicar que ya no estamos en modo local:

APP_ENV=ngrok
APP_URL=https://0f06-185-248-78-225.ngrok-free.app

Tras hacer este cambio volvamos a levantar los contenedores:

vendor/bin/sail up

Y también vamos a hacer un cambio en el comportamiento de Laravel para que nos permita usar sin problemas un esquema de direcciones basado en HTTPS. En app/Providers/AppServiceProvider.php incluir este código en el método boot():

public function boot(\Illuminate\Routing\UrlGenerator $url): void
{
    if (env('APP_ENV') !== 'local') {
        $url->forceScheme('https');
    }
}

Si lanzamos nuestra aplicación en https://0f06-185-248-78-225.ngrok-free.app nos aparecerá un aviso indicando que estamos accediendo a través de un túnel y al aceptar volveremos a ver nuestra aplicación pero sin el nombre de la tienda y con el error en la consola al tratar de acceder al Proxy.

Para que el proxy funcione deberemos hacer dos cosas. Primero, en la definición del perfil de la aplicación de BigCommerce indicar las nuevas URLs de los callbacks, usando las URLs generadas por ngrok. Para ello iremos al Developer Portal buscaremos nuestra App y pulsaremos el botón de Edit App:

Cuando hayamos terminado de editar las URLs pulsaremos Update & close. A continuación pulsaremos en View Client ID para obtener los parámetros que necesitamos para el segundo paso.

Los valores obtenidos debemos incluirlos en nuestro .env en las siguientes variables:

BC_APP_CLIENT_ID=<your-app-client-id>
BC_APP_SECRET=<your-app-client-secret>

Finalmente para probar nuestra App tendremos que instalarla en una tienda. Para ello inicia sesión en el panel de una tienda y vete a Apps > My Apps > My Draft Apps

Tu aplicación debería aparecer listada aquí:


Pulsa en el logo de la App, luego en el botón Install y finalmente en Confirm:

Si has ejecutado bien todos los pasos deberías ver la App mostrando el nombre de la tienda:

Si miras el contenido de la tabla bc_authirzed_users de tu base de datos local, comprobarás que se ha creado un registro para un store_hash determinado y para el owner de esa tienda. Este registro contiene el access_token que se está usando para pedirle a BigCommerce los datos de configuración básicos de la tienda.

Tokens JWT

¡¡Enhorabuena!! Tienes todas las piezas para desarrollar tu App en un entorno dockerizado, pero pudiendo probar de forma integrada con BigCommerce. Ha llegado la hora de hablar de aquél token que decidimos ignorar hace unos cuantos párrafos.

Vimos como nuestro controlador recibía un parámetro de entrada token, que luego en la vista convertía en una variable Javascript auth_token y que el contenido de esta variable se enviaba en una cabecera X-Auth-Token en las llamadas al proxy. Ésto es lo primero que hay que comprender:

  • El token se nos entrega al cargarse la aplicación.
  • Debemos hacerlo persistir en nuestra aplicación.
  • Debemos enviarlo en las futuras llamadas al proxy de BigCommerce.

De hecho la forma de hacer persistir el token que hemos empleado en el ejemplo no sería para nada la recomendada en un caso de uso genérico. Se debería guardar en una cookie o en Local Storage.

Como puedes suponer uno de los propósitos del token es que el proxy de BigCommerce pueda validar que las llamadas que se le hacen son lícitas, ya que solo el módulo bigcommerce-app-adapter puede generar tokens que luego él mismo reconozca como válidos.

Pero para ese propósito podríamos haber generado una clave de API cualquiera. Lo importante de un token JWT es que además nos provee de un mecanismo para adjuntar información.

Este es un ejemplo de token JWT:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwic3ViIjoiMjNrZTd1dThnMCIsImlzcyI6IkxhcmF2ZWwiLCJpYXQiOjE2OTI3MjI3MzksImV4cCI6MTY5Mjc1MTUzOX0.1TczhZx25HUSovWiORdMORfIFcyFYBf_MWl-0AYHDCw

Aunque inicialmente parece un secreto criptográfico, no lo es. Este token está firmado, pero no encriptado. Por lo demás simplemente se ha codificado usando BASE64URL que es un algoritmo reversible.

¿Cuál es la diferencia? Para decodificar un token encriptado necesitaríamos la clave (simétrica) o una parte de la clave (asimétrica). Un token firmado, por el contrario, no exige que conozcamos ningún secreto para poder decodificarlo. Sin embargo contiene una firma digital que nos impide alterar el contenido del token sin hacerlo inválido. Así que con una firma criptográfica sabemos: 1) que el token fue generado por la entidad que creo la firma (o por otra que conozca la clave); 2) que el contenido del token no se ha alterado.

Si copiamos el token anterior y lo pegamos en esta página podremos ver el contenido en claro del mismo:

El secreto que se ha usado para generar este token es ‘sample-insecure-signature-secret’. Puedes pegarlo en your-256-bit-secret y entonces te dirá que la firma es válida.

El token tiene tres partes (si te fijas en la estructura del mismo hay dos ‘.’ que delimitan estas tres partes). La cabecera dice el algoritmo que se ha usado para firmar el token. La segunda y la que mas nos interesa es el payload o datos que incluye el token. La firma es la tercera parte.

Como hemos dicho estos datos no están encriptados y podríamos recuperarlos en nuestra aplicación con una sentencia como:

$payload = json_decode(base64_decode(str_replace('_', '/', str_replace('-','+',explode('.', $token)[1]))));

Entendamos el significado de los datos que bigcommerce-app-adapter incluye en el token:

  • id: Es un identificador numérico único que representa al usuario administrador de BigCommerce que está usando la App.
  • sub: (subject) Es un identificador único de la tienda desde la que se está usando la App.
  • iss: (issuer): Identifica a la aplicación que generó el token (en Laravel se toma el parámetro APP_NAME)
  • iat: (issued at) Indica el instante de tiempo en que se generó el token (formato timestamp de Unix)
  • exp: (expires) Indica el instante de tiempo en que el token expira (formato timestamp de Unix)

Primera cosa que aprendemos: los tokens expiran. Podemos validar si el nuestro ya ha expirado en cualquier momento. El periodo de validez de los tokens que genera bigcommerce-app-adapter se puede configurar a través de token_duration_hours en config/bigcommerce-app-adapter.php. Cuando el token expira, es como si la sesión de la App de BigCommerce se cancelase. El usuario tendrá que volver a cargar la App para que se genere un token nuevo.

Gestión multi-tenant

El token también es importante para que nuestra App pueda gestionar los aspectos multi-tenant.

Repetimos que una App de BigCommerce debe ser diseñada como multi-tenant y que el caso de que haya una sola tienda en la que esté instalada (App llave en mano para un cliente) no deja de ser un caso de uso particular.

Para que nuestra App almacene o recupere datos de un tenant concreto, debemos basarnos en dos de los campos que van incluidos en el token:

  • Cuando tengamos datos que van asociados a un usuario, debemos asociarlos al campo id, que es un identificador único que representa a un usuario administrador concreto de una tienda en particular.
  • Si por el contrario los datos deben estar asociados a nivel de tienda, entonces debemos usar el campo sub.

Podemos además extraer información adicional sobre el usuario como su e-mail y su id en BigCommerce (no tiene por que ser único). Para ello debemos acceder a la tabla bc_authorized_users con el campo id = $id o usar el modelo Eloquent Ebolution\BigcommerceAppAdapter\Infrastructure\Repositories\BCAuthorizedUser (si tu aplicación usa arquitectura hexagonal usa en su lugar Ebolution\BigcommerceAppAdapter\Domain\BCAuthorizedUser).

Conclusión

Siguiendo estos pasos puedes crear tu primera aplicación para BigCommerce, evitándote gran parte de la complejidad que ello supone gracias al módulo bigcommerce-app-adapter. Tendrás no obstante que desarrollar las vistas, controladores, modelos y demás elementos de la funcionalidad de tu App propiamente dicha.

Puedes desarrollar el dashboard de tu App de forma local abstrayéndote totalmente de la capa de BigCommerce. Cuando necesites integrar tu App con BigCommerce, puedes definir un perfil de aplicación en modo borrador y continuar con un modelo de desarrollo local pero de forma integrada con BigCommerce. El dashboard de tu App dentro del back-office de BigCommerce se muestra como un iframe dentro del panel de control de BigCommerce.

bigcommerce-app-adapter te permite gestionar las llamadas a la API de BigCommerce tanto en un modo de desarrollo local como en un modo de desarrollo integrado. También te da la información base para que puedas realizar la gestión multi-tenant.

No lo hemos cubierto en este artículo, pero es de suponer que desarrolles también una librería JavaScript que, partiendo del conocimiento de la estructura del DOM de tu plantilla, gestione los eventos que sean oportunos para que la funcionalidad de la App repercuta sobre el funcionamiento del front-office de la tienda.

Una App bien diseñada debería ser transparente para el desarrollador de front-end de la tienda, inyectando un script en el código HTML de la tienda y creando hooks a las funciones de la App vinculados con eventos sobre los elementos del DOM de tu plantilla.

¿Has usado bigcommerce-app-adapter y tienes sugerencias de ampliación o mejora? Eres bienvenido a contribuir con una pull request. También puedes contactar con Ebolution a través de https://www.ebolution.com/contacto/ o puedes escribirme directamente a carlos.cid@ebolution.com