Статьи

Что у тебя в кошельке? Обработка iOS Passbook с помощью Ruby

Passbook (так называемый «кошелек», начиная с iOS 9) – это встроенное приложение для iOS, которое позволяет пользователям хранить и получать «пропуска». Рассматривайте пропуск как цифровой токен для чего-то, например, билеты на мероприятия, членские билеты, купоны на скидку, посадочные талоны и т. Д. Пропуски могут быть добавлены в книжку по почте, сообщениями, приложением и т. Д.

Пропуск имеет много преимуществ для пользователя. Например, скажем, пользователь добавил пропуск в билет в кино, который начинается в 10.00. Этот проход будет отображаться на главном экране, когда даже пользователь приближается к нему, чтобы получить к нему прямой доступ. Если был добавлен членский билет с некоторой скидкой на Store X, он будет отображаться всякий раз, когда пользователь входит в Store X.

Проходы также выгодны для прохождения провайдеров С одной стороны, пропуск может быть использован в качестве средства уведомления всякий раз, когда есть некоторые изменения в информации. Например, фильм отменен или посадочный талон изменен, или провайдеры хотят предоставить дополнительную скидку владельцам пропусков. Все это возможно с использованием пропусков.

Сегодня мы рассмотрим, как создать серверную часть iOS для обработки пропусков и push-уведомлений. Я собираюсь использовать для этого Руби и Синатру. Давайте начнем.

Примечание. Это руководство охватывает только серверную часть книжки. Я бы посоветовал вам заранее ознакомиться с дизайном и схемой пропуска. Есть много хороших уроков, и я бы порекомендовал эту серию из двух частей .

Требования

Прежде чем мы начнем писать код, есть несколько вещей, которые необходимы для создания проходов, все они связаны с сертификатами. Они есть,

  1. p12Certficate.pem
  2. p12Key.pem
  3. p12Password – строка
  4. WWDRCertificate.pem
  5. NotificationCert.pem (p12Certificate подписан с помощью p12Key)

Сертификаты могут быть сгенерированы только на портале доступа iOS Developer. Информация о том, как сгенерировать эти сертификаты, доступна здесь (Раздел – Получи мне сертификат!)

Создание приложения

Структура папок

Давайте создадим приложение. Начните с создания папки, структуры папок и файлов, показанных ниже. Если вы используете любую другую структуру каталогов, отличную от описанной ниже, обязательно исправьте ее и в коде.

Обязательно поместите файлы в папку активов с тем же именем, как указано выше, так как это соглашение. Поместите сертификаты, которые вы создали ранее, в папку сертификатов .

Обязательные файлы

Теперь, когда структура папок создана, добавьте следующее содержимое в ваш Gemfile :

source 'https://rubygems.org' gem 'sinatra' gem 'passbook' gem 'grocer' gem 'activerecord' gem 'sinatra-activerecord' gem 'pg' 

Мы используем самоцвет Passbook для генерации пасов и самоцвет бакалейщика для push-уведомлений. На стороне Rails мы будем использовать ActiveRecord ORM и PostgreSQL.

После обновления Gemfile запустите пакетную bundle install . Все необходимые драгоценные камни будут установлены.

Затем добавьте следующее в app.rb :

  require 'sinatra' require 'bundler/setup' require 'passbook' require 'sinatra/activerecord' 

Это модули, которые мы собираемся использовать в приложении. Откройте Rakefile и добавьте:

  require './app' require 'sinatra/activerecord/rake' Dir.glob('lib/tasks/*.rake').each { |r| load r} 

Rake-файл необходим для работы команд rake . Нам потребовался основной файл приложения и модуль Rake ActiveRecord. Наконец, загрузите все файлы rake из lib / tasks на тот случай, если мы захотим создать какие-либо пользовательские задачи Rake.

Затем заполните детали конфигурации. Откройте файлы, которые мы создали в папке config /, и добавьте содержимое ниже.

Конфигурация базы данных

 # database.yml development: adapter: postgresql database: appname_development host: localhost production: adapter: postgresql encoding: unicode 

Конфигурация ActiveRecord

 #environments.rb require "sinatra/activerecord" configure :production, :development do db = URI.parse(ENV['DATABASE_URL'] || 'postgres://localhost/appname_development') ActiveRecord::Base.establish_connection( :adapter => db.scheme == 'postgres' ? 'postgresql' : db.scheme, :host => db.host, :username => db.user, :password => db.password, :database => db.path[1..-1], :encoding => 'utf8' ) end 

Конфигурация Passbook

 #passbook.rb Passbook.configure do |passbook| passbook.p12_password = 'r@nd0mpa$sw0rd' passbook.p12_key = 'certificates/p12_key.pem' passbook.p12_certificate = 'certificates/p12_certificate.pem' passbook.wwdc_cert = 'certificates/wwdr.pem' passbook.notification_gateway = 'gateway.push.apple.com' passbook.notification_cert = 'certificates/push_notificfation_certificate.pem' end 

База данных и конфигурация ActiveRecord тривиальны. В конфигурации Passbook мы предоставляем сертификаты и учетные данные, необходимые для генерации пропусков. Требуйте config / environment и config / passbook в app.rb, и мы в порядке.

Хорошо, мы успешно загрузили структуру каталогов и поместили в нее все необходимые файлы. Далее, давайте создадим схему базы данных.

Создание схемы базы данных

Миграции

Сначала создайте базу данных, введя в консоли следующую команду:

 rake db:create 

Нам понадобятся 4 таблицы для нашего приложения, чтобы обрабатывать проходные регистрации и уведомления. Вот схема:

Быстрый просмотр таблиц:
1. пассы – содержит необходимую информацию для прохождения.
2. устройства – идентификатор устройства и push-токен, который Apple будет отправлять при добавлении нашего пропуска на устройство. Информация используется для отправки пропусков обновлений.
3. регистрация – это облегчает отношения «многие ко многим» между пропусками и устройствами. Устройство может добавить много проходов, и пропуск может быть добавлен ко многим устройствам, поэтому мы должны отправлять обновления проходов на все устройства.
4. Журналы – всякий раз, когда что-то идет не так, Apple отправляет сообщение об ошибке / предупреждение на наш сервер. Эта таблица будет хранить эти журналы и использоваться для устранения неполадок.

Примечание. Apple явно заявляет о наличии таблицы регистрации для отношения и об удалении строки всякий раз, когда устройство отменяет регистрацию прохода. Если вы хотите сохранить данные, вы можете добавить столбец, чтобы отметить регистрацию active или inactive .

Давайте быстро создадим четыре таблицы. Введите следующие команды для создания миграций (соблюдайте соглашение об именах):

 rake db:create_migration NAME=create_passes rake db:create_migration NAME=create_devices rake db:create_migration NAME=create_registrations rake db:create_migration NAME=create_logs 

В файлах миграции добавьте следующее содержимое соответственно в соответствующие файлы:

 ### create_passes def change create_table :passes do |t| t.string :serial_number, null: false t.jsonb :data, null: false t.integer :version, default: 1 end end ### create_devices def change create_table :devices do |t| t.string :identifier, null: false t.string :push_token, null: false end end ### create_registrations def change create_table :registrations do |t| t.integer :pass_id, null: false t.integer :device_id, null: false end end ### create_logs def change create_table :logs do |t| t.text :log end end 

Как видите, таблица passes содержит данные прохода JSON, которые будут разными для каждого прохода. Каждый проход будет идентифицирован своим серийным номером, который является уникальным. Также, version содержит версию пропущенных данных. Это используется при отправке обновлений через push-уведомление.

Запустите rake db:migrate чтобы создать таблицы. db / folder и db / schema.rb должны существовать сейчас.

модели

Миграции завершены, и таблицы созданы. Давайте создадим модели ActiveRecord для схемы. Внутри ранее созданной папки / models создайте следующие файлы. Опять же, соблюдайте правила именования.

 ### models/pass.rb class Pass < ActiveRecord::Base validates_uniqueness_of :serial_number has_many :registrations has_many :devices, through: :registrations end ### models/device.rb class Device < ActiveRecord::Base validates_uniqueness_of :device_identifier validates_uniqueness_of :push_token has_many :registrations has_many :passes, through: :registrations end ### models/registration.rb class Registration < ActiveRecord::Base belongs_to :pass belongs_to :device end ### models/log.rb class Log < ActiveRecord::Base end 

Ничего особенного, просто установление отношений для ActiveRecord, чтобы идентифицировать и действовать соответственно. Опять же, будьте осторожны с именами файлов и именами классов. Затем потребуйте модели, которые мы только что создали в app.rb :

 ### app.rb require './models/pass' require './models/device' require './models/registration' require './models/log' 

Endpoints

Пасс поколения

Хорошо, мы готовы написать нашу первую конечную точку, которая будет использоваться для генерации проходов. Конечная точка принимает JSON и возвращает файл .pkpass . Откройте файл app.rb и добавьте следующее после обязательных параметров:

 post '/passbooks' do request.body.rewind data = JSON.parse request.body.read unless @pass = Pass.find_by(serial_number: data['serialNumber']) @pass = Pass.create(serial_number: data['serialNumber'], data: data) end passbook = Passbook::PKPass.new @pass.data.to_json.to_s passbook.addFiles ['assets/logo.png', 'assets/logo@2x.png', 'assets/icon.png', 'assets/icon@2x.png'] gen_pass = passbook.file send_file(gen_pass.path, type: 'application/vnd.apple.pkpass', disposition: 'attachment', filename: "pass.pkpass") end 

Мы только что создали конечную точку для отправки данных JSON и ответа в .pkpass файла .pkpass . Обратите внимание: если JSON содержит серийный номер ранее сгенерированного прохода, мы игнорируем опубликованные данные и переносим его на существующий проход. Есть причины для этого:

  1. Мы не должны допускать дублирования серийных номеров.
  2. Мы не обновляем этот проход, потому что он просто не является конечной точкой обновления. Схема включает в себя столбец version и push-уведомления для проходных обновлений. Если мы обновляем проход для каждого запроса на генерацию прохода без проверки данных, мы можем отправить слишком много уведомлений нашим пользователям без фактического обновления.

Лучше поддерживать отдельные конечные точки для проходных обновлений, но если вы все еще хотите сделать это в одной конечной точке, есть несколько способов. Сравните новые данные со старыми и обновляйте только если они изменились. Таким образом, будет легко поддерживать парольную версию.

Давайте посмотрим, как это работает, я отправляю этот JSON в приложение, и вот ответ:

8vz6AjO

Круто, а? Стиль прохода полностью настраиваемый. Вы можете прочитать больше об этом здесь – Pass files

Мы закончили с частью генерации пропусков. Далее давайте добавим конечные точки обновления pass.

Передать конечную точку обновлений

Всего существует 5 конечных точек, необходимых для полной связи между нашим сервером, серверами Apple и устройством. Подробнее о них можно прочитать здесь – Ссылка на iOS Passbook WebService

По сути, всякий раз, когда устройство добавляет проход, оно будет вызывать наш сервер (URL будет указан в проходе JSON), чтобы зарегистрироваться. Мы должны записать push-уведомление и идентификатор DeviceLibrary, который он отправляет. После этого, как только мы получим какие-либо обновления, отправьте запрос уведомления на серверы Apple (без данных) с помощью push-токена устройства. Как только запрос размещен, Apple позвонит в конечную точку, чтобы получить проходы, которые необходимо обновить на устройстве (помните, что устройство может иметь несколько проходов). Мы вышлем серийные номера, которые необходимо обновить. Затем Apple отправляет вызов для каждого серийного номера с запросом последней версии пропуска. Наконец, Apple отправит обновление на устройство пользователя. Вот и все.

Две другие конечные точки, помимо этого потока, – это регистрация и регистрация ошибок или журналов предупреждений.

К счастью, гем passbook предоставляет конечные точки из коробки, поэтому нам просто нужно расширить его и написать нашу логику. Откройте app.rb и добавьте эту строку после всех требований и перед конечной точкой генерации прохода:

 use Rack::PassbookRack 

Затем добавьте следующие строки после конечной точки генерации прохода:

 module Passbook class PassbookNotification def self.register_pass(options) status = verify_pass_and_token options if status return status end @device = Device.where(identifier: options['deviceLibraryIdentifier'], push_token: options['pushToken']).first_or_create if Registration.find_by(pass_id: @pass.id, device_id: @device.id).present? return {:status => 200} else Registration.create(pass_id: @pass.id, device_id: @device.id) return {:status => 201} end end def self.passes_for_device(options) unless valid_device? options['deviceLibraryIdentifier'] return end update_tag = options['passesUpdatedSince'] || 0 passes = @device.passes.where('version > ?', update_tag.to_i) if passes.present? {'lastUpdated' => Time.now.utc.to_i.to_s, 'serialNumbers' => passes.map{|p| p.serial_number}} else return end end def self.unregister_pass(options) status = verify_pass_and_token options if status return status[:status] == 401 ? status : {:status => 200} end unless valid_device? options['deviceLibraryIdentifier'] return {:status => 401} end registrations = @device.registrations.where(pass_id: @pass.id) if registrations.present? registrations.destroy_all end return {:status => 200} end def self.latest_pass(options) @pass = find_pass_with options['serialNumber'] unless @pass return end passbook = Passbook::PKPass.new @pass.data.to_json.to_s passbook.addFiles ['assets/logo.png', 'assets/logo@2x.png', 'assets/icon.png', 'assets/icon@2x.png'] {:status => 200, :latest_pass => passbook.stream.string, :last_modified => Time.now.utc.to_i.to_s} end def self.passbook_log(log) log.values.flatten.compact.each do |l| Log.create(log: l) end end end end def verify_pass_and_token options token = ENV['AUTH_TOKEN'] @pass = find_pass_with options['serialNumber'] if options['authToken'] != token return {:status => 401} elsif !@pass return {:status => 404} else return end end def find_pass_with serial Pass.find_by(serial_number: serial) end def valid_device? identifier @device = Device.find_by(identifier: identifier) end 

Вот и все. Код довольно понятен, но давайте просто быстро пройдемся по тому, что происходит в каждой конечной точке.

Регистрация Pass

Первый запрос сначала проверяется. Маркер должен быть включен в проходной JSON во время создания, поскольку он используется для проверки запросов. Далее, проверить серийный номер действительно для уже созданного прохода. Мы действительный запрос, получить или создать устройство по идентификатору и нажать токен, затем создать регистрацию для устройства и передать. Другой код ответа – это то, что Apple ожидает.

Проходит для устройства

Это конечная точка, вызываемая сразу после отправки запроса push-уведомления.

В этой конечной точке мы получим идентификатор и флаг passesUpdatedSince . Флаг passesUpdatedSince очень важен, потому что он идентифицирует версию прохода. Вы можете использовать любой вариант для управления версией, но использование временных меток в качестве целых чисел является хорошим подходом, поскольку оно также служит для других целей. Если имеется много последней версии прохода, ответьте серийными номерами в виде массива.

Последний проход

Это конечная конечная точка, которая будет вызываться как часть цикла обмена push-уведомлениями. Мы получим один запрос на серийный номер, который мы отправили в предыдущем ответе, и можем сгенерировать пропуск точно так же, как конечная точка генерации прохода, ответив файлом .pkpass .

Незарегистрированный пропуск

Эта конечная точка получит информацию о проходе и устройстве. Мы проверяем пропуск и устройство и, если оба присутствуют И активны, удаляем его.

Журнал Passbook

Эта конечная точка принимает ошибки / предупреждения, отправленные Apple, и просто сохраняет их в таблице logs .

Отправка Push-уведомления

Мы готовы отправлять push-уведомления. Сначала добавьте конечную точку для обновлений прохода. Откройте app.rb , требует grocer и добавьте следующее после конечной точки создания прохода:

 get '/passbooks/update' do request.body.rewind data = JSON.parse request.body.read unless @pass = find_pass_with(data['serialNumber']) @pass = Pass.create(serial_number: data['serialNumber'], data: data) {:response => 'Pass newly created.'}.to_json else @pass.update(data: data, version: Time.now.utc.to_i) push_updates_for_pass {:response => 'Pass updated and sent push notifications.'}.to_json end end def push_updates_for_pass @pass.devices.each do |device| puts "Sending push notification for device - #{device.push_token}" Passbook::PushNotification.send_notification device.push_token end end 

Мы используем метод gem PushNotification.send_notification для отправки push-уведомлений. Как только эта команда будет выполнена, начнется цикл связи, и обновленные данные будут доставлены на устройство пользователя:

Вывод

На этом мы подошли к завершению этого урока. Весь код, используемый в этом уроке, размещен на github , не стесняйтесь раскошелиться.