Skip to content

[PR-26997] Support Apple poster pass styles - #5

Open
ilovepinkpony1 wants to merge 4 commits into
mainfrom
vo-PR-26997
Open

[PR-26997] Support Apple poster pass styles#5
ilovepinkpony1 wants to merge 4 commits into
mainfrom
vo-PR-26997

Conversation

@ilovepinkpony1

Copy link
Copy Markdown

Summary

Adds opt-in support for the iOS 27 Apple Wallet poster styles, and fixes boarding_pass keys never reaching pass.json.

Problem

  1. The gem can only emit the classic pass layouts (storeCard, coupon, eventTicket, generic, boardingPass). iOS 27 adds the poster styles, which use a different layout: logo -> logo text -> header fields -> background image -> barcode -> primary fields -> footer fields. There is no secondary or auxiliary row, and there is a footer row the classic layouts do not have. Wallet also renders up to two action buttons on the face of the pass from a top-level featuredActions key.

  2. boarding_pass was dead code. The old line was pass[:boardingPass].merge(@pass.boarding_pass)Hash#merge returns a new hash and the result was thrown away, so keys like transitType never reached pass.json.

Solution

Four new hooks on Passkit::BasePass, all with safe defaults so nothing changes for existing passes:

hook default purpose
poster_pass_type nil poster style dictionary to emit, e.g. :posterGeneric
poster_primary_fields primary_fields the row above the poster footer
footer_fields [] the poster footer row
featured_actions [] up to two action buttons

A pass that sets poster_pass_type emits both style dictionaries in one pass.json. This is Apple's own backward-compatibility pattern: older iOS does not know the posterGeneric key, ignores it, and renders the classic dictionary instead. One pass file works on every device.

poster_primary_fields is a separate hook rather than reusing primary_fields because both dictionaries ship in the same file and the two slots hold different content — the classic layout puts primary fields above the barcode, the poster below it. It defaults to primary_fields so passes that want the same content in both get it for free.

posterGeneric is the only poster style Wallet accepts as a top-level key. posterEventTicket is a preferredStyleSchemes array entry, not a style dictionary.

The boarding pass fix assigns the merge result back: pass[:boardingPass] = pass[:boardingPass].merge(@pass.boarding_pass).

generate_json_pass is split into a one-line file writer plus a pass_json builder so the payload can be tested without a signing certificate or a booted app.

Version bumped to 0.8.0.

Demo

https://admin.bastion.talkable.com/

Screenshots

Checklist

  • Updated the documentation accordingly
  • Added tests to cover the changes
  • Deployed the changes to Void
  • Deployed the changes to Bastion
  • Deployed the changes to Bart
  • Can be reverted easily on Production

Related Stories

Test Plan

test/test_generator_poster.rb — 7 tests, 15 assertions, all green:

bundle exec ruby -Itest test/test_generator_poster.rb
7 runs, 15 assertions, 0 failures, 0 errors, 0 skips

Coverage:

  • the poster dictionary is emitted alongside the classic one, sharing header and back fields
  • the poster dictionary carries no secondary or auxiliary rows
  • poster primary fields are independent of the classic ones
  • poster_primary_fields falls back to primary_fields when a pass does not override it
  • featuredActions is emitted
  • passes with no poster style are byte-for-byte unchanged (no posterGeneric, no featuredActions)
  • boarding_pass keys are merged into the boardingPass dictionary without wiping the generated fields

Both fixes were reverted locally to confirm the tests fail without them — 4 failures, then green again after restoring.

bundle exec standardrb adds no new offenses (42 before this branch, 42 after).

bundle exec rake cannot run on this machine: it needs a .env (the repo ships .example.env), and even with the env vars set there is a pre-existing gem conflict — can't activate sqlite3 (>= 2.1), already activated sqlite3-1.7.3. Both failures reproduce on main.

Files Changed

  • lib/passkit/base_pass.rb — the four new hooks with defaults
  • lib/passkit/generator.rb — emit the poster dictionary and featuredActions; fix the boarding pass merge; split out pass_json
  • app/models/passkit/pass.rb — delegate the four new hooks
  • lib/passkit/version.rb — 0.7.0 -> 0.8.0
  • CHANGELOG.md — 0.8.0 entry
  • test/test_generator_poster.rb — new

Vitalii Oliinyk and others added 4 commits August 26, 2026 01:21
Emit a poster style dictionary (posterGeneric, iOS 27+) alongside the classic
one so a single pass renders everywhere: devices that know the poster key use
it, older ones ignore it and fall back to pass_type.

Poster layouts drop the secondary/auxiliary rows for a single footer field, so
the dictionary carries header, footer and back fields only.

Also adds featured_actions (PR-26998) to the pass surface, and fixes the
boardingPass merge silently discarding its result.
The posterGeneric layout renders header -> barcode -> primary -> footer, but
the poster dictionary only carried header/footer/back, so poster passes drew an
empty primary row. Add a poster_primary_fields hook (defaulting to the classic
primary_fields) and emit it.

The classic and poster dictionaries ship in the same pass.json, so one accessor
cannot serve both slots -- the Talkable app maps its secondary_fields to the
poster primaries.

Also fix the featured_actions doc example: "link" is not one of Apple's
featured action types.

Tests move to test/test_generator_poster.rb / TestGeneratorPoster so they no
longer collide with the test/test_generator.rb added by PR #4, and the
Rails.root stub is now guarded so it cannot repoint the dummy app that the
controller tests boot in the same rake process.
Add a 0.8.0 changelog entry. It names the poster hooks and the boarding_pass
fix, which is a behaviour change: those keys used to be merged into a discarded
copy of the boardingPass dictionary and never reached pass.json.

Test that fix, and test that poster_primary_fields falls back to primary_fields
-- PosterCard overrides the hook, so the default was uncovered.

Name posterGeneric as the only poster style Wallet takes as a top-level key.
posterEventTicket is a preferredStyleSchemes entry, not a style dictionary.
Every other optional key in pass_json uses a plain truthy check. This one
called the hook twice and used .any?, which raises NoMethodError when a
subclass returns nil instead of the [] default.

present? covers both nil and [], so the default still emits no key.
@ilovepinkpony1
ilovepinkpony1 requested a review from necroua August 27, 2026 10:53
@ilovepinkpony1
ilovepinkpony1 marked this pull request as ready for review August 27, 2026 10:53

@zhuravel zhuravel left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No problems found in this PR.

Reviewed main@6c0174dvo-PR-26997@122af06 only.

Checks

  • ruby -Itest test/test_generator_poster.rb — 8 runs and 16 assertions passed.
  • Ruby syntax — five changed Ruby files passed.
  • Full suite — not run: sqlite3 ~> 1.4 is not installed.
  • StandardRB — not run: the standard gem is not installed.
  • Apple Wallet docs — poster and featured action schemas matched.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants