The expanded voucher card — merchant, offer, price and validity above a notched divider, with the description and terms below. Two ways of presenting the terms.
Layout setting, slots for the merchant logo and status badge, and text properties throughout. It composes the DS Accordion for the collapsible terms. The redesign into a component was deliberate.Opens when a voucher is tapped from the grid or the list. Accordion keeps long terms collapsed so the price and validity stay above the fold; TextBlock shows shorter terms outright and links out to the full mechanics.
- Valid from March 11 to 14, 2021
- Dine in, Take out, or Drive-thru: 11am until closing, or until supplies last
- The promo is not valid in conjunction with other promos or discounts.
- Metro Manila only.
Header → Details → strip → Body, and shares Price Container, #price, #originalPrice and #validity with Voucher.⤷ LogoSlot and a status badge through ⤷ BadgeSlot, and the Accordion layout instances the DS Accordion rather than redrawing it.| State | iOS | Android | Figma Property | Notes |
|---|---|---|---|---|
| Layout=Accordion | N/A | N/A | 336 × 507 | Terms collapse behind a DS Accordion instance. The only tappable part of the card. |
| Layout=TextBlock | N/A | N/A | 336 × 472 | Terms run as a paragraph with a link to the full mechanics. Drawn locally rather than instanced — see the note on that below. |
| ⤷ LogoSlot | N/A | N/A | 40 × 40 | Merchant logo. |
| ⤷ BadgeSlot | N/A | N/A | Badge | Offer status, top right of the header. |
| strip | N/A | N/A | 336 × 16 | Notched divider separating the offer summary from its detail. |
- It is a card now, not a screen. The previous version was one 336×704 symbol with no variants — the reason it was ruled Product Layer. What exists now is a card with a real setting, sized to sit inside a screen rather than to be one. The redesign was deliberate, so the Product Layer verdict no longer applies. C4 · Native Mappability
- One setting replaced a set of visibility switches. Terms display used to be driven by booleans that toggled which child subtree rendered, so two paths could be on at once.
Layout = Accordion | TextBlockmakes them mutually exclusive. C2 · Variant & Property Naming - The notched divider is a vector. It was a raster image fill that could not resize or recolour. It is now a
fillrectangle with aUnionof two circles subtracted, plus aseparatorline. C6 · Asset & Icon Quality - The logo and badge are slots.
logo-containerand the frame that positioned the badge were ordinary layers. They are⤷ LogoSlotand⤷ BadgeSlot— real Figma slots, so a merchant supplies its own logo without detaching. C1 · Layer Structure & Naming - No layer name is used twice. Two frames were both called
Content, and#titleheld both the merchant name and the offer name. They are nowDetailsandBody, and the merchant line is#headerwith#subheaderbeneath — named for their place in the hierarchy rather than their content. C1 · Layer Structure & Naming - Price reads the same as it does on the card.
#priceand#originalPrice, one string each, matching Voucher — so the two components describe money the same way and the platform can format it for the locale. C2 · Variant & Property Naming - Names follow the system.
termsAndConditionbecameLayout, itsText Blockvalue becameTextBlock,merchant-detailsbecameMerchant Details, and the boolean operation insidestripisshaperather than repeating its parent's name. C2 · Variant & Property Naming
- Note that only the accordion is tappable. The card has no states of its own by design. The accordion header expands and collapses, and TextBlock's "See full promo mechanics." is a link; everything else is read-only. Say so in the Code tab so nobody wires a tap target around the whole card. Docs
- Announce the card as a region, with the terms reachable. Group the merchant, offer, price and validity so a screen reader gives the offer in one pass, then expose the terms as their own region. In the Accordion layout the collapsed state must be announced, or the terms are invisible to anyone not looking at the screen. A11y
- Give the discount a place, or confirm it belongs only on the card. Voucher carries a discount badge in its vertical layout; this card shows the original price struck through but no discount badge. That may be right — the saving is visible from the two prices — but the two components currently tell the story differently. Family
- Document that the two layouts compose differently. v2.0.1: Applied — the property mapping now carries it. TextBlock draws its own terms frame and owns the three text layers in it, so a link is something this component renders. Accordion places a Terms and Conditions Accordion instance and inherits whatever that component draws, so
hasLinkhas nothing to act on and the rows arrive as content rather than as strings. Two shapes behind one enum, and the choice between them follows the terms you have to show. Docs
Everything above the notched strip is identical in the two layouts — the header, the details block and the strip itself match to the pixel. Only the terms presentation differs, and it differs in kind rather than in colour: TextBlock draws its own frame and owns those three text layers, while Accordion places a Terms and Conditions Accordion instance and inherits whatever that component paints. That is also why the link is TextBlock-only.
| Role | Token | TextBlock | Accordion |
|---|---|---|---|
| Card | bg/color-bg-main | #FFFFFF | #FFFFFF |
| Borders and separator | border/color-border-weak | #E5EBF4 | #E5EBF4 |
| #header | text/color-text | #0A2757 | #0A2757 |
| #subheader | text/color-text-weaker | #6780A9 | #6780A9 |
| #title | text/color-text | #0A2757 | #0A2757 |
| #price | text/color-text-primary | #005CE5 | #005CE5 |
| #originalPrice | text/color-text-weakest | #90A8D0 | #90A8D0 |
| #validity | text/color-text-weaker | #6780A9 | #6780A9 |
| #description | text/color-text-weak | #445C85 | #445C85 |
| Badge | bg/color-bg-info — set by the Badge instance in ⤷ BadgeSlot | #2340A9 | #2340A9 |
| Badge label | text/color-text-inverse | #FFFFFF | #FFFFFF |
| Terms #title | text/color-text | #0A2757 | – |
| Terms #message | text/color-text-weak | #445C85 | – |
| Terms #footer | text/color-text-primary | #005CE5 | – |
| Terms surface | bg/color-bg — set by the Accordion instance | – | #F6F9FD |
| Terms label | text/color-text-weak — set by the same instance | – | #445C85 |
- Valid from March 11 to 14, 2021
- Dine in, Take out, or Drive-thru: 11am until closing, or until supplies last
- The promo is not valid in conjunction with other promos or discounts.
- Metro Manila only.
iOS — Swift Package Manager
.package(url: "https://github.com/AY-Org/eb-ds-ios", from: "1.0.0")
Android — Gradle (Kotlin DSL)
implementation("com.eastblue.ds:voucher:1.0.0")
Import
import EastBlueDS import com.eastblue.ds.voucher.*
Planned API — the native library does not exist yet. The artifact is the Voucher family: this component, <a href="/components/voucher">Voucher</a> and the other five members all ship in <code>com.eastblue.ds:voucher</code> and import <code>com.eastblue.ds.voucher.*</code>.
One enum, five booleans and two slots. The booleans are not parameters: each asks whether an optional piece of content is present, so the optional itself is what a caller passes. The two layouts compose differently, and that is the one thing a mapping has to carry forward. TextBlock draws its own terms frame and owns the three text layers inside it, so a link is something this component renders. Accordion places a Terms and Conditions Accordion instance and inherits whatever that component draws — which is why hasLink has nothing to act on there, and why the terms rows arrive as content rather than as strings. Natively that is two shapes behind one enum, not one shape with a flag.
| Figma Property | SwiftUI | Compose |
|---|---|---|
| Layout — TextBlock, Accordion | layout: EBVoucherDetailsLayout | layout: EBVoucherDetailsLayout |
| hasLimitedOffer — true, false | derived — badge != nil | derived — badge != null |
| hasCurrentPrice — true, false | derived — price != nil | derived — price != null |
| hasOriginalPrice — true, false | derived — originalPrice != nil | derived — originalPrice != null |
| hasValidityPeriod — true, false | derived — validity != nil | derived — validity != null |
| hasLink — true, false | derived — linkLabel != nil — TextBlock only | derived — linkLabel != null — TextBlock only |
| ⤷ LogoSlot (slot) | @ViewBuilder logo: () -> Logo | logo: @Composable () -> Unit |
| ⤷ BadgeSlot (slot) | badge: (() -> Badge)? = nil | badge: (@Composable () -> Unit)? = null |
EBVoucherDetails( layout: .textblock, merchant: "Brand", branch: "All branches", title: "Voucher Title", price: "PHP 200.00", originalPrice: "PHP 280.00", validity: "Mar 11 2023 - Mar 14 2023", description: "For every 12 oz or larger beverage purchase…", badge: { EBBadge("Limited") }, linkLabel: "See full promo mechanics." ) { EBBrandLogo(image: "brand-mark") }
EBVoucherDetails( layout = EBVoucherDetailsLayout.Textblock, merchant = "Brand", branch = "All branches", title = "Voucher Title", price = "PHP 200.00", originalPrice = "PHP 280.00", validity = "Mar 11 2023 - Mar 14 2023", description = "For every 12 oz or larger beverage purchase…", badge = { EBBadge("Limited") }, linkLabel = "See full promo mechanics." ) { EBBrandLogo(image = "brand-mark") }
EBVoucherDetails( layout: .accordion, merchant: "Brand", branch: "All branches", title: "Voucher Title", price: "PHP 200.00", originalPrice: "PHP 280.00", validity: "Mar 11 2023 - Mar 14 2023", description: "For every 12 oz or larger beverage purchase…", badge: { EBBadge("Limited") } ) { EBBrandLogo(image: "brand-mark") }
EBVoucherDetails( layout = EBVoucherDetailsLayout.Accordion, merchant = "Brand", branch = "All branches", title = "Voucher Title", price = "PHP 200.00", originalPrice = "PHP 280.00", validity = "Mar 11 2023 - Mar 14 2023", description = "For every 12 oz or larger beverage purchase…", badge = { EBBadge("Limited") } ) { EBBrandLogo(image = "brand-mark") }
| Requirement | iOS | Android |
|---|---|---|
| The offer summary reads as one unit | Group merchant, title, price and validity with .accessibilityElement(children: .combine) | Modifier.semantics(mergeDescendants = true) on the Details frame |
| Collapsed terms are announced as collapsed | The Accordion supplies .isExpanded and its own toggle label | Role.Button with stateDescription of expanded or collapsed |
| The mechanics link is a link | .accessibilityAddTraits(.isLink) | Role.Button with a descriptive onClickLabel |
| The notched divider is decorative | .accessibilityHidden(true) | contentDescription = null |
Do
Use Accordion when the terms run long enough to push the price off screen.
Don't
Don't collapse two lines of terms; the extra tap buys nothing.
Do
Use TextBlock with a link out when the full mechanics live elsewhere.
Don't
Don't put the entire mechanics text in the block — that is what the link is for.
Do
Let the merchant supply its own logo through the slot.
Don't
Don't detach the card to swap a logo.
| ID | Criterion | Status | Notes |
|---|---|---|---|
| C1 | Layer Structure & Naming | Ready | Header → Details → strip → Body, no duplicated names, text layers all prefixed. |
| C2 | Variant & Property Naming | Ready | One enum with two clean values, five booleans all on the has* prefix, and two slots. The one wrinkle is that hasLink exists on both layouts while only TextBlock has a #footer for it to act on — Accordion’s terms come from a nested instance. |
| C3 | Token Coverage | Ready | Every colour resolves to a system token, and all ten are the ones the file’s own selection list carries: bg/color-bg-main, bg/color-bg, bg/color-bg-info, border/color-border-weak, text/color-text and its four weak steps, plus text/color-text-inverse on the badge label. All ten text styles resolve too. |
| C4 | Native Mappability | Ready | A card with a header, a summary and a body. The two layouts compose differently on purpose — TextBlock draws its own terms frame and owns the text inside it, Accordion places a Terms and Conditions Accordion instance and inherits its content — and which one you reach for depends on the terms you have to show. Natively that is two shapes behind one enum: hasLink only reaches TextBlock, and the accordion’s rows arrive as content rather than as strings. Documented in the property mapping rather than smoothed over. |
| C5 | Interaction State Coverage | Not Applicable | Display only by design. The accordion header and the mechanics link own their own states. |
| C6 | Asset & Icon Quality | Ready | The notched divider is a vector; the logo arrives through a slot. No rasters. |
1 Layout setting × 2 values. Everything above the notched divider is identical between them.
| Layout | Size | Terms | Node |
|---|---|---|---|
| Accordion | 336 × 507 | DS Accordion instance, collapsible | 5450:32408 |
| TextBlock | 336 × 472 | Locally drawn block with a mechanics link | 5450:32877 |
Layout is the only variant property and the only axis, so TextBlock and Accordion each get a card and the five booleans become controls.get_node_info, which returns variant properties only — they exist nowhere but the property panel. The Properties section and the demo panel now carry all eight.border/color-border-subtle, text/color-text-heading, -link, -placeholder and main/badge/info/bg appear nowhere in the file’s selection colours. Every hex was right; every name was made up. Four more colours had no row at all — the description, the badge label and the terms block’s three text layers. The ten names now on the page are exactly the ten the file uses..eb-preview-vdet declared font-family: inherit. Five of the seven layers this component always draws are Primary/*, so Proxima Soft is the root and the branch line, the description and the whole terms block name BarkAda for themselves.Secondary/Bold/Base, and were set at 14/21 weight 500, 13/20, 13/18 and 13 bold. One rule covers all four now, which is what stops them drifting again.line-height and no tracking, so it read small and its pill sat short. The two prices had no line-height either, so each inherited the page’s and rendered 25.6 against a container that is 18 — the row was 7 too tall before anything else was wrong. And Merchant Details had no display, so its two spans laid out inline and the brand name sat beside the branch line instead of above it.2px 8px gave the right 16 height by coincidence and made the pill 68 wide against Figma’s 52. It is 4 either side of a 44-wide label now, with 2 above and below, and reconstructs on both axes.Chevron Up instance Select places, M23 20L16 13L9 20 at stroke-width 2. The row ticks were a ✓ text glyph in a pseudo-element, standing in for a 16 × 16 stroked path nobody had exported. The list gap was 12 where the item positions measure 6.padding: 12px 16px 16px, which inset both terms treatments by 16 — they run the full 336, and only the Description insets its own text. The seven keys are the component’s own, and the heights reconstruct exactly: 64 + 95 + 16 + 297 = 472 and 64 + 95 + 16 + 332 = 507.#header / #subheader, #title / #description, #price / #originalPrice, #validity. Eight rows now, one per panel property, with the five booleans marked derived: each asks whether an optional is present, so the optional is what a caller passes.gcash/east-blue-ios and com.gcash.eastblue:components:1.0.0, with no Import line. It now cites the Voucher family artifact com.eastblue.ds:voucher:1.0.0, the same one Voucher uses.hasLink has nothing to act on there. Two shapes behind one enum, chosen by the terms you have to show. C4 moves to Ready and the recommendation to Applied.Layout value. The three Code Connect rows are gone; C7 stays blocked while no native library exists.Layout = Accordion | TextBlock makes the two terms paths mutually exclusive; previously both could render.⤷ LogoSlot and ⤷ BadgeSlot.Content became Details and Body; the two #title layers became #header / #subheader and #title.