├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SampleMerchant ├── .gitignore ├── firebase.json └── public │ ├── 404.html │ └── index.html ├── SamplePay ├── .gitignore ├── .idea │ ├── codeStyles │ │ ├── Project.xml │ │ └── codeStyleConfig.xml │ ├── copyright │ │ ├── Google_LLC.xml │ │ └── profiles_settings.xml │ ├── jarRepositories.xml │ └── vcs.xml ├── app │ ├── .gitignore │ ├── build.gradle.kts │ ├── proguard-rules.pro │ └── src │ │ ├── androidTest │ │ └── java │ │ │ └── com │ │ │ └── example │ │ │ └── android │ │ │ └── samplepay │ │ │ ├── PackageManagerUtilsTest.kt │ │ │ └── model │ │ │ └── PaymentDetailsTest.kt │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── aidl │ │ └── org │ │ │ └── chromium │ │ │ ├── IsReadyToPayService.aidl │ │ │ ├── IsReadyToPayServiceCallback.aidl │ │ │ └── components │ │ │ └── payments │ │ │ ├── IPaymentDetailsUpdateService.aidl │ │ │ └── IPaymentDetailsUpdateServiceCallback.aidl │ │ ├── ic_launcher-web.png │ │ ├── java │ │ └── com │ │ │ └── example │ │ │ └── android │ │ │ └── samplepay │ │ │ ├── model │ │ │ ├── AddressErrors.kt │ │ │ ├── BundleUtils.kt │ │ │ ├── PaymentAddress.kt │ │ │ ├── PaymentDetails.kt │ │ │ ├── PaymentDetailsUpdate.kt │ │ │ ├── PaymentOptions.kt │ │ │ ├── PaymentParams.kt │ │ │ └── ShippingOption.kt │ │ │ ├── service │ │ │ ├── SampleIsReadyToPayService.kt │ │ │ └── SamplePaymentDetailsUpdateService.kt │ │ │ ├── ui │ │ │ ├── MainActivity.kt │ │ │ ├── PaymentApp.kt │ │ │ ├── PaymentScreen.kt │ │ │ ├── PaymentViewModel.kt │ │ │ └── theme │ │ │ │ ├── Color.kt │ │ │ │ ├── Theme.kt │ │ │ │ └── Type.kt │ │ │ └── util │ │ │ ├── ActivitiyUtils.kt │ │ │ └── PackageManagerUtils.kt │ │ └── res │ │ ├── anim │ │ ├── slide_from_bottom.xml │ │ ├── slide_from_bottom_20.xml │ │ ├── slide_to_bottom.xml │ │ └── slide_to_bottom_20.xml │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── arrays.xml │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml ├── build.gradle.kts ├── firebase.json ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── public │ ├── 404.html │ ├── icon-web.png │ ├── icon.png │ ├── index.html │ ├── manifest.json │ └── payment-manifest.json └── settings.gradle.kts ├── app-debug.apk └── payment-app-repo.gif /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution; 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. Consult 22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 23 | information on using pull requests. 24 | 25 | ## Community Guidelines 26 | 27 | This project follows 28 | [Google's Open Source Community Guidelines](https://opensource.google.com/conduct/). 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | Copyright [yyyy] [name of copyright owner] 179 | 180 | Licensed under the Apache License, Version 2.0 (the "License"); 181 | you may not use this file except in compliance with the License. 182 | You may obtain a copy of the License at 183 | 184 | http://www.apache.org/licenses/LICENSE-2.0 185 | 186 | Unless required by applicable law or agreed to in writing, software 187 | distributed under the License is distributed on an "AS IS" BASIS, 188 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 189 | See the License for the specific language governing permissions and 190 | limitations under the License. 191 | 192 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | This is an Android payment app sample that works with Web PaymentRequest API. 3 | 4 | - SamplePay 5 | - An Android payment app 6 | - A web payment app (The implementation is empty; it can only delegate to the Android app) 7 | - SampleMerchant 8 | - A web merchant site 9 | 10 | ## Test the Android payment App 11 | 12 | 1. Build and run the Android app in the `SamplePay` folder using Android Studio (or other tools or IDEs using Gradle) or run the `app-debug.apk` file included in the repo. 13 | 2. Open the Chrome browser and navigate to https://sample-pay-ss.web.app 14 | 3. Click on "PAY" button. 15 | 16 | 17 | 18 | 19 | 42 | 45 | 46 |
20 | 21 | ## SampleMerchant 22 | 23 | The project can be deployed to [Firebase Hosting](https://firebase.google.com/docs/hosting). 24 | 25 | 1. Install [Firebase CLI](https://firebase.google.com/docs/cli#install_the_firebase_cli). 26 | 2. Create a new Firebase project. 27 | 3. Edit `SampleMerchant/.firebaserc` and change the project ID to yours. 28 | 4. Edit `SampleMerchant/public/index.html` and change the `supportedMethods` to your SamplePay's domain. 29 | 5. Run `$ firebase deploy`. 30 | 31 | ## SamplePay (Android) 32 | 33 | 1. Import the project path (`SamplePay/`) to Android Studio. 34 | 2. Modify `SamplePay/gradle.properties` and change the domain name to yours. 35 | 3. Run 36 | 37 | 38 | 39 | > **Note:** This app generates invalid responses to show how the Payment Request API responds to incomplete payloads (e.g.: an empty shipping address). Make sure that your payment objects are well formed. 40 | 41 | 43 | Screencast of a complete payment flow using the applications in this repository 44 |
47 | 48 | ## SamplePay (Web) 49 | 50 | The project can be deployed to [Firebase Hosting](https://firebase.google.com/docs/hosting). 51 | 52 | 1. Install [Firebase CLI](https://firebase.google.com/docs/cli#install_the_firebase_cli). 53 | 2. Create a new Firebase project. 54 | 3. Edit `SamplePay/.firebaserc` and change the project ID to yours. 55 | 4. Edit `SamplePay/public/manifest.json` and change the domain nams to yours. Also, change the fingerprint SHA256 hash to your SamplePay's app. 56 | 5. Edit `SamplePay/public/payment-manifest.json` and change the domain name to yours. 57 | 6. Edit `SamplePay/firebase.json` and change the domain name to yours. 58 | 6. Run `$ firebase deploy`. 59 | 60 | 61 | -------------------------------------------------------------------------------- /SampleMerchant/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | firebase-debug.log* 8 | 9 | # Firebase cache 10 | .firebase/ 11 | 12 | # Firebase config 13 | 14 | # Uncomment this if you'd like others to create their own Firebase project. 15 | # For a team working on the same Firebase project(s), it is recommended to leave 16 | # it commented so all members can deploy to the same project(s) in .firebaserc. 17 | .firebaserc 18 | 19 | # Runtime data 20 | pids 21 | *.pid 22 | *.seed 23 | *.pid.lock 24 | 25 | # Directory for instrumented libs generated by jscoverage/JSCover 26 | lib-cov 27 | 28 | # Coverage directory used by tools like istanbul 29 | coverage 30 | 31 | # nyc test coverage 32 | .nyc_output 33 | 34 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 35 | .grunt 36 | 37 | # Bower dependency directory (https://bower.io/) 38 | bower_components 39 | 40 | # node-waf configuration 41 | .lock-wscript 42 | 43 | # Compiled binary addons (http://nodejs.org/api/addons.html) 44 | build/Release 45 | 46 | # Dependency directories 47 | node_modules/ 48 | 49 | # Optional npm cache directory 50 | .npm 51 | 52 | # Optional eslint cache 53 | .eslintcache 54 | 55 | # Optional REPL history 56 | .node_repl_history 57 | 58 | # Output of 'npm pack' 59 | *.tgz 60 | 61 | # Yarn Integrity file 62 | .yarn-integrity 63 | 64 | # dotenv environment variables file 65 | .env 66 | -------------------------------------------------------------------------------- /SampleMerchant/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "public", 4 | "ignore": [ 5 | "firebase.json", 6 | "**/.*", 7 | "**/node_modules/**" 8 | ] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /SampleMerchant/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Page Not Found 7 | 8 | 23 | 24 | 25 |
26 |

404

27 |

Page Not Found

28 |

The specified file was not found on this website. Please check the URL for mistakes and try again.

29 |

Why am I seeing this?

30 |

This page was generated by the Firebase Command-Line Interface. To modify it, edit the 404.html file in your project's configured public directory.

31 |
32 | 33 | 34 | -------------------------------------------------------------------------------- /SampleMerchant/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 9 | Sample Merchant 10 | 11 | 28 | 29 | 30 |
31 |

Welcome

32 |

Sample Merchant

33 |

This is a sample merchant page.

34 | Pay 35 |
36 |
37 | 320 | 321 | 322 | -------------------------------------------------------------------------------- /SamplePay/.gitignore: -------------------------------------------------------------------------------- 1 | # ======== 2 | # Android 3 | 4 | *.iml 5 | .gradle 6 | /local.properties 7 | /.idea/caches 8 | /.idea/libraries 9 | /.idea/modules.xml 10 | /.idea/workspace.xml 11 | /.idea/navEditor.xml 12 | /.idea/assetWizardSettings.xml 13 | /.idea/deploymentTargetDropDown.xml 14 | /.idea/androidTestResultsUserPreferences.xml 15 | .DS_Store 16 | /build 17 | /captures 18 | .externalNativeBuild 19 | .cxx 20 | 21 | # ================ 22 | # Firebase Hosting 23 | 24 | # Logs 25 | logs 26 | *.log 27 | npm-debug.log* 28 | yarn-debug.log* 29 | yarn-error.log* 30 | firebase-debug.log* 31 | 32 | # Firebase cache 33 | .firebase/ 34 | 35 | # Firebase config 36 | 37 | # Uncomment this if you'd like others to create their own Firebase project. 38 | # For a team working on the same Firebase project(s), it is recommended to leave 39 | # it commented so all members can deploy to the same project(s) in .firebaserc. 40 | .firebaserc 41 | 42 | # Runtime data 43 | pids 44 | *.pid 45 | *.seed 46 | *.pid.lock 47 | 48 | # Directory for instrumented libs generated by jscoverage/JSCover 49 | lib-cov 50 | 51 | # Coverage directory used by tools like istanbul 52 | coverage 53 | 54 | # nyc test coverage 55 | .nyc_output 56 | 57 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 58 | .grunt 59 | 60 | # Bower dependency directory (https://bower.io/) 61 | bower_components 62 | 63 | # node-waf configuration 64 | .lock-wscript 65 | 66 | # Compiled binary addons (http://nodejs.org/api/addons.html) 67 | build/Release 68 | 69 | # Dependency directories 70 | node_modules/ 71 | 72 | # Optional npm cache directory 73 | .npm 74 | 75 | # Optional eslint cache 76 | .eslintcache 77 | 78 | # Optional REPL history 79 | .node_repl_history 80 | 81 | # Output of 'npm pack' 82 | *.tgz 83 | 84 | # Yarn Integrity file 85 | .yarn-integrity 86 | 87 | # dotenv environment variables file 88 | .env 89 | -------------------------------------------------------------------------------- /SamplePay/.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 9 | 10 | 11 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | xmlns:android 20 | 21 | ^$ 22 | 23 | 24 | 25 |
26 |
27 | 28 | 29 | 30 | xmlns:.* 31 | 32 | ^$ 33 | 34 | 35 | BY_NAME 36 | 37 |
38 |
39 | 40 | 41 | 42 | .*:id 43 | 44 | http://schemas.android.com/apk/res/android 45 | 46 | 47 | 48 |
49 |
50 | 51 | 52 | 53 | .*:name 54 | 55 | http://schemas.android.com/apk/res/android 56 | 57 | 58 | 59 |
60 |
61 | 62 | 63 | 64 | name 65 | 66 | ^$ 67 | 68 | 69 | 70 |
71 |
72 | 73 | 74 | 75 | style 76 | 77 | ^$ 78 | 79 | 80 | 81 |
82 |
83 | 84 | 85 | 86 | .* 87 | 88 | ^$ 89 | 90 | 91 | BY_NAME 92 | 93 |
94 |
95 | 96 | 97 | 98 | .* 99 | 100 | http://schemas.android.com/apk/res/android 101 | 102 | 103 | ANDROID_ATTRIBUTE_ORDER 104 | 105 |
106 |
107 | 108 | 109 | 110 | .* 111 | 112 | .* 113 | 114 | 115 | BY_NAME 116 | 117 |
118 |
119 |
120 |
121 | 122 | 124 |
125 |
-------------------------------------------------------------------------------- /SamplePay/.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /SamplePay/.idea/copyright/Google_LLC.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /SamplePay/.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /SamplePay/.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | -------------------------------------------------------------------------------- /SamplePay/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /SamplePay/app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /SamplePay/app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | plugins { 18 | id("com.android.application") 19 | id("org.jetbrains.kotlin.android") 20 | id("org.jetbrains.kotlin.plugin.compose") 21 | id("org.jetbrains.kotlin.plugin.serialization") 22 | id("kotlin-parcelize") 23 | } 24 | 25 | android { 26 | namespace = "com.example.android.samplepay" 27 | compileSdk = 35 28 | 29 | defaultConfig { 30 | applicationId = "com.example.android.samplepay" 31 | minSdk = 21 32 | targetSdk = 35 33 | versionCode = 1 34 | versionName = "1.0" 35 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 36 | 37 | // Use the method name configuration in the 'gradle.properties' file. 38 | buildConfigField("String", "SAMPLE_PAY_METHOD_NAME", "\"${project.property("samplePayMethodName")}\"") 39 | resValue("string", "sample_pay_method_name", project.properties.get("samplePayMethodName") as String) 40 | } 41 | 42 | buildFeatures { 43 | aidl = true 44 | buildConfig = true 45 | compose = true 46 | } 47 | 48 | buildTypes { 49 | getByName("release") { 50 | isMinifyEnabled = true 51 | proguardFiles( 52 | getDefaultProguardFile("proguard-android.txt"), 53 | "proguard-rules.pro" 54 | ) 55 | } 56 | } 57 | 58 | compileOptions { 59 | sourceCompatibility = JavaVersion.VERSION_17 60 | targetCompatibility = JavaVersion.VERSION_17 61 | } 62 | 63 | kotlinOptions { 64 | jvmTarget = "17" 65 | } 66 | } 67 | 68 | 69 | composeCompiler { 70 | reportsDestination = layout.buildDirectory.dir("compose_compiler") 71 | stabilityConfigurationFiles = listOf(rootProject.layout.projectDirectory.file("stability_config.conf")) 72 | } 73 | 74 | dependencies { 75 | val composeBom = platform("androidx.compose:compose-bom:2025.02.00") 76 | implementation(composeBom) 77 | 78 | implementation("androidx.appcompat:appcompat:1.7.0") 79 | implementation("androidx.compose.material3:material3-android:1.3.2") 80 | implementation("androidx.compose.ui:ui:1.8.1") 81 | implementation("androidx.compose.ui:ui-tooling:1.8.1") 82 | implementation("androidx.activity:activity-compose:1.10.1") 83 | implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.9.0") 84 | implementation("androidx.core:core-ktx:1.16.0") 85 | implementation("androidx.activity:activity-ktx:1.10.1") 86 | implementation("androidx.navigation:navigation-compose:2.9.0") 87 | 88 | implementation("androidx.constraintlayout:constraintlayout:2.2.1") 89 | implementation("androidx.coordinatorlayout:coordinatorlayout:1.3.0") 90 | implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.9.0") 91 | implementation("androidx.fragment:fragment-ktx:1.8.6") 92 | implementation("com.google.android.material:material:1.12.0") 93 | 94 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") 95 | implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.0") 96 | 97 | testImplementation("junit:junit:4.13.2") 98 | testImplementation("com.google.truth:truth:1.1.3") 99 | 100 | androidTestImplementation(composeBom) 101 | androidTestImplementation("androidx.test:runner:1.6.2") 102 | androidTestImplementation("androidx.test:rules:1.6.1") 103 | androidTestImplementation("androidx.test.ext:junit:1.2.1") 104 | androidTestImplementation("androidx.test.ext:truth:1.6.0") 105 | androidTestImplementation("androidx.test:runner:1.6.2") 106 | androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1") 107 | } -------------------------------------------------------------------------------- /SamplePay/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle.kts. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /SamplePay/app/src/androidTest/java/com/example/android/samplepay/PackageManagerUtilsTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.samplepay 18 | 19 | import android.content.Context 20 | import androidx.test.core.app.ApplicationProvider 21 | import androidx.test.ext.junit.runners.AndroidJUnit4 22 | import com.example.android.samplepay.util.getApplicationSignatures 23 | import com.google.common.truth.Truth.assertThat 24 | import org.junit.Test 25 | import org.junit.runner.RunWith 26 | 27 | @RunWith(AndroidJUnit4::class) 28 | class PackageManagerUtilsTest { 29 | 30 | @Test 31 | fun checkArbitrarySignatures() { 32 | val context: Context = ApplicationProvider.getApplicationContext() 33 | val chromeSignatures = context.packageManager.getApplicationSignatures("com.android.chrome") 34 | assertThat(chromeSignatures).isNotEmpty() 35 | 36 | val appSignatures = context.packageManager.getApplicationSignatures("com.example.android.samplepay") 37 | assertThat(appSignatures).isNotEmpty() 38 | 39 | assertThat(chromeSignatures).isNotEqualTo(appSignatures) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /SamplePay/app/src/androidTest/java/com/example/android/samplepay/model/PaymentDetailsTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.samplepay.model 18 | 19 | import androidx.test.ext.junit.runners.AndroidJUnit4 20 | import com.google.common.truth.Truth.assertThat 21 | import org.junit.Test 22 | import org.junit.runner.RunWith 23 | 24 | private const val SAMPLE_TOTAL = "{\"currency\":\"USD\",\"value\":\"25.00\"}" 25 | 26 | @RunWith(AndroidJUnit4::class) 27 | class PaymentDetailsTest { 28 | 29 | @Test 30 | fun parseTotal() { 31 | val total = PaymentAmount.parse(SAMPLE_TOTAL) 32 | assertThat(total.currency).isEqualTo("$") 33 | assertThat(total.value).isEqualTo("25.00") 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /SamplePay/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 38 | 39 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 52 | 55 | 56 | 57 | 60 | 65 | 66 | 67 | 68 | 69 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /SamplePay/app/src/main/aidl/org/chromium/IsReadyToPayService.aidl: -------------------------------------------------------------------------------- 1 | package org.chromium; 2 | 3 | import org.chromium.IsReadyToPayServiceCallback; 4 | 5 | interface IsReadyToPayService { 6 | oneway void isReadyToPay(IsReadyToPayServiceCallback callback); 7 | } 8 | -------------------------------------------------------------------------------- /SamplePay/app/src/main/aidl/org/chromium/IsReadyToPayServiceCallback.aidl: -------------------------------------------------------------------------------- 1 | package org.chromium; 2 | 3 | interface IsReadyToPayServiceCallback { 4 | oneway void handleIsReadyToPay(boolean isReadyToPay); 5 | } 6 | -------------------------------------------------------------------------------- /SamplePay/app/src/main/aidl/org/chromium/components/payments/IPaymentDetailsUpdateService.aidl: -------------------------------------------------------------------------------- 1 | package org.chromium.components.payments; 2 | 3 | import android.os.Bundle; 4 | 5 | import org.chromium.components.payments.IPaymentDetailsUpdateServiceCallback; 6 | 7 | /** 8 | * Helper interface used by the invoked native payment app to notify the 9 | * browser that the user has selected a different payment method, shipping 10 | * option, or shipping address. 11 | */ 12 | interface IPaymentDetailsUpdateService { 13 | /** 14 | * Called to notify the browser that the user has selected a different 15 | * payment method. 16 | * 17 | * @param paymentHandlerMethodData The data containing the selected payment 18 | * method's name and optional stringified details. 19 | */ 20 | oneway void changePaymentMethod(in Bundle paymentHandlerMethodData, 21 | IPaymentDetailsUpdateServiceCallback callback); 22 | 23 | /** 24 | * Called to notify the browser that the user has selected a different 25 | * shipping option. 26 | * 27 | * @param shippingOptionId The identifier of the selected shipping option. 28 | */ 29 | oneway void changeShippingOption(in String shippingOptionId, 30 | IPaymentDetailsUpdateServiceCallback callback); 31 | 32 | /** 33 | * Called to notify the browser that the user has selected a different 34 | * shipping address. 35 | * 36 | * @param shippingAddress The selected shipping address. 37 | */ 38 | oneway void changeShippingAddress(in Bundle shippingAddress, 39 | IPaymentDetailsUpdateServiceCallback callback); 40 | } 41 | -------------------------------------------------------------------------------- /SamplePay/app/src/main/aidl/org/chromium/components/payments/IPaymentDetailsUpdateServiceCallback.aidl: -------------------------------------------------------------------------------- 1 | package org.chromium.components.payments; 2 | 3 | import android.os.Bundle; 4 | 5 | import org.chromium.components.payments.IPaymentDetailsUpdateService; 6 | 7 | /** 8 | * Helper interface used by the browser to notify the invoked native app about 9 | * merchant's response to one of the paymentmethodchange, shippingoptionchange, 10 | * or shippingaddresschange events. 11 | */ 12 | interface IPaymentDetailsUpdateServiceCallback { 13 | /** 14 | * Called to notify the invoked payment app about updated payment details 15 | * received from the merchant. 16 | * 17 | * @param updatedPaymentDetails The updated payment details received from 18 | * the merchant. 19 | */ 20 | oneway void updateWith(in Bundle updatedPaymentDetails); 21 | 22 | /** 23 | * Called to notify the invoked payment app that the merchant has not 24 | * modified the payment details. 25 | */ 26 | oneway void paymentDetailsNotUpdated(); 27 | 28 | /** 29 | * Called during a payment flow to point the payment app back to the payment 30 | * details update service to invoke when the user changes the payment 31 | * method, the shipping address, or the shipping option. 32 | * 33 | * @param service The payment details update service to invoke when the user 34 | * changes the payment method, the shipping address, or the shipping 35 | * option. 36 | */ 37 | oneway void setPaymentDetailsUpdateService( 38 | IPaymentDetailsUpdateService service); 39 | } 40 | -------------------------------------------------------------------------------- /SamplePay/app/src/main/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/android-web-payment/1528cf7d2470e11e33ff52ac6444cdd75e7a233d/SamplePay/app/src/main/ic_launcher-web.png -------------------------------------------------------------------------------- /SamplePay/app/src/main/java/com/example/android/samplepay/model/AddressErrors.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.samplepay.model 18 | 19 | import android.os.Bundle 20 | 21 | data class AddressErrors( 22 | val addressLines: String?, 23 | val countryCode: String?, 24 | val city: String?, 25 | val dependentLocality: String?, 26 | val organization: String?, 27 | val phone: String?, 28 | val postalCode: String?, 29 | val recipient: String?, 30 | val region: String?, 31 | val sortingCode: String? 32 | ) { 33 | companion object { 34 | fun from(extras: Bundle): AddressErrors { 35 | return AddressErrors( 36 | addressLines = extras.getString("addressLines"), 37 | countryCode = extras.getString("countryCode"), 38 | city = extras.getString("city"), 39 | dependentLocality = extras.getString("dependentLocality"), 40 | organization = extras.getString("organization"), 41 | phone = extras.getString("phone"), 42 | postalCode = extras.getString("postalCode"), 43 | recipient = extras.getString("recipient"), 44 | region = extras.getString("region"), 45 | sortingCode = extras.getString("sortingCode") 46 | ) 47 | } 48 | } 49 | 50 | override fun toString(): String { 51 | return buildString { 52 | if (!addressLines.isNullOrEmpty()) { 53 | appendLine(addressLines) 54 | } 55 | if (!countryCode.isNullOrEmpty()) { 56 | appendLine(countryCode) 57 | } 58 | if (!city.isNullOrEmpty()) { 59 | appendLine(city) 60 | } 61 | if (!dependentLocality.isNullOrEmpty()) { 62 | appendLine(dependentLocality) 63 | } 64 | if (!organization.isNullOrEmpty()) { 65 | appendLine(organization) 66 | } 67 | if (!phone.isNullOrEmpty()) { 68 | appendLine(phone) 69 | } 70 | if (!postalCode.isNullOrEmpty()) { 71 | appendLine(postalCode) 72 | } 73 | if (!recipient.isNullOrEmpty()) { 74 | appendLine(recipient) 75 | } 76 | if (!region.isNullOrEmpty()) { 77 | appendLine(region) 78 | } 79 | if (!sortingCode.isNullOrEmpty()) { 80 | appendLine(sortingCode) 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /SamplePay/app/src/main/java/com/example/android/samplepay/model/BundleUtils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.samplepay.model 18 | 19 | import android.os.Build 20 | import android.os.Bundle 21 | import android.os.Parcelable 22 | import androidx.lifecycle.SavedStateHandle 23 | import org.json.JSONException 24 | 25 | @Suppress("DEPRECATION", "UNCHECKED_CAST") 26 | private fun Bundle.getBundleArray(key: String): List? { 27 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 28 | getParcelableArray(key, Bundle::class.java)?.toList() 29 | } else { 30 | getParcelableArray(key)?.map {it as Bundle} 31 | } 32 | } 33 | 34 | internal fun Bundle.getPaymentAmount(key: String): PaymentAmount? { 35 | val s = getString(key) 36 | return if (s != null) { 37 | try { 38 | PaymentAmount.parse(s) 39 | } catch (e: JSONException) { 40 | e.printStackTrace() 41 | null 42 | } 43 | } else { 44 | null 45 | } 46 | } 47 | 48 | internal fun Bundle.getShippingOptions(key: String): List { 49 | return getBundleArray(key)?.map(ShippingOption::from) ?: emptyList() 50 | } 51 | 52 | internal fun Bundle.getMethodData(key: String): Map { 53 | val b = getBundle(key) ?: return emptyMap() 54 | return b.keySet().associateWith { b.getString(it, "[]") } 55 | } 56 | 57 | internal fun SavedStateHandle.getMethodData(key: String): Map { 58 | val b = get(key) 59 | return b?.keySet()?.associateWith { b.getString(it, "[]") } ?: emptyMap() 60 | } 61 | 62 | internal fun SavedStateHandle.getShippingOptions(key: String): List { 63 | return get>(key)?.map { ShippingOption.from(it as Bundle) } ?: emptyList() 64 | } -------------------------------------------------------------------------------- /SamplePay/app/src/main/java/com/example/android/samplepay/model/PaymentAddress.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.samplepay.model 18 | 19 | import android.os.Bundle 20 | 21 | data class PaymentAddress( 22 | val id: String, 23 | val label: String, 24 | val addressLines: List, 25 | val countryCode: String, 26 | val country: String, 27 | val city: String, 28 | val dependentLocality: String, 29 | val organization: String, 30 | val phone: String, 31 | val postalCode: String, 32 | val recipient: String, 33 | val region: String, 34 | val sortingCode: String 35 | ) { 36 | fun asBundle(): Bundle { 37 | val address = Bundle() 38 | address.putStringArray("addressLines", addressLines.toTypedArray()) 39 | address.putString("countryCode", countryCode) 40 | address.putString("country", country) 41 | address.putString("city", city) 42 | address.putString("dependentLocality", dependentLocality) 43 | address.putString("organization", organization) 44 | address.putString("phone", phone) 45 | address.putString("postalCode", postalCode) 46 | address.putString("recipient", recipient) 47 | address.putString("region", region) 48 | address.putString("sortingCode", sortingCode) 49 | return address 50 | } 51 | 52 | override fun toString(): String { 53 | var address = "$recipient, $organization, " 54 | addressLines.forEach { addressLine -> address += ("$addressLine, ") } 55 | val cityLine = if (region.isNotEmpty()) { 56 | "$city, $region, $postalCode, $country\n" 57 | } else { 58 | "$dependentLocality, $city, $postalCode, $country\n" 59 | } 60 | address += cityLine 61 | return address 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /SamplePay/app/src/main/java/com/example/android/samplepay/model/PaymentDetails.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.samplepay.model 18 | 19 | import android.os.Bundle 20 | import org.json.JSONException 21 | import org.json.JSONObject 22 | import java.util.Currency 23 | 24 | data class PaymentAmount( 25 | val currency: String, val value: String 26 | ) { 27 | companion object { 28 | fun parse(json: String): PaymentAmount { 29 | try { 30 | val obj = JSONObject(json) 31 | val currencySymbol = Currency.getInstance(obj.getString("currency")).symbol 32 | return PaymentAmount( 33 | currencySymbol, obj.getString("value") 34 | ) 35 | } catch (e: JSONException) { 36 | throw RuntimeException("Cannot parse JSON", e) 37 | } 38 | } 39 | 40 | fun from(extras: Bundle): PaymentAmount { 41 | val currencySymbol = 42 | extras.getString("currency")?.let { Currency.getInstance(it).symbol }.orEmpty() 43 | return PaymentAmount( 44 | currency = currencySymbol, value = extras.getString("value")!! 45 | ) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /SamplePay/app/src/main/java/com/example/android/samplepay/model/PaymentDetailsUpdate.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.samplepay.model 18 | 19 | import android.os.Bundle 20 | 21 | data class PaymentDetailsUpdate( 22 | val total: PaymentAmount?, 23 | val shippingOptions: List?, 24 | val error: String?, 25 | val paymentMethodErrors: String?, 26 | val addressErrors: AddressErrors? 27 | ) { 28 | companion object { 29 | fun from(extras: Bundle): PaymentDetailsUpdate { 30 | return PaymentDetailsUpdate( 31 | total = extras.getBundle("total")?.let { PaymentAmount.from(it) }, 32 | shippingOptions = extras.getShippingOptions("shippingOptions"), 33 | error = extras.getString("error"), 34 | paymentMethodErrors = extras.getString("stringifiedPaymentMethodErrors"), 35 | addressErrors = extras.getBundle("addressErrors")?.let { AddressErrors.from(it) } 36 | ) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /SamplePay/app/src/main/java/com/example/android/samplepay/model/PaymentOptions.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.samplepay.model 18 | 19 | import android.os.Bundle 20 | 21 | data class PaymentOptions( 22 | val requestPayerName: Boolean = false, 23 | val requestPayerPhone: Boolean = false, 24 | val requestPayerEmail: Boolean = false, 25 | val requestShipping: Boolean = false, 26 | val shippingType: String = "shipping" 27 | ) { 28 | companion object { 29 | fun from(extras: Bundle?): PaymentOptions { 30 | return extras?.let { 31 | PaymentOptions(requestPayerName = it.getBoolean("requestPayerName", false), 32 | requestPayerPhone = it.getBoolean("requestPayerPhone", false), 33 | requestPayerEmail = it.getBoolean("requestPayerEmail", false), 34 | requestShipping = it.getBoolean("requestShipping", false), 35 | shippingType = it.getString("shippingType", "shipping")) 36 | } ?: PaymentOptions() 37 | } 38 | } 39 | 40 | val requireContact: Boolean 41 | get() = requestPayerName || requestPayerPhone || requestPayerEmail 42 | } 43 | -------------------------------------------------------------------------------- /SamplePay/app/src/main/java/com/example/android/samplepay/model/PaymentParams.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.samplepay.model 18 | 19 | import android.os.Bundle 20 | import androidx.lifecycle.SavedStateHandle 21 | 22 | /** 23 | * Represents all the parameters passed to the activity for the PAY action. 24 | */ 25 | data class PaymentParams( 26 | /** 27 | * The names of the methods being used. The elements are the keys in the [methodData] 28 | * dictionary, and indicate the methods that the payment app supports. 29 | */ 30 | val methodNames: List, 31 | 32 | /** 33 | * A mapping from each [methodName][methodNames] to the 34 | * [methodData](https://w3c.github.io/payment-request/#declaring-multiple-ways-of-paying). 35 | */ 36 | val methodData: Map, 37 | 38 | /** 39 | * The contents of the `` HTML tag of the top-level browsing context on the checkout web 40 | * page. 41 | */ 42 | val merchantName: String, 43 | 44 | /** 45 | * The schemeless origin of the top-level browsing context. For example, 46 | * `https://mystore.com/checkout` will be passed as `mystore.com`. 47 | */ 48 | val topLevelOrigin: String, 49 | 50 | /** 51 | * The schemeless origin of the iframe browsing context that invoked the `new 52 | * PaymentRequest(methodData, details, options)` constructor in Javascript. If the constructor 53 | * was invoked from the top-level context, then the value of this parameter equals the value of 54 | * [topLevelOrigin] parameter. 55 | */ 56 | val paymentRequestOrigin: String, 57 | 58 | /** 59 | * The total amount of the checkout. 60 | */ 61 | val total: PaymentAmount?, 62 | 63 | /** 64 | * The output of JSON.stringify(details.modifiers), where details.modifiers contain only 65 | * supportedMethods and total. 66 | */ 67 | val modifiers: String, 68 | 69 | /** 70 | * The [PaymentRequest.id](https://w3c.github.io/payment-request/#id-attribute) field that 71 | * “push-payment” apps should associate with transaction state. Merchant websites will use 72 | * this field to query the “push-payment” apps for the state of transaction out of band. 73 | */ 74 | val paymentRequestId: String, 75 | 76 | /** 77 | * The additional information requested by the merchant. 78 | */ 79 | val paymentOptions: PaymentOptions, 80 | 81 | /** 82 | * Merchant specified shipping options; will be non-null whenever shipping is requested. 83 | */ 84 | val shippingOptions: List<ShippingOption> 85 | ) { 86 | companion object { 87 | fun from(extras: Bundle) = PaymentParams( 88 | methodNames = extras.getStringArrayList("methodNames") ?: emptyList(), 89 | methodData = extras.getMethodData("methodData"), 90 | merchantName = extras.getString("merchantName", ""), 91 | topLevelOrigin = extras.getString("topLevelOrigin", ""), 92 | paymentRequestOrigin = extras.getString("paymentRequestOrigin", ""), 93 | total = extras.getPaymentAmount("total"), 94 | modifiers = extras.getString("modifiers", "[]"), 95 | paymentRequestId = extras.getString("paymentRequestId", ""), 96 | paymentOptions = PaymentOptions.from(extras.getBundle("paymentOptions")), 97 | shippingOptions = extras.getShippingOptions("shippingOptions") 98 | ) 99 | 100 | fun from(state: SavedStateHandle) = 101 | PaymentParams( 102 | methodNames = state["methodNames"] ?: emptyList(), 103 | methodData = state.getMethodData("methodData"), 104 | merchantName = state["merchantName"] ?: "", 105 | topLevelOrigin = state["topLevelOrigin"] ?: "", 106 | paymentRequestOrigin = state["paymentRequestOrigin"] ?: "", 107 | total = state.get<String>("total")?.let(PaymentAmount::parse), 108 | modifiers = state["modifiers"] ?: "[]", 109 | paymentRequestId = state["paymentRequestId"] ?: "", 110 | paymentOptions = PaymentOptions.from(state["paymentOptions"]), 111 | shippingOptions = state.getShippingOptions("shippingOptions") 112 | ) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /SamplePay/app/src/main/java/com/example/android/samplepay/model/ShippingOption.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.samplepay.model 18 | 19 | import android.os.Bundle 20 | 21 | data class ShippingOption( 22 | val id: String, 23 | val label: String, 24 | val amountCurrency: String, 25 | val amountValue: String, 26 | val selected: Boolean 27 | ) { 28 | companion object { 29 | fun from(extras: Bundle): ShippingOption { 30 | return ShippingOption( 31 | id = extras.getString("id", ""), 32 | label = extras.getString("label", ""), 33 | amountCurrency = extras.getBundle("amount")?.getString("currency", "") ?: "", 34 | amountValue = extras.getBundle("amount")?.getString("value", "") ?: "", 35 | selected = extras.getBoolean("selected", false) 36 | ) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /SamplePay/app/src/main/java/com/example/android/samplepay/service/SampleIsReadyToPayService.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.samplepay.service 18 | 19 | import android.app.Service 20 | import android.content.Intent 21 | import android.os.RemoteException 22 | import android.util.Log 23 | import org.chromium.IsReadyToPayService 24 | import org.chromium.IsReadyToPayServiceCallback 25 | 26 | /** This service handles the IS_READY_TO_PAY action from Chrome. */ 27 | class SampleIsReadyToPayService : Service() { 28 | 29 | private val TAG = "IsReadyToPayService" 30 | 31 | private val binder = object : IsReadyToPayService.Stub() { 32 | override fun isReadyToPay(callback: IsReadyToPayServiceCallback?) { 33 | try { 34 | val callingPackage: String? = packageManager.getNameForUid(getCallingUid()) 35 | Log.d(TAG, "The caller is $callingPackage") 36 | 37 | // Allow non-Chrome callers. 38 | callback?.handleIsReadyToPay(true) 39 | } catch (_: RemoteException) { 40 | // Ignore 41 | } 42 | } 43 | } 44 | 45 | override fun onBind(intent: Intent?) = binder 46 | } -------------------------------------------------------------------------------- /SamplePay/app/src/main/java/com/example/android/samplepay/service/SamplePaymentDetailsUpdateService.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.samplepay.service 18 | 19 | import android.app.Service 20 | import android.content.Intent 21 | import android.os.Bundle 22 | import com.example.android.samplepay.ui.ApplicationIdentity 23 | import com.example.android.samplepay.util.getApplicationSignatures 24 | import org.chromium.components.payments.IPaymentDetailsUpdateService 25 | import org.chromium.components.payments.IPaymentDetailsUpdateServiceCallback 26 | 27 | 28 | /** A service to handle the UPDATE_PAYMENT_DETAILS service connection from Chrome. */ 29 | class SamplePaymentDetailsUpdateService : Service() { 30 | 31 | /** 32 | * The service received from the caller to send payment updates back and update the 33 | * original checkout flow. 34 | */ 35 | private var updateService: IPaymentDetailsUpdateService? = null 36 | 37 | /** A local binder to connect from the payment application and fetch the remote service. */ 38 | private val binder = LocalBinder() 39 | 40 | inner class LocalBinder : IPaymentDetailsUpdateServiceCallback.Stub() { 41 | 42 | /** 43 | * The identity of the remote caller is registered at creation time, and verified against 44 | * identity of the Web application initiating the payment process. 45 | */ 46 | private var remoteCallerIdentity: ApplicationIdentity? = null 47 | 48 | override fun updateWith(updatedPaymentDetails: Bundle?) { 49 | // No-op 50 | } 51 | 52 | override fun paymentDetailsNotUpdated() { 53 | // No-op 54 | } 55 | 56 | override fun setPaymentDetailsUpdateService(service: IPaymentDetailsUpdateService) { 57 | val callingAppId = packageManager.getNameForUid(getCallingUid()) 58 | remoteCallerIdentity = callingAppId?.let { appId -> 59 | ApplicationIdentity(appId, packageManager.getApplicationSignatures(appId)) 60 | } 61 | 62 | updateService = service 63 | } 64 | 65 | /** 66 | * Retrieves the remote service so that the local payment app can use it as a channel to 67 | * report checkout changes in the Web application. 68 | * 69 | * @param appIdentity the identity that initiated the payment process. 70 | * @return the remote service to send updates back to the Web application. 71 | * @throws IllegalStateException if the identity that initiated the payment and delivered 72 | * the service don't match, or an [UnsupportedCallerException] if the caller does not 73 | * support the `UPDATE_PAYMENT_DETAILS` needed for this application to run. 74 | */ 75 | fun getUpdateService(appIdentity: ApplicationIdentity): IPaymentDetailsUpdateService? { 76 | 77 | if (remoteCallerIdentity == null) { 78 | throw UnsupportedCallerException(""" 79 | The service that initiated the payment does not support the `UPDATE_PAYMENT_DETAILS` service, which is necessary to run this application. 80 | """.trimIndent() 81 | ) 82 | } 83 | 84 | if (remoteCallerIdentity == appIdentity) { 85 | return updateService 86 | } 87 | 88 | throw IllegalStateException(""" 89 | Multiple callers are attempting to interact with this payment application. 90 | The identities of the application initiating the payment and receiving updates don't match. 91 | """.trimIndent() 92 | ) 93 | } 94 | } 95 | 96 | override fun onBind(intent: Intent?) = binder 97 | } 98 | 99 | /** A type to inform about support for the services required for callers of this payment app */ 100 | class UnsupportedCallerException(message: String? = null, cause: Throwable? = null) : 101 | IllegalStateException(message, cause) -------------------------------------------------------------------------------- /SamplePay/app/src/main/java/com/example/android/samplepay/ui/MainActivity.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.samplepay.ui 18 | 19 | import android.os.Bundle 20 | import androidx.activity.ComponentActivity 21 | import androidx.activity.compose.setContent 22 | import androidx.activity.enableEdgeToEdge 23 | import androidx.activity.viewModels 24 | import androidx.compose.runtime.getValue 25 | import androidx.compose.runtime.mutableStateOf 26 | import androidx.compose.runtime.saveable.rememberSaveable 27 | import androidx.compose.ui.res.stringResource 28 | import androidx.lifecycle.compose.collectAsStateWithLifecycle 29 | import androidx.lifecycle.viewmodel.MutableCreationExtras 30 | import com.example.android.samplepay.R 31 | import com.example.android.samplepay.util.overrideCloseTransition 32 | import com.example.android.samplepay.util.overrideOpenTransition 33 | 34 | class MainActivity : ComponentActivity() { 35 | 36 | private val viewModel: PaymentViewModel by viewModels( 37 | factoryProducer = { PaymentViewModel.Factory }, 38 | extrasProducer = { 39 | MutableCreationExtras(defaultViewModelCreationExtras).apply { 40 | set(PaymentViewModel.CALLING_PACKAGE_KEY, callingPackage) 41 | } 42 | }) 43 | 44 | override fun onCreate(savedInstanceState: Bundle?) { 45 | enableEdgeToEdge() 46 | super.onCreate(savedInstanceState) 47 | 48 | // Refrain from taking action if the intent data is inappropriate 49 | if ("org.chromium.intent.action.PAY" == intent.action && intent.extras == null) { 50 | cancel() 51 | } 52 | 53 | setContent { 54 | val payIntent by viewModel.paymentIntent.collectAsStateWithLifecycle() 55 | val payResult by viewModel.paymentResult.collectAsStateWithLifecycle() 56 | val openErrorDialog = rememberSaveable { mutableStateOf(false) } 57 | 58 | PaymentApp( 59 | payIntent = payIntent, 60 | openErrorDialog = openErrorDialog.value, 61 | errorMessage = messageResourceForResult(payResult) 62 | ?.let { stringResource(it) } 63 | .orEmpty(), 64 | onErrorDismissed = { 65 | openErrorDialog.value = false 66 | cancel() 67 | }, 68 | onAddPromoCode = viewModel::applyPromotionCode, 69 | onShippingOptionChange = viewModel::updateShippingOption, 70 | onShippingAddressChange = viewModel::updateShippingAddress, 71 | onPayButtonClicked = viewModel::pay 72 | ) 73 | 74 | when (payResult) { 75 | PaymentResult.None -> {} 76 | is PaymentResult.ResultIntent -> { 77 | setResult(RESULT_OK, (payResult as PaymentResult.ResultIntent).intent) 78 | finish() 79 | } 80 | 81 | is PaymentResult.Error, PaymentResult.UnsupportedCaller -> { 82 | openErrorDialog.value = true 83 | } 84 | } 85 | } 86 | } 87 | 88 | override fun onResume() { 89 | super.onResume() 90 | overrideOpenTransition(R.anim.slide_from_bottom, R.anim.slide_to_bottom_20) 91 | } 92 | 93 | override fun onPause() { 94 | super.onPause() 95 | overrideCloseTransition(R.anim.slide_from_bottom_20, R.anim.slide_to_bottom) 96 | } 97 | 98 | private fun cancel() { 99 | setResult(RESULT_CANCELED) 100 | finish() 101 | } 102 | 103 | private fun messageResourceForResult(result: PaymentResult) = when (result) { 104 | is PaymentResult.Error -> R.string.error_multiple_callers 105 | is PaymentResult.UnsupportedCaller -> R.string.error_caller_not_supported 106 | else -> null 107 | } 108 | } -------------------------------------------------------------------------------- /SamplePay/app/src/main/java/com/example/android/samplepay/ui/PaymentApp.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.samplepay.ui 18 | 19 | import android.annotation.SuppressLint 20 | import androidx.compose.foundation.layout.padding 21 | import androidx.compose.material3.AlertDialog 22 | import androidx.compose.material3.ExperimentalMaterial3Api 23 | import androidx.compose.material3.MaterialTheme 24 | import androidx.compose.material3.Scaffold 25 | import androidx.compose.material3.Text 26 | import androidx.compose.material3.TextButton 27 | import androidx.compose.material3.TopAppBar 28 | import androidx.compose.material3.TopAppBarDefaults 29 | import androidx.compose.runtime.Composable 30 | import androidx.compose.ui.Modifier 31 | import androidx.compose.ui.graphics.Color 32 | import androidx.compose.ui.res.dimensionResource 33 | import androidx.compose.ui.res.stringResource 34 | import com.example.android.samplepay.R 35 | import com.example.android.samplepay.ui.theme.AppTheme 36 | 37 | @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") 38 | @Composable 39 | fun PaymentApp( 40 | payIntent: PaymentIntent, 41 | openErrorDialog: Boolean, 42 | errorMessage: String, 43 | onErrorDismissed: () -> Unit, 44 | onAddPromoCode: (String) -> Unit, 45 | onShippingOptionChange: (String) -> Unit, 46 | onShippingAddressChange: (String) -> Unit, 47 | onPayButtonClicked: (PaymentFormInfo) -> Unit 48 | ) { 49 | AppTheme { 50 | when (payIntent) { 51 | is PaymentIntent.None -> DefaultScreen() 52 | is PaymentIntent.Started -> { 53 | PaymentScreen( 54 | payIntent = payIntent, 55 | onAddPromoCode = onAddPromoCode, 56 | onShippingOptionChange = onShippingOptionChange, 57 | onShippingAddressChange = onShippingAddressChange, 58 | onPayButtonClicked = onPayButtonClicked 59 | ) 60 | SecurityErrorDialog( 61 | openDialog = openErrorDialog, 62 | errorMessage = errorMessage, 63 | onDismissed = onErrorDismissed 64 | ) 65 | } 66 | } 67 | } 68 | } 69 | 70 | @OptIn(ExperimentalMaterial3Api::class) 71 | @Composable 72 | fun DefaultScreen(modifier: Modifier = Modifier) { 73 | Scaffold( 74 | topBar = { 75 | TopAppBar( 76 | title = { 77 | Text(stringResource(R.string.app_name)) 78 | }, colors = TopAppBarDefaults.topAppBarColors( 79 | containerColor = MaterialTheme.colorScheme.primary, 80 | titleContentColor = MaterialTheme.colorScheme.inversePrimary 81 | ) 82 | ) 83 | }) { innerPadding -> 84 | Text( 85 | text = stringResource(R.string.home_explanation), 86 | color = Color.Black, 87 | modifier = modifier 88 | .padding(innerPadding) 89 | .padding(dimensionResource(R.dimen.spacing_medium)) 90 | ) 91 | } 92 | } 93 | 94 | @Composable 95 | fun SecurityErrorDialog(openDialog: Boolean, errorMessage: String, onDismissed: () -> Unit) { 96 | if (openDialog) { 97 | AlertDialog(title = { 98 | Text(text = stringResource(R.string.error)) 99 | }, text = { 100 | Text(text = errorMessage) 101 | }, onDismissRequest = onDismissed, dismissButton = { 102 | TextButton(onClick = onDismissed) { 103 | Text(stringResource(R.string.dismiss)) 104 | } 105 | }, confirmButton = {}) 106 | } 107 | } -------------------------------------------------------------------------------- /SamplePay/app/src/main/java/com/example/android/samplepay/ui/PaymentScreen.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.samplepay.ui 18 | 19 | import android.content.res.Configuration 20 | import android.os.Parcelable 21 | import androidx.compose.foundation.background 22 | import androidx.compose.foundation.layout.Box 23 | import androidx.compose.foundation.layout.Column 24 | import androidx.compose.foundation.layout.PaddingValues 25 | import androidx.compose.foundation.layout.Row 26 | import androidx.compose.foundation.layout.Spacer 27 | import androidx.compose.foundation.layout.fillMaxWidth 28 | import androidx.compose.foundation.layout.height 29 | import androidx.compose.foundation.layout.padding 30 | import androidx.compose.foundation.rememberScrollState 31 | import androidx.compose.foundation.selection.selectable 32 | import androidx.compose.foundation.selection.selectableGroup 33 | import androidx.compose.foundation.shape.RoundedCornerShape 34 | import androidx.compose.foundation.text.BasicText 35 | import androidx.compose.foundation.text.KeyboardActions 36 | import androidx.compose.foundation.text.KeyboardOptions 37 | import androidx.compose.foundation.text.TextAutoSize 38 | import androidx.compose.foundation.verticalScroll 39 | import androidx.compose.material.icons.Icons 40 | import androidx.compose.material.icons.rounded.AddCircle 41 | import androidx.compose.material3.Button 42 | import androidx.compose.material3.ExperimentalMaterial3Api 43 | import androidx.compose.material3.Icon 44 | import androidx.compose.material3.LocalTextStyle 45 | import androidx.compose.material3.MaterialTheme 46 | import androidx.compose.material3.RadioButton 47 | import androidx.compose.material3.Scaffold 48 | import androidx.compose.material3.SegmentedButton 49 | import androidx.compose.material3.SegmentedButtonDefaults 50 | import androidx.compose.material3.SingleChoiceSegmentedButtonRow 51 | import androidx.compose.material3.Surface 52 | import androidx.compose.material3.Text 53 | import androidx.compose.material3.TextField 54 | import androidx.compose.material3.TextFieldDefaults 55 | import androidx.compose.material3.TopAppBar 56 | import androidx.compose.material3.TopAppBarDefaults 57 | import androidx.compose.runtime.Composable 58 | import androidx.compose.runtime.getValue 59 | import androidx.compose.runtime.mutableStateOf 60 | import androidx.compose.runtime.saveable.rememberSaveable 61 | import androidx.compose.runtime.setValue 62 | import androidx.compose.ui.Alignment 63 | import androidx.compose.ui.Modifier 64 | import androidx.compose.ui.draw.drawWithCache 65 | import androidx.compose.ui.geometry.CornerRadius 66 | import androidx.compose.ui.graphics.Brush 67 | import androidx.compose.ui.graphics.Color 68 | import androidx.compose.ui.graphics.drawscope.Stroke 69 | import androidx.compose.ui.input.key.Key 70 | import androidx.compose.ui.input.key.key 71 | import androidx.compose.ui.input.key.onKeyEvent 72 | import androidx.compose.ui.platform.LocalSoftwareKeyboardController 73 | import androidx.compose.ui.platform.testTag 74 | import androidx.compose.ui.res.dimensionResource 75 | import androidx.compose.ui.res.stringResource 76 | import androidx.compose.ui.semantics.Role 77 | import androidx.compose.ui.text.TextStyle 78 | import androidx.compose.ui.text.input.ImeAction 79 | import androidx.compose.ui.text.input.KeyboardType 80 | import androidx.compose.ui.tooling.preview.Preview 81 | import androidx.compose.ui.unit.dp 82 | import androidx.compose.ui.unit.sp 83 | import com.example.android.samplepay.R 84 | import com.example.android.samplepay.model.PaymentAddress 85 | import com.example.android.samplepay.model.PaymentAmount 86 | import com.example.android.samplepay.model.PaymentOptions 87 | import com.example.android.samplepay.model.ShippingOption 88 | import com.example.android.samplepay.ui.theme.Typography 89 | import kotlinx.parcelize.Parcelize 90 | 91 | @Composable 92 | fun PaymentScreen( 93 | payIntent: PaymentIntent.Started, 94 | onAddPromoCode: (String) -> Unit, 95 | onShippingOptionChange: (String) -> Unit, 96 | onShippingAddressChange: (String) -> Unit, 97 | onPayButtonClicked: (PaymentFormInfo) -> Unit, 98 | modifier: Modifier = Modifier 99 | ) { 100 | val (_, merchantName, merchantOrigin, errorText, promoCodeErrorText, amount, paymentOptions, shippingOptions, defaultShippingOptionId, paymentAddresses) = payIntent 101 | Surface { 102 | PaymentScaffold( 103 | merchantName = merchantName, merchantOrigin = merchantOrigin, modifier = modifier 104 | ) { innerPadding -> 105 | PaymentSummary( 106 | paymentOptions = paymentOptions, 107 | errorText = errorText, 108 | promotionCodeErrorText = promoCodeErrorText, 109 | amount = amount, 110 | onAddPromoCode = onAddPromoCode, 111 | shippingOptions = shippingOptions, 112 | defaultShippingOptionId = defaultShippingOptionId, 113 | onShippingOptionChange = onShippingOptionChange, 114 | paymentAddresses = paymentAddresses, 115 | onShippingAddressChange = onShippingAddressChange, 116 | onPayButtonClicked = onPayButtonClicked, 117 | modifier = modifier.padding(innerPadding) 118 | ) 119 | } 120 | } 121 | } 122 | 123 | @OptIn(ExperimentalMaterial3Api::class) 124 | @Composable 125 | private fun PaymentScaffold( 126 | merchantName: String?, 127 | merchantOrigin: String?, 128 | modifier: Modifier = Modifier, 129 | content: @Composable (innerPadding: PaddingValues) -> Unit 130 | ) { 131 | Box( 132 | modifier = modifier.background( 133 | Brush.linearGradient( 134 | colorStops = arrayOf( 135 | 0.0f to MaterialTheme.colorScheme.primary.copy(alpha = 0.1f), 136 | 0.5f to MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.35f), 137 | 0.8f to MaterialTheme.colorScheme.tertiary.copy(alpha = 0.25f) 138 | ) 139 | ) 140 | ) 141 | ) { 142 | Scaffold( 143 | topBar = { 144 | TopAppBar( 145 | title = { 146 | Column { 147 | merchantName?.let { 148 | Text( 149 | text = it, 150 | style = Typography.titleLarge, 151 | color = MaterialTheme.colorScheme.primary 152 | ) 153 | } 154 | merchantOrigin?.let { 155 | Text( 156 | text = it, 157 | style = Typography.labelMedium, 158 | color = MaterialTheme.colorScheme.secondary 159 | ) 160 | } 161 | } 162 | }, colors = TopAppBarDefaults.topAppBarColors( 163 | titleContentColor = MaterialTheme.colorScheme.inversePrimary, 164 | containerColor = Color.Transparent 165 | ) 166 | ) 167 | }, containerColor = Color.Transparent, content = content 168 | ) 169 | } 170 | } 171 | 172 | @Composable 173 | private fun PaymentSummary( 174 | amount: PaymentAmount?, 175 | paymentOptions: PaymentOptions, 176 | onAddPromoCode: (String) -> Unit, 177 | shippingOptions: List<ShippingOption>, 178 | defaultShippingOptionId: String?, 179 | onShippingOptionChange: (String) -> Unit, 180 | paymentAddresses: List<PaymentAddress>, 181 | onShippingAddressChange: (String) -> Unit, 182 | onPayButtonClicked: (PaymentFormInfo) -> Unit, 183 | errorText: String? = null, 184 | promotionCodeErrorText: String? = null, 185 | modifier: Modifier = Modifier 186 | ) { 187 | val scrollState = rememberScrollState() 188 | 189 | var promoCode: String by rememberSaveable { mutableStateOf("") } 190 | var contactInfo: ContactInfo by rememberSaveable { mutableStateOf(ContactInfo()) } 191 | var shippingOptionSelection: String by rememberSaveable { mutableStateOf(defaultShippingOptionId.orEmpty()) } 192 | var shippingAddressSelection: String by rememberSaveable { mutableStateOf("") } 193 | 194 | Column( 195 | modifier = modifier 196 | .padding(dimensionResource(R.dimen.spacing_medium)) 197 | .fillMaxWidth() 198 | .verticalScroll(scrollState) 199 | ) { 200 | errorText?.let { 201 | Text( 202 | modifier = Modifier.padding(horizontal = dimensionResource(R.dimen.spacing_medium)), 203 | text = it, 204 | style = MaterialTheme.typography.bodySmall, 205 | color = MaterialTheme.colorScheme.error 206 | ) 207 | Spacer(modifier = Modifier.height(dimensionResource(R.dimen.spacing_medium))) 208 | } 209 | amount?.let { 210 | Spacer(modifier = Modifier.height(dimensionResource(R.dimen.spacing_small))) 211 | AmountLabel(currency = it.currency, value = it.value) 212 | } 213 | Spacer(modifier = Modifier.height(dimensionResource(R.dimen.spacing_medium))) 214 | PromoCodeTextField(onAddPromoCode = { 215 | promoCode = it 216 | onAddPromoCode(it) 217 | }) 218 | promotionCodeErrorText?.let { 219 | Text( 220 | modifier = Modifier.padding(horizontal = dimensionResource(R.dimen.spacing_medium)), 221 | text = it, 222 | style = MaterialTheme.typography.bodySmall, 223 | color = MaterialTheme.colorScheme.error 224 | ) 225 | } 226 | if (paymentOptions.requireContact) { 227 | val (showNameInfo, showPhoneInfo, showEmailInfo, requestShipping, shippingType) = paymentOptions 228 | Spacer(modifier = Modifier.height(dimensionResource(R.dimen.spacing_medium))) 229 | ContactForm( 230 | showName = showNameInfo, 231 | onContactInfoChange = { contactInfo = it }, 232 | showPhoneNumber = showPhoneInfo, 233 | showEmailAddress = showEmailInfo, 234 | shippingType = shippingType, 235 | showShippingInfo = requestShipping, 236 | shippingOptions = shippingOptions, 237 | shippingOptionSelection = shippingOptionSelection, 238 | onShippingOptionChange = { 239 | shippingOptionSelection = it 240 | onShippingOptionChange(it) 241 | }, 242 | paymentAddresses = paymentAddresses, 243 | onShippingAddressChange = { 244 | shippingAddressSelection = it 245 | onShippingAddressChange(it) 246 | }) 247 | } 248 | Spacer(modifier = Modifier.height(dimensionResource(R.dimen.spacing_medium))) 249 | InfoText(message = stringResource(R.string.payment_explanation)) 250 | Spacer(modifier = Modifier.height(dimensionResource(R.dimen.spacing_small))) 251 | Button( 252 | onClick = { 253 | onPayButtonClicked( 254 | PaymentFormInfo( 255 | promotionCode = promoCode, 256 | contactInfo = contactInfo, 257 | shippingOption = shippingOptionSelection, 258 | shippingAddress = shippingAddressSelection 259 | ) 260 | ) 261 | }, 262 | enabled = errorText == null, 263 | modifier = Modifier 264 | .padding(horizontal = dimensionResource(R.dimen.spacing_medium)) 265 | .fillMaxWidth() 266 | ) { 267 | Text(stringResource(R.string.pay)) 268 | } 269 | } 270 | } 271 | 272 | @Composable 273 | private fun AmountLabel(currency: String, value: String, modifier: Modifier = Modifier) { 274 | Column( 275 | horizontalAlignment = Alignment.CenterHorizontally, modifier = modifier.fillMaxWidth() 276 | ) { 277 | Text(text = stringResource(R.string.total_price), style = Typography.labelLarge) 278 | BasicText( 279 | text = stringResource(R.string.amount_label, currency, value), 280 | maxLines = 1, 281 | autoSize = TextAutoSize.StepBased( 282 | minFontSize = 45.sp, maxFontSize = 95.sp, stepSize = 5.sp 283 | ), 284 | style = Typography.displayLarge.copy( 285 | brush = Brush.linearGradient( 286 | colors = listOf( 287 | MaterialTheme.colorScheme.primaryContainer, 288 | MaterialTheme.colorScheme.primary, 289 | MaterialTheme.colorScheme.inversePrimary 290 | ) 291 | ) 292 | ) 293 | ) 294 | } 295 | } 296 | 297 | @Composable 298 | private fun PaymentFormTextField( 299 | value: String?, 300 | onValueChange: (String) -> Unit, 301 | modifier: Modifier = Modifier, 302 | label: @Composable (() -> Unit)? = null, 303 | placeholder: @Composable (() -> Unit)? = null, 304 | leadingIcon: @Composable (() -> Unit)? = null, 305 | textStyle: TextStyle = LocalTextStyle.current, 306 | keyboardType: KeyboardType = KeyboardType.Unspecified, 307 | keyboardActions: KeyboardActions = KeyboardActions.Default, 308 | ) { 309 | TextField( 310 | colors = TextFieldDefaults.colors( 311 | focusedIndicatorColor = Color.Transparent, 312 | unfocusedIndicatorColor = Color.Transparent, 313 | disabledIndicatorColor = Color.Transparent, 314 | ), 315 | placeholder = placeholder, 316 | label = label, 317 | leadingIcon = leadingIcon, 318 | textStyle = textStyle, 319 | onValueChange = onValueChange, 320 | modifier = modifier.height(54.dp), 321 | shape = RoundedCornerShape(16.dp), 322 | value = value.orEmpty(), 323 | keyboardOptions = KeyboardOptions.Default.copy( 324 | imeAction = ImeAction.Done, keyboardType = keyboardType 325 | ), 326 | keyboardActions = keyboardActions, 327 | maxLines = 1, 328 | singleLine = true, 329 | ) 330 | } 331 | 332 | @Composable 333 | private fun PromoCodeTextField( 334 | value: String = "", onAddPromoCode: (String) -> Unit, modifier: Modifier = Modifier 335 | ) { 336 | val keyboardController = LocalSoftwareKeyboardController.current 337 | var internalPromoCode: String by rememberSaveable { mutableStateOf(value) } 338 | 339 | val onAddPromoCodeTriggered = { 340 | keyboardController?.hide() 341 | onAddPromoCode(internalPromoCode) 342 | } 343 | 344 | PaymentFormTextField( 345 | value = internalPromoCode, 346 | onValueChange = { 347 | internalPromoCode = it 348 | }, 349 | modifier = modifier 350 | .fillMaxWidth() 351 | .padding(16.dp) 352 | .testTag("promoCodeTextField") 353 | .onKeyEvent { 354 | if (it.key == Key.Enter) { 355 | if (internalPromoCode.isBlank()) return@onKeyEvent false 356 | onAddPromoCodeTriggered() 357 | true 358 | } else { 359 | false 360 | } 361 | }, 362 | placeholder = { 363 | Text(text = stringResource(R.string.promotion_code)) 364 | }, 365 | label = { 366 | Text(text = stringResource(R.string.promotion_title)) 367 | }, 368 | leadingIcon = { 369 | Icon( 370 | imageVector = Icons.Rounded.AddCircle, 371 | contentDescription = stringResource( 372 | id = R.string.promotion_code, 373 | ), 374 | tint = MaterialTheme.colorScheme.onSurface, 375 | ) 376 | }, 377 | textStyle = MaterialTheme.typography.labelSmall, 378 | keyboardActions = KeyboardActions( 379 | onDone = { 380 | if (internalPromoCode.isBlank()) return@KeyboardActions 381 | onAddPromoCodeTriggered() 382 | }) 383 | ) 384 | } 385 | 386 | @Composable 387 | fun ContactForm( 388 | showName: Boolean, 389 | showPhoneNumber: Boolean, 390 | showEmailAddress: Boolean, 391 | showShippingInfo: Boolean, 392 | onContactInfoChange: (ContactInfo) -> Unit, 393 | shippingType: String, 394 | shippingOptions: List<ShippingOption>, 395 | shippingOptionSelection: String?, 396 | onShippingOptionChange: (String) -> Unit, 397 | paymentAddresses: List<PaymentAddress>, 398 | onShippingAddressChange: (String) -> Unit, 399 | modifier: Modifier = Modifier, 400 | ) { 401 | var contactInfo: ContactInfo by rememberSaveable { mutableStateOf(ContactInfo()) } 402 | 403 | Column(modifier = modifier.padding(horizontal = dimensionResource(R.dimen.spacing_medium))) { 404 | Text( 405 | text = stringResource(R.string.contact_information), 406 | style = MaterialTheme.typography.titleMedium, 407 | color = MaterialTheme.colorScheme.secondary 408 | ) 409 | 410 | if (showName) { 411 | Spacer(modifier = modifier.height(dimensionResource(R.dimen.spacing_small))) 412 | PaymentFormTextField( 413 | value = contactInfo.name, 414 | label = { 415 | Text(stringResource(R.string.name)) 416 | }, 417 | onValueChange = { 418 | contactInfo = contactInfo.copy(name = it) 419 | onContactInfoChange(contactInfo) 420 | }, 421 | keyboardType = KeyboardType.Text, 422 | ) 423 | } 424 | 425 | if (showPhoneNumber) { 426 | Spacer(modifier = modifier.height(dimensionResource(R.dimen.spacing_small))) 427 | PaymentFormTextField( 428 | value = contactInfo.phoneNumber, label = { 429 | Text(stringResource(R.string.phone_number)) 430 | }, onValueChange = { 431 | contactInfo = contactInfo.copy(phoneNumber = it) 432 | onContactInfoChange(contactInfo) 433 | }, keyboardType = KeyboardType.Phone) 434 | } 435 | 436 | if (showEmailAddress) { 437 | Spacer(modifier = modifier.height(dimensionResource(R.dimen.spacing_small))) 438 | PaymentFormTextField( 439 | value = contactInfo.emailAddress, label = { 440 | Text(stringResource(R.string.email_address)) 441 | }, onValueChange = { 442 | contactInfo = contactInfo.copy(emailAddress = it) 443 | onContactInfoChange(contactInfo) 444 | }, keyboardType = KeyboardType.Email) 445 | } 446 | 447 | if (showShippingInfo) { 448 | Spacer(modifier = modifier.height(dimensionResource(R.dimen.spacing_medium))) 449 | Text( 450 | text = stringResource(R.string.option_title_format, shippingType), 451 | style = MaterialTheme.typography.titleMedium, 452 | color = MaterialTheme.colorScheme.secondary 453 | ) 454 | Spacer(modifier = modifier.height(dimensionResource(R.dimen.spacing_tiny))) 455 | ShippingSelector( 456 | shippingOptions = shippingOptions, 457 | selectedOption = shippingOptionSelection, 458 | onValueChange = onShippingOptionChange 459 | ) 460 | Spacer(modifier = modifier.height(dimensionResource(R.dimen.spacing_small))) 461 | Text( 462 | text = stringResource(R.string.address_title_format, shippingType), 463 | style = MaterialTheme.typography.titleMedium, 464 | color = MaterialTheme.colorScheme.secondary 465 | ) 466 | Spacer(modifier = modifier.height(dimensionResource(R.dimen.spacing_tiny))) 467 | SingleChoiceSegmentedButton( 468 | options = paymentAddresses, onValueChange = onShippingAddressChange 469 | ) 470 | } 471 | } 472 | } 473 | 474 | @Composable 475 | private fun InfoText(message: String) { 476 | val warningContainerColor = MaterialTheme.colorScheme.inversePrimary 477 | val warningTextColor = MaterialTheme.colorScheme.primary 478 | Text( 479 | text = message, 480 | color = warningTextColor, 481 | style = MaterialTheme.typography.bodySmall, 482 | modifier = Modifier 483 | .padding(horizontal = dimensionResource(R.dimen.spacing_medium)) 484 | .drawWithCache { 485 | onDrawBehind { 486 | drawRoundRect( 487 | warningContainerColor, 488 | cornerRadius = CornerRadius(15.dp.toPx()), 489 | ) 490 | drawRoundRect( 491 | warningTextColor, 492 | cornerRadius = CornerRadius(15.dp.toPx()), 493 | style = Stroke(width = 0.5.dp.toPx()) 494 | ) 495 | } 496 | } 497 | .padding( 498 | horizontal = dimensionResource(R.dimen.spacing_medium), 499 | vertical = dimensionResource(R.dimen.spacing_small) 500 | )) 501 | } 502 | 503 | @Composable 504 | fun ShippingSelector( 505 | shippingOptions: List<ShippingOption>, 506 | selectedOption: String?, 507 | onValueChange: (String) -> Unit, 508 | modifier: Modifier = Modifier 509 | ) { 510 | Column(modifier.selectableGroup()) { 511 | shippingOptions.forEach { option -> 512 | Row( 513 | Modifier 514 | .fillMaxWidth() 515 | .height(35.dp) 516 | .selectable( 517 | selected = (option.id == selectedOption), 518 | onClick = { onValueChange(option.id) }, 519 | role = Role.RadioButton 520 | ) 521 | .padding(horizontal = 16.dp), verticalAlignment = Alignment.CenterVertically 522 | ) { 523 | RadioButton( 524 | selected = (option.id == selectedOption), onClick = null 525 | ) 526 | Text( 527 | text = stringResource( 528 | R.string.option_format, 529 | option.label, 530 | option.amountCurrency, 531 | option.amountValue 532 | ), 533 | style = MaterialTheme.typography.bodyMedium, 534 | modifier = Modifier.padding(start = 16.dp) 535 | ) 536 | } 537 | } 538 | } 539 | } 540 | 541 | @Composable 542 | fun SingleChoiceSegmentedButton( 543 | options: List<PaymentAddress>, onValueChange: (String) -> Unit, modifier: Modifier = Modifier 544 | ) { 545 | var selectedOption: String? by rememberSaveable { mutableStateOf(null) } 546 | 547 | SingleChoiceSegmentedButtonRow(modifier = modifier.fillMaxWidth()) { 548 | options.forEachIndexed { index, paymentAddress -> 549 | SegmentedButton( 550 | shape = SegmentedButtonDefaults.itemShape( 551 | index = index, 552 | count = options.size, 553 | ), 554 | colors = SegmentedButtonDefaults.colors( 555 | inactiveContainerColor = Color.Transparent 556 | ), 557 | onClick = { 558 | selectedOption = paymentAddress.id 559 | onValueChange(paymentAddress.id) 560 | }, 561 | selected = paymentAddress.id == selectedOption, 562 | label = { Text(paymentAddress.label) }, 563 | ) 564 | } 565 | } 566 | } 567 | 568 | @Preview("Default") 569 | @Preview("Dark theme", uiMode = Configuration.UI_MODE_NIGHT_YES) 570 | @Composable 571 | fun PreviewPaymentScreen() { 572 | Surface { 573 | PaymentScaffold( 574 | merchantName = "Sample Merchant", merchantOrigin = "https://example.com" 575 | ) { innerPadding -> 576 | PaymentSummary( 577 | modifier = Modifier.padding(innerPadding), 578 | amount = PaymentAmount(currency = "$", value = "30"), 579 | onAddPromoCode = {}, 580 | paymentOptions = PaymentOptions( 581 | requestPayerName = true, 582 | requestPayerEmail = true, 583 | requestPayerPhone = true, 584 | requestShipping = true 585 | ), 586 | shippingOptions = listOf( 587 | ShippingOption( 588 | id = "US", 589 | label = "Express national", 590 | amountCurrency = "USD", 591 | amountValue = "12", 592 | selected = true, 593 | ) 594 | ), 595 | defaultShippingOptionId = "US", 596 | onShippingOptionChange = {}, 597 | paymentAddresses = paymentAddresses, 598 | onShippingAddressChange = {}, 599 | onPayButtonClicked = {}) 600 | } 601 | } 602 | } 603 | 604 | @Parcelize 605 | data class PaymentFormInfo( 606 | val promotionCode: String? = null, 607 | val contactInfo: ContactInfo, 608 | val shippingOption: String? = null, 609 | val shippingAddress: String? = null 610 | ) : Parcelable 611 | 612 | @Parcelize 613 | data class ContactInfo( 614 | val name: String? = null, val phoneNumber: String? = null, val emailAddress: String? = null 615 | ) : Parcelable -------------------------------------------------------------------------------- /SamplePay/app/src/main/java/com/example/android/samplepay/ui/PaymentViewModel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.samplepay.ui 18 | 19 | import android.app.Application 20 | import android.content.ComponentName 21 | import android.content.Context 22 | import android.content.Intent 23 | import android.content.ServiceConnection 24 | import android.content.pm.Signature 25 | import android.os.Bundle 26 | import android.os.IBinder 27 | import android.util.Log 28 | import androidx.lifecycle.SavedStateHandle 29 | import androidx.lifecycle.ViewModel 30 | import androidx.lifecycle.ViewModelProvider 31 | import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY 32 | import androidx.lifecycle.createSavedStateHandle 33 | import androidx.lifecycle.viewModelScope 34 | import androidx.lifecycle.viewmodel.CreationExtras 35 | import androidx.lifecycle.viewmodel.initializer 36 | import androidx.lifecycle.viewmodel.viewModelFactory 37 | import com.example.android.samplepay.BuildConfig 38 | import com.example.android.samplepay.model.PaymentAddress 39 | import com.example.android.samplepay.model.PaymentAmount 40 | import com.example.android.samplepay.model.PaymentDetailsUpdate 41 | import com.example.android.samplepay.model.PaymentOptions 42 | import com.example.android.samplepay.model.PaymentParams 43 | import com.example.android.samplepay.model.ShippingOption 44 | import com.example.android.samplepay.service.SamplePaymentDetailsUpdateService 45 | import com.example.android.samplepay.service.UnsupportedCallerException 46 | import com.example.android.samplepay.util.getApplicationSignatures 47 | import kotlinx.coroutines.flow.MutableStateFlow 48 | import kotlinx.coroutines.flow.StateFlow 49 | import kotlinx.coroutines.flow.asStateFlow 50 | import kotlinx.coroutines.flow.update 51 | import kotlinx.coroutines.launch 52 | import org.chromium.components.payments.IPaymentDetailsUpdateService 53 | import org.chromium.components.payments.IPaymentDetailsUpdateServiceCallback 54 | import org.json.JSONObject 55 | 56 | private const val TAG = "PaymentViewModel" 57 | 58 | class PaymentViewModel( 59 | private val application: Application, private val state: SavedStateHandle, 60 | 61 | /** The package that started the payment operation. */ 62 | private val callingPackage: String? 63 | 64 | ) : ViewModel() { 65 | 66 | // Define view model factory to include the calling package 67 | companion object { 68 | val CALLING_PACKAGE_KEY = object : CreationExtras.Key<String?> {} 69 | val Factory: ViewModelProvider.Factory = viewModelFactory { 70 | initializer { 71 | val application = this[APPLICATION_KEY] as Application 72 | val callingPackage = this[CALLING_PACKAGE_KEY] 73 | PaymentViewModel( 74 | application = application, 75 | state = createSavedStateHandle(), 76 | callingPackage = callingPackage 77 | ) 78 | } 79 | } 80 | } 81 | 82 | private val _paymentIntent: MutableStateFlow<PaymentIntent> = 83 | MutableStateFlow(PaymentIntent.None) 84 | val paymentIntent: StateFlow<PaymentIntent> = _paymentIntent.asStateFlow() 85 | 86 | private val _paymentResult: MutableStateFlow<PaymentResult> = 87 | MutableStateFlow(PaymentResult.None) 88 | val paymentResult: StateFlow<PaymentResult> = _paymentResult.asStateFlow() 89 | 90 | /** 91 | * The remote service created on the Web end to issue updates to the payment metadata based on 92 | * changes in the payment app. 93 | * */ 94 | private var paymentDetailsUpdateService: IPaymentDetailsUpdateService? = null 95 | 96 | private val connection = object : ServiceConnection { 97 | override fun onServiceConnected(className: ComponentName, service: IBinder) { 98 | val payIntentInfo = paymentIntent.value 99 | if (payIntentInfo is PaymentIntent.Started) { 100 | val binder = service as SamplePaymentDetailsUpdateService.LocalBinder 101 | try { 102 | paymentDetailsUpdateService = 103 | binder.getUpdateService(payIntentInfo.callingIdentity) 104 | 105 | } catch (e: Exception) { 106 | _paymentResult.update { 107 | when (e) { 108 | is UnsupportedCallerException -> PaymentResult.UnsupportedCaller 109 | else -> PaymentResult.Error(e) 110 | } 111 | } 112 | } 113 | } 114 | } 115 | 116 | override fun onServiceDisconnected(className: ComponentName?) { 117 | paymentDetailsUpdateService = null 118 | } 119 | } 120 | 121 | private val updatePaymentCallback = object : IPaymentDetailsUpdateServiceCallback.Stub() { 122 | override fun paymentDetailsNotUpdated() { 123 | Log.d(TAG, "Payment details did not change.") 124 | } 125 | 126 | override fun updateWith(newPaymentDetails: Bundle) { 127 | Log.d(TAG, "Payment details changed.") 128 | 129 | // Create an object with conditional updates received from the remote checkout form. 130 | viewModelScope.launch { 131 | if (paymentIntent.value is PaymentIntent.Started) { 132 | val updatedDetails = PaymentDetailsUpdate.from(newPaymentDetails) 133 | _paymentIntent.update { 134 | (it as PaymentIntent.Started).copy( 135 | shippingOptions = updatedDetails.shippingOptions!!, 136 | amount = updatedDetails.total, 137 | promotionCodeErrorText = updatedDetails.paymentMethodErrors, 138 | errorText = updatedDetails.error?.let { e -> 139 | buildString { 140 | append(e) 141 | updatedDetails.addressErrors?.let { ae -> 142 | appendLine() 143 | append(ae.toString()) 144 | } 145 | } 146 | }) 147 | } 148 | } 149 | } 150 | } 151 | 152 | override fun setPaymentDetailsUpdateService(service: IPaymentDetailsUpdateService?) { 153 | // No-op 154 | } 155 | } 156 | 157 | init { 158 | // Analyze the contents of the intent and initiate the payment process 159 | if (callingPackage != null && state.contains("paymentOptions")) { 160 | val (_, _, merchantName, merchantOrigin, _, amount, _, _, paymentOptions, shippingOptions) = PaymentParams.from( 161 | state 162 | ) 163 | 164 | val callingIdentity = callingPackage.let { 165 | val signatures = application.packageManager.getApplicationSignatures(it) 166 | ApplicationIdentity(it, signatures) 167 | } 168 | 169 | _paymentIntent.value = PaymentIntent.Started( 170 | callingIdentity = callingIdentity, 171 | merchantName = merchantName, 172 | merchantOrigin = merchantOrigin, 173 | amount = amount, 174 | paymentOptions = paymentOptions, 175 | shippingOptions = shippingOptions, 176 | defaultShippingOptionId = shippingOptions.find { it.selected }?.id, 177 | paymentAddresses = paymentAddresses, 178 | ) 179 | } 180 | 181 | // Start service to listen to payment update changes 182 | Intent(application, SamplePaymentDetailsUpdateService::class.java).also { intent -> 183 | application.bindService(intent, connection, Context.BIND_AUTO_CREATE) 184 | } 185 | } 186 | 187 | /** 188 | * Creates a bundle with a promotion code to send it back to the Web caller via the remote 189 | * service, if available. 190 | * 191 | * @param promotionCode the code identifying the promotion applied 192 | */ 193 | fun applyPromotionCode(promotionCode: String) { 194 | val bundle = Bundle().apply { 195 | putString("methodName", BuildConfig.SAMPLE_PAY_METHOD_NAME) 196 | putString("details", JSONObject().apply { 197 | put("promotionCode", promotionCode) 198 | }.toString()) 199 | } 200 | paymentDetailsUpdateService?.changePaymentMethod(bundle, updatePaymentCallback) 201 | } 202 | 203 | /** 204 | * Reports shipping option changes back to the Web caller via the remote service, if available. 205 | * 206 | * @param shippingOptionId the identifier for the shipping option selected by the user 207 | */ 208 | fun updateShippingOption(shippingOptionId: String) { 209 | paymentDetailsUpdateService?.changeShippingOption(shippingOptionId, updatePaymentCallback) 210 | } 211 | 212 | /** 213 | * Reports shipping address changes back to the Web caller via the remote service, if available. 214 | * 215 | * @param shippingAddressId the identifier for the shipping address selected by the user 216 | */ 217 | fun updateShippingAddress(shippingAddressId: String) { 218 | paymentDetailsUpdateService?.changeShippingAddress( 219 | paymentAddresses.find { it.id == shippingAddressId }!!.asBundle(), updatePaymentCallback 220 | ) 221 | } 222 | 223 | /** 224 | * Creates a result intent with the payment configuration based on the choices made on screen 225 | * by the user and issues a state update with it. 226 | * 227 | * @param paymentInfo a collection with the payment choices made by the user 228 | */ 229 | fun pay(paymentInfo: PaymentFormInfo) { 230 | val paymentOptions = (_paymentIntent.value as PaymentIntent.Started).paymentOptions 231 | 232 | _paymentResult.update { 233 | PaymentResult.ResultIntent(Intent().apply { 234 | putExtra("methodName", BuildConfig.SAMPLE_PAY_METHOD_NAME) 235 | putExtra("details", "{\"token\": \"put-some-data-here\"}") 236 | 237 | val (name, phoneNumber, emailAddress) = paymentInfo.contactInfo 238 | if (paymentOptions.requestPayerName) { 239 | putExtra("payerName", name) 240 | } 241 | 242 | if (paymentOptions.requestPayerPhone) { 243 | putExtra("payerPhone", phoneNumber) 244 | } 245 | 246 | if (paymentOptions.requestPayerEmail) { 247 | putExtra("payerEmail", emailAddress) 248 | } 249 | 250 | if (paymentOptions.requestShipping) { 251 | val shippingAddress = 252 | paymentAddresses.find { it.id == paymentInfo.shippingAddress }?.asBundle() 253 | putExtra("shippingAddress", shippingAddress) 254 | putExtra("shippingOptionId", paymentInfo.shippingOption) 255 | } 256 | 257 | putExtra("promoCode", paymentInfo.promotionCode) 258 | }) 259 | } 260 | } 261 | 262 | override fun onCleared() { 263 | if (paymentDetailsUpdateService != null) { 264 | application.applicationContext.unbindService(connection) 265 | } 266 | } 267 | } 268 | 269 | /** 270 | * A representation of the action processed by the application. When a payment intent is received 271 | * from the remote caller, the [PaymentIntent.Started] type collects the incoming information to 272 | * initiate the payment process. 273 | */ 274 | abstract class PaymentIntent { 275 | data object None : PaymentIntent() 276 | data class Started( 277 | val callingIdentity: ApplicationIdentity, 278 | val merchantName: String?, 279 | val merchantOrigin: String?, 280 | val errorText: String? = null, 281 | val promotionCodeErrorText: String? = null, 282 | val amount: PaymentAmount?, 283 | val paymentOptions: PaymentOptions, 284 | val shippingOptions: List<ShippingOption>, 285 | val defaultShippingOptionId: String?, 286 | val paymentAddresses: List<PaymentAddress> 287 | ) : PaymentIntent() 288 | } 289 | 290 | /** A representation of the result of the payment operation. */ 291 | sealed class PaymentResult { 292 | data object None : PaymentResult() 293 | data object UnsupportedCaller : PaymentResult() 294 | data class Error(val exception: Exception) : PaymentResult() 295 | data class ResultIntent(val intent: Intent) : PaymentResult() 296 | } 297 | 298 | /** A collection of data that determines the identity of an application. */ 299 | data class ApplicationIdentity( 300 | val packageName: String, val signatures: List<Signature> 301 | ) 302 | 303 | val paymentAddresses: List<PaymentAddress> = listOf( 304 | PaymentAddress( 305 | "canada_address", 306 | "Canada", 307 | listOf("111 Richmond st. West #12"), 308 | "CA", 309 | "Canada", 310 | "Toronto", 311 | "", 312 | "Google", 313 | "+14169158200", 314 | "M5H2G4", 315 | "John Smith", 316 | "Ontario", 317 | "" 318 | 319 | ), PaymentAddress( 320 | "us_address", 321 | "US", 322 | listOf("1875 Explorer St #1000"), 323 | "US", 324 | "United States", 325 | "Reston", 326 | "", 327 | "Google", 328 | "+12023705600", 329 | "20190", 330 | "John Smith", 331 | "Virginia", 332 | "" 333 | 334 | ), PaymentAddress( 335 | "uk_address", 336 | "UK", 337 | listOf("1-13 St Giles High St"), 338 | "UK", 339 | "United Kingdom", 340 | "London", 341 | "West End", 342 | "Google", 343 | "+442070313000", 344 | "WC2H 8AG", 345 | "John Smith", 346 | "", 347 | "" 348 | ) 349 | ) -------------------------------------------------------------------------------- /SamplePay/app/src/main/java/com/example/android/samplepay/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.samplepay.ui.theme 18 | import androidx.compose.ui.graphics.Color 19 | 20 | val primaryLight = Color(0xFF415F91) 21 | val onPrimaryLight = Color(0xFFFFFFFF) 22 | val primaryContainerLight = Color(0xFFD6E3FF) 23 | val onPrimaryContainerLight = Color(0xFF284777) 24 | val secondaryLight = Color(0xFF565F71) 25 | val onSecondaryLight = Color(0xFFFFFFFF) 26 | val secondaryContainerLight = Color(0xFFDAE2F9) 27 | val onSecondaryContainerLight = Color(0xFF3E4759) 28 | val tertiaryLight = Color(0xFF705575) 29 | val onTertiaryLight = Color(0xFFFFFFFF) 30 | val tertiaryContainerLight = Color(0xFFFAD8FD) 31 | val onTertiaryContainerLight = Color(0xFF573E5C) 32 | val errorLight = Color(0xFFBA1A1A) 33 | val onErrorLight = Color(0xFFFFFFFF) 34 | val errorContainerLight = Color(0xFFFFDAD6) 35 | val onErrorContainerLight = Color(0xFF93000A) 36 | val backgroundLight = Color(0xFFF9F9FF) 37 | val onBackgroundLight = Color(0xFF191C20) 38 | val surfaceLight = Color(0xFFF9F9FF) 39 | val onSurfaceLight = Color(0xFF191C20) 40 | val surfaceVariantLight = Color(0xFFE0E2EC) 41 | val onSurfaceVariantLight = Color(0xFF44474E) 42 | val outlineLight = Color(0xFF74777F) 43 | val outlineVariantLight = Color(0xFFC4C6D0) 44 | val scrimLight = Color(0xFF000000) 45 | val inverseSurfaceLight = Color(0xFF2E3036) 46 | val inverseOnSurfaceLight = Color(0xFFF0F0F7) 47 | val inversePrimaryLight = Color(0xFFAAC7FF) 48 | val surfaceDimLight = Color(0xFFD9D9E0) 49 | val surfaceBrightLight = Color(0xFFF9F9FF) 50 | val surfaceContainerLowestLight = Color(0xFFFFFFFF) 51 | val surfaceContainerLowLight = Color(0xFFF3F3FA) 52 | val surfaceContainerLight = Color(0xFFEDEDF4) 53 | val surfaceContainerHighLight = Color(0xFFE7E8EE) 54 | val surfaceContainerHighestLight = Color(0xFFE2E2E9) 55 | 56 | val primaryLightMediumContrast = Color(0xFF133665) 57 | val onPrimaryLightMediumContrast = Color(0xFFFFFFFF) 58 | val primaryContainerLightMediumContrast = Color(0xFF506DA0) 59 | val onPrimaryContainerLightMediumContrast = Color(0xFFFFFFFF) 60 | val secondaryLightMediumContrast = Color(0xFF2E3647) 61 | val onSecondaryLightMediumContrast = Color(0xFFFFFFFF) 62 | val secondaryContainerLightMediumContrast = Color(0xFF646D80) 63 | val onSecondaryContainerLightMediumContrast = Color(0xFFFFFFFF) 64 | val tertiaryLightMediumContrast = Color(0xFF452E4A) 65 | val onTertiaryLightMediumContrast = Color(0xFFFFFFFF) 66 | val tertiaryContainerLightMediumContrast = Color(0xFF7F6484) 67 | val onTertiaryContainerLightMediumContrast = Color(0xFFFFFFFF) 68 | val errorLightMediumContrast = Color(0xFF740006) 69 | val onErrorLightMediumContrast = Color(0xFFFFFFFF) 70 | val errorContainerLightMediumContrast = Color(0xFFCF2C27) 71 | val onErrorContainerLightMediumContrast = Color(0xFFFFFFFF) 72 | val backgroundLightMediumContrast = Color(0xFFF9F9FF) 73 | val onBackgroundLightMediumContrast = Color(0xFF191C20) 74 | val surfaceLightMediumContrast = Color(0xFFF9F9FF) 75 | val onSurfaceLightMediumContrast = Color(0xFF0F1116) 76 | val surfaceVariantLightMediumContrast = Color(0xFFE0E2EC) 77 | val onSurfaceVariantLightMediumContrast = Color(0xFF33363E) 78 | val outlineLightMediumContrast = Color(0xFF4F525A) 79 | val outlineVariantLightMediumContrast = Color(0xFF6A6D75) 80 | val scrimLightMediumContrast = Color(0xFF000000) 81 | val inverseSurfaceLightMediumContrast = Color(0xFF2E3036) 82 | val inverseOnSurfaceLightMediumContrast = Color(0xFFF0F0F7) 83 | val inversePrimaryLightMediumContrast = Color(0xFFAAC7FF) 84 | val surfaceDimLightMediumContrast = Color(0xFFC5C6CD) 85 | val surfaceBrightLightMediumContrast = Color(0xFFF9F9FF) 86 | val surfaceContainerLowestLightMediumContrast = Color(0xFFFFFFFF) 87 | val surfaceContainerLowLightMediumContrast = Color(0xFFF3F3FA) 88 | val surfaceContainerLightMediumContrast = Color(0xFFE7E8EE) 89 | val surfaceContainerHighLightMediumContrast = Color(0xFFDCDCE3) 90 | val surfaceContainerHighestLightMediumContrast = Color(0xFFD1D1D8) 91 | 92 | val primaryLightHighContrast = Color(0xFF032B5B) 93 | val onPrimaryLightHighContrast = Color(0xFFFFFFFF) 94 | val primaryContainerLightHighContrast = Color(0xFF2A497A) 95 | val onPrimaryContainerLightHighContrast = Color(0xFFFFFFFF) 96 | val secondaryLightHighContrast = Color(0xFF232C3D) 97 | val onSecondaryLightHighContrast = Color(0xFFFFFFFF) 98 | val secondaryContainerLightHighContrast = Color(0xFF41495B) 99 | val onSecondaryContainerLightHighContrast = Color(0xFFFFFFFF) 100 | val tertiaryLightHighContrast = Color(0xFF3A2440) 101 | val onTertiaryLightHighContrast = Color(0xFFFFFFFF) 102 | val tertiaryContainerLightHighContrast = Color(0xFF59405E) 103 | val onTertiaryContainerLightHighContrast = Color(0xFFFFFFFF) 104 | val errorLightHighContrast = Color(0xFF600004) 105 | val onErrorLightHighContrast = Color(0xFFFFFFFF) 106 | val errorContainerLightHighContrast = Color(0xFF98000A) 107 | val onErrorContainerLightHighContrast = Color(0xFFFFFFFF) 108 | val backgroundLightHighContrast = Color(0xFFF9F9FF) 109 | val onBackgroundLightHighContrast = Color(0xFF191C20) 110 | val surfaceLightHighContrast = Color(0xFFF9F9FF) 111 | val onSurfaceLightHighContrast = Color(0xFF000000) 112 | val surfaceVariantLightHighContrast = Color(0xFFE0E2EC) 113 | val onSurfaceVariantLightHighContrast = Color(0xFF000000) 114 | val outlineLightHighContrast = Color(0xFF292C33) 115 | val outlineVariantLightHighContrast = Color(0xFF464951) 116 | val scrimLightHighContrast = Color(0xFF000000) 117 | val inverseSurfaceLightHighContrast = Color(0xFF2E3036) 118 | val inverseOnSurfaceLightHighContrast = Color(0xFFFFFFFF) 119 | val inversePrimaryLightHighContrast = Color(0xFFAAC7FF) 120 | val surfaceDimLightHighContrast = Color(0xFFB8B8BF) 121 | val surfaceBrightLightHighContrast = Color(0xFFF9F9FF) 122 | val surfaceContainerLowestLightHighContrast = Color(0xFFFFFFFF) 123 | val surfaceContainerLowLightHighContrast = Color(0xFFF0F0F7) 124 | val surfaceContainerLightHighContrast = Color(0xFFE2E2E9) 125 | val surfaceContainerHighLightHighContrast = Color(0xFFD3D4DB) 126 | val surfaceContainerHighestLightHighContrast = Color(0xFFC5C6CD) 127 | 128 | val primaryDark = Color(0xFFAAC7FF) 129 | val onPrimaryDark = Color(0xFF0A305F) 130 | val primaryContainerDark = Color(0xFF284777) 131 | val onPrimaryContainerDark = Color(0xFFD6E3FF) 132 | val secondaryDark = Color(0xFFBEC6DC) 133 | val onSecondaryDark = Color(0xFF283141) 134 | val secondaryContainerDark = Color(0xFF3E4759) 135 | val onSecondaryContainerDark = Color(0xFFDAE2F9) 136 | val tertiaryDark = Color(0xFFDDBCE0) 137 | val onTertiaryDark = Color(0xFF3F2844) 138 | val tertiaryContainerDark = Color(0xFF573E5C) 139 | val onTertiaryContainerDark = Color(0xFFFAD8FD) 140 | val errorDark = Color(0xFFFFB4AB) 141 | val onErrorDark = Color(0xFF690005) 142 | val errorContainerDark = Color(0xFF93000A) 143 | val onErrorContainerDark = Color(0xFFFFDAD6) 144 | val backgroundDark = Color(0xFF111318) 145 | val onBackgroundDark = Color(0xFFE2E2E9) 146 | val surfaceDark = Color(0xFF111318) 147 | val onSurfaceDark = Color(0xFFE2E2E9) 148 | val surfaceVariantDark = Color(0xFF44474E) 149 | val onSurfaceVariantDark = Color(0xFFC4C6D0) 150 | val outlineDark = Color(0xFF8E9099) 151 | val outlineVariantDark = Color(0xFF44474E) 152 | val scrimDark = Color(0xFF000000) 153 | val inverseSurfaceDark = Color(0xFFE2E2E9) 154 | val inverseOnSurfaceDark = Color(0xFF2E3036) 155 | val inversePrimaryDark = Color(0xFF415F91) 156 | val surfaceDimDark = Color(0xFF111318) 157 | val surfaceBrightDark = Color(0xFF37393E) 158 | val surfaceContainerLowestDark = Color(0xFF0C0E13) 159 | val surfaceContainerLowDark = Color(0xFF191C20) 160 | val surfaceContainerDark = Color(0xFF1D2024) 161 | val surfaceContainerHighDark = Color(0xFF282A2F) 162 | val surfaceContainerHighestDark = Color(0xFF33353A) 163 | 164 | val primaryDarkMediumContrast = Color(0xFFCDDDFF) 165 | val onPrimaryDarkMediumContrast = Color(0xFF002551) 166 | val primaryContainerDarkMediumContrast = Color(0xFF7491C7) 167 | val onPrimaryContainerDarkMediumContrast = Color(0xFF000000) 168 | val secondaryDarkMediumContrast = Color(0xFFD4DCF2) 169 | val onSecondaryDarkMediumContrast = Color(0xFF1D2636) 170 | val secondaryContainerDarkMediumContrast = Color(0xFF8891A5) 171 | val onSecondaryContainerDarkMediumContrast = Color(0xFF000000) 172 | val tertiaryDarkMediumContrast = Color(0xFFF3D2F7) 173 | val onTertiaryDarkMediumContrast = Color(0xFF331D39) 174 | val tertiaryContainerDarkMediumContrast = Color(0xFFA487A9) 175 | val onTertiaryContainerDarkMediumContrast = Color(0xFF000000) 176 | val errorDarkMediumContrast = Color(0xFFFFD2CC) 177 | val onErrorDarkMediumContrast = Color(0xFF540003) 178 | val errorContainerDarkMediumContrast = Color(0xFFFF5449) 179 | val onErrorContainerDarkMediumContrast = Color(0xFF000000) 180 | val backgroundDarkMediumContrast = Color(0xFF111318) 181 | val onBackgroundDarkMediumContrast = Color(0xFFE2E2E9) 182 | val surfaceDarkMediumContrast = Color(0xFF111318) 183 | val onSurfaceDarkMediumContrast = Color(0xFFFFFFFF) 184 | val surfaceVariantDarkMediumContrast = Color(0xFF44474E) 185 | val onSurfaceVariantDarkMediumContrast = Color(0xFFDADCE6) 186 | val outlineDarkMediumContrast = Color(0xFFAFB2BB) 187 | val outlineVariantDarkMediumContrast = Color(0xFF8E9099) 188 | val scrimDarkMediumContrast = Color(0xFF000000) 189 | val inverseSurfaceDarkMediumContrast = Color(0xFFE2E2E9) 190 | val inverseOnSurfaceDarkMediumContrast = Color(0xFF282A2F) 191 | val inversePrimaryDarkMediumContrast = Color(0xFF294878) 192 | val surfaceDimDarkMediumContrast = Color(0xFF111318) 193 | val surfaceBrightDarkMediumContrast = Color(0xFF43444A) 194 | val surfaceContainerLowestDarkMediumContrast = Color(0xFF06070C) 195 | val surfaceContainerLowDarkMediumContrast = Color(0xFF1B1E22) 196 | val surfaceContainerDarkMediumContrast = Color(0xFF26282D) 197 | val surfaceContainerHighDarkMediumContrast = Color(0xFF313238) 198 | val surfaceContainerHighestDarkMediumContrast = Color(0xFF3C3E43) 199 | 200 | val primaryDarkHighContrast = Color(0xFFEBF0FF) 201 | val onPrimaryDarkHighContrast = Color(0xFF000000) 202 | val primaryContainerDarkHighContrast = Color(0xFFA6C3FC) 203 | val onPrimaryContainerDarkHighContrast = Color(0xFF000B20) 204 | val secondaryDarkHighContrast = Color(0xFFEBF0FF) 205 | val onSecondaryDarkHighContrast = Color(0xFF000000) 206 | val secondaryContainerDarkHighContrast = Color(0xFFBAC3D8) 207 | val onSecondaryContainerDarkHighContrast = Color(0xFF030B1A) 208 | val tertiaryDarkHighContrast = Color(0xFFFFE9FF) 209 | val onTertiaryDarkHighContrast = Color(0xFF000000) 210 | val tertiaryContainerDarkHighContrast = Color(0xFFD8B8DC) 211 | val onTertiaryContainerDarkHighContrast = Color(0xFF16041D) 212 | val errorDarkHighContrast = Color(0xFFFFECE9) 213 | val onErrorDarkHighContrast = Color(0xFF000000) 214 | val errorContainerDarkHighContrast = Color(0xFFFFAEA4) 215 | val onErrorContainerDarkHighContrast = Color(0xFF220001) 216 | val backgroundDarkHighContrast = Color(0xFF111318) 217 | val onBackgroundDarkHighContrast = Color(0xFFE2E2E9) 218 | val surfaceDarkHighContrast = Color(0xFF111318) 219 | val onSurfaceDarkHighContrast = Color(0xFFFFFFFF) 220 | val surfaceVariantDarkHighContrast = Color(0xFF44474E) 221 | val onSurfaceVariantDarkHighContrast = Color(0xFFFFFFFF) 222 | val outlineDarkHighContrast = Color(0xFFEEEFF9) 223 | val outlineVariantDarkHighContrast = Color(0xFFC0C2CC) 224 | val scrimDarkHighContrast = Color(0xFF000000) 225 | val inverseSurfaceDarkHighContrast = Color(0xFFE2E2E9) 226 | val inverseOnSurfaceDarkHighContrast = Color(0xFF000000) 227 | val inversePrimaryDarkHighContrast = Color(0xFF294878) 228 | val surfaceDimDarkHighContrast = Color(0xFF111318) 229 | val surfaceBrightDarkHighContrast = Color(0xFF4E5056) 230 | val surfaceContainerLowestDarkHighContrast = Color(0xFF000000) 231 | val surfaceContainerLowDarkHighContrast = Color(0xFF1D2024) 232 | val surfaceContainerDarkHighContrast = Color(0xFF2E3036) 233 | val surfaceContainerHighDarkHighContrast = Color(0xFF393B41) 234 | val surfaceContainerHighestDarkHighContrast = Color(0xFF45474C) 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | -------------------------------------------------------------------------------- /SamplePay/app/src/main/java/com/example/android/samplepay/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.samplepay.ui.theme 18 | import android.os.Build 19 | import androidx.compose.foundation.isSystemInDarkTheme 20 | import androidx.compose.material3.MaterialTheme 21 | import androidx.compose.material3.lightColorScheme 22 | import androidx.compose.material3.darkColorScheme 23 | import androidx.compose.material3.dynamicDarkColorScheme 24 | import androidx.compose.material3.dynamicLightColorScheme 25 | import androidx.compose.runtime.Composable 26 | import androidx.compose.runtime.Immutable 27 | import androidx.compose.ui.graphics.Color 28 | import androidx.compose.ui.platform.LocalContext 29 | 30 | private val lightScheme = lightColorScheme( 31 | primary = primaryLight, 32 | onPrimary = onPrimaryLight, 33 | primaryContainer = primaryContainerLight, 34 | onPrimaryContainer = onPrimaryContainerLight, 35 | secondary = secondaryLight, 36 | onSecondary = onSecondaryLight, 37 | secondaryContainer = secondaryContainerLight, 38 | onSecondaryContainer = onSecondaryContainerLight, 39 | tertiary = tertiaryLight, 40 | onTertiary = onTertiaryLight, 41 | tertiaryContainer = tertiaryContainerLight, 42 | onTertiaryContainer = onTertiaryContainerLight, 43 | error = errorLight, 44 | onError = onErrorLight, 45 | errorContainer = errorContainerLight, 46 | onErrorContainer = onErrorContainerLight, 47 | background = backgroundLight, 48 | onBackground = onBackgroundLight, 49 | surface = surfaceLight, 50 | onSurface = onSurfaceLight, 51 | surfaceVariant = surfaceVariantLight, 52 | onSurfaceVariant = onSurfaceVariantLight, 53 | outline = outlineLight, 54 | outlineVariant = outlineVariantLight, 55 | scrim = scrimLight, 56 | inverseSurface = inverseSurfaceLight, 57 | inverseOnSurface = inverseOnSurfaceLight, 58 | inversePrimary = inversePrimaryLight, 59 | surfaceDim = surfaceDimLight, 60 | surfaceBright = surfaceBrightLight, 61 | surfaceContainerLowest = surfaceContainerLowestLight, 62 | surfaceContainerLow = surfaceContainerLowLight, 63 | surfaceContainer = surfaceContainerLight, 64 | surfaceContainerHigh = surfaceContainerHighLight, 65 | surfaceContainerHighest = surfaceContainerHighestLight, 66 | ) 67 | 68 | private val darkScheme = darkColorScheme( 69 | primary = primaryDark, 70 | onPrimary = onPrimaryDark, 71 | primaryContainer = primaryContainerDark, 72 | onPrimaryContainer = onPrimaryContainerDark, 73 | secondary = secondaryDark, 74 | onSecondary = onSecondaryDark, 75 | secondaryContainer = secondaryContainerDark, 76 | onSecondaryContainer = onSecondaryContainerDark, 77 | tertiary = tertiaryDark, 78 | onTertiary = onTertiaryDark, 79 | tertiaryContainer = tertiaryContainerDark, 80 | onTertiaryContainer = onTertiaryContainerDark, 81 | error = errorDark, 82 | onError = onErrorDark, 83 | errorContainer = errorContainerDark, 84 | onErrorContainer = onErrorContainerDark, 85 | background = backgroundDark, 86 | onBackground = onBackgroundDark, 87 | surface = surfaceDark, 88 | onSurface = onSurfaceDark, 89 | surfaceVariant = surfaceVariantDark, 90 | onSurfaceVariant = onSurfaceVariantDark, 91 | outline = outlineDark, 92 | outlineVariant = outlineVariantDark, 93 | scrim = scrimDark, 94 | inverseSurface = inverseSurfaceDark, 95 | inverseOnSurface = inverseOnSurfaceDark, 96 | inversePrimary = inversePrimaryDark, 97 | surfaceDim = surfaceDimDark, 98 | surfaceBright = surfaceBrightDark, 99 | surfaceContainerLowest = surfaceContainerLowestDark, 100 | surfaceContainerLow = surfaceContainerLowDark, 101 | surfaceContainer = surfaceContainerDark, 102 | surfaceContainerHigh = surfaceContainerHighDark, 103 | surfaceContainerHighest = surfaceContainerHighestDark, 104 | ) 105 | 106 | private val mediumContrastLightColorScheme = lightColorScheme( 107 | primary = primaryLightMediumContrast, 108 | onPrimary = onPrimaryLightMediumContrast, 109 | primaryContainer = primaryContainerLightMediumContrast, 110 | onPrimaryContainer = onPrimaryContainerLightMediumContrast, 111 | secondary = secondaryLightMediumContrast, 112 | onSecondary = onSecondaryLightMediumContrast, 113 | secondaryContainer = secondaryContainerLightMediumContrast, 114 | onSecondaryContainer = onSecondaryContainerLightMediumContrast, 115 | tertiary = tertiaryLightMediumContrast, 116 | onTertiary = onTertiaryLightMediumContrast, 117 | tertiaryContainer = tertiaryContainerLightMediumContrast, 118 | onTertiaryContainer = onTertiaryContainerLightMediumContrast, 119 | error = errorLightMediumContrast, 120 | onError = onErrorLightMediumContrast, 121 | errorContainer = errorContainerLightMediumContrast, 122 | onErrorContainer = onErrorContainerLightMediumContrast, 123 | background = backgroundLightMediumContrast, 124 | onBackground = onBackgroundLightMediumContrast, 125 | surface = surfaceLightMediumContrast, 126 | onSurface = onSurfaceLightMediumContrast, 127 | surfaceVariant = surfaceVariantLightMediumContrast, 128 | onSurfaceVariant = onSurfaceVariantLightMediumContrast, 129 | outline = outlineLightMediumContrast, 130 | outlineVariant = outlineVariantLightMediumContrast, 131 | scrim = scrimLightMediumContrast, 132 | inverseSurface = inverseSurfaceLightMediumContrast, 133 | inverseOnSurface = inverseOnSurfaceLightMediumContrast, 134 | inversePrimary = inversePrimaryLightMediumContrast, 135 | surfaceDim = surfaceDimLightMediumContrast, 136 | surfaceBright = surfaceBrightLightMediumContrast, 137 | surfaceContainerLowest = surfaceContainerLowestLightMediumContrast, 138 | surfaceContainerLow = surfaceContainerLowLightMediumContrast, 139 | surfaceContainer = surfaceContainerLightMediumContrast, 140 | surfaceContainerHigh = surfaceContainerHighLightMediumContrast, 141 | surfaceContainerHighest = surfaceContainerHighestLightMediumContrast, 142 | ) 143 | 144 | private val highContrastLightColorScheme = lightColorScheme( 145 | primary = primaryLightHighContrast, 146 | onPrimary = onPrimaryLightHighContrast, 147 | primaryContainer = primaryContainerLightHighContrast, 148 | onPrimaryContainer = onPrimaryContainerLightHighContrast, 149 | secondary = secondaryLightHighContrast, 150 | onSecondary = onSecondaryLightHighContrast, 151 | secondaryContainer = secondaryContainerLightHighContrast, 152 | onSecondaryContainer = onSecondaryContainerLightHighContrast, 153 | tertiary = tertiaryLightHighContrast, 154 | onTertiary = onTertiaryLightHighContrast, 155 | tertiaryContainer = tertiaryContainerLightHighContrast, 156 | onTertiaryContainer = onTertiaryContainerLightHighContrast, 157 | error = errorLightHighContrast, 158 | onError = onErrorLightHighContrast, 159 | errorContainer = errorContainerLightHighContrast, 160 | onErrorContainer = onErrorContainerLightHighContrast, 161 | background = backgroundLightHighContrast, 162 | onBackground = onBackgroundLightHighContrast, 163 | surface = surfaceLightHighContrast, 164 | onSurface = onSurfaceLightHighContrast, 165 | surfaceVariant = surfaceVariantLightHighContrast, 166 | onSurfaceVariant = onSurfaceVariantLightHighContrast, 167 | outline = outlineLightHighContrast, 168 | outlineVariant = outlineVariantLightHighContrast, 169 | scrim = scrimLightHighContrast, 170 | inverseSurface = inverseSurfaceLightHighContrast, 171 | inverseOnSurface = inverseOnSurfaceLightHighContrast, 172 | inversePrimary = inversePrimaryLightHighContrast, 173 | surfaceDim = surfaceDimLightHighContrast, 174 | surfaceBright = surfaceBrightLightHighContrast, 175 | surfaceContainerLowest = surfaceContainerLowestLightHighContrast, 176 | surfaceContainerLow = surfaceContainerLowLightHighContrast, 177 | surfaceContainer = surfaceContainerLightHighContrast, 178 | surfaceContainerHigh = surfaceContainerHighLightHighContrast, 179 | surfaceContainerHighest = surfaceContainerHighestLightHighContrast, 180 | ) 181 | 182 | private val mediumContrastDarkColorScheme = darkColorScheme( 183 | primary = primaryDarkMediumContrast, 184 | onPrimary = onPrimaryDarkMediumContrast, 185 | primaryContainer = primaryContainerDarkMediumContrast, 186 | onPrimaryContainer = onPrimaryContainerDarkMediumContrast, 187 | secondary = secondaryDarkMediumContrast, 188 | onSecondary = onSecondaryDarkMediumContrast, 189 | secondaryContainer = secondaryContainerDarkMediumContrast, 190 | onSecondaryContainer = onSecondaryContainerDarkMediumContrast, 191 | tertiary = tertiaryDarkMediumContrast, 192 | onTertiary = onTertiaryDarkMediumContrast, 193 | tertiaryContainer = tertiaryContainerDarkMediumContrast, 194 | onTertiaryContainer = onTertiaryContainerDarkMediumContrast, 195 | error = errorDarkMediumContrast, 196 | onError = onErrorDarkMediumContrast, 197 | errorContainer = errorContainerDarkMediumContrast, 198 | onErrorContainer = onErrorContainerDarkMediumContrast, 199 | background = backgroundDarkMediumContrast, 200 | onBackground = onBackgroundDarkMediumContrast, 201 | surface = surfaceDarkMediumContrast, 202 | onSurface = onSurfaceDarkMediumContrast, 203 | surfaceVariant = surfaceVariantDarkMediumContrast, 204 | onSurfaceVariant = onSurfaceVariantDarkMediumContrast, 205 | outline = outlineDarkMediumContrast, 206 | outlineVariant = outlineVariantDarkMediumContrast, 207 | scrim = scrimDarkMediumContrast, 208 | inverseSurface = inverseSurfaceDarkMediumContrast, 209 | inverseOnSurface = inverseOnSurfaceDarkMediumContrast, 210 | inversePrimary = inversePrimaryDarkMediumContrast, 211 | surfaceDim = surfaceDimDarkMediumContrast, 212 | surfaceBright = surfaceBrightDarkMediumContrast, 213 | surfaceContainerLowest = surfaceContainerLowestDarkMediumContrast, 214 | surfaceContainerLow = surfaceContainerLowDarkMediumContrast, 215 | surfaceContainer = surfaceContainerDarkMediumContrast, 216 | surfaceContainerHigh = surfaceContainerHighDarkMediumContrast, 217 | surfaceContainerHighest = surfaceContainerHighestDarkMediumContrast, 218 | ) 219 | 220 | private val highContrastDarkColorScheme = darkColorScheme( 221 | primary = primaryDarkHighContrast, 222 | onPrimary = onPrimaryDarkHighContrast, 223 | primaryContainer = primaryContainerDarkHighContrast, 224 | onPrimaryContainer = onPrimaryContainerDarkHighContrast, 225 | secondary = secondaryDarkHighContrast, 226 | onSecondary = onSecondaryDarkHighContrast, 227 | secondaryContainer = secondaryContainerDarkHighContrast, 228 | onSecondaryContainer = onSecondaryContainerDarkHighContrast, 229 | tertiary = tertiaryDarkHighContrast, 230 | onTertiary = onTertiaryDarkHighContrast, 231 | tertiaryContainer = tertiaryContainerDarkHighContrast, 232 | onTertiaryContainer = onTertiaryContainerDarkHighContrast, 233 | error = errorDarkHighContrast, 234 | onError = onErrorDarkHighContrast, 235 | errorContainer = errorContainerDarkHighContrast, 236 | onErrorContainer = onErrorContainerDarkHighContrast, 237 | background = backgroundDarkHighContrast, 238 | onBackground = onBackgroundDarkHighContrast, 239 | surface = surfaceDarkHighContrast, 240 | onSurface = onSurfaceDarkHighContrast, 241 | surfaceVariant = surfaceVariantDarkHighContrast, 242 | onSurfaceVariant = onSurfaceVariantDarkHighContrast, 243 | outline = outlineDarkHighContrast, 244 | outlineVariant = outlineVariantDarkHighContrast, 245 | scrim = scrimDarkHighContrast, 246 | inverseSurface = inverseSurfaceDarkHighContrast, 247 | inverseOnSurface = inverseOnSurfaceDarkHighContrast, 248 | inversePrimary = inversePrimaryDarkHighContrast, 249 | surfaceDim = surfaceDimDarkHighContrast, 250 | surfaceBright = surfaceBrightDarkHighContrast, 251 | surfaceContainerLowest = surfaceContainerLowestDarkHighContrast, 252 | surfaceContainerLow = surfaceContainerLowDarkHighContrast, 253 | surfaceContainer = surfaceContainerDarkHighContrast, 254 | surfaceContainerHigh = surfaceContainerHighDarkHighContrast, 255 | surfaceContainerHighest = surfaceContainerHighestDarkHighContrast, 256 | ) 257 | 258 | @Immutable 259 | data class ColorFamily( 260 | val color: Color, 261 | val onColor: Color, 262 | val colorContainer: Color, 263 | val onColorContainer: Color 264 | ) 265 | 266 | val unspecified_scheme = ColorFamily( 267 | Color.Unspecified, Color.Unspecified, Color.Unspecified, Color.Unspecified 268 | ) 269 | 270 | @Composable 271 | fun AppTheme( 272 | darkTheme: Boolean = isSystemInDarkTheme(), 273 | // Dynamic color is available on Android 12+ 274 | dynamicColor: Boolean = true, 275 | content: @Composable() () -> Unit 276 | ) { 277 | val colorScheme = when { 278 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { 279 | val context = LocalContext.current 280 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) 281 | } 282 | 283 | darkTheme -> darkScheme 284 | else -> lightScheme 285 | } 286 | 287 | MaterialTheme( 288 | colorScheme = colorScheme, 289 | content = content 290 | ) 291 | } 292 | 293 | -------------------------------------------------------------------------------- /SamplePay/app/src/main/java/com/example/android/samplepay/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.samplepay.ui.theme 18 | 19 | import androidx.compose.material3.Typography 20 | import androidx.compose.ui.text.TextStyle 21 | import androidx.compose.ui.text.font.FontWeight 22 | import androidx.compose.ui.text.style.LineHeightStyle 23 | import androidx.compose.ui.text.style.LineHeightStyle.Alignment 24 | import androidx.compose.ui.text.style.LineHeightStyle.Trim 25 | import androidx.compose.ui.unit.sp 26 | 27 | internal val Typography = Typography( 28 | displayLarge = TextStyle( 29 | fontWeight = FontWeight.Normal, 30 | fontSize = 95.sp, 31 | lineHeight = 64.sp, 32 | letterSpacing = (-0.25).sp, 33 | ), 34 | displayMedium = TextStyle( 35 | fontWeight = FontWeight.Normal, 36 | fontSize = 75.sp, 37 | lineHeight = 52.sp, 38 | letterSpacing = 0.sp, 39 | ), 40 | displaySmall = TextStyle( 41 | fontWeight = FontWeight.Normal, 42 | fontSize = 55.sp, 43 | lineHeight = 44.sp, 44 | letterSpacing = 0.sp, 45 | ), 46 | headlineLarge = TextStyle( 47 | fontWeight = FontWeight.Normal, 48 | fontSize = 32.sp, 49 | lineHeight = 40.sp, 50 | letterSpacing = 0.sp, 51 | ), 52 | headlineMedium = TextStyle( 53 | fontWeight = FontWeight.Normal, 54 | fontSize = 28.sp, 55 | lineHeight = 36.sp, 56 | letterSpacing = 0.sp, 57 | ), 58 | headlineSmall = TextStyle( 59 | fontWeight = FontWeight.Normal, 60 | fontSize = 24.sp, 61 | lineHeight = 32.sp, 62 | letterSpacing = 0.sp, 63 | lineHeightStyle = LineHeightStyle( 64 | alignment = Alignment.Bottom, 65 | trim = Trim.None, 66 | ), 67 | ), 68 | titleLarge = TextStyle( 69 | fontWeight = FontWeight.Bold, 70 | fontSize = 22.sp, 71 | lineHeight = 28.sp, 72 | letterSpacing = 0.sp, 73 | lineHeightStyle = LineHeightStyle( 74 | alignment = Alignment.Bottom, 75 | trim = Trim.LastLineBottom, 76 | ), 77 | ), 78 | titleMedium = TextStyle( 79 | fontWeight = FontWeight.Bold, 80 | fontSize = 18.sp, 81 | lineHeight = 24.sp, 82 | letterSpacing = 0.1.sp, 83 | ), 84 | titleSmall = TextStyle( 85 | fontWeight = FontWeight.Medium, 86 | fontSize = 14.sp, 87 | lineHeight = 20.sp, 88 | letterSpacing = 0.1.sp, 89 | ), 90 | // Default text style 91 | bodyLarge = TextStyle( 92 | fontWeight = FontWeight.Normal, 93 | fontSize = 16.sp, 94 | lineHeight = 24.sp, 95 | letterSpacing = 0.5.sp, 96 | lineHeightStyle = LineHeightStyle( 97 | alignment = Alignment.Center, 98 | trim = Trim.None, 99 | ), 100 | ), 101 | bodyMedium = TextStyle( 102 | fontWeight = FontWeight.Normal, 103 | fontSize = 14.sp, 104 | lineHeight = 20.sp, 105 | letterSpacing = 0.25.sp, 106 | ), 107 | bodySmall = TextStyle( 108 | fontWeight = FontWeight.Normal, 109 | fontSize = 12.sp, 110 | lineHeight = 16.sp, 111 | letterSpacing = 0.4.sp, 112 | ), 113 | // Used for Button 114 | labelLarge = TextStyle( 115 | fontWeight = FontWeight.Light, 116 | fontSize = 22.sp, 117 | lineHeight = 20.sp, 118 | letterSpacing = 0.2.sp, 119 | lineHeightStyle = LineHeightStyle( 120 | alignment = Alignment.Center, 121 | trim = Trim.LastLineBottom, 122 | ), 123 | ), 124 | // Used for Navigation items 125 | labelMedium = TextStyle( 126 | fontWeight = FontWeight.Light, 127 | fontSize = 16.sp, 128 | lineHeight = 16.sp, 129 | letterSpacing = 0.5.sp, 130 | lineHeightStyle = LineHeightStyle( 131 | alignment = Alignment.Center, 132 | trim = Trim.LastLineBottom, 133 | ), 134 | ), 135 | // Used for Tag 136 | labelSmall = TextStyle( 137 | fontWeight = FontWeight.Light, 138 | fontSize = 10.sp, 139 | lineHeight = 14.sp, 140 | letterSpacing = 0.sp, 141 | lineHeightStyle = LineHeightStyle( 142 | alignment = Alignment.Center, 143 | trim = Trim.LastLineBottom, 144 | ), 145 | ), 146 | ) 147 | 148 | -------------------------------------------------------------------------------- /SamplePay/app/src/main/java/com/example/android/samplepay/util/ActivitiyUtils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:Suppress("DEPRECATION") 18 | 19 | package com.example.android.samplepay.util 20 | 21 | import android.annotation.SuppressLint 22 | import android.app.Activity.OVERRIDE_TRANSITION_CLOSE 23 | import android.app.Activity.OVERRIDE_TRANSITION_OPEN 24 | import android.os.Build 25 | import androidx.activity.ComponentActivity 26 | 27 | @SuppressLint("InlinedApi") 28 | fun ComponentActivity.overrideOpenTransition(enterAnim: Int, exitAnim: Int) = 29 | overrideTransition(OVERRIDE_TRANSITION_OPEN, enterAnim, exitAnim) 30 | 31 | @SuppressLint("InlinedApi") 32 | fun ComponentActivity.overrideCloseTransition(enterAnim: Int, exitAnim: Int) = 33 | overrideTransition(OVERRIDE_TRANSITION_CLOSE, enterAnim, exitAnim) 34 | 35 | /** Decides the signature of `overrideTransition` to use based on the API version. */ 36 | private fun ComponentActivity.overrideTransition(overrideType: Int, enterAnim: Int, exitAnim: Int) { 37 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { 38 | overrideActivityTransition(overrideType, enterAnim, exitAnim) 39 | } else { 40 | overridePendingTransition(enterAnim, exitAnim) 41 | } 42 | } -------------------------------------------------------------------------------- /SamplePay/app/src/main/java/com/example/android/samplepay/util/PackageManagerUtils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.samplepay.util 18 | 19 | import android.content.pm.PackageManager 20 | import android.content.pm.Signature 21 | import android.os.Build 22 | 23 | /** 24 | * Collects a list of signatures for a given package name based on the API version. 25 | * 26 | * @param packageName the name of the package to gather signatures for. 27 | */ 28 | fun PackageManager.getApplicationSignatures(packageName: String): List<Signature> { 29 | try { 30 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 31 | val signingInfo = 32 | getPackageInfo(packageName, PackageManager.GET_SIGNING_CERTIFICATES).signingInfo 33 | signingInfo?.let { 34 | if (it.hasMultipleSigners()) { 35 | it.apkContentsSigners.toList() 36 | } else { 37 | it.signingCertificateHistory.toList() 38 | } 39 | }.orEmpty() 40 | } else { 41 | @Suppress("DEPRECATION") val signatures = 42 | getPackageInfo(packageName, PackageManager.GET_SIGNATURES).signatures 43 | signatures?.toList().orEmpty() 44 | } 45 | } catch (_: Exception) { 46 | return emptyList() 47 | } 48 | } -------------------------------------------------------------------------------- /SamplePay/app/src/main/res/anim/slide_from_bottom.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <!-- 3 | ~ Copyright 2025 Google LLC 4 | ~ 5 | ~ Licensed under the Apache License, Version 2.0 (the "License"); 6 | ~ you may not use this file except in compliance with the License. 7 | ~ You may obtain a copy of the License at 8 | ~ 9 | ~ http://www.apache.org/licenses/LICENSE-2.0 10 | ~ 11 | ~ Unless required by applicable law or agreed to in writing, software 12 | ~ distributed under the License is distributed on an "AS IS" BASIS, 13 | ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | ~ See the License for the specific language governing permissions and 15 | ~ limitations under the License. 16 | --> 17 | 18 | <set xmlns:android="http://schemas.android.com/apk/res/android"> 19 | <translate 20 | android:duration="@android:integer/config_longAnimTime" 21 | android:fromYDelta="100%" 22 | android:toYDelta="0" /> 23 | </set> -------------------------------------------------------------------------------- /SamplePay/app/src/main/res/anim/slide_from_bottom_20.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <!-- 3 | ~ Copyright 2025 Google LLC 4 | ~ 5 | ~ Licensed under the Apache License, Version 2.0 (the "License"); 6 | ~ you may not use this file except in compliance with the License. 7 | ~ You may obtain a copy of the License at 8 | ~ 9 | ~ http://www.apache.org/licenses/LICENSE-2.0 10 | ~ 11 | ~ Unless required by applicable law or agreed to in writing, software 12 | ~ distributed under the License is distributed on an "AS IS" BASIS, 13 | ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | ~ See the License for the specific language governing permissions and 15 | ~ limitations under the License. 16 | --> 17 | 18 | <set xmlns:android="http://schemas.android.com/apk/res/android"> 19 | <translate 20 | android:duration="@android:integer/config_longAnimTime" 21 | android:fromYDelta="20%" 22 | android:toYDelta="0" /> 23 | </set> -------------------------------------------------------------------------------- /SamplePay/app/src/main/res/anim/slide_to_bottom.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <!-- 3 | ~ Copyright 2025 Google LLC 4 | ~ 5 | ~ Licensed under the Apache License, Version 2.0 (the "License"); 6 | ~ you may not use this file except in compliance with the License. 7 | ~ You may obtain a copy of the License at 8 | ~ 9 | ~ http://www.apache.org/licenses/LICENSE-2.0 10 | ~ 11 | ~ Unless required by applicable law or agreed to in writing, software 12 | ~ distributed under the License is distributed on an "AS IS" BASIS, 13 | ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | ~ See the License for the specific language governing permissions and 15 | ~ limitations under the License. 16 | --> 17 | 18 | <set xmlns:android="http://schemas.android.com/apk/res/android"> 19 | <translate 20 | android:duration="@android:integer/config_longAnimTime" 21 | android:fromYDelta="0" 22 | android:toYDelta="100%" /> 23 | </set> -------------------------------------------------------------------------------- /SamplePay/app/src/main/res/anim/slide_to_bottom_20.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <!-- 3 | ~ Copyright 2025 Google LLC 4 | ~ 5 | ~ Licensed under the Apache License, Version 2.0 (the "License"); 6 | ~ you may not use this file except in compliance with the License. 7 | ~ You may obtain a copy of the License at 8 | ~ 9 | ~ http://www.apache.org/licenses/LICENSE-2.0 10 | ~ 11 | ~ Unless required by applicable law or agreed to in writing, software 12 | ~ distributed under the License is distributed on an "AS IS" BASIS, 13 | ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | ~ See the License for the specific language governing permissions and 15 | ~ limitations under the License. 16 | --> 17 | 18 | <set xmlns:android="http://schemas.android.com/apk/res/android"> 19 | <translate 20 | android:duration="@android:integer/config_longAnimTime" 21 | android:fromYDelta="0" 22 | android:toYDelta="20%" /> 23 | </set> -------------------------------------------------------------------------------- /SamplePay/app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <!-- 3 | ~ Copyright 2019 Google LLC 4 | ~ 5 | ~ Licensed under the Apache License, Version 2.0 (the "License"); 6 | ~ you may not use this file except in compliance with the License. 7 | ~ You may obtain a copy of the License at 8 | ~ 9 | ~ http://www.apache.org/licenses/LICENSE-2.0 10 | ~ 11 | ~ Unless required by applicable law or agreed to in writing, software 12 | ~ distributed under the License is distributed on an "AS IS" BASIS, 13 | ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | ~ See the License for the specific language governing permissions and 15 | ~ limitations under the License. 16 | --> 17 | <vector xmlns:android="http://schemas.android.com/apk/res/android" 18 | android:width="108dp" 19 | android:height="108dp" 20 | android:tint="@color/primary_text" 21 | android:viewportWidth="41.37931" 22 | android:viewportHeight="41.37931"> 23 | <group 24 | android:translateX="8.689655" 25 | android:translateY="8.689655"> 26 | <path 27 | android:fillColor="#FF000000" 28 | android:pathData="M20,4L4,4c-1.11,0 -1.99,0.89 -1.99,2L2,18c0,1.11 0.89,2 2,2h16c1.11,0 2,-0.89 2,-2L22,6c0,-1.11 -0.89,-2 -2,-2zM20,18L4,18v-6h16v6zM20,8L4,8L4,6h16v2z" /> 29 | </group> 30 | </vector> 31 | -------------------------------------------------------------------------------- /SamplePay/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <!-- 3 | ~ Copyright 2019 Google LLC 4 | ~ 5 | ~ Licensed under the Apache License, Version 2.0 (the "License"); 6 | ~ you may not use this file except in compliance with the License. 7 | ~ You may obtain a copy of the License at 8 | ~ 9 | ~ http://www.apache.org/licenses/LICENSE-2.0 10 | ~ 11 | ~ Unless required by applicable law or agreed to in writing, software 12 | ~ distributed under the License is distributed on an "AS IS" BASIS, 13 | ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | ~ See the License for the specific language governing permissions and 15 | ~ limitations under the License. 16 | --> 17 | <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> 18 | <background android:drawable="@color/primary_light" /> 19 | <foreground android:drawable="@drawable/ic_launcher_foreground" /> 20 | </adaptive-icon> 21 | -------------------------------------------------------------------------------- /SamplePay/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <!-- 3 | ~ Copyright 2019 Google LLC 4 | ~ 5 | ~ Licensed under the Apache License, Version 2.0 (the "License"); 6 | ~ you may not use this file except in compliance with the License. 7 | ~ You may obtain a copy of the License at 8 | ~ 9 | ~ http://www.apache.org/licenses/LICENSE-2.0 10 | ~ 11 | ~ Unless required by applicable law or agreed to in writing, software 12 | ~ distributed under the License is distributed on an "AS IS" BASIS, 13 | ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | ~ See the License for the specific language governing permissions and 15 | ~ limitations under the License. 16 | --> 17 | <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> 18 | <background android:drawable="@color/primary_light" /> 19 | <foreground android:drawable="@drawable/ic_launcher_foreground" /> 20 | </adaptive-icon> 21 | -------------------------------------------------------------------------------- /SamplePay/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/android-web-payment/1528cf7d2470e11e33ff52ac6444cdd75e7a233d/SamplePay/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /SamplePay/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/android-web-payment/1528cf7d2470e11e33ff52ac6444cdd75e7a233d/SamplePay/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /SamplePay/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/android-web-payment/1528cf7d2470e11e33ff52ac6444cdd75e7a233d/SamplePay/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /SamplePay/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/android-web-payment/1528cf7d2470e11e33ff52ac6444cdd75e7a233d/SamplePay/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /SamplePay/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/android-web-payment/1528cf7d2470e11e33ff52ac6444cdd75e7a233d/SamplePay/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /SamplePay/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/android-web-payment/1528cf7d2470e11e33ff52ac6444cdd75e7a233d/SamplePay/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /SamplePay/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/android-web-payment/1528cf7d2470e11e33ff52ac6444cdd75e7a233d/SamplePay/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /SamplePay/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/android-web-payment/1528cf7d2470e11e33ff52ac6444cdd75e7a233d/SamplePay/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /SamplePay/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/android-web-payment/1528cf7d2470e11e33ff52ac6444cdd75e7a233d/SamplePay/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /SamplePay/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/android-web-payment/1528cf7d2470e11e33ff52ac6444cdd75e7a233d/SamplePay/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /SamplePay/app/src/main/res/values/arrays.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <!-- 3 | ~ Copyright 2019 Google LLC 4 | ~ 5 | ~ Licensed under the Apache License, Version 2.0 (the "License"); 6 | ~ you may not use this file except in compliance with the License. 7 | ~ You may obtain a copy of the License at 8 | ~ 9 | ~ http://www.apache.org/licenses/LICENSE-2.0 10 | ~ 11 | ~ Unless required by applicable law or agreed to in writing, software 12 | ~ distributed under the License is distributed on an "AS IS" BASIS, 13 | ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | ~ See the License for the specific language governing permissions and 15 | ~ limitations under the License. 16 | --> 17 | <resources> 18 | <string-array name="supported_delegations"> 19 | <item>shippingAddress</item> 20 | <item>payerName</item> 21 | <item>payerPhone</item> 22 | <item>payerEmail</item> 23 | </string-array> 24 | </resources> 25 | -------------------------------------------------------------------------------- /SamplePay/app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <!-- 3 | ~ Copyright 2019 Google LLC 4 | ~ 5 | ~ Licensed under the Apache License, Version 2.0 (the "License"); 6 | ~ you may not use this file except in compliance with the License. 7 | ~ You may obtain a copy of the License at 8 | ~ 9 | ~ http://www.apache.org/licenses/LICENSE-2.0 10 | ~ 11 | ~ Unless required by applicable law or agreed to in writing, software 12 | ~ distributed under the License is distributed on an "AS IS" BASIS, 13 | ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | ~ See the License for the specific language governing permissions and 15 | ~ limitations under the License. 16 | --> 17 | <resources> 18 | <color name="primary">#1a237e</color> 19 | <color name="primary_light">#534bae</color> 20 | <color name="primary_dark">#000051</color> 21 | <color name="secondary">#006064</color> 22 | <color name="secondary_light">#428e92</color> 23 | <color name="primary_text">#ffffff</color> 24 | <color name="secondary_text">#ffffff</color> 25 | </resources> 26 | -------------------------------------------------------------------------------- /SamplePay/app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <!-- 3 | ~ Copyright 2019 Google LLC 4 | ~ 5 | ~ Licensed under the Apache License, Version 2.0 (the "License"); 6 | ~ you may not use this file except in compliance with the License. 7 | ~ You may obtain a copy of the License at 8 | ~ 9 | ~ http://www.apache.org/licenses/LICENSE-2.0 10 | ~ 11 | ~ Unless required by applicable law or agreed to in writing, software 12 | ~ distributed under the License is distributed on an "AS IS" BASIS, 13 | ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | ~ See the License for the specific language governing permissions and 15 | ~ limitations under the License. 16 | --> 17 | <resources> 18 | <dimen name="toolbar_elevation">4dp</dimen> 19 | <dimen name="spacing_large">24dp</dimen> 20 | <dimen name="spacing_medium">16dp</dimen> 21 | <dimen name="spacing_small">8dp</dimen> 22 | <dimen name="spacing_tiny">4dp</dimen> 23 | </resources> 24 | -------------------------------------------------------------------------------- /SamplePay/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <!-- 3 | ~ Copyright 2019 Google LLC 4 | ~ 5 | ~ Licensed under the Apache License, Version 2.0 (the "License"); 6 | ~ you may not use this file except in compliance with the License. 7 | ~ You may obtain a copy of the License at 8 | ~ 9 | ~ http://www.apache.org/licenses/LICENSE-2.0 10 | ~ 11 | ~ Unless required by applicable law or agreed to in writing, software 12 | ~ distributed under the License is distributed on an "AS IS" BASIS, 13 | ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | ~ See the License for the specific language governing permissions and 15 | ~ limitations under the License. 16 | --> 17 | <resources> 18 | <string name="app_name">SamplePay</string> 19 | 20 | <string name="home_explanation">This is a sample payment app. Use the SampleMerchant website (at https://sample-pay-ss.web.app/) to initiate a transaction.</string> 21 | <string name="total_price">Total price:</string> 22 | <string name="total_format">Total: %1$s %2$s</string> 23 | <string name="amount_label">%1$s%2$s</string> 24 | <string name="payment_explanation"> 25 | This is a sample payment app, and no actual payment will be processed. 26 | </string> 27 | <string name="pay">Pay</string> 28 | <string name="error">Error</string> 29 | <string name="dismiss">Dismiss</string> 30 | <string name="error_multiple_callers">"This payment app is being called by multiple applications, which is not supported by this provider."</string> 31 | <string name="error_caller_not_supported">The caller does not support a payments update service, which is required by this payment application.</string> 32 | <string name="name">Name</string> 33 | <string name="phone_number">Phone number</string> 34 | <string name="email_address">Email address</string> 35 | <string name="payment_response">paymentActivityResponse</string> 36 | <string name="title_activity_payment_details_update">PaymentDetailsUpdate</string> 37 | <string name="us_address">US address</string> 38 | <string name="canada_address">Canadian Address</string> 39 | <string name="uk_address">British Address</string> 40 | <string name="contact_information">Contact Information:</string> 41 | <string name="submit">Submit</string> 42 | <string name="promotion_code">Enter a Valid Code: e.g. 123</string> 43 | <string name="promotion_title">Promo code</string> 44 | <string name="option_title_format">Options (%s):</string> 45 | <string name="option_format">%1$s, %2$s %3$s</string> 46 | <string name="address_title_format">Address (%s):</string> 47 | </resources> 48 | -------------------------------------------------------------------------------- /SamplePay/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <!-- 3 | ~ Copyright 2019 Google LLC 4 | ~ 5 | ~ Licensed under the Apache License, Version 2.0 (the "License"); 6 | ~ you may not use this file except in compliance with the License. 7 | ~ You may obtain a copy of the License at 8 | ~ 9 | ~ http://www.apache.org/licenses/LICENSE-2.0 10 | ~ 11 | ~ Unless required by applicable law or agreed to in writing, software 12 | ~ distributed under the License is distributed on an "AS IS" BASIS, 13 | ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | ~ See the License for the specific language governing permissions and 15 | ~ limitations under the License. 16 | --> 17 | <resources xmlns:tools="http://schemas.android.com/tools"> 18 | 19 | <style name="Theme.SamplePay" parent="Theme.MaterialComponents.DayNight.NoActionBar"> 20 | <item name="android:statusBarColor" tools:targetApi="21">@color/primary_dark</item> 21 | <item name="colorPrimary">@color/primary</item> 22 | <item name="colorPrimaryVariant">@color/primary_dark</item> 23 | <item name="colorOnPrimary">@color/primary_text</item> 24 | <item name="colorSecondary">@color/secondary</item> 25 | <item name="colorSecondaryVariant">@color/secondary_light</item> 26 | <item name="colorOnSecondary">@color/secondary_text</item> 27 | </style> 28 | 29 | <style name="Theme.SamplePay.Dialog" parent="Theme.MaterialComponents.DayNight.Dialog"> 30 | <item name="windowNoTitle">true</item> 31 | <item name="colorPrimary">@color/primary</item> 32 | <item name="colorPrimaryVariant">@color/primary_dark</item> 33 | <item name="colorOnPrimary">@color/primary_text</item> 34 | <item name="colorSecondary">@color/secondary</item> 35 | <item name="colorSecondaryVariant">@color/secondary_light</item> 36 | <item name="colorOnSecondary">@color/secondary_text</item> 37 | </style> 38 | 39 | </resources> 40 | -------------------------------------------------------------------------------- /SamplePay/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | plugins { 18 | id("com.android.application") version "8.9.1" apply false 19 | id("org.jetbrains.kotlin.android") version "2.1.10" apply false 20 | id("org.jetbrains.kotlin.plugin.compose") version "2.0.0" 21 | kotlin("plugin.serialization") version "2.1.20" 22 | } -------------------------------------------------------------------------------- /SamplePay/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "public", 4 | "ignore": [ 5 | "firebase.json", 6 | "**/.*", 7 | "**/node_modules/**" 8 | ], 9 | "headers": [ 10 | { 11 | "source": "/", 12 | "headers": [ 13 | { 14 | "key": "Link", 15 | "value": "<https://sample-pay-web-app.web.app/payment-manifest.json>; rel=\"payment-method-manifest\"" 16 | } 17 | ] 18 | } 19 | ] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /SamplePay/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx1536m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official 22 | 23 | # Change this line with your domain. 24 | samplePayMethodName=https://sample-pay-web-app.web.app 25 | -------------------------------------------------------------------------------- /SamplePay/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/android-web-payment/1528cf7d2470e11e33ff52ac6444cdd75e7a233d/SamplePay/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /SamplePay/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Mar 28 13:52:40 CET 2025 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /SamplePay/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /SamplePay/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /SamplePay/public/404.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html> 3 | <head> 4 | <meta charset="utf-8"> 5 | <meta name="viewport" content="width=device-width, initial-scale=1"> 6 | <title>Page Not Found 7 | 8 | 23 | 24 | 25 |
26 |

404

27 |

Page Not Found

28 |

The specified file was not found on this website. Please check the URL for mistakes and try again.

29 |

Why am I seeing this?

30 |

This page was generated by the Firebase Command-Line Interface. To modify it, edit the 404.html file in your project's configured public directory.

31 |
32 | 33 | 34 | -------------------------------------------------------------------------------- /SamplePay/public/icon-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/android-web-payment/1528cf7d2470e11e33ff52ac6444cdd75e7a233d/SamplePay/public/icon-web.png -------------------------------------------------------------------------------- /SamplePay/public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/android-web-payment/1528cf7d2470e11e33ff52ac6444cdd75e7a233d/SamplePay/public/icon.png -------------------------------------------------------------------------------- /SamplePay/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Sample Pay 7 | 8 | 21 | 22 | 23 |
24 |

Welcome

25 |

Sample Pay

26 |

This is a sample payment service.

27 | OK 28 |
29 | 30 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /SamplePay/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Pay with SamplePay", 3 | "short_name": "SamplePay", 4 | "icons": [ 5 | { 6 | "src": "https://sample-pay-web-app.web.app/icon.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "https://sample-pay-web-app.web.app/icon-web.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "prefer_related_applications": true, 17 | "related_applications": [ 18 | { 19 | "platform": "play", 20 | "id": "com.example.android.samplepay", 21 | "min_version": "1", 22 | "fingerprints": [ 23 | { 24 | "type": "sha256_cert", 25 | "value": "A6:B6:55:A1:02:05:3A:E5:5D:4D:77:E2:31:2A:7C:81:7C:63:C4:AB:7C:26:BB:7C:12:BC:2C:D8:A0:BC:88:46" 26 | } 27 | ] 28 | }, 29 | { 30 | "platform": "play", 31 | "id": "com.example.android.samplepay", 32 | "min_version": "1", 33 | "fingerprints": [ 34 | { 35 | "type": "sha256_cert", 36 | "value": "D7:C2:43:3F:D9:AD:70:8B:87:1B:3D:7C:A5:0C:B8:06:12:98:BB:52:2F:37:62:9A:FA:11:D8:B5:22:5B:74:6E" 37 | } 38 | ] 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /SamplePay/public/payment-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "default_applications": [ 3 | "https://sample-pay-web-app.web.app/manifest.json" 4 | ], 5 | "supported_origins": [ 6 | "https://sample-pay-web-app.web.app" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /SamplePay/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | pluginManagement { 18 | repositories { 19 | gradlePluginPortal() 20 | google() 21 | mavenCentral() 22 | } 23 | } 24 | 25 | dependencyResolutionManagement { 26 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 27 | repositories { 28 | google() 29 | mavenCentral() 30 | } 31 | } 32 | 33 | rootProject.name = "SamplePay" 34 | include(":app") -------------------------------------------------------------------------------- /app-debug.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/android-web-payment/1528cf7d2470e11e33ff52ac6444cdd75e7a233d/app-debug.apk -------------------------------------------------------------------------------- /payment-app-repo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/android-web-payment/1528cf7d2470e11e33ff52ac6444cdd75e7a233d/payment-app-repo.gif --------------------------------------------------------------------------------