Приложения-хранилища банковских карт стремительно вошли в нашу жизнь благодаря Apple Wallet и Google Pay. Обе платформы, помимо банковских, позволяют также работать и с другими типами карт — картами лояльности, подарочными картами, билетами на мероприятия, посадочными талонами и т.д.
Работая в компании, которая занимается обслуживанием одной немаленькой ритейловой сети, мне пришлось интегрировать карты лояльности этой сети в Apple Wallet и Google Pay. И если с Apple Wallet пришлось повозиться только потому что интеграционный слой довольно многофункционален, то с Google Pay большая часть усилий и нервных клеток была потрачена на то чтобы разобраться в документации, найти подходящие инструменты и разработать первый proof of concept. Хоть в целом остальная часть работы прошла гораздо быстрее, чем для Apple Wallet, я потратил день на то чтобы разобраться в том как завести сервис, так что был бы не против, если бы кто-то написал подобную статью до меня.
1. Матчасть
Карты лояльности представлены при помощи двух сущностей — Loyalty Class и Loyalty Object.
- Loyalty Class — своего рода шаблон для всех карт лояльности. Содержит поля общие для всех карт, такие как цвет шрифта, ссылки на ассеты иконок, бэкграундов, текстовые поля и т.д., полное описание можно найти тут: Loyaltyclass
- Loyalty Object — экземпляр LoyaltyClass. Он же содержит в себе данные конкретной карты конкретного клиента — номер карты, имя, дополнительные текстовые поля. Описание тут: Loyaltyobject
Для того чтобы получить карту, пользователь должен перейти по ссылке формата
www.android.com/payapp/savetoandroidpay/{JWT}, где JWT — токен, создержащий JSON с данными LoyaltyClass. Но так как длина URL имеет ограничения, рекомендуется предварительно создать Loyalty Class, если его структура содержит много символов.
Если необходимо обновить карту в связи с изменением шаблона, стилей, необходимостью добавления или изменения текстовых полей, сделать это можно при помощи API. Необходимо только выполнить POST-запрос с новой версией карты, сервисы Google сами синхронизируют все приложения пользователей, у которых сохранена эта карта.
2. Инструменты и документация
Проблемой оказалось то, что все примеры в документации к Google Pay API for Passes были исключительно для Java, PHP и Python, а сама документация настойчиво рекомендовала «использовать клиентские библиотеки для того чтобы упростить процесс» работы с API.
Последовав этому совету я радостно отправился в nuget, но библиотеки для Google Pay там не оказалось. Слава Брину, первая строчка в гугле по запросу «google pay for passes dotnet» выдала страницу Google Pay API for Passes utility libraries, на которой нашлось то, что мне было нужно, правда в формате ZIP-архива, в котором находился .net проект со сгенерированным классом, являющимся враппером к API Google Pay — Google Pay API for Passes Client library.
Судя по наличию в проекте файла Google.Apis.Walletobjects.v1.1.9.2.00.nuspec, деплой nuget-пакета все же входил в планы команды Google. Открыв этот файл в поисках документации, я ничего конкретного не нашел, а некоторые ссылки, которые были в секции description и вовсе отправляли на несуществующие страницы.
3. Получение access token
Для начала непосредственной работы с Google Pay API for Passes необходимо получить access token, для этого же необходимо:
- Иметь Google Merchant Account, получить который можно здесь
- Cоздать Service Account, получить для него файл с credentials — json документ с деталями service account, включающий идентификатор, email, private key и т.д. Как рекомендует документация, хранить этот файл нужно в защищенном месте.
- Связать Merchant Account и Service Account в Google Merchant Center
При помощи этого файла можно авторизоваться, используя библиотеку Google.Apis.Auth.OAuth2:
private await Task<string> GetOAuthToken()
{
string serviceAccountFile = string.Empty;
serviceAccountFile = ConfigurationManager.AppSettings["GooglePayServiceAccountConfigPath"];
/*
Библиотека авторизации имеет несколько типов Credential, GoogleCredential был выбран потому что его без проблем можно создать из файла с деталями Service Account, используя статический метод, при этом не теряются необходимые scopes API, указанные в этом файле
*/
var credential = GoogleCredential.FromFile(serviceAccountFile)
.CreateScoped(WalletobjectsService.Scope.WalletObjectIssuer);
/*
Access token имеет срок годности и должен быть обновлен вовремя, но метод GetAccessTokenForRequestAsync берет все действия по управлению валидностью токена на себя, так что об этом можно не волноваться
*/
var token = async credential.UnderlyingCredential.GetAccessTokenForRequestAsync();
return token;
}
4. Создание карты
Для того чтобы создать карту лояльности, нужно сперва создать Loyalty Class. Loyalty Class можно создать как при помощи API, так и используя веб-интерфейс Google Merchant Center. Внимание стоит уделить названию класса, так как это название должно быть уникальным в инфраструктуре Google Pay.
После создания Loyalty Class, можно создать Loyalty Object. Для этого уже понадобится библиотека, которую мы добавили в проект ранее: создаем объект запроса, указываем OAuth token, передаем созданный объект LoyaltyObject, выполняем запрос:
public GooglePayApiService()
{
//Идентификатор эмиттера, выданный после создания Merchant Account
IssuerId = ConfigurationManager.AppSettings["GooglePayIssuerId"];
_wobService = new WalletobjectsService();
}
private async Task<LoyaltyObject> ConvertLoyaltyObjectFromTemplate(GooglePayPassTemplate template)
{
string id = $"{IssuerId}.{template.SerialNumber}";
string loyaltyClassName = ConfigurationManager.AppSettings["GooglePayStoreCardClassName"];
var loyaltyClass = await GetLoyaltyClass(loyaltyClassName);
var result = new LoyaltyObject
{
Id = id,
AccountName = template.AccountName,
Barcode = new Barcode
{
AlternateText = template.BarcodeText,
Value = template.SerialNumber,
Type = "pdf417"
},
Kind = "walletObject#loyaltyObject",
ClassId = loyaltyClass.Id,
ClassReference = loyaltyClass,
State = "active"
};
return result;
}
private async Task<LoyaltyObject> CreateLoyaltyObject(LoyaltyObject loyaltyObject)
{
var saveRequest = _wobService.Loyaltyobject.Insert(loyaltyObject);
saveRequest.OauthToken = await GetOAuthToken();
var savedObject = await saveRequest.ExecuteAsync();
return savedObject;
}
GooglePayPassTemplate в данном примере — это DTO, хранящий в себе шаблон карты для пользователя, который генерируется отдельным разработанным сервисом.
5. Обновление карты
Здесь принцип тот же, что и при создании: генерируем запрос, передаем обновленный объект LoyaltyObject, выполняем:
private async Task<LoyaltyObject> UpdateLoyaltyObject(LoyaltyObject loyaltyObject)
{
var updateRequest = _wobService.Loyaltyobject.Update(loyaltyObject, loyaltyObject.Id);
updateRequest.OauthToken = await GetOAuthToken();
var savedObject = await updateRequest.ExecuteAsync();
return savedObject;
}
После выполнения запроса, карта в приложениях пользователей, у которых она установлена, будет обновлена спустя несколько секунд, если устройство в сети и есть подключение к Интернет.
6. Генерация JWT
Для установки карты необходимо перенаправить пользователя по ссылке www.android.com/payapp/savetoandroidpay/{JWT}. Описание структуры токена можно найти по этой ссылке.
Токен подписывается RSA-SHA256 подписью, которую можно сгенерировать, используя все тот же файл с Service Account credentials:
public static class JwtHelper
{
public static string CreateJwtForLoyaltyObject(LoyaltyObject loyaltyObject)
{
/*
В этот раз для работы с credential используем этот класс, потому что у него
статический метод создания подписи
*/
ServiceAccountCredential credential;
var now = DateTime.UtcNow;
DateTime unixEpoch = new DateTime(1970, 01, 01); // 1970-01-01 00:00:00 UTC
var secondsSinceEpoch = (int)Math.Round((now - unixEpoch).TotalSeconds);
string serviceAccountFile = serviceAccountFile = ConfigurationManager.AppSettings["GooglePayServiceAccountConfigPath"];
using (var fs = new FileStream(serviceAccountFile, FileMode.Open, FileAccess.Read, FileShare.Read))
{
credential = ServiceAccountCredential.FromServiceAccountData(fs);
}
/*
Создаем экземпляр класса JwtPayload, был создан для хранения payload-сегментa токена JWT
*/
var jwtPayload = new JwtPayload
{
iat = secondsSinceEpoch,
iss = credential.Id,
payload = new JwtInternalPayload
{
loyaltyObjects = new[]
{
new LoyaltyObjectPayload
{
id = loyaltyObject.Id
}
}
}
};
string header = @"{""alg"":""RS256"",""typ"":""JWT""}";
string payload = JsonConverter.SerializeObject(jwtPayload);
string base64Header = EscapedBase64(Convert.ToBase64String(Encoding.UTF8.GetBytes(header)));
string base64Payload = EscapedBase64(Convert.ToBase64String(Encoding.UTF8.GetBytes(payload)));
//считаем саму подпись при помощи статического метода класса Signature
string signature =
EscapedBase64(credential.CreateSignature(
Encoding.UTF8.GetBytes($"{base64Header}.{base64Payload}")
));
var token = $"{base64Header}.{base64Payload}.{signature}";
return token;
}
private static string EscapedBase64(string base64)
{
return base64.Replace('+', '-')
.Replace('/', '_')
.Replace("=", "");
}
}
Заключение
В этой статье мы поверхностно прошли по основам работы с Google Pay API for Passes: настройке аккаунтов, подключению к API, созданию Loyalty Class и Loyalty Object.
Если НЛО благоволит, расскажу отдельно про то как работать с Apple Wallet (там все сложнее в плане реализации), как подружить Apple Wallet с Google Pay в одном веб-сервисе и не испытывать боли.
Areso
Карта создается сразу всем пользователям, или каждый пользователь получает свою?
knizhnikov Автор
Да, конечно, карта создаётся в том же тираже, как и пластиковая: обычно одна или несколько на пользователя. Каждый Loyalty Object имеет свой идентификатор, которым по логике программы лояльности, для которой было разработано решение, является комбинация Issuer Id и номер пластиковой карты. Так что, соответственно, карты персонализированные