# ZimmerBiomet v2 ###### tags: `active` Esta API permite consultar los PDFs y vídeos que se tienen que mostrar en la nueva app gestionable de Zimmer. Las peticiones se realizarán a la URL del servidor web seguido de la petición que se quiere realizar: http://zbdental.com/[path-to-api]/[petición]. Por ejemplo: http://zbdental.com/[path-to-api]/step-1. Para poder hacer una previsualización de los cambios, se crearan dos entornos de desarrollo: * PRODUCCIÓN: URL zbdental.com/[path-to-api], con la versión del contenido a la que acceden los usuarios para actualizar el contenido de la app. * DESARROLLO: URL test.zbdental.com/[path-to-api], con la versión del contenido a la que acceden los administradores para comprobar como quedan los cambios antes de pasarlos a producción. La respuesta siempre será en formato `JSON`. Para identificar el usuario, se creará un token único para cada usuario. Este token se enviará a la API en las consultas mediante la cabecera HTTP en el campo `Authorization`. Para identificar el idioma que utiliza el usuario, se enviará la cabecera HTTP `Accept-Language` con el código del idioma utilizado (en, es, it,...). Si un usuario no tiene permisos para acceder a una petición, la respuesta enviará un código HTTP `401` indicando a la app que de debe cerrar la sesión. ## 1. API ### 1.1. register-device/{uuid} [POST] Añadir un dispositivo a las notificaciones push. En el [punto 2](https://hackmd.io/ZWgOiy4aT-eTeoFzaD8lqQ#2-Notificaciones-push) de esta documentación, se explica detalladamente el funcionamiento de las notificaciones push. Se deberá guardar/actualizar en la base de datos el idioma usado por el dispositivo con este `uuid`. El idioma se indica en la cabecera `Accept-Language`. #### URL - **uuid:** identificador el dispositivo #### Header * **Authorization:** identificador único del usuario * **Accept-Language:** código del idioma utilizado en la app #### Params - **push_token:** token para enviar notificaciones push - **platform:** plataforma del dispocitivo `android|ios` - **model:** modelo del dispositivo - **os_version:** versión del sistema operativo del disposotiva - **app_version:** versión de la aplicación que tiene insal·lada el dispositivo #### Response - **error:** Si se ha producido un error, es el mensaje que se deberá mostrar al usuario (por ejemplo: error en el servidor).`false` si no se ha producido ningún error ### 1.2. register-device/{uuid} [DELETE] Eliminar un dispositivo de las notificaciones push. #### URL - **uuid:** identificador el dispositivo #### Response - **error:** Si se ha producido un error, es el mensaje que se deberá mostrar al usuario (por ejemplo: error en el servidor).`false` si no se ha producido ningún error ### 1.3. settings [GET] Obtener los logos configurables de la app y los idiomas que estan disponibles en la interficie. #### Response - **error:** Si se ha producido un error, es el mensaje que se deberá mostrar al usuario (por ejemplo: error en el servidor).`false` si no se ha producido ningún error - **settings:** imágenes de la interfaz - **languages:** idiomas disponibles en la interfaz ```javascript { "settings": { "logo-login": URL del logo que aparece en la pantalla de login, "logo-main": URL del logo que aparece en la cabecera de la pagina de seciones, "logo-dark": URL del logo que aparece en la cabecera de las pantallas con cabecera blanca, "logo-light": URL del logo que aparece en la cabecera de las pantallas con cabecera azul, }, "languages": [ { "lang": "en", "name": "English" }, { "lang": "es", "name": "Castellano" }, { "lang": "fr", "name": "Français" }, { "lang": "de", "name": "Deutsch" }, { "lang": "it", "name": "Italiano" }, { "lang": "pt", "name": "Português" }, { "lang": "nl", "name": "Nederlands" } ] } ``` ### 1.4. sign-in [POST] Iniciar sesión. #### Params - **email:** correo electrónico - **password:** contraseña #### Response - **error:** Si se ha producido un error, es el mensaje que se deberá mostrar al usuario (por ejemplo: constraseña incorrecta).`false` si no se ha producido ningún error - **user:** array con la información del usuario ```javascript { "error": false, "user": { "token": identificador único del usuario, "name": nombre del usuario } } ``` ### 1.5. check-access [GET] Petición para saber si se debe forzar la actualización de los datos **por cambios en los permisos**. #### Header * **Authorization:** identificador único del usuario #### Response * **force_content:** true | false indicando se debe forzar la actulización - ==**regions:** regiones a las que tiene acceso el usuario== ```javascript { "error": false, "force_content": true, "regions": [ { "id": 1, "name": [ { "lang": código idoma en formato dos letras (es, en, it,..), "name": nombre de la región }, ... ] }, ... ] } ``` ### 1.6. update-language/{uuid} [GET] Se deberá guardar/actualizar en la base de datos el idioma usado por el dispositivo con este `uuid`. El idioma se indica en la cabecera `Accept-Language`. #### Header * **Authorization:** identificador único del usuario * **Accept-Language:** código del idioma utilizado en la app #### Response ```javascript { "error": false } ``` ### 1.7. configuration [GET] Indica el resumen de la actualización. Servirá para comprobar si el proceso de actualización está a punto para llevarse a cabo. Los datos enviados, harán referencia únicamente a los contenidos a los cuales tiene acceso el usuario. #### Header * **Authorization:** identificador único del usuario #### Response * **bytes:** peso del `JSON` devuelto en la petición `content` en bytes * **num_pdfs:** número total de PDFs que contiene la actualización * **num_videos:** número total de vídeos que contiene la actualización * **version:** de la app actual y del contenido actual del gestor. Se trata del número de versión de la app nativa, concatenado con el valor del contenido del gestor. El valor final tiene el formato: 2.4.3420: * 2.4: versión de la app administrable desde el gestor en http://www.zbdental.com.mialias.net/admin/pages/versiones.php * 3420: versión del contenido en el gestor, que se incrementa automáticamente * **last_change:** `timestamp` del último cambio realizado en el gestor en el entorno correspondiente (producción o desarrollo) ```javascript { "error": false, "bytes": peso del `JSON` devuelto en la petición `content` en bytes, "num_pdfs": número total de PDFs que contiene la actualización, "num_videos": número total de vídeos que contiene la actualización, "version": de la app actual y del contenido actual del gestor, "last_change": `timestamp` } ``` ### 1.8. content [GET] Esquema de todo el contenido de la app: títulos, secciones, subsecciones, PDFs, vídeos,... ++**Esta información estará ordenada dentro del array tal y cómo lo haya indicado el administrador en el gestor.**++ Los datos enviados, harán referencia únicamente a los contenidos a los cuales tiene acceso el usuario. #### Header * **Authorization:** identificador único del usuario #### Response Siempre devuelve un array associativo con seis claves correspondientes a las seis grandes secciones de la app que no son gestionables, y una séptima clave que indica el texto gestionable que se muestra en la cabecera de la pantalla principal: * **home:** texto gestionable en la cabecera de la pantalla principal * **header:** diccionario por idioma del texto gestionable en la cabecera de la pantalla principal. **Si en este idioma está vacío: se pone el texto en inglés siempre por defecto.** * **sections:** contenido de las secciones. Se añade el campo `translated_name` com el diccionario por idioma del nombre de la sección. **Si en este idioma está vacío: se pone el texto en inglés siempre por defecto.** ==Un nuevo campo "region_id" indica a que región pertenece cada sección.== :::danger En un fututo, de deberá eliminar el campo "header" y mantener sólo "translated_name". ::: ```javascript { "error": false, "home": texto gestionable en la cabecera de la pantalla principal, "header": [ { "lang": código idoma en formato dos letras (es, en, it,..), "name": texto cabecera en el idioma indicado }, ... ], "sections": [ { "name": nombre de la sección, "translated_name": [ { "lang": código idoma en formato dos letras (es, en, it,..), "name": nombre de la sección en el idioma indicado }, ... ], "image": imagen de background en el listado de contenidos de primer nivel, "image-button": imagen del boton en la home, "content": array con formato especificado en 1.6.2, "region_id": identificador de la región a las cual pertenece }, ... ] } ``` ![](https://i.imgur.com/ioFEHgw.png) Cada una de estas claves, tiene como valor otro array associativo con el contenido de esa sección en concreto. A continuación se pasa a detallar como es el formato de cada una de las secciones. ##### 1.8.1. home Sección especial con la información del texto gestionable en la cabecera de la pantalla principal. **Si el idioma está vacío: se pone el texto en inglés siempre por defecto.** ```javascript "home": texto cabecera "header": [ { "lang": "en", "name": "Together, let's do more!" }, { "lang": "es", "name": "¡Juntos, hacemos más!" }, { "lang": "it", "name": "Together, let's do more!" }, ... ], ``` ##### 1.8.2. content Las demás siete secciones tienen el mismo formato. Se añade el campo `translated_name` com el diccionario por idioma del nombre del nivel. **Si en este idioma está vacío: se pone el texto en inglés siempre por defecto.** :::danger En un fututo, de deberá eliminar el campo "name" y mantener sólo "translated_name". ::: ```javascript "content": [ { "item_id": identificador del primer nivel, "name": nombre del primer nivel, "translated_name": [ { "lang": código idoma en formato dos letras (es, en, it,..), "name": nombre del primer nivel en el idioma indicado }, ... ], "image": imagen de fondo gestionable, "items": [ { "item_id": identificador del segundo nivel, "name": nombre del segundo nivel, "translated_name": [ { "lang": código idoma en formato dos letras (es, en, it,..), "name": nombre del segundo nivel en el idioma indicado }, ... ], "files" [optional]: array de objectos del tipo archivo "items" [optional]: [ { "item_id": identificador del tercer nivel, "name": nombre del tercer nivel, "translated_name": [ { "lang": código idoma en formato dos letras (es, en, it,..), "name": nombre del tercer nivel en el idioma indicado }, ... ], "files" [optional]: array de objectos del tipo archivo "items" [optional]: [ { "item_id": identificador del cuarto nivel, "name": nombre del cuarto nivel, "translated_name": [ { "lang": código idoma en formato dos letras (es, en, it,..), "name": nombre del cuarto nivel en el idioma indicado }, ... ], "files" [optional]: array de objectos del tipo archivo "items" [optional]: [ { "item_id": identificador del quinto nivel, "name": nombre del quinto nivel, "translated_name": [ { "lang": código idoma en formato dos letras (es, en, it,..), "name": nombre del quinto nivel en el idioma indicado }, ... ], "files": [ array de objectos del tipo archivo (explicado más abajo) (una nivel puede contener n archivos) ] } ... (puede haber n quintos niveles) ] } ... (puede haber n cuartos niveles) ] } ... (puede haber n terceros niveles) ] } ... (puede haber n segundos niveles) ] } ... (puede haber n primeros niveles) ] ``` **Primer nivel**: * puede tener `items`: indica los subniveles que tiene. ![](https://i.imgur.com/ZZtJs2I.png) **Segundo, tercer, cuarto nivel**: * pueden tener (o no) `items`, si los tiene: indica que tiene más subniveles. * pueden tener (o no) `files`, si los tiene: indica que este nivel tiene n archivos. ![](https://i.imgur.com/hqZILzV.png) **Quinto nivel**: * al ser el último sólo puede tener `files`. ##### 1.7.3. información del archivo A continuación, se detalla como es el array asociativo que contiene la información de un archivo. **++En el campo media, sólo se enviarán los idiomas que tengan archivo. Los que no tienen archivo NO APARECEN. Además, se envian en un array ordenados por orden de idioma, tal cómo se tenga que mostrar en la app++**. Se añade el campo `translated_name` com el diccionario por idioma del nombre del archivo. **Si en este idioma está vacío: se pone el texto en inglés siempre por defecto.** :::danger En un fututo, de deberá eliminar el campo "name" y mantener sólo "translated_name". ::: ```javascript { "file_id": identificador del archivo, "name": null | nombre del archivo, "translated_name": [ { "lang": código idoma en formato dos letras (es, en, it,..), "name": nombre del archivo en el idioma indicado }, ... ], "type": "pdf" | "video", "allowsEmail": true | false indicando si ese archivo se puede enviar o no por email, "media": [ { "lang": código idoma en formato dos letras (es, en, it,..), "url": URL del archivo en el idioma indicado. Hay tres tipos: URL del archivo cargado en el gestor, URL YouTube o URL Vimeo, "externalUrl": null | URL del archivo en una plataforma externa para enviarlo por email, "size": tamaño del archivo en bytes, "timestamp": fecha de la última modificación para indicar a la app si se debe actualizar este archivo, "cloud_file": true | false indicando si ese archivo se obre como archivo en la app o se carga de una plataforma externa, "platform": "youtube" | "vimeo" } ] } ``` ### 1.9. statistics [POST] Se envian las estadísticas de la app. #### Params - **statistics:** array de estadísticas que se deben guardar en el servidor, ya que se enviarán de golpe cuando el dispositivo tenga acceso a internet. ```javascript { "statistics": [ { "type": "open-app" | "update-content", "version": versión de la app (si type = "open-app") o del contenido (si type = "update-content"), "timestamp": fecha en que se ha producido la estadística } ] } ``` #### Response - **error:** true | false indicando si se ha producido un error ```javascript { "error": false } ``` ### 1.10. user [GET] Información del perfil del usuario. #### Response - **error:** true | false indicando si se ha producido un error ```javascript { "user": { "name": nombre usuario, "email": email usuario } } ``` ## 2. Notificaciones push Para enviar notificaciones automáticas, la API decidirá en qué momento se debe generar una notificación push (al crear una sección, al publicar cambios, ...). Para notificaciones customizadas se deberá implementar un formulario que permita introducir el texto de la notificación y, si se desea, filtrar de algún modo a que dispositivos se enviará esta notificación. :::danger Importante tener versión de curl igual o superior a 7.43.0. Para comprovarlo con PHP: ```php defined('CURL_VERSION_HTTP2') || define('CURL_VERSION_HTTP2', 65536); $version = curl_version(); if (($version['features'] & CURL_VERSION_HTTP2) !== 0) { echo 'HTTP/2 supported'.PHP_EOL; } else { echo 'HTTP/2 not supported'.PHP_EOL; } ``` ::: ### 2.1. Certificado push Para enviar las notificaciones necesitamos cierta información por parte de los desarrolladores de las apps. **En este caso es Zimmer Biomet quien lo gestiona.** Deberán generar un único certificados .p8. #### bundle id Identificador de la app: - Producción: com.zimmerbiomet.productinformation - Test: com.zimmerbiomet.productinformation.test #### iOS Team identifier Proporcionado por Zimmer (cadena de 10 caracteres de números y letras). #### iOS Key identifier Proporcionado por Zimmer (cadena de 10 caracteres de números y letras). #### certificado iOS Generado por Zimmer (archivo con extensión .p8). #### password certificado iOS [optional] Password del certificado. #### certificado Android Generado por Zimmer (archivo con extensión .json). #### Android Project Identificador del proyecto para las notificaciones. #### Android Developer Key Clave del pryecto para las notificaciones. ### 2.2. JWT Para iOS, la información se envia encriptada en formato JWT. En PHP, se puede utilzar la libreria de Firebase https://github.com/firebase/php-jwt. ### 2.3. Libreria de Google Para Android, se envian las notificaciones mediante una libreria de gOOGLE. Para PHP, se puede obtener en https://packagist.org/packages/google/apiclient. ### 2.4. Base de datos Los datos que se deben guardar en la base de datos son (los opcionales sirven de filtros que se pueden usar a la hora de enviar notificaciones customizadas): #### token_push El identificador utilizado para enviar la notificación push. #### uuid Es un identificador único en el mundo que tiene cada dispositivo. El token puede cambiar en el tiempo así que debemos guardar sólo un token por dispositivo, de otra manera, se enviarían dos notificaciones a la vez al mismo dispositivo. #### platform [opcional] Plataforma del dispocitivo `android|ios`. En este caso será siempre iOS. #### model [opcional] Modelo del dispositivo. #### os_version [opcional] Versión del sistema operativo del disposotivo. #### app_version [opcional] Versión de la aplicación que tiene insalada el dispositivo. #### last_connection [opcional] Cada vez que se abre la app, se enviará la petición `/register-device/{uuid}` actualizando este campo con la fecha actual. ### 2.5. Enviar una notificación El mensaje a enviar el las push puede ser traducido a calquier idioma, pero al dispositivo sólo se le enviará en el idioma guarado en la base de datos (si no tiene idioma asociado: inglés por defecto). El código PHP para enviar un anotificación, ha de tener una estructura parecida a esta: #### Push.php ```php abstract class Push { public function deleteDevice($token) { // TODO: delete token from database } abstract public function send(); abstract public function open(); abstract public function close(); } ``` #### iOS.php ```php <?php namespace API; use Firebase\JWT\JWT; abstract class iOS extends Push { private int $APNS_PORT = 443; private string $APNS_HOST = 'https://api.push.apple.com'; // https://api.sandbox.push.apple.com for development private string $APNS_CERT; private string $APNS_PASSWORD; // optional private string $APNS_TEAM; private string $APNS_KEY; private string $APP_BUNDLE; private ?\CurlHandle $currentSocket = null; private string $payload; private int $jwtTime; private string $jwt; public function __construct($message, $tokens, $urlScheme) { $this->checkTokens($tokens); $this->preparePayload($message, $urlScheme); } public function send() { $this->open(); for ($i = 0; $i < count($this->tokens); $i++) { $token = $this->tokens[ $i ]; $result = $this->sendHTTP2Push($token); $this->checkAppleErrorResponse($result, $token); } $this->close(); } public function open() { if (!defined('CURL_HTTP_VERSION_2_0')) { define('CURL_HTTP_VERSION_2_0', 3); } $this->currentSocket = curl_init(); curl_setopt($this->currentSocket, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0); } public function close() { curl_close($this->currentSocket); } private function checkTokens($tokens) { // check token format foreach ($tokens as $token) { if (ctype_xdigit($token)) { $this->tokens[] = $token; } else { $this->deleteDevice($token); } } } private function preparePayload($message, $urlScheme) { $payload['aps'] = array( 'alert' => $message, 'badge' => 0, 'sound' => 'default' ); if ($urlScheme) { $payload['aps']['link'] = $urlScheme; } $this->payload = json_encode($payload); } function sendHTTP2Push($token) { $this->prepareJWT(); curl_setopt_array( $this->currentSocket, array( CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_2_0, CURLOPT_URL => $this->APNS_HOST . '/3/device/' . $token, CURLOPT_PORT => $this->APNS_PORT, CURLOPT_HTTPHEADER => array( 'apns-topic: ' . $this->APP_BUNDLE, 'Authorization: Bearer ' . $this->jwt ), CURLOPT_POST => true, CURLOPT_POSTFIELDS => $this->payload, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 30, CURLOPT_HEADER => 1 ) ); $result = curl_exec($this->currentSocket); $httpCode = curl_getinfo($this->currentSocket, CURLINFO_HTTP_CODE); if ($httpCode != 200) { $result .= ' ' . $httpCode; preg_match('/(.*){(.*?)}/', $result, $match); if (count($match)) { $error = json_decode($match[0], true); if (array_key_exists('reason', $error)) { $result .= ' ' . $error['reason']; } } } return $result; } private function prepareJWT() { $renovate = false; if ($this->jwtTime == null) { $this->jwtTime = time(); $renovate = true; } else { $minutes = (time() - $this->jwtTime) / 60; if ($minutes > 50) { $this->jwtTime = time(); $renovate = true; } } if ($renovate) { $payload = array( 'iss' => $this->APNS_TEAM, 'iat' => $this->jwtTime ); $key = openssl_pkey_get_private('file://' . $this->APNS_CERT); $this->jwt = JWT::encode($payload, $key, 'ES256', $this->APNS_KEY); } } private function checkAppleErrorResponse($response, $token) { if ($response !== true) { $hasToDeleteDevice = false; switch ($response) { case 'BadDeviceToken': case 'DeviceTokenNotForTopic': case 'Unregistered': $hasToDeleteDevice = true; break; } // delete device if ($hasToDeleteDevice) { $this->deleteDevice($token); } } } } ``` #### Android.php ```php <?php namespace API; use Google\Client; class Android extends Push { private $API_URL = 'https://fcm.googleapis.com/v1/projects/app-project-d0d07/messages:send'; private $tokens = array(); private $total = 0; private $message = array(); private $httpClient = null; public function __construct($message, $tokens, $urlScheme = '') { $this->tokens = $tokens; $this->total = count($this->tokens); $this->message = [ 'message' => [ 'notification' => [ 'body' => $message, 'title' => 'ZimVie Product Information' ] ] ]; if ($urlScheme) { $this->message['message']['data']['link'] = $urlScheme; } $this->open(); } public function send() { $this->tokens = array_chunk($this->tokens, 300); foreach ($this->tokens as $tokens) { foreach ($tokens as $token) { $this->message['message']['token'] = $token; $response = $this->httpClient->post($this->API_URL, ['json' => $this->message]); if ($response->getStatusCode() == 200) { // ok } else { if (in_array($response->getStatusCode(), array(400, 404))) { $this->deleteDevice($token); } $error = $response->getReasonPhrase(); // handle error } } } } public function open() { $client = new Client(); $client->setDeveloperKey('22c6f9d7f43ea9549d19a7ca5d56238a51f394b1'); $client->setAuthConfig('<PATH_TO>/app-project-d0d07-8bzjz-22c6f9d7f4.json'); $client->addScope('https://www.googleapis.com/auth/firebase.messaging'); $this->httpClient = $client->authorize(); } public function close() { // nothing } } ``` #### Ejemplo ```php $message = 'Test Push'; $urlScheme = 'zimmerbiomet:://home'; $tokensAndroid = array('token1', 'token2', 'token3'); $android = new Android($message, $tokensAndroid, $urlScheme); $android->send(); $android->close(); $tokensIOS = array('token100', 'token101'); $iOS = new iOS($message, $tokensIOS, $urlScheme); $iOS->send(); $iOS->close(); ```