Skip to content

Add Google Pay display items support for line item breakdown.#13136

Open
jaynewstrom-stripe wants to merge 4 commits into
masterfrom
jaynewstrom/google-pay-display-items
Open

Add Google Pay display items support for line item breakdown.#13136
jaynewstrom-stripe wants to merge 4 commits into
masterfrom
jaynewstrom/google-pay-display-items

Conversation

@jaynewstrom-stripe
Copy link
Copy Markdown
Collaborator

Summary

Adds support for displaying line items in the Google Pay payment sheet, allowing merchants to show a detailed breakdown of charges including subtotals, taxes, and discounts.

Motivation

Merchants need to display itemized pricing information in the Google Pay payment sheet to provide transparency to customers about what they're paying for. This feature enables showing individual line items alongside the total price.

Testing

  • Added tests
  • Modified tests
  • Manually verified

Changelog

  • [Added] Added support for Google Pay display items to show line item breakdowns in the payment sheet

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 20, 2026

Diffuse output:

OLD: paymentsheet-example-release-master.apk (signature: V1, V2)
NEW: paymentsheet-example-release-pr.apk (signature: V1, V2)

          │            compressed            │           uncompressed           
          ├───────────┬───────────┬──────────┼───────────┬───────────┬──────────
 APK      │ old       │ new       │ diff     │ old       │ new       │ diff     
──────────┼───────────┼───────────┼──────────┼───────────┼───────────┼──────────
      dex │   4.6 MiB │   4.6 MiB │ +3.3 KiB │   9.7 MiB │   9.7 MiB │ +2.8 KiB 
     arsc │   3.8 MiB │   3.8 MiB │      0 B │   3.8 MiB │   3.8 MiB │      0 B 
 manifest │   5.8 KiB │   5.8 KiB │      0 B │  30.8 KiB │  30.8 KiB │      0 B 
      res │     1 MiB │     1 MiB │      0 B │   1.7 MiB │   1.7 MiB │      0 B 
   native │   2.7 MiB │   2.7 MiB │      0 B │   2.7 MiB │   2.7 MiB │      0 B 
    asset │  23.5 KiB │    25 KiB │ +1.5 KiB │  44.2 KiB │  45.8 KiB │ +1.5 KiB 
    other │ 245.4 KiB │ 245.4 KiB │     -9 B │ 505.1 KiB │ 505.1 KiB │      0 B 
──────────┼───────────┼───────────┼──────────┼───────────┼───────────┼──────────
    total │  12.3 MiB │  12.3 MiB │ +4.9 KiB │  18.4 MiB │  18.4 MiB │ +4.4 KiB 

 DEX     │ old   │ new   │ diff              
─────────┼───────┼───────┼───────────────────
   files │     1 │     1 │   0               
 strings │ 45280 │ 45293 │ +13 (+27 -14)     
   types │ 14270 │ 14273 │  +3 (+14 -11)     
 classes │ 11592 │ 11593 │  +1 (+4 -3)       
 methods │ 63328 │ 63342 │ +14 (+1616 -1602) 
  fields │ 41128 │ 41140 │ +12 (+297 -285)   

 ARSC    │ old  │ new  │ diff 
─────────┼──────┼──────┼──────
 configs │  319 │  319 │  0   
 entries │ 7475 │ 7475 │  0
APK
     compressed      │     uncompressed     │                                           
──────────┬──────────┼───────────┬──────────┤                                           
 size     │ diff     │ size      │ diff     │ path                                      
──────────┼──────────┼───────────┼──────────┼───────────────────────────────────────────
  4.6 MiB │ +3.3 KiB │   9.7 MiB │ +2.8 KiB │ ∆ classes.dex                             
  8.9 KiB │ +1.6 KiB │   8.8 KiB │ +1.6 KiB │ ∆ assets/dexopt/baseline.prof             
 58.9 KiB │    -11 B │ 130.6 KiB │      0 B │ ∆ META-INF/CERT.SF                        
  1.2 KiB │     -5 B │     1 KiB │     -5 B │ ∆ assets/dexopt/baseline.profm            
  1.2 KiB │     +2 B │   1.2 KiB │      0 B │ ∆ META-INF/CERT.RSA                       
    272 B │     +1 B │     120 B │      0 B │ ∆ META-INF/version-control-info.textproto 
 55.6 KiB │     -1 B │ 130.6 KiB │      0 B │ ∆ META-INF/MANIFEST.MF                    
──────────┼──────────┼───────────┼──────────┼───────────────────────────────────────────
  4.7 MiB │ +4.9 KiB │    10 MiB │ +4.4 KiB │ (total)
DEX
STRINGS:

   old   │ new   │ diff          
  ───────┼───────┼───────────────
   45280 │ 45293 │ +13 (+27 -14) 
  
  + , displayItems=
  + , price=
  + DISCOUNT
  + DisplayItem(label=
  + LINE_ITEM
  + Loo/l;
  + Lxc/r0;
  + Lxc/s0;
  + Lyc/e;
  + SUBTOTAL
  + TAX
  + Total
  + VLLJLLLLLZLL
  + [Lxc/d0;
  + [Lxc/e0;
  + [Lxc/k0;
  + [Lxc/k;
  + [Lxc/q;
  + [Lxc/t;
  + [Lxc/u;
  + [Lyc/c;
  + [Lz9/b;
  + [Lz9/h;
  + displayItems
  + price
  + r8-map-id-502683f47ac976caa7658b87c6b31b284f4a9c517fb63092d6e88afe7d70089e
  + ~~R8{"backend":"dex","compilation-mode":"release","has-checksums":false,"min-api":23,"pg-map-id":"502683f47ac976caa7658b87c6b31b284f4a9c517fb63092d6e88afe7d70089e","r8-mode":"full","version":"8.13.19"}
  
  - Lp5/n;
  - Lz8/h;
  - Lz9/l;
  - VLLJLLLLLZL
  - [Lxc/g0;
  - [Lxc/m;
  - [Lxc/s;
  - [Lxc/y;
  - [Lxc/z;
  - [Lyc/b;
  - [Lz9/c;
  - [Lz9/i;
  - r8-map-id-bf412f4af929aaf8518c02b789318e575940b79a569f8c28d791e67c3c775bd5
  - ~~R8{"backend":"dex","compilation-mode":"release","has-checksums":false,"min-api":23,"pg-map-id":"bf412f4af929aaf8518c02b789318e575940b79a569f8c28d791e67c3c775bd5","r8-mode":"full","version":"8.13.19"}
  

TYPES:

   old   │ new   │ diff         
  ───────┼───────┼──────────────
   14270 │ 14273 │ +3 (+14 -11) 
  
  + Loo/l;
  + Lxc/r0;
  + Lxc/s0;
  + Lyc/e;
  + [Lxc/d0;
  + [Lxc/e0;
  + [Lxc/k0;
  + [Lxc/k;
  + [Lxc/q;
  + [Lxc/t;
  + [Lxc/u;
  + [Lyc/c;
  + [Lz9/b;
  + [Lz9/h;
  
  - Lp5/n;
  - Lz8/h;
  - Lz9/l;
  - [Lxc/g0;
  - [Lxc/m;
  - [Lxc/s;
  - [Lxc/y;
  - [Lxc/z;
  - [Lyc/b;
  - [Lz9/c;
  - [Lz9/i;
  

METHODS:

   old   │ new   │ diff              
  ───────┼───────┼───────────────────
   63328 │ 63342 │ +14 (+1616 -1602) 
  
  + a.a A(int) → float
  + a.a A0(ViewGroup_MarginLayoutParams, int)
  + a.a B() → boolean
  + a.a B0(ViewGroup_MarginLayoutParams, int, int)
  + a.a C(o, Object, Object) → boolean
  + a.a D(o, n, n) → boolean
  + a.a E(long)
  + a.a F(s, Throwable)
  + a.a H(Context) → b0
  + a.a I(Class) → h1
  + a.a J(float[], float, float, float) → float[]
  + a.a K(float[], float, float, float) → float[]
  + a.a L(int, int, float[], float[]) → float
  + a.a M(d, d)
  + a.a N(o) → d
  + a.a O(o) → n
  + a.a P() → String
  + a.a Q(r) → c
  + a.a R(ViewGroup_MarginLayoutParams) → int
  + a.a S(double) → long
  + a.a T(e) → Throwable
  + a.a U() → int
  + a.a V() → int
  + a.a Y(View) → int
  + a.a Z(CoordinatorLayout) → int
  + a.a a0() → int
  + a.a b0() → int
  + a.a c0(double) → long
  + a.a d0(int) → long
  + a.a e0(float[]) → float[]
  + a.a f0(e) → boolean
  + a.a g0(float) → boolean
  + a.a h0(View) → boolean
  + a.a i0() → boolean
  + a.a j0(float, float) → boolean
  + a.a k0(Class, Object, u0)
  + a.a l0(char) → boolean
  + a.a m0(Context) → f3
  + a.a n0(Context, y)
  + a.a o0(float, long) → long
  + a.a p0(n, n)
  + a.a q(b, c, a, a, r, int)
  + a.a q0(n, Thread)
  + a.a r(e, r, int)
  + a.a r0(f0, Object, Object) → boolean
  + a.a s(boolean, e, r, int)
  + a.a s0(f0, Object)
  + a.a t(boolean, boolean, z, r, int)
  + a.a t0(da)
  + a.a u(q0, long, LinkBrand, r, int)
  + a.a u0()
  + a.a v(c, r, int)
  + a.a v0(Object) → Set
  + a.a w(z, a, a, a, r, int)
  + a.a w0(View, float) → boolean
  + a.a x(d) → e
  + a.a x0()
  + a.a y(f0, Object, Object)
  + a.a y0()
  + a.a z(ViewGroup_MarginLayoutParams) → int
  + a.a z0(x) → ExtractedText
  + a5.g D(InputFilter[]) → InputFilter[]
  + a5.g M() → boolean
  + a5.g T(boolean)
  + a5.g U(boolean)
  + a5.g c0(TransformationMethod) → TransformationMethod
  + a5.h D(InputFilter[]) → InputFilter[]
  + a5.h M() → boolean
  + a5.h T(boolean)
  + a5.h U(boolean)
  + a5.h c0(TransformationMethod) → TransformationMethod
  + a6.a0 g(l, int)
  + a6.e0 G0(View, int, int, int, int)
  + a6.e0 H0(View, Matrix)
  + a6.e0 I0(View, Matrix)
  + a6.e0 x0(View, int)
  + a6.f0 G0(View, int, int, int, int)
  + a6.f0 H0(View, Matrix)
  + a6.f0 I0(View, Matrix)
  + a6.f0 R(View) → float
  + a6.f0 w0(View, float)
  + a6.f0 x0(View, int)
  + ab.e y(x, float, float)
  + ab.i M(Object, float)
  + ab.l y(x, float, float)
  + af.k f(w4, l0)
  + af.k h(s4)
  + af.k i(int) → boolean
  + af.k j()
  + af.k k(int, e, e, r, boolean)
  + af.k l()
  + af.k m(q, q, r)
  + androidx.activity.result.ActivityResultCallerLauncher_resultContract_2_1 H(Intent, int) → Object
  + androidx.activity.result.ActivityResultCallerLauncher_resultContract_2_1 m(Context, Object) → Intent
  + androidx.activity.result.contract.ActivityResultContracts_CaptureVideo H
...✂

@jaynewstrom-stripe jaynewstrom-stripe force-pushed the jaynewstrom/google-pay-display-items branch from bb39b4a to 486abb1 Compare June 2, 2026 14:13
@jaynewstrom-stripe jaynewstrom-stripe force-pushed the jaynewstrom/google-pay-display-items branch from 486abb1 to 8b2a30c Compare June 2, 2026 17:06
@jaynewstrom-stripe jaynewstrom-stripe marked this pull request as ready for review June 2, 2026 17:34
@jaynewstrom-stripe jaynewstrom-stripe requested review from a team as code owners June 2, 2026 17:34
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should we extract these detekt changes?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

return GooglePayJsonFactory.DisplayItem(
label = label,
type = GooglePayJsonFactory.DisplayItem.Type.LINE_ITEM,
price = unitAmount ?: total,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
price = unitAmount ?: total,
price = subtotal,

from claude's suggestion.

Current:

Widget x2:  $10.00  (unit amount)
Tax:         $1.60
Total:      $21.60   ← doesn't add up

Should this be subtotal instead? That way:

Widget x2:  $20.00  (line subtotal)
Tax:         $1.60
Total:      $21.60   ← adds up

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Also we should have tests for this.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

The fix needs #13187

I was going to follow up with a more complete fix.


checkoutSession.totalSummary?.let { summary ->
items += summary.discountAmounts.map { it.asDisplayItem() }
items += summary.taxAmounts.map { it.asDisplayItem() }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

For the TaxAmount that is already inclusive, this would double-counts the tax.
So if a line item costs $10.00 with 8% inclusive tax ($0.74 tax included in the $10.00):

Widget:    $10.00   ← already includes tax
Tax:        $0.74   ← adding this double-counts
---
Displayed: $10.74   but actual total is $10.00

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Line items won't include tax after #13187

args: GooglePayPaymentMethodLauncherContractV2.Args
): GooglePayJsonFactory.TransactionInfo {
// Google Pay requires totalPriceLabel when displayItems are present.
val label = args.label ?: if (args.displayItems.isNotEmpty()) "Total" else null
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do we need localized strings?

Comment on lines +506 to +510
class DisplayItem(
val label: String,
val type: Type,
val price: Long,
) : Parcelable {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do we need the status as well?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

What is status? What would we need it for?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

From goole pay documentation

The following variables define price variance:

FINAL
PENDING
Default to FINAL if not provided.

I only see this optional field on SHIPPING_OPTION in their example though.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is SHIPPING_OPTION out of scope here?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

For now, yes.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants