Рекомендую обратиться к статье в википедии, если слышите о приложении Wallet(Passbook) впервые.
Существует документация для разработчиков, желающих реализовать генерацию пассов.
Обычно добавление пассов в приложение реализуют через мобильное приложение, в котором есть функционал генерации пассов, либо через отправку на почту.
Я же расскажу о более редком виде распространения пассов. Предположим, что есть задача дать возможность пользователям iPhone добавить себе в приложение Wallet(Passbook) сгенерированный пасс с определенными данными по нажатию кнопки на веб-сайте. Не буду описывать логику на клиенте. В примере будет код обычного скрипта, при вызове которого будет происходить генерация пасса.
Самым большим препятствием для реализации подобной задачи является потребность в валидном сертификате разработчика под ios. Сертификат можно получить только при наличии учетной записи разработчика, которая доступна по годовой платной подписке.
Если кратко, то пасс - это упакованные в один файл изображения и несколько конфигов, подписанный сертификатами.
Писать свой генератор пассов - занятие, на которое у рядового разработчика просто нет времени. Правильным решением будет воспользоваться готовым решением, если таковое имеется. Я рекомендую eo/passbook, отличный php-пакет, который поможет решить задачу быстро и эффективно.
Устанавливаем eo/passbook
$ composer require "eo/passbook" "1.*"
Подключаем автозагрузку Composer.
require 'vendor/autoload.php';
Согласно документации существует несколько типов пассов. Различия между этими типами заключаются в дизайне и наборе полей, доступных на лицевой стороне пасса. Допустим, что типом пасса будет “Билет”.
// каждый пасс должен иметь уникальный идентификатор
$date = new \DateTime();
$uniqueId = 'best_pass_ever_' . $date->format('Y_m_d_h_i_s');
$pass = new EventTicket($uniqueId, 'Wallet example');
// устанавливаем цвет бэкграунда
$pass->setBackgroundColor('rgb(255, 255, 255)');
// устанавливаем цвет шрифта
$pass->setForegroundColor('rgb(0, 0, 0)');
// устанавливаем цвет шрифта лейблов
$pass->setLabelColor('rgb(0, 0, 0)');
Далее создаем структуру полей пасса. Начнем с поля заголовка и вторичного поля.
$structure = new Structure();
// поле заголовка
// конструктор поля должен состоять из уникального ключа и значения поля
$headerField = new Field('header', 'Поле заголовка');
// также можно добавить лейбл
$headerField->setLabel('Лейбл поля заголовка');
$structure->addHeaderField($headerField);
// вторичное поле
$secondaryField = new Field('secondary', 'Вторичное поле');
$secondaryField->setLabel('Лейбл вторичного поля');
$structure->addSecondaryField($secondaryField);
Продолжим с дополнительным полем и полями задней стороны пасса.
// дополнительное поле
$auxiliaryField = new Field('auxiliary', 'Дополнительное поле');
$auxiliaryField->setLabel('Лейбл дополнительного поля');
$structure->addAuxiliaryField($auxiliaryField);
// первое поле задней стороны пасса
$backField = new Field('back_field_one', '+7 (999) 999-99-99');
$backField->setLabel('Пример с номером телефона:');
$structure->addBackField($backField);
// второе поле задней стороны пасса
$backField = new Field('back_field_two', '<a href="https://google.com">Ссылка</a>');
$backField->setLabel('Пример ссылки:');
$structure->addBackField($backField);
// можно не ограничиваться одним или двумя!
Пришло время добавить изображения. Важно отметить, что изображения должны быть разных размеров для обычных и ретина дисплеев.
// логотип
$logoImage = new Image('/full/path/to/logo/', 'logo');
$pass->addImage($logoImage);
$logoImage = new Image('/full/path/to/retina/logo/', 'logo');
$logoImage->setIsRetina(true);
$pass->addImage($logoImage);
// иконка, видна на экране блокировки
$iconImage = new Image('/full/path/to/icon/', 'icon');
$pass->addImage($iconImage);
$iconImage = new Image('/full/path/to/retina/icon/', 'icon');
$iconImage->setIsRetina(true);
$pass->addImage($iconImage);
// strip
$stripImage = new Image('/full/path/to/strip/', 'strip');
$pass->addImage($stripImage);
$stripImage = new Image('/full/path/to/retina/strip/', 'strip');
$stripImage->setIsRetina(true);
$pass->addImage($stripImage);
Есть возможность добавить отдельную ссылку на какое-либо приложение в AppStore. Для примера добавим приложение Youtube. Для добавления требуется идентификатор приложения. Узнать его можно из ссылки на приложение.
$pass->addAssociatedStoreIdentifier(544007664);
Добавление bar/qr-кода опционально. Добавим qr-код, ведущий на главную страницу поисковика Google.
$barcode = new Barcode(Barcode::TYPE_QR, 'http://google.com');
$pass->setBarcode($barcode);
Заканчиваем генерацию структуры и создаем пасс. Параметры, передаваемые в конструктор PassFactory в большинстве своем можно узнать из аккаунта разработчика под ios.
$pass->setStructure($structure);
$factory = new PassFactory(
// Идентификатор типа пасса (bundle id)
'TYPE_IDENTIFIER',
// Идентификатор команды разработчика
'TEAM_ID',
// Название компании разработчика
'ORGANIZATION_NAME',
// Путь до сертификата разработчика
'/full/path/to/developer/certificate/',
// Пароль к сертификату разработчика
'DEVELOPER_CERTIFICATE_PASSWORD',
// Путь до WWDR сертификата
'/full/path/to/wwdr/certificate/',
);
$factory->setOverwrite(true);
// Путь до папки, в которую будут сохраняться готовые пассы
$factory->setOutputPath('/path/to/generated/passes/');
$file = $factory->package($pass);
Имеется возможность провалидировать готовый пасс. Воспользуемся этой возможностью.
$validator = new PassValidator();
$validator->validate($pass);
if ($validator->getErrors()) {
$errorsString = implode(' | ', $validator->getErrors());
throw new \Exception("Ошибки валидации passbook: $errorsString");
}
Теперь, когда пасс готов, его еще нужно отдать клиенту с определенными заголовками.
$passBookFullPath = $file->getPath() . "/{$uniqueId}.pkpass";
header("Pragma: no-cache");
header("Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0");
header("Content-Type: application/vnd.apple.pkpass");
header('Content-Disposition: attachment; filename="pass.pkpass"');
clearstatcache();
$fileSize = filesize($passBookFullPath);
if ($fileSize) {
header("Content-Length: " . $fileSize);
}
header('Content-Transfer-Encoding: binary');
if (filemtime($passBookFullPath)) {
date_default_timezone_set("UTC");
header(
'Last-Modified: '
. date("D, d M Y H:i:s", filemtime($passBookFullPath))
. ' GMT'
);
}
flush();
readfile($passBookFullPath);
exit();
Собираем куски кода воедино:
<?php
require 'vendor/autoload.php';
use Passbook\Pass\Barcode;
use Passbook\Pass\Field;
use Passbook\Pass\Image;
use Passbook\Pass\Structure;
use Passbook\PassFactory;
use Passbook\PassValidator;
use Passbook\Type\EventTicket;
// каждый пасс должен иметь уникальный идентификатор
$date = new \DateTime();
$uniqueId = 'best_pass_ever_' . $date->format('Y_m_d_h_i_s');
$pass = new EventTicket($uniqueId, 'Wallet example');
// устанавливаем цвет бэкграунда
$pass->setBackgroundColor('rgb(255, 255, 255)');
// устанавливаем цвет шрифта
$pass->setForegroundColor('rgb(0, 0, 0)');
// устанавливаем цвет шрифта лейблов
$pass->setLabelColor('rgb(0, 0, 0)');
$structure = new Structure();
// поле заголовка
// конструктор поля должен состоять из уникального ключа и значения поля
$headerField = new Field('header', 'Поле заголовка');
// также можно добавить лейбл
$headerField->setLabel('Лейбл поля заголовка');
$structure->addHeaderField($headerField);
// вторичное поле
$secondaryField = new Field('secondary', 'Вторичное поле');
$secondaryField->setLabel('Лейбл вторичного поля');
$structure->addSecondaryField($secondaryField);
// дополнительное поле
$auxiliaryField = new Field('auxiliary', 'Дополнительное поле');
$auxiliaryField->setLabel('Лейбл дополнительного поля');
$structure->addAuxiliaryField($auxiliaryField);
// первое поле задней стороны пасса
$backField = new Field('back_field_one', '+7 (999) 999-99-99');
$backField->setLabel('Пример с номером телефона:');
$structure->addBackField($backField);
// второе поле задней стороны пасса
$backField = new Field('back_field_two', '<a href="https://google.com">Ссылка</a>');
$backField->setLabel('Пример ссылки:');
$structure->addBackField($backField);
// можно не ограничиваться одним или двумя!
// логотип
$logoImage = new Image('/full/path/to/logo/', 'logo');
$pass->addImage($logoImage);
$logoImage = new Image('/full/path/to/retina/logo/', 'logo');
$logoImage->setIsRetina(true);
$pass->addImage($logoImage);
// иконка, видна на экране блокировки
$iconImage = new Image('/full/path/to/icon/', 'icon');
$pass->addImage($iconImage);
$iconImage = new Image('/full/path/to/retina/icon/', 'icon');
$iconImage->setIsRetina(true);
$pass->addImage($iconImage);
// strip
$stripImage = new Image('/full/path/to/strip/', 'strip');
$pass->addImage($stripImage);
$stripImage = new Image('/full/path/to/retina/strip/', 'strip');
$stripImage->setIsRetina(true);
$pass->addImage($stripImage);
$pass->addAssociatedStoreIdentifier(544007664);
$pass->setStructure($structure);
$barcode = new Barcode(Barcode::TYPE_QR, 'http://google.com');
$pass->setBarcode($barcode);
$factory = new PassFactory(
// Идентификатор типа пасса (bundle id)
'TYPE_IDENTIFIER',
// Идентификатор команды разработчика
'TEAM_ID',
// Название компании разработчика
'ORGANIZATION_NAME',
// Путь до сертификата разработчика
'/full/path/to/developer/certificate/',
// Пароль к сертификату разработчика
'DEVELOPER_CERTIFICATE_PASSWORD',
// Путь до WWDR сертификата
'/full/path/to/wwdr/certificate/',
);
$factory->setOverwrite(true);
// Путь до папки, в которую будут сохраняться готовые пассы
$factory->setOutputPath('/path/to/generated/passes/');
$file = $factory->package($pass);
$validator = new PassValidator();
$validator->validate($pass);
if ($validator->getErrors()) {
$errorsString = implode(' | ', $validator->getErrors());
throw new \Exception("Ошибки валидации passbook: $errorsString");
}
$passBookFullPath = $file->getPath() . "/{$uniqueId}.pkpass";
header("Pragma: no-cache");
header("Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0");
header("Content-Type: application/vnd.apple.pkpass");
header('Content-Disposition: attachment; filename="pass.pkpass"');
clearstatcache();
$fileSize = filesize($passBookFullPath);
if ($fileSize) {
header("Content-Length: " . $fileSize);
}
header('Content-Transfer-Encoding: binary');
if (filemtime($passBookFullPath)) {
date_default_timezone_set("UTC");
header(
'Last-Modified: '
. date("D, d M Y H:i:s", filemtime($passBookFullPath))
. ' GMT'
);
}
flush();
readfile($passBookFullPath);
exit();
В результате получим пасс примерно следующего содержания:
А вот его задняя сторона:
Перечислять достоинства этой технологии я не буду, т.к. на мой взгляд они достаточно субъективны.
А вот по недостаткам и подводным камням стоит пройтись:
- Ужасная документация и это не только мое мнение.
- Слишком много места отводится qr/bar кодам, остальной контент не всегда возможно уместить на лицевой стороне.
- Ограничения на количество символов для лейблов как на лицевой так и на задней сторонах.
- Если не указать какое-либо значение для полей на лицевой или задней стороне, то пасс будет считаться невалидным.
- В целом, очень мало возможностей для кастомизации дизайна.
Интересным моментом, незатронутым мной в этой статье, я считаю возможность обновлять пасс с помощью веб-сервиса. Об этом можно прочитать в документации.