├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── other.md └── workflows │ └── auto-reply-to-raised-issues.yml ├── .gitignore ├── .idea ├── .name └── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── quickstart-kotlin ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── paypal │ │ └── checkoutsamples │ │ ├── KotlinQuickStartActivity.kt │ │ ├── QuickStartApp.kt │ │ ├── QuickStartConstants.kt │ │ ├── order │ │ ├── CreateItemDialog.kt │ │ ├── OrdersQuickStartActivity.kt │ │ └── usecase │ │ │ ├── CreateAmountUseCase.kt │ │ │ ├── CreateItemsUseCase.kt │ │ │ ├── CreateOrderUseCase.kt │ │ │ ├── CreatePurchaseUnitUseCase.kt │ │ │ └── CreateShippingUseCase.kt │ │ ├── paymentbutton │ │ └── PaymentButtonQuickStartActivity.kt │ │ └── token │ │ ├── TokenQuickStartActivity.kt │ │ └── repository │ │ ├── AuthTokenRepository.kt │ │ ├── CheckoutApi.kt │ │ ├── OrderRepository.kt │ │ ├── request │ │ └── OrderRequest.kt │ │ └── response │ │ ├── OAuthTokenResponse.kt │ │ └── OrderResponse.kt │ └── res │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ └── ic_launcher_background.xml │ ├── layout │ ├── activity_kotlin_quick_start.xml │ ├── activity_orders_quick_start.xml │ ├── activity_payment_button_quick_start.xml │ ├── activity_token_quick_start.xml │ ├── dialog_create_item.xml │ └── item_preview_item.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 │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml └── settings.gradle /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report a bug for the Checkout Android SDK (https://developer.paypal.com/docs/business/native-checkout/android). 4 | title: '' 5 | labels: "\U0001F41E bug" 6 | assignees: '' 7 | 8 | --- 9 | 10 | Before you create a new issue, please search for similar issues. It's possible somebody has encountered this bug already. **PLEASE REMOVE THIS LINE TO ACKNOWLEDGE THAT AN ISSUE DOESN'T ALREADY EXIST FOR THIS BUG** 11 | 12 | **Describe the bug** 13 | A clear and concise description of what the bug is. 14 | 15 | **To Reproduce** 16 | Steps to reproduce the behavior: 17 | 1. Go to '...' 18 | 2. Click on '....' 19 | 3. Scroll down to '....' 20 | 4. See error 21 | 22 | **Expected behavior** 23 | A clear and concise description of what you expected to happen. 24 | 25 | **Screenshots** 26 | If applicable, add screenshots to help explain your problem. 27 | 28 | **Smartphone (please complete the following information):** 29 | - Android Version: [e.g. API 30] 30 | - Device: [e.g. Emulator or One Plus 9] 31 | - Browser [e.g. chrome, safari] <- only applicable for app switches or fallback situations 32 | - SDK Version [e.g. 0.1.0] 33 | - Package name of your app [e.g. com.example.my_app] 34 | - Client ID 35 | 36 | **Additional context** 37 | Add any other context about the problem here. 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for the Checkout Android SDK (https://developer.paypal.com/docs/business/native-checkout/android). 4 | title: '' 5 | labels: "\U0001F9DE‍♂️ feature" 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/other.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Other 3 | about: If the issue is not a bug or a feature request then please use this option. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/workflows/auto-reply-to-raised-issues.yml: -------------------------------------------------------------------------------- 1 | # from https://docs.github.com/en/actions/managing-issues-and-pull-requests/commenting-on-an-issue-when-a-label-is-added 2 | 3 | # This workflow uses actions that are not certified by GitHub. 4 | # They are provided by a third-party and are governed by 5 | # separate terms of service, privacy policy, and support 6 | # documentation. 7 | 8 | # GitHub recommends pinning actions to a commit SHA. 9 | # To get a newer version, you will need to update the SHA. 10 | # You can also reference a tag or branch, but the action may change without warning. 11 | 12 | name: Auto response to raised live issues 13 | on: 14 | issues: 15 | types: [opened] 16 | 17 | jobs: 18 | add-comment: 19 | runs-on: ubuntu-latest 20 | permissions: 21 | issues: write 22 | steps: 23 | - name: Add automated comment 24 | uses: peter-evans/create-or-update-comment@v3 25 | with: 26 | issue-number: ${{ github.event.issue.number }} 27 | body: | 28 | Thank you for reaching out to the Native Checkout SDK team. This integration path is now inactive for new merchants. 29 | If you are an existing merchant, please contact us [here](https://www.paypal.com/us/business/contact-sales) for further assistance. 30 | 31 | New merchants can integrate the Native Checkout experience via the Braintree Android SDK or PayPal Android SDK. 32 | For more information please see their respective developer documentation linked below. 33 | * [Braintree Android SDK](https://developer.paypal.com/braintree/docs/guides/overview) 34 | * [PayPal Android SDK](https://developer.paypal.com/beta/advanced-mobile/integrate-android/) 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | *.aab 5 | 6 | # Files for the ART/Dalvik VM 7 | *.dex 8 | 9 | # Java class files 10 | *.class 11 | 12 | # Generated files 13 | bin/ 14 | gen/ 15 | out/ 16 | release/ 17 | 18 | # Gradle files 19 | .gradle/ 20 | build/ 21 | 22 | # Local configuration file (sdk path, etc) 23 | local.properties 24 | 25 | # Proguard folder generated by Eclipse 26 | proguard/ 27 | 28 | # Log Files 29 | *.log 30 | 31 | # Android Studio Navigation editor temp files 32 | .navigation/ 33 | 34 | # Android Studio captures folder 35 | captures/ 36 | 37 | # IntelliJ 38 | *.iml 39 | .idea/workspace.xml 40 | .idea/tasks.xml 41 | .idea/gradle.xml 42 | .idea/assetWizardSettings.xml 43 | .idea/dictionaries 44 | .idea/libraries 45 | .idea/misc.xml 46 | .idea/jarRepositories.xml 47 | # Android Studio 3 in .gitignore file. 48 | .idea/caches 49 | .idea/modules.xml 50 | .idea/navEditor.xml 51 | .idea/compiler.xml 52 | 53 | # Keystore files 54 | *.jks 55 | *.keystore 56 | 57 | # External native build folder generated in Android Studio 2.2 and later 58 | .externalNativeBuild 59 | 60 | # Google Services (e.g. APIs or Firebase) 61 | # google-services.json 62 | 63 | # Freeline 64 | freeline.py 65 | freeline/ 66 | freeline_project_description.json 67 | 68 | # fastlane 69 | fastlane/report.xml 70 | fastlane/Preview.html 71 | fastlane/screenshots 72 | fastlane/test_output 73 | fastlane/readme.md 74 | 75 | # Version control 76 | vcs.xml 77 | 78 | # lint 79 | lint/intermediates/ 80 | lint/generated/ 81 | lint/outputs/ 82 | lint/tmp/ 83 | lint/reports/ 84 | 85 | # macOS 86 | .DS_Store 87 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | Android Native Checkout Samples -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 13 | 14 | 15 | 17 | 18 | 19 | 20 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | xmlns:android 29 | 30 | ^$ 31 | 32 | 33 | 34 |
35 |
36 | 37 | 38 | 39 | xmlns:.* 40 | 41 | ^$ 42 | 43 | 44 | BY_NAME 45 | 46 |
47 |
48 | 49 | 50 | 51 | .*:id 52 | 53 | http://schemas.android.com/apk/res/android 54 | 55 | 56 | 57 |
58 |
59 | 60 | 61 | 62 | .*:name 63 | 64 | http://schemas.android.com/apk/res/android 65 | 66 | 67 | 68 |
69 |
70 | 71 | 72 | 73 | name 74 | 75 | ^$ 76 | 77 | 78 | 79 |
80 |
81 | 82 | 83 | 84 | style 85 | 86 | ^$ 87 | 88 | 89 | 90 |
91 |
92 | 93 | 94 | 95 | .* 96 | 97 | ^$ 98 | 99 | 100 | BY_NAME 101 | 102 |
103 |
104 | 105 | 106 | 107 | .* 108 | 109 | http://schemas.android.com/apk/res/android 110 | 111 | 112 | ANDROID_ATTRIBUTE_ORDER 113 | 114 |
115 |
116 | 117 | 118 | 119 | .* 120 | 121 | .* 122 | 123 | 124 | BY_NAME 125 | 126 |
127 |
128 |
129 |
130 | 131 | 136 |
137 |
-------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## Version 1.3.2 4 | 5 | * Upgraded the Magnes SDK to version 5.5.1 6 | 7 | ## Version 1.3.1 8 | 9 | * Upgraded the Cardinal SDK to version 2.2.7-5 10 | 11 | ## Version 1.3.0 12 | 13 | * Add `hasUserLocationConsent` parameter to `PayPalCheckout.start()`, `PayPalCheckout.startCheckout()`, `PaymentButtonContainer.setup()`, and `PaymentButton.setup()` 14 | 15 | Note: Merchant applications are responsible for collecting user data consent. If your app has obtained consent from the user to collect location data in compliance with 16 | [Google Play Developer Program policies](https://support.google.com/googleplay/android-developer/answer/10144311#personal-sensitive), 17 | set `hasUserLocationConsent` to `true`. This flag enables PayPal to collect necessary information required for Fraud Detection and Risk Management. 18 | 19 | Merchant applications may be required to display a disclosure before collecting user location data in accordance with Google’s 20 | [Best practices for prominent disclosures and consent](https://support.google.com/googleplay/android-developer/answer/11150561?hl=en&ref_topic=12797379&sjid=10421482417907285178-NC). 21 | By setting `userHasLocationConsent` to true, your app is enabled to share device location data with a third party (PayPal) for Fraud Detection and Risk Management. 22 | 23 | [Examples of prominent in-app disclosures](https://support.google.com/googleplay/android-developer/answer/9799150?hl=en#Prominent%20in-app%20disclosure) 24 | 25 | Note: If you are seeing the Play Store flag your APK after updating to this version, please try following these steps: 26 | 1. Go to your Play Console 27 | 2. Select the app 28 | 3. Go to App bundle explorer 29 | 4. Select the violating APK/app bundle's App version at the top right dropdown menu, and make a note of which releases they are under 30 | 5. Go to the track with the violation. It will be one of these 4 pages: Internal / Closed / Open testing or Production 31 | 6. Near the top right of the page, click Create new release. (You may need to click Manage track first) 32 | If the release with the violating APK is in a draft state, discard the release 33 | 7. Add the new version of app bundles or APKs 34 | Make sure the non-compliant version of app bundles or APKs is under the Not included section of this release 35 | 8. To save any changes you make to your release, select Save 36 | 9. When you've finished preparing your release, select Review release, and then proceed to roll out the release to 100%. 37 | 10. If the violating APK is released to multiple tracks, repeat steps 5-9 in each track 38 | 39 | ## Version 1.2.1 40 | 41 | * Upgraded the data-collector SDK to version 3.21.0 which made updates to Device Data collection related to Google Play's User Data Policy 42 | 43 | Note: If you are seeing the Play Store flag your APK after updating to this version, please try following these steps: 44 | 1. Go to your Play Console 45 | 2. Select the app 46 | 3. Go to App bundle explorer 47 | 4. Select the violating APK/app bundle's App version at the top right dropdown menu, and make a note of which releases they are under 48 | 5. Go to the track with the violation. It will be one of these 4 pages: Internal / Closed / Open testing or Production 49 | 6. Near the top right of the page, click Create new release. (You may need to click Manage track first) 50 | If the release with the violating APK is in a draft state, discard the release 51 | 7. Add the new version of app bundles or APKs 52 | Make sure the non-compliant version of app bundles or APKs is under the Not included section of this release 53 | 8. To save any changes you make to your release, select Save 54 | 9. When you've finished preparing your release, select Review release, and then proceed to roll out the release to 100%. 55 | 10. If the violating APK is released to multiple tracks, repeat steps 5-9 in each track 56 | 57 | ## Version 1.2.0 58 | 59 | **This build enables the native flows for buyers in Australia. All other buyers, except those in US, Canada, EU, UK and Australia, will experience web fallbacks.** 60 | 61 | The following changes are included in this release: 62 | 63 | * Minor UI fixes 64 | * Internal improvements. 65 | 66 | ## Version 1.1.0 67 | 68 | **This build enables the native flows for buyers in the US, Canada, EU, and UK. All other buyers will see web fallback experiences.** 69 | 70 | The following changes are included in this release: 71 | 72 | * Buy Now Pay Later entry points 73 | * Improved authentication flows in SCA regions 74 | * Minor UI fixes 75 | 76 | ## Version 1.0.0 77 | 78 | **This build enables the native flows for buyers in the US and Canada. EU buyers will see a web fallback experience.** 79 | 80 | The following changes are included in this release: 81 | 82 | * Fixed UI bugs to ensure a consistent experience for multiple countries and languages 83 | * Fixed an issue that caused users to continue checkout on web when an invalid currency was selected 84 | * Ensures return of relevant billing information such as First Name, Last Name, and Email in the onApprove callback 85 | * Added resource name qualifier to avoid name collisions 86 | * Deprecated the following order actions 87 | * Patch 88 | * Capture 89 | * Authorize 90 | * GetDetails 91 | * ExecuteBillingAgreement 92 | * onShippingChangeAction.Patch 93 | 94 | 95 | ## Version 0.112.2 96 | 97 | * Added a Discount field to the Cart Details UI on the checkout home page 98 | * Addressed some issues on Vault flows 99 | * Minor UI fixes 100 | * Change policies link to point to user agreement screen 101 | * Third party providers compliance work 102 | 103 | ## Version 0.112.0 104 | **Features Updates** 105 | * Introduced Pay Later PayPal offerings 106 | * Added the ability for users to add an initial payment method to their PayPal account 107 | * Updated the UI of the native add-card flow to include complete address input 108 | * Added the ability to programmatically logout a PayPal user 109 | 110 | **Breaking Changes** 111 | * `Order` was renamed to `OrderRequest` 112 | * `SettingsConfig.shouldFailEligibility` was renamed to `SettingsConfig.showWebCheckout` 113 | * `Address` returned in the `OnShippingChange` callback was renamed to `ShippingChangeAddress` 114 | 115 | **Non-Breaking Changes** 116 | * Updated version to 0.112.0 to align with iOS 117 | * Additional bugfixes 118 | 119 | **GitHub Issues Resolved** 120 | https://github.com/paypal/android-checkout-sdk/issues/99 121 | https://github.com/paypal/android-checkout-sdk/issues/100 122 | https://github.com/paypal/android-checkout-sdk/issues/104 123 | https://github.com/paypal/android-checkout-sdk/issues/226 124 | https://github.com/paypal/android-checkout-sdk/issues/184 125 | https://github.com/paypal/android-checkout-sdk/issues/216 126 | 127 | ## Version 0.8.8 128 | * Adding the `ReturnUrl` requirement back into the `CheckoutConfig` 129 | 130 | ## Version 0.8.7 131 | * Upgraded the Cardinal SDK to version 2.2.7-2 which made updates to Device Data collection related to Google Play's User Data Policy 132 | 133 | Note: If you are seeing the Play Store flag your APK after updating to this version, please try following these steps: 134 | 1. Go to your Play Console 135 | 2. Select the app 136 | 3. Go to App bundle explorer 137 | 4. Select the violating APK/app bundle's App version at the top right dropdown menu, and make a note of which releases they are under 138 | 5. Go to the track with the violation. It will be one of these 4 pages: Internal / Closed / Open testing or Production 139 | 6. Near the top right of the page, click Create new release. (You may need to click Manage track first) 140 | If the release with the violating APK is in a draft state, discard the release 141 | 7. Add the new version of app bundles or APKs 142 | Make sure the non-compliant version of app bundles or APKs is under the Not included section of this release 143 | 8. To save any changes you make to your release, select Save 144 | 9. When you've finished preparing your release, select Review release, and then proceed to roll out the release to 100%. 145 | 10. If the violating APK is released to multiple tracks, repeat steps 5-9 in each track 146 | 147 | ## Version 0.8.6 148 | * Added `CorrelationIds` to the `Approval` object returned from the approve order call 149 | 150 | ## Version 0.8.5 151 | * Fixed a [web fallback issue](https://github.com/paypal/android-checkout-sdk/issues/153) causing an infinite spinner when coming back from web fallbacks flows. 152 | * Fixed two issues related to auth native and legacy web auth flows. 153 | 154 | ## Version 0.8.4 155 | * Added the `phone`, `birthDate` and `taxInfo` fields to the `Payer`object that comes as part of the `OrderResponse` object in `getOrderDetails()`, `capture()` and `authorize()` order actions. 156 | 157 | ## Version 0.8.3 158 | * Added `getOrderDetails()` to `OrderActions` in the onApprove callback for getting order information after an order has been approved 159 | 160 | ## Version 0.8.2 161 | * Added the ability to accept a shipping change without having to patching the order 162 | * Resolved crashes caused by Encrypted Shared Preferences 163 | * Fixed an issue caused by a duplicate package name when minifying 164 | * Accessibility fixes 165 | 166 | ## Version 0.8.1 167 | * Removing the requirement for a returnUrl(If there are issues with returning back to the original app, try adding *nativexo://paypalpay* as another returnUrl on the developer portal) 168 | * Adding the ability to add a card on US and EU 169 | * Fixing memory leak issues 170 | 171 | ## Version 0.8.0 172 | * Adding in support for native one time password 173 | * Adding in support for checking out with crypto 174 | 175 | ## Version 0.7.3 176 | * The original returnUrl was added back into this release and needs to be set in the merchant portal if it wasn't already set 177 | 178 | ## Version 0.7.2 179 | * This version introduces a breaking change around removing the use for a returnUrl 180 | * In order to get authenticatation working correctly, there needs to be a returnUrl of *nativexo://paypalpay* added to the clientId of the app 181 | * If this returnUrl is not added the app will not redirect correctly after auth 182 | 183 | ## Version 0.7.1 184 | * Adding in native support for Billing Agreements 185 | ``` fun setBillingAgreementId(billingAgreementId: String) { 186 | CoroutineScope(coroutineContext).launch { 187 | val convertedEcToken = try { 188 | baTokenToEcTokenAction.execute(billingAgreementId) 189 | } catch (exception: Exception) { 190 | internalOnOrderCreated( 191 | OrderCreateResult.Error( 192 | PYPLException("exception with setting BA id: ${exception.message}") 193 | ) 194 | ) 195 | null 196 | } 197 | convertedEcToken?.let { 198 | with(DebugConfigManager.getInstance()) { 199 | checkoutToken = convertedEcToken 200 | repo.isVaultFlow = false 201 | applicationContext?.let { Cache.cacheIsVaultFlow(it, repo.isVaultFlow) } 202 | internalOnOrderCreated(OrderCreateResult.Success(convertedEcToken)) 203 | } 204 | } 205 | } 206 | } 207 | ``` 208 | ## Version 0.7.0 209 | * Extracting 3ds and cardinal into its own optional module controlled by build flavors 210 | ``` 211 | productFlavors { 212 | external { 213 | dimension "clientType" 214 | } 215 | } 216 | ``` 217 | * Adding in native smart payment buttons 218 | * Adding in support for Overcapture 219 | * UI updates 220 | 221 | ## Version 0.6.3 222 | * Updated paypal authentication SDK to 1.6.0 223 | * Resolved refresh token to access token exchange for subsequent logins use cases 224 | 225 | ## Version 0.6.2 226 | * Updated Proguard rules resolving a crash related to EncryptedSharedPreferences on minified builds 227 | * OnShippingChange callback is now invoked when the checkout experience is launched 228 | * Added ability to customize the cancellation dialog shown to users 229 | 230 | ```kotlin 231 | val config = CheckoutConfig( 232 | ... 233 | uiConfig = UIConfig( 234 | showExitSurveyDialog = true 235 | ) 236 | ) 237 | ``` 238 | 239 | ## Version 0.6.1 240 | * Added support for v2/vault web fallback 241 | * Resolves a navigation issue specific to Android 6 242 | * Animation updates 243 | 244 | ### Required Proguard Rules for 0.6.1 245 | ``` 246 | -keepclassmembers class * extends com.google.crypto.tink.shaded.protobuf.GeneratedMessageLite { 247 | ; 248 | } 249 | ``` 250 | There's an issue with EncryptedSharedPreferences where the library is missing a required proguard rule. The temporary fix is to include the above rule in your app module's `proguard-rules.pro` file. Future versions of the SDK will automatically include this rule. 251 | 252 | ## Version 0.6.0 253 | * SDK now targets Android 12 version (API 31) as well as Java 11. 254 | * Resolved a crash that happens when the app gets destroyed and then re-created by Android OS due to a config change. 255 | * Simplified Amount model returned in the OnApprove callback. 256 | 257 | Note: There are some functions that were deprecated and some return types that were changed. Please refer to the upgrade guide when migrating from an earlier version: https://developer.paypal.com/sdk/in-app/android/upgrade/ 258 | 259 | ## Version 0.5.4 260 | * Resolved an issue with the shipping callback not getting invoked 261 | * Resolved a crash caused by internal instrumentation 262 | * Resolved a crash related gradients on Android 6 263 | * Added a web checkout fallback for when there is a 3DS challenge 264 | 265 | ## Version 0.5.2 266 | * Improvements to logging 267 | * Resolved a crash in the address selection screen 268 | * Updated to AndroidX libraries - Jetifier is no longer needed. 269 | 270 | ## Version 0.5.1 271 | * Resolved a crash in the buyer authentication process 272 | 273 | ## Version 0.5.0 274 | * Added OnShippingChanged callback 275 | * Resolved a bug where adding a new shipping address would break the checkout experience 276 | * Added additional error information in the OnError callback 277 | 278 | ## Version 0.4.5 279 | * Fix bug for disappearing payment button 280 | * Added payee info to order 281 | 282 | ## Version 0.4.4 283 | * Resolved a crash caused by registering network callbacks on Android 11 284 | 285 | ## Version 0.4.3 286 | * Resolved a crash caused by null checkout session 287 | * Resolved conflicting attribute names 288 | * Added buyer's name, phone and address to Approval 289 | 290 | ## Version 0.4.2 291 | * Resolved a crash caused by the funding eligibility call 292 | 293 | ## Version 0.4.1 294 | * Resolved a bug where rapid, multiple clicks of the payment button would stop the checkout flow 295 | * Resolved a bug where setting the config in the Application class would render payment buttons to be ineligible 296 | 297 | ## Version 0.4.0 298 | * Invoking the SDK now requires API level 23 and up (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) 299 | * Added `paymentButtonIntent` to `CheckoutConfig` 300 | * `Cart` and `Buyer` are now returned in `OnApproves`'s `ApprovalData` 301 | * `ProcessingInstruction` can be set when creating an order client side. 302 | * Resolved a bug where the SDK would crash with `kotlin.UninitializedPropertyAccessException: lateinit property accessToken has not been initialized` 303 | 304 | ## Version 0.3.1 305 | 306 | * Resolved a bug where the SDK would crash when the buyer tries to authenticate with PayPal 307 | 308 | ## Version 0.3.0 309 | 310 | * Added the ability to pass in a Billing Agreement token through `CreateOrderActions` 311 | * Added the ability to patch an order through `OrderActions` in the `OnApprove` callback 312 | * Resolved a bug where the SDK would invoke the `OnError` callback on a background thread 313 | 314 | ## Version 0.2.0 315 | * Added Cardinal to support 3DS, this will require adding a private maven repository in order to import the SDK. 316 | 317 | ```groovy 318 | url "https://cardinalcommerceprod.jfrog.io/artifactory/android" 319 | credentials { 320 | // Be sure to add these non-sensitive credentials in order to retrieve dependencies related to the Cardinal SDK. 321 | username 'paypal_sgerritz' 322 | password 'AKCp8jQ8tAahqpT5JjZ4FRP2mW7GMoFZ674kGqHmupTesKeAY2G8NcmPKLuTxTGkKjDLRzDUQ' 323 | } 324 | ``` 325 | 326 | * Added the ability to cancel checkout through `CreateOrderActions`, this is useful if an error occurs while generating an Order ID from a server-side integration. 327 | * Resolved a bug where the SDK would crash if a buyer clicked the "Cancel checkout and Return" text while authenticating. 328 | * Resolved a bug where the SDK would occasionally get stuck after the buyer approved an order. 329 | 330 | ## Version 0.1.0 331 | Initial release. Please see [official documentation](https://developer.paypal.com/docs/business/native-checkout/android/) for full integration steps. 332 | 333 | * Added `PayPalCheckout` as one of the main interfaces for launching the pay sheet. 334 | * Added `PaymentButton` along with `PayPalButton`, `PayLaterButton`, and `PayPalCreditButton` which can also be used to launch the pay sheet (this is the preferred option as well). 335 | * Added `CreateOrder` interface which allows for orders to be created with both client-side and server-side integrations. 336 | * Added `OnApprove` interface which notifies the client when a buyer approves an order, at this point the client application can either `Capture` or `Authorize` an order. 337 | * Added `OnError` interface which notifies the client when a terminal error occurred in the experience, in these situations the pay sheet will be dismissed. 338 | * Added `OnCancel` interface which notifies the client application that a buyer cancelled out of the experience. 339 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | https://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 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2020 PayPal 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | https://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PayPal Checkout Samples for Android 2 | 3 | ![Maven Central](https://img.shields.io/maven-central/v/com.paypal.checkout/android-sdk?style=for-the-badge) ![Sonatype Nexus (Snapshots)](https://img.shields.io/nexus/s/com.paypal.checkout/android-sdk?server=https%3A%2F%2Foss.sonatype.org&style=for-the-badge) 4 | 5 | ### Notice: 6 | This integration path is only active for existing developers who have previously integrated the PayPal Android Checkout SDK. For any new developers seeking the Native Checkout experience, this integration path is considered inactive. Please integrate via [BrainTree Android SDK](https://github.com/braintree/braintree_android) or [PayPal Android SDK](https://github.com/paypal/Android-SDK/). 7 | 8 | --- 9 | 10 | This repository contains various sample applications for the PayPal Checkout SDK for Android. If you have questions, comments, or ideas related to the Android Checkout SDK or the sample apps please create a new [issue](https://github.com/paypal/paypalcheckout-samples-android/issues) if one related to your question does not already exist. 11 | 12 | ### Android Version Requirement 13 | The SDK will work with apps that have a minimum version of 21. However, to launch the PayPal flow, 14 | a check for Android 23 or higher must be done. 15 | 16 | ```kotlin 17 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 18 | PayPalCheckout.setConfig(...) 19 | } else { 20 | Toast.makeText(this, "Checkout SDK only available for API 23+", Toast.LENGTH_SHORT).show() 21 | } 22 | 23 | ... 24 | 25 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 26 | PayPalCheckout.startCheckout(...) 27 | } else { 28 | Toast.makeText(this, "Checkout SDK only available for API 23+", Toast.LENGTH_SHORT).show() 29 | } 30 | ``` 31 | 32 | ## Sample App Preparation 33 | 34 | The sample project is intended to be as hands off as possible. With that in mind, there are only two 35 | values which are required to this sample app and they include: 36 | 37 | 1. An app client ID. This is used by the CheckoutConfig and ensures your application can authorize 38 | customers to place orders. 39 | 2. A corresponding app secret. This is required for generating payment tokens. This is not required 40 | for your own implementation of the PayPal Checkout SDK and is only used to illustrate how you could 41 | generate tokens for customer orders with a backend system. 42 | 3. Setting a return URL. 43 | 44 | Please reference our [developer documentation](https://developer.paypal.com/docs/business/native-checkout/android/) 45 | overview to learn about how to create a new PayPal application as well as how to find those details. **At this time, the SDK is in limited release so please be sure to follow all of the steps outlined.** 46 | Once you have the credentials available you will want to add them to `QuickStartConstants.kt`. 47 | 48 | ```kotlin 49 | // QuickStartConstants.kt 50 | const val PAYPAL_CLIENT_ID = "YOUR-CLIENT-ID-HERE" 51 | const val PAYPAL_SECRET = "ONLY-FOR-QUICKSTART-DO-NOT-INCLUDE-SECRET-IN-CLIENT-SIDE-APPLICATIONS" 52 | ``` 53 | 54 | ### Setting a Return URL 55 | 56 | A return URL is required for redirecting users back to the sample app after authenticating. For more details on setting a return URL please see our [developer documentation](https://developer.paypal.com/docs/business/native-checkout/android/#know-before-you-code), 57 | however instead of setting the Live App Settings you want to ensure you are setting your Sandbox App Settings. The return URL you should use is `com.paypal.checkoutsamples://paypalpay`. 58 | 59 | ## Releases 60 | 61 | New versions of the Android Checkout SDK are published via MavenCentral. Please refer to the badge at the top of this repository for the latest version of the SDK. Please see our [change log](CHANGELOG.md) to understand what changed from one version to the next. 62 | 63 | ### Adding Dependency via Gradle Groovy DSL 64 | ```groovy 65 | implementation 'com.paypal.checkout:android-sdk:' 66 | ``` 67 | 68 | ### Adding Dependency via Gradle Kotlin DSL 69 | ```kotlin 70 | implementation("com.paypal.checkout:android-sdk:") 71 | ``` 72 | 73 | ### Snapshots 74 | 75 | Snapshot builds are available [through Sonatype](https://oss.sonatype.org/content/repositories/snapshots/) and can be used for early testing of new features or validating a reported issue has been resolved. **Snapshots should not be considered stable or production ready**. Please use the latest stable release of the Android Checkout SDK for production builds. 76 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | buildscript { 3 | ext.kotlin_version = "1.6.10" 4 | repositories { 5 | google() 6 | mavenCentral() 7 | } 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:7.1.2' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" 12 | 13 | // NOTE: Do not place your application dependencies here; they belong 14 | // in the individual module build.gradle files 15 | } 16 | } 17 | 18 | allprojects { 19 | repositories { 20 | google() 21 | mavenCentral() 22 | maven { 23 | url 'https://oss.sonatype.org/content/repositories/snapshots/' 24 | } 25 | maven { 26 | url "https://cardinalcommerceprod.jfrog.io/artifactory/android" 27 | credentials { 28 | // Be sure to add these non-sensitive credentials in order to retrieve dependencies from 29 | // the private repository. 30 | username 'paypal_sgerritz' 31 | password 'AKCp8jQ8tAahqpT5JjZ4FRP2mW7GMoFZ674kGqHmupTesKeAY2G8NcmPKLuTxTGkKjDLRzDUQ' 32 | } 33 | } 34 | mavenLocal() 35 | } 36 | 37 | configurations.all { 38 | resolutionStrategy.cacheChangingModulesFor 0, 'seconds' 39 | } 40 | } 41 | 42 | task clean(type: Delete) { 43 | delete rootProject.buildDir 44 | } 45 | -------------------------------------------------------------------------------- /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=-Xmx2048m 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.enableJetifier=true 18 | android.useAndroidX=true 19 | # Kotlin code style for this project: "official" or "obsolete": 20 | kotlin.code.style=official -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paypal/android-checkout-sdk/991dd4c462bfc954c29e1b8528d2e4ae043ee99f/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Mar 30 09:15:39 CDT 2021 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /quickstart-kotlin/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /quickstart-kotlin/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | apply plugin: 'kotlinx-serialization' 5 | 6 | android { 7 | compileSdkVersion 31 8 | 9 | defaultConfig { 10 | applicationId "com.paypal.checkoutsamples" 11 | minSdkVersion 21 12 | targetSdkVersion 31 13 | versionCode 1 14 | versionName "1.0" 15 | 16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 17 | } 18 | 19 | buildTypes { 20 | release { 21 | minifyEnabled false 22 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 23 | } 24 | } 25 | 26 | // Required For Checkouts SDK To Work Properly (OkHttp Requirement) - https://stackoverflow.com/a/59448917 27 | compileOptions { 28 | sourceCompatibility JavaVersion.VERSION_1_8 29 | targetCompatibility JavaVersion.VERSION_1_8 30 | 31 | // Used for the sample app (Kotlin Serialization Library) not required for Checkouts SDK. 32 | kotlinOptions { 33 | freeCompilerArgs += "-Xopt-in=org.mylibrary.OptInAnnotation" 34 | } 35 | } 36 | 37 | kotlinOptions { 38 | jvmTarget = "1.8" 39 | } 40 | } 41 | 42 | dependencies { 43 | // PayPal Checkout SDK Libraries 44 | implementation 'com.paypal.checkout:android-sdk:1.3.2' 45 | 46 | // Other Dependencies Related To Sample App 47 | implementation fileTree(dir: "libs", include: ["*.jar"]) 48 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 49 | implementation 'androidx.core:core-ktx:1.7.0' 50 | implementation 'androidx.appcompat:appcompat:1.4.1' 51 | implementation 'androidx.constraintlayout:constraintlayout:2.1.3' 52 | implementation 'com.squareup.retrofit2:retrofit:2.9.0' 53 | implementation 'com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:0.8.0' 54 | implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.0' 55 | implementation 'com.google.android.material:material:1.5.0' 56 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0' 57 | } 58 | -------------------------------------------------------------------------------- /quickstart-kotlin/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. 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 -------------------------------------------------------------------------------- /quickstart-kotlin/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 15 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 27 | 30 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /quickstart-kotlin/src/main/java/com/paypal/checkoutsamples/KotlinQuickStartActivity.kt: -------------------------------------------------------------------------------- 1 | package com.paypal.checkoutsamples 2 | 3 | import android.os.Bundle 4 | import androidx.appcompat.app.AppCompatActivity 5 | import com.google.android.material.snackbar.Snackbar 6 | import com.paypal.checkoutsamples.order.OrdersQuickStartActivity 7 | import com.paypal.checkoutsamples.paymentbutton.PaymentButtonQuickStartActivity 8 | import com.paypal.checkoutsamples.token.TokenQuickStartActivity 9 | import kotlinx.android.synthetic.main.activity_kotlin_quick_start.* 10 | 11 | class KotlinQuickStartActivity : AppCompatActivity() { 12 | 13 | private val clientIdWasUpdated by lazy { 14 | PAYPAL_CLIENT_ID != "YOUR-CLIENT-ID-HERE" 15 | } 16 | 17 | private val secretWasUpdated by lazy { 18 | PAYPAL_SECRET != "ONLY-FOR-QUICKSTART-DO-NOT-INCLUDE-SECRET-IN-CLIENT-SIDE-APPLICATIONS" 19 | } 20 | 21 | override fun onCreate(savedInstanceState: Bundle?) { 22 | super.onCreate(savedInstanceState) 23 | setContentView(R.layout.activity_kotlin_quick_start) 24 | 25 | buyWithOrder.setOnClickListener { 26 | if (clientIdWasUpdated) { 27 | startActivity(OrdersQuickStartActivity.startIntent(this)) 28 | } else { 29 | displayErrorSnackbar("Please Update PAYPAL_CLIENT_ID In QuickStartConstants.") 30 | } 31 | } 32 | 33 | buyWithOrderToken.setOnClickListener { 34 | if (clientIdWasUpdated && secretWasUpdated) { 35 | startActivity(TokenQuickStartActivity.startIntent(this)) 36 | } else { 37 | displayErrorSnackbar("Please Update PAYPAL_CLIENT_ID and PAYPAL_SECRET In QuickStartConstants.") 38 | } 39 | } 40 | 41 | buyWithPaymentButton.setOnClickListener { 42 | if (clientIdWasUpdated) { 43 | startActivity(PaymentButtonQuickStartActivity.startIntent(this)) 44 | } else { 45 | displayErrorSnackbar("Please Update PAYPAL_CLIENT_ID In QuickStartConstants.") 46 | } 47 | } 48 | } 49 | 50 | private fun displayErrorSnackbar(errorMessage: String) { 51 | Snackbar.make(rootQuickStart, errorMessage, Snackbar.LENGTH_INDEFINITE) 52 | .apply { setAction("Got It 👍") { dismiss() } } 53 | .show() 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /quickstart-kotlin/src/main/java/com/paypal/checkoutsamples/QuickStartApp.kt: -------------------------------------------------------------------------------- 1 | package com.paypal.checkoutsamples 2 | 3 | import android.app.Application 4 | import com.paypal.checkout.PayPalCheckout 5 | import com.paypal.checkout.config.CheckoutConfig 6 | import com.paypal.checkout.config.Environment 7 | import com.paypal.checkout.config.SettingsConfig 8 | import com.paypal.checkout.createorder.CurrencyCode 9 | import com.paypal.checkout.createorder.UserAction 10 | 11 | class QuickStartApp : Application() { 12 | override fun onCreate() { 13 | super.onCreate() 14 | PayPalCheckout.setConfig( 15 | checkoutConfig = CheckoutConfig( 16 | application = this, 17 | clientId = PAYPAL_CLIENT_ID, 18 | environment = Environment.SANDBOX, 19 | currencyCode = CurrencyCode.USD, 20 | userAction = UserAction.PAY_NOW, 21 | settingsConfig = SettingsConfig( 22 | loggingEnabled = true, 23 | showWebCheckout = false 24 | ), 25 | returnUrl = "${BuildConfig.APPLICATION_ID}://paypalpay" 26 | ) 27 | ) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /quickstart-kotlin/src/main/java/com/paypal/checkoutsamples/QuickStartConstants.kt: -------------------------------------------------------------------------------- 1 | package com.paypal.checkoutsamples 2 | 3 | /** 4 | * You can get a Client ID by signing into your PayPal developer account or by signing up for one if 5 | * you haven't already. 6 | * 7 | * @see [Developer Portal](https://developer.paypal.com/developer/accounts/) 8 | * @see [Managing Sandbox Apps](https://developer.paypal.com/docs/api-basics/manage-apps/#create-or-edit-sandbox-and-live-apps) 9 | */ 10 | const val PAYPAL_CLIENT_ID = "YOUR-CLIENT-ID-HERE" 11 | 12 | /** 13 | * You can get a Secret by signing into your PayPal developer account or by signing up for one if 14 | * you haven't already. 15 | * 16 | * NOTE: This is only required for the samples which are generating tokens and you should not include 17 | * your secret in development or production Android apps. 18 | * 19 | * @see [Developer Portal](https://developer.paypal.com/developer/accounts/) 20 | * @see [Managing Sandbox Apps](https://developer.paypal.com/docs/api-basics/manage-apps/#create-or-edit-sandbox-and-live-apps) 21 | */ 22 | const val PAYPAL_SECRET = "ONLY-FOR-QUICKSTART-DO-NOT-INCLUDE-SECRET-IN-CLIENT-SIDE-APPLICATIONS" 23 | -------------------------------------------------------------------------------- /quickstart-kotlin/src/main/java/com/paypal/checkoutsamples/order/CreateItemDialog.kt: -------------------------------------------------------------------------------- 1 | package com.paypal.checkoutsamples.order 2 | 3 | import android.annotation.SuppressLint 4 | import android.app.AlertDialog 5 | import android.app.Dialog 6 | import android.os.Bundle 7 | import android.view.View 8 | import androidx.fragment.app.DialogFragment 9 | import com.google.android.material.textfield.TextInputLayout 10 | import com.paypal.checkout.createorder.ItemCategory 11 | import com.paypal.checkoutsamples.R 12 | import kotlinx.android.synthetic.main.dialog_create_item.view.* 13 | 14 | /** 15 | * CreateItemDialog provides an entry point for the user to create a new item and for the host view 16 | * to be notified when the item is created along with it's contents. 17 | * 18 | * @see [CreatedItem] 19 | */ 20 | class CreateItemDialog : DialogFragment() { 21 | 22 | var onItemCreated: ((createdItem: CreatedItem) -> Unit)? = null 23 | 24 | @SuppressLint("InflateParams") 25 | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { 26 | return activity?.let { nonNullActivity -> 27 | val dialogBuilder = AlertDialog.Builder(nonNullActivity) 28 | val layoutInflater = nonNullActivity.layoutInflater 29 | val dialogView = layoutInflater.inflate(R.layout.dialog_create_item, null) 30 | 31 | with(dialogView) { 32 | selectItemCategory.setOnCheckedChangeListener { _, _ -> errorTextView.text = "" } 33 | 34 | createItemButton.setOnClickListener { 35 | if (!canSaveItem(dialogView)) return@setOnClickListener 36 | 37 | onItemCreated?.invoke( 38 | CreatedItem( 39 | name = itemNameInput.text, 40 | quantity = itemQuantityInput.text, 41 | amount = itemAmountInput.text, 42 | taxAmount = itemTaxInput.text, 43 | itemCategory = selectedItemCategory(selectItemCategory.checkedRadioButtonId) 44 | ) 45 | ) 46 | 47 | dismiss() 48 | } 49 | } 50 | 51 | dialogBuilder.setView(dialogView).create() 52 | } ?: throw IllegalStateException("Activity cannot be null.") 53 | } 54 | 55 | private fun canSaveItem(view: View): Boolean = with(view) { 56 | itemNameInput.validateField() 57 | itemQuantityInput.validateField() 58 | itemAmountInput.validateField() 59 | itemTaxInput.validateField() 60 | 61 | if (!itemCategoryPhysicalGoods.isChecked && !itemCategoryDigitalGoods.isChecked) { 62 | errorTextView.text = getString(R.string.dialog_create_error_item_category) 63 | } 64 | 65 | return itemNameInput.text.isNotEmpty() && itemQuantityInput.text.isNotEmpty() 66 | && itemAmountInput.text.isNotEmpty() && itemTaxInput.text.isNotEmpty() 67 | && (itemCategoryPhysicalGoods.isChecked || itemCategoryDigitalGoods.isChecked) 68 | } 69 | 70 | private fun TextInputLayout.validateField() { 71 | if (text.isEmpty()) error = getString(R.string.dialog_create_error_required) 72 | } 73 | 74 | private fun selectedItemCategory(selectedId: Int): ItemCategory { 75 | return when (selectedId) { 76 | R.id.itemCategoryPhysicalGoods -> ItemCategory.PHYSICAL_GOODS 77 | R.id.itemCategoryDigitalGoods -> ItemCategory.DIGITAL_GOODS 78 | else -> { 79 | throw IllegalArgumentException( 80 | "Expected one of the following ids: ${R.id.itemCategoryPhysicalGoods}, or " + 81 | "${R.id.itemCategoryDigitalGoods} but was $selectedId" 82 | ) 83 | } 84 | } 85 | } 86 | 87 | private val TextInputLayout.text: String 88 | get() { 89 | return editText?.run { text.toString() } ?: "" 90 | } 91 | 92 | } 93 | 94 | /** 95 | * CreatedItem is a simple data class which is used for sending [CreatedItem] details from one input 96 | * screen to another. 97 | */ 98 | data class CreatedItem( 99 | val name: String, 100 | val quantity: String, 101 | val amount: String, 102 | val taxAmount: String, 103 | val itemCategory: ItemCategory 104 | ) 105 | -------------------------------------------------------------------------------- /quickstart-kotlin/src/main/java/com/paypal/checkoutsamples/order/OrdersQuickStartActivity.kt: -------------------------------------------------------------------------------- 1 | package com.paypal.checkoutsamples.order 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.os.Bundle 7 | import android.util.Log 8 | import android.view.View 9 | import androidx.appcompat.app.AppCompatActivity 10 | import com.google.android.material.snackbar.Snackbar 11 | import com.paypal.checkout.PayPalCheckout 12 | import com.paypal.checkout.approve.OnApprove 13 | import com.paypal.checkout.cancel.OnCancel 14 | import com.paypal.checkout.createorder.CreateOrder 15 | import com.paypal.checkout.createorder.CurrencyCode 16 | import com.paypal.checkout.createorder.OrderIntent 17 | import com.paypal.checkout.createorder.ShippingPreference 18 | import com.paypal.checkout.createorder.UserAction 19 | import com.paypal.checkout.error.OnError 20 | import com.paypal.checkout.order.AuthorizeOrderResult 21 | import com.paypal.checkout.order.CaptureOrderResult 22 | import com.paypal.checkoutsamples.R 23 | import com.paypal.checkoutsamples.order.usecase.CreateOrderRequest 24 | import com.paypal.checkoutsamples.order.usecase.CreateOrderUseCase 25 | import kotlinx.android.synthetic.main.activity_orders_quick_start.* 26 | import kotlinx.android.synthetic.main.item_preview_item.view.* 27 | 28 | class OrdersQuickStartActivity : AppCompatActivity() { 29 | 30 | private val tag = javaClass.simpleName 31 | 32 | private val checkoutSdk: PayPalCheckout 33 | get() = PayPalCheckout 34 | 35 | private val selectedUserAction: UserAction 36 | get() { 37 | return when (val selectedId = selectUserAction.checkedRadioButtonId) { 38 | R.id.userActionOptionContinue -> UserAction.CONTINUE 39 | R.id.userActionOptionPayNow -> UserAction.PAY_NOW 40 | else -> { 41 | throw IllegalArgumentException( 42 | "Expected one of the following ids: ${R.id.userActionOptionContinue}, or " + 43 | "${R.id.userActionOptionPayNow} but was $selectedId" 44 | ) 45 | } 46 | } 47 | } 48 | 49 | private val selectedOrderIntent: OrderIntent 50 | get() { 51 | return when (val selectedId = selectOrderIntent.checkedRadioButtonId) { 52 | R.id.orderIntentOptionAuthorize -> OrderIntent.AUTHORIZE 53 | R.id.orderIntentOptionCapture -> OrderIntent.CAPTURE 54 | else -> { 55 | throw IllegalArgumentException( 56 | "Expected one of the following ids: ${R.id.orderIntentOptionAuthorize}, or " + 57 | "${R.id.orderIntentOptionCapture} but was $selectedId" 58 | ) 59 | } 60 | } 61 | } 62 | 63 | private val selectedShippingPreference: ShippingPreference 64 | get() { 65 | return when (val selectedId = selectShippingPreference.checkedRadioButtonId) { 66 | R.id.shippingPreferenceOptionGetFromFile -> ShippingPreference.GET_FROM_FILE 67 | R.id.shippingPreferenceOptionNoShipping -> ShippingPreference.NO_SHIPPING 68 | R.id.shippingPreferenceOptionSetProvidedAddress -> ShippingPreference.SET_PROVIDED_ADDRESS 69 | else -> { 70 | throw IllegalArgumentException( 71 | "Expected one of the following ids: ${R.id.shippingPreferenceOptionGetFromFile}, " + 72 | "${R.id.shippingPreferenceOptionNoShipping}, or " + 73 | "${R.id.shippingPreferenceOptionSetProvidedAddress} but was $selectedId" 74 | ) 75 | } 76 | } 77 | } 78 | 79 | private val selectedCurrencyCode: CurrencyCode 80 | get() { 81 | return when (val selectedId = selectCurrencyCode.checkedRadioButtonId) { 82 | R.id.currencyCodeUsd -> CurrencyCode.USD 83 | R.id.currencyCodeEur -> CurrencyCode.EUR 84 | R.id.currencyCodeGbp -> CurrencyCode.GBP 85 | else -> { 86 | throw IllegalArgumentException( 87 | "Expected one of the following ids: ${R.id.currencyCodeUsd}, " + 88 | "${R.id.currencyCodeEur}, or ${R.id.currencyCodeGbp} but was $selectedId" 89 | ) 90 | } 91 | } 92 | } 93 | 94 | private val createItemDialog: CreateItemDialog by lazy { 95 | CreateItemDialog().apply { onItemCreated = ::onItemCreated } 96 | } 97 | 98 | private val createdItems = mutableListOf() 99 | 100 | private val createOrderUseCase by lazy { CreateOrderUseCase() } 101 | 102 | override fun onCreate(savedInstanceState: Bundle?) = with(applicationContext) { 103 | super.onCreate(savedInstanceState) 104 | setContentView(R.layout.activity_orders_quick_start) 105 | 106 | checkoutSdk.registerCallbacks( 107 | onApprove = OnApprove { approval -> 108 | Log.i(tag, "OnApprove: $approval") 109 | when (selectedOrderIntent) { 110 | OrderIntent.AUTHORIZE -> approval.orderActions.authorize { result -> 111 | val message = when (result) { 112 | is AuthorizeOrderResult.Success -> { 113 | Log.i(tag, "Success: $result") 114 | "💰 Order Authorization Succeeded 💰" 115 | } 116 | is AuthorizeOrderResult.Error -> { 117 | Log.i(tag, "Error: $result") 118 | "🔥 Order Authorization Failed 🔥" 119 | } 120 | } 121 | showSnackbar(message) 122 | } 123 | OrderIntent.CAPTURE -> approval.orderActions.capture { result -> 124 | val message = when (result) { 125 | is CaptureOrderResult.Success -> { 126 | Log.i(tag, "Success: $result") 127 | "💰 Order Capture Succeeded 💰" 128 | } 129 | is CaptureOrderResult.Error -> { 130 | Log.i(tag, "Error: $result") 131 | "🔥 Order Capture Failed 🔥" 132 | } 133 | } 134 | showSnackbar(message) 135 | } 136 | } 137 | }, 138 | onCancel = OnCancel { 139 | Log.d(tag, "OnCancel") 140 | showSnackbar("😭 Buyer Cancelled Checkout 😭") 141 | }, 142 | onError = OnError { errorInfo -> 143 | Log.d(tag, "ErrorInfo: $errorInfo") 144 | showSnackbar("🚨 An Error Occurred 🚨") 145 | } 146 | ) 147 | 148 | addItemButton.setOnClickListener { 149 | createItemDialog.show(supportFragmentManager, "CreateItemDialog") 150 | } 151 | submitOrderButton.setOnClickListener { 152 | if (createdItems.isEmpty()) { 153 | itemErrorTextView.visibility = View.VISIBLE 154 | } else { 155 | startCheckoutWithSampleOrders(createdItems, selectedCurrencyCode) 156 | } 157 | } 158 | } 159 | 160 | @SuppressLint("InflateParams") 161 | private fun onItemCreated(createdItem: CreatedItem) { 162 | val itemView = layoutInflater.inflate(R.layout.item_preview_item, itemsContainer, false) 163 | .apply { 164 | itemNameText.text = createdItem.name 165 | itemAmountText.text = createdItem.amount 166 | itemTaxText.text = createdItem.taxAmount 167 | itemQuantityText.text = getString( 168 | R.string.orders_quick_start_activity_created_item_quantity, 169 | createdItem.quantity 170 | ) 171 | } 172 | itemsContainer.addView(itemView) 173 | 174 | createdItems.add(createdItem) 175 | itemErrorTextView.visibility = View.GONE 176 | } 177 | 178 | private fun startCheckoutWithSampleOrders( 179 | createdItems: List, 180 | currencyCode: CurrencyCode 181 | ) { 182 | val createOrderRequest = 183 | CreateOrderRequest( 184 | orderIntent = selectedOrderIntent, 185 | userAction = selectedUserAction, 186 | shippingPreference = selectedShippingPreference, 187 | currencyCode = currencyCode, 188 | createdItems = createdItems 189 | ) 190 | val order = createOrderUseCase.execute(createOrderRequest) 191 | 192 | checkoutSdk.startCheckout( 193 | createOrder = CreateOrder { actions -> 194 | actions.create(order) { id -> 195 | Log.d(tag, "Order ID: $id") 196 | } 197 | }, 198 | // This sample app has not obtained consent from the buyer to collect location data. 199 | // This flag enables PayPal to collect necessary information required for Fraud Detection and Risk Management 200 | hasUserLocationConsent = false 201 | ) 202 | } 203 | 204 | private fun showSnackbar(text: String) { 205 | Snackbar.make(rootOrdersQuickStart, text, Snackbar.LENGTH_LONG).show() 206 | } 207 | 208 | companion object { 209 | fun startIntent(context: Context): Intent { 210 | return Intent(context, OrdersQuickStartActivity::class.java) 211 | } 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /quickstart-kotlin/src/main/java/com/paypal/checkoutsamples/order/usecase/CreateAmountUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.paypal.checkoutsamples.order.usecase 2 | 3 | import com.paypal.checkout.createorder.CurrencyCode 4 | import com.paypal.checkout.order.Amount 5 | import com.paypal.checkout.order.BreakDown 6 | import com.paypal.checkout.order.UnitAmount 7 | import com.paypal.checkoutsamples.order.CreatedItem 8 | import java.math.BigDecimal 9 | import java.math.RoundingMode 10 | import java.text.DecimalFormat 11 | 12 | /** 13 | * CreateAmountRequest contains all of the necessary properties to successfully create an Amount with 14 | * the PayPal Checkout SDK. 15 | */ 16 | data class CreateAmountRequest( 17 | val createdItems: List, 18 | val currencyCode: CurrencyCode 19 | ) 20 | 21 | /** 22 | * CreateOrderUseCase provides a way to construct an [Amount] given a [CreateAmountRequest]. In 23 | * order to successfully create an [Amount] for a PurchaseUnit the value (what the customer is 24 | * charged). The following calculation is used when determining the total value: 25 | * itemTotal + taxTotal + shipping + handling + insurance - shippingDiscount - discount 26 | */ 27 | class CreateAmountUseCase { 28 | 29 | fun execute(request: CreateAmountRequest): Amount = with(request) { 30 | val itemTotal = createdItems.map { it.amount.toDouble() * it.quantity.toInt() } 31 | .sum().toBigDecimal().scaledForMoney 32 | val taxTotal = createdItems.map { it.taxAmount.toDouble() * it.quantity.toInt() } 33 | .sum().toBigDecimal().scaledForMoney 34 | val shippingTotal = BigDecimal(0.00).scaledForMoney 35 | val handlingTotal = BigDecimal(0.00).scaledForMoney 36 | val shippingDiscountTotal = BigDecimal(0.00).scaledForMoney 37 | val itemDiscountTotal = BigDecimal(0.00).scaledForMoney 38 | val totalValue = itemTotal 39 | .add(taxTotal) 40 | .add(shippingTotal) 41 | .add(handlingTotal) 42 | .subtract(shippingDiscountTotal) 43 | .subtract(itemDiscountTotal) 44 | 45 | return Amount.Builder() 46 | .currencyCode(currencyCode) 47 | .value(totalValue.asMoneyString) 48 | .breakdown( 49 | BreakDown.Builder() 50 | .itemTotal(itemTotal.unitAmountFor(currencyCode)) 51 | .shipping(shippingTotal.unitAmountFor(currencyCode)) 52 | .handling(handlingTotal.unitAmountFor(currencyCode)) 53 | .taxTotal(taxTotal.unitAmountFor(currencyCode)) 54 | .shippingDiscount(shippingDiscountTotal.unitAmountFor(currencyCode)) 55 | .discount(itemDiscountTotal.unitAmountFor(currencyCode)) 56 | .build() 57 | ) 58 | .build() 59 | } 60 | 61 | private fun BigDecimal.unitAmountFor(currencyCode: CurrencyCode): UnitAmount { 62 | return UnitAmount.Builder() 63 | .value(asMoneyString) 64 | .currencyCode(currencyCode) 65 | .build() 66 | } 67 | 68 | private val BigDecimal.asMoneyString: String 69 | get() = DecimalFormat("#0.00").format(this) 70 | 71 | private val BigDecimal.scaledForMoney: BigDecimal 72 | get() = setScale(2, RoundingMode.HALF_UP) 73 | } 74 | -------------------------------------------------------------------------------- /quickstart-kotlin/src/main/java/com/paypal/checkoutsamples/order/usecase/CreateItemsUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.paypal.checkoutsamples.order.usecase 2 | 3 | import com.paypal.checkout.createorder.CurrencyCode 4 | import com.paypal.checkout.order.Items 5 | import com.paypal.checkout.order.UnitAmount 6 | import com.paypal.checkoutsamples.order.CreatedItem 7 | 8 | /** 9 | * CreateItemRequest contains all of the necessary properties to successfully create a list of Items 10 | * with the PayPal Checkout SDK. 11 | */ 12 | data class CreateItemsRequest( 13 | val createdItems: List, 14 | val currencyCode: CurrencyCode 15 | ) 16 | 17 | /** 18 | * CreateItemsUseCase provides a way to construct a [List] of [Items] given a [CreateItemsRequest]. 19 | */ 20 | class CreateItemsUseCase { 21 | fun execute(request: CreateItemsRequest): List = with(request) { 22 | return createdItems.map { createdItem -> 23 | Items.Builder() 24 | .name(createdItem.name) 25 | .quantity(createdItem.quantity) 26 | .category(createdItem.itemCategory) 27 | .unitAmount( 28 | UnitAmount.Builder() 29 | .value(createdItem.amount) 30 | .currencyCode(currencyCode) 31 | .build() 32 | ) 33 | .tax( 34 | UnitAmount.Builder() 35 | .value(createdItem.taxAmount) 36 | .currencyCode(currencyCode) 37 | .build() 38 | ) 39 | .build() 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /quickstart-kotlin/src/main/java/com/paypal/checkoutsamples/order/usecase/CreateOrderUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.paypal.checkoutsamples.order.usecase 2 | 3 | import com.paypal.checkout.createorder.CurrencyCode 4 | import com.paypal.checkout.createorder.OrderIntent 5 | import com.paypal.checkout.createorder.ShippingPreference 6 | import com.paypal.checkout.createorder.UserAction 7 | import com.paypal.checkout.order.AppContext 8 | import com.paypal.checkout.order.OrderRequest 9 | import com.paypal.checkoutsamples.order.CreatedItem 10 | 11 | /** 12 | * CreateOrderRequest contains all of the necessary properties to successfully create an [Order] with 13 | * the PayPal Checkout SDK. 14 | */ 15 | data class CreateOrderRequest( 16 | val orderIntent: OrderIntent, 17 | val userAction: UserAction, 18 | val shippingPreference: ShippingPreference, 19 | val currencyCode: CurrencyCode, 20 | val createdItems: List 21 | ) 22 | 23 | /** 24 | * CreateOrderUseCase provides a way to construct an [Order] given a [CreateOrderRequest]. 25 | */ 26 | class CreateOrderUseCase( 27 | private val createPurchaseUnitUseCase: CreatePurchaseUnitUseCase = CreatePurchaseUnitUseCase() 28 | ) { 29 | 30 | fun execute(request: CreateOrderRequest): OrderRequest = with(request) { 31 | val createPurchaseUnitRequest = CreatePurchaseUnitRequest( 32 | createdItems = createdItems, 33 | shippingPreference = shippingPreference, 34 | currencyCode = currencyCode 35 | ) 36 | val purchaseUnit = createPurchaseUnitUseCase.execute(createPurchaseUnitRequest) 37 | 38 | return OrderRequest.Builder() 39 | .intent(orderIntent) 40 | .purchaseUnitList(listOf(purchaseUnit)) 41 | .appContext( 42 | AppContext.Builder() 43 | .brandName("Acme Inc") 44 | .userAction(userAction) 45 | .shippingPreference(shippingPreference) 46 | .build() 47 | ) 48 | .build() 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /quickstart-kotlin/src/main/java/com/paypal/checkoutsamples/order/usecase/CreatePurchaseUnitUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.paypal.checkoutsamples.order.usecase 2 | 3 | import com.paypal.checkout.createorder.CurrencyCode 4 | import com.paypal.checkout.createorder.ShippingPreference 5 | import com.paypal.checkout.order.PurchaseUnit 6 | import com.paypal.checkoutsamples.order.CreatedItem 7 | import java.util.UUID 8 | 9 | /** 10 | * CreatePurchaseUnitRequest contains all of the necessary properties to successfully create a 11 | * [PurchaseUnit] with the PayPal Checkout SDK. 12 | */ 13 | data class CreatePurchaseUnitRequest( 14 | val createdItems: List, 15 | val shippingPreference: ShippingPreference, 16 | val currencyCode: CurrencyCode 17 | ) 18 | 19 | /** 20 | * CreatePurchaseUnitUseCase provides a way to construct a [PurchaseUnit] given a 21 | * [CreatePurchaseUnitRequest]. It's worth noting that a [PurchaseUnit] contains the bulk of an 22 | * Order's information as it contains items, shipping information, along with the total amount and 23 | * a breakdown of those totals. 24 | */ 25 | class CreatePurchaseUnitUseCase( 26 | private val createItemsUseCase: CreateItemsUseCase = CreateItemsUseCase(), 27 | private val createShippingUseCase: CreateShippingUseCase = CreateShippingUseCase(), 28 | private val createAmountUseCase: CreateAmountUseCase = CreateAmountUseCase() 29 | ) { 30 | 31 | fun execute(request: CreatePurchaseUnitRequest): PurchaseUnit = with(request) { 32 | val createItemsRequest = CreateItemsRequest(createdItems, currencyCode) 33 | val items = createItemsUseCase.execute(createItemsRequest) 34 | 35 | val createShippingRequest = CreateShippingRequest(shippingPreference, currencyCode) 36 | val shipping = createShippingUseCase.execute(createShippingRequest) 37 | 38 | val amountRequest = CreateAmountRequest(createdItems, currencyCode) 39 | val amount = createAmountUseCase.execute(amountRequest) 40 | 41 | return PurchaseUnit.Builder() 42 | .referenceId(UUID.randomUUID().toString()) 43 | .amount(amount) 44 | .items(items) 45 | /* 46 | * Omitting shipping will default to the customer's default shipping address. 47 | */ 48 | .shipping(shipping) 49 | /* 50 | * The API caller-provided external ID. Used to reconcile API caller-initiated transactions 51 | * with PayPal transactions. Appears in transaction and settlement reports. 52 | */ 53 | .customId("CUSTOM-123") 54 | /* 55 | * The purchase description. 56 | */ 57 | .description("Purchase from Orders Quick Start") 58 | /* 59 | * The soft descriptor is the dynamic text used to construct the statement descriptor 60 | * that appears on a payer's card statement. 61 | * 62 | * Maximum Length: 22 characters 63 | */ 64 | .softDescriptor("800-123-1234") 65 | .build() 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /quickstart-kotlin/src/main/java/com/paypal/checkoutsamples/order/usecase/CreateShippingUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.paypal.checkoutsamples.order.usecase 2 | 3 | import com.paypal.checkout.createorder.CurrencyCode 4 | import com.paypal.checkout.createorder.ShippingPreference 5 | import com.paypal.checkout.order.Address 6 | import com.paypal.checkout.order.Shipping 7 | 8 | /** 9 | * CreateOrderRequest contains all of the necessary properties to successfully create a [Shipping] 10 | * instance with the PayPal Checkout SDK. 11 | */ 12 | data class CreateShippingRequest( 13 | val shippingPreference: ShippingPreference, 14 | val currencyCode: CurrencyCode 15 | ) 16 | 17 | /** 18 | * CreateShippingUseCase provides a way to construct a [Shipping] instance given a 19 | * [CreateShippingRequest]. 20 | */ 21 | class CreateShippingUseCase { 22 | 23 | fun execute(request: CreateShippingRequest): Shipping { 24 | /* 25 | * Options for Shipping such as Standard, Express, Next Day, etc. 26 | * 27 | * Only supported for [ShippingPreference.GET_FROM_FILE], and displaying the shipping 28 | * options are currently disabled in 0.0.4, for now this can safely default to null for all 29 | * cases. 30 | */ 31 | val shippingOptions = null 32 | 33 | return Shipping.Builder() 34 | .address( 35 | Address.Builder() 36 | /* 37 | * The first line of the address. For example, number or street. For example, 38 | * 173 Drury Lane. Required for data entry and compliance and risk checks. 39 | * Must contain the full address. 40 | * Maximum length: 300. 41 | */ 42 | .addressLine1("123 Townsend St") 43 | /* 44 | * The second line of the address. For example, suite or apartment number. 45 | * Maximum length: 300. 46 | */ 47 | .addressLine2("Floor 6") 48 | /* 49 | * A city, town, or village. Smaller than adminArea1 50 | */ 51 | .adminArea2("San Francisco") 52 | /* 53 | * The highest level sub-division in a country, which is usually a province, 54 | * state, or ISO-3166-2 subdivision. Format for postal delivery. 55 | * For example, CA and not California. Value, by country, is: 56 | * UK. A county. 57 | * US. A state. 58 | * Canada. A province. 59 | * Japan. A prefecture. 60 | * Switzerland. A kanton. 61 | * Maximum length: 300. 62 | */ 63 | .adminArea1("CA") 64 | /* 65 | * The postal code, which is the zip code or equivalent. Typically required 66 | * for countries with a postal code or an equivalent. 67 | * @see [Postal Code](https://en.wikipedia.org/wiki/Postal_code) 68 | * Maximum length: 60. 69 | */ 70 | .postalCode("94107") 71 | /* 72 | * The two-character ISO 3166-1 code that identifies the country or region. 73 | * 74 | * @see [Country Codes](https://developer.paypal.com/docs/integration/direct/rest/country-codes/) 75 | */ 76 | .countryCode("US") 77 | .build() 78 | ) 79 | .options(shippingOptions) 80 | .build() 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /quickstart-kotlin/src/main/java/com/paypal/checkoutsamples/paymentbutton/PaymentButtonQuickStartActivity.kt: -------------------------------------------------------------------------------- 1 | package com.paypal.checkoutsamples.paymentbutton 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import android.util.Log 7 | import androidx.appcompat.app.AppCompatActivity 8 | import com.paypal.checkout.approve.OnApprove 9 | import com.paypal.checkout.cancel.OnCancel 10 | import com.paypal.checkout.createorder.CreateOrder 11 | import com.paypal.checkout.createorder.CurrencyCode 12 | import com.paypal.checkout.createorder.OrderIntent 13 | import com.paypal.checkout.createorder.UserAction 14 | import com.paypal.checkout.error.OnError 15 | import com.paypal.checkout.order.Amount 16 | import com.paypal.checkout.order.AppContext 17 | import com.paypal.checkout.order.OrderRequest 18 | import com.paypal.checkout.order.PurchaseUnit 19 | import com.paypal.checkout.paymentbutton.PaymentButtonEligibilityStatus 20 | import com.paypal.checkoutsamples.R 21 | import kotlinx.android.synthetic.main.activity_payment_button_quick_start.* 22 | 23 | class PaymentButtonQuickStartActivity : AppCompatActivity() { 24 | 25 | private val tag = javaClass.simpleName 26 | 27 | override fun onCreate(savedInstanceState: Bundle?) { 28 | super.onCreate(savedInstanceState) 29 | setContentView(R.layout.activity_payment_button_quick_start) 30 | paymentButton.onEligibilityStatusChanged = { buttonEligibilityStatus: PaymentButtonEligibilityStatus -> 31 | Log.v(tag, "OnEligibilityStatusChanged") 32 | Log.d(tag, "Button eligibility status: $buttonEligibilityStatus") 33 | } 34 | setupPaymentButton() 35 | } 36 | 37 | private fun setupPaymentButton() { 38 | paymentButton.setup( 39 | createOrder = CreateOrder { createOrderActions -> 40 | Log.v(tag, "CreateOrder") 41 | createOrderActions.create( 42 | OrderRequest.Builder() 43 | .appContext( 44 | AppContext( 45 | userAction = UserAction.PAY_NOW 46 | ) 47 | ) 48 | .intent(OrderIntent.CAPTURE) 49 | .purchaseUnitList( 50 | listOf( 51 | PurchaseUnit.Builder() 52 | .amount( 53 | Amount.Builder() 54 | .value("0.01") 55 | .currencyCode(CurrencyCode.USD) 56 | .build() 57 | ) 58 | .build() 59 | ) 60 | ) 61 | .build() 62 | .also { Log.d(tag, "Order: $it") } 63 | ) 64 | }, 65 | onApprove = OnApprove { approval -> 66 | Log.v(tag, "OnApprove") 67 | Log.d(tag, "Approval details: $approval") 68 | approval.orderActions.capture { captureOrderResult -> 69 | Log.v(tag, "Capture Order") 70 | Log.d(tag, "Capture order result: $captureOrderResult") 71 | } 72 | }, 73 | onCancel = OnCancel { 74 | Log.v(tag, "OnCancel") 75 | Log.d(tag, "Buyer cancelled the checkout experience.") 76 | }, 77 | onError = OnError { errorInfo -> 78 | Log.v(tag, "OnError") 79 | Log.d(tag, "Error details: $errorInfo") 80 | }, 81 | // This sample app has not obtained consent from the buyer to collect location data. 82 | // This flag enables PayPal to collect necessary information required for Fraud Detection and Risk Management 83 | hasUserLocationConsent = false 84 | ) 85 | } 86 | 87 | companion object { 88 | fun startIntent(context: Context): Intent { 89 | return Intent(context, PaymentButtonQuickStartActivity::class.java) 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /quickstart-kotlin/src/main/java/com/paypal/checkoutsamples/token/TokenQuickStartActivity.kt: -------------------------------------------------------------------------------- 1 | package com.paypal.checkoutsamples.token 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import android.util.Log 7 | import androidx.appcompat.app.AppCompatActivity 8 | import androidx.core.widget.addTextChangedListener 9 | import com.paypal.checkout.PayPalCheckout 10 | import com.paypal.checkout.createorder.CreateOrder 11 | import com.paypal.checkout.createorder.CurrencyCode 12 | import com.paypal.checkout.createorder.OrderIntent 13 | import com.paypal.checkout.createorder.UserAction 14 | import com.paypal.checkoutsamples.R 15 | import com.paypal.checkoutsamples.token.repository.CheckoutApi 16 | import com.paypal.checkoutsamples.token.repository.CreatedOrder 17 | import com.paypal.checkoutsamples.token.repository.OrderRepository 18 | import com.paypal.checkoutsamples.token.repository.request.AmountRequest 19 | import com.paypal.checkoutsamples.token.repository.request.ApplicationContextRequest 20 | import com.paypal.checkoutsamples.token.repository.request.OrderRequest 21 | import com.paypal.checkoutsamples.token.repository.request.PurchaseUnitRequest 22 | import kotlinx.android.synthetic.main.activity_token_quick_start.* 23 | import kotlinx.coroutines.MainScope 24 | import kotlinx.coroutines.cancelChildren 25 | import kotlinx.coroutines.launch 26 | import java.io.IOException 27 | 28 | class TokenQuickStartActivity : AppCompatActivity() { 29 | 30 | private val tag = this::class.java.toString() 31 | 32 | private val checkoutApi = CheckoutApi() 33 | 34 | private val orderRepository = OrderRepository(checkoutApi) 35 | 36 | private val checkoutSdk: PayPalCheckout 37 | get() = PayPalCheckout 38 | 39 | private val selectedUserAction: UserAction 40 | get() { 41 | return when (val selectedId = selectUserAction.checkedRadioButtonId) { 42 | R.id.userActionOptionContinue -> UserAction.CONTINUE 43 | R.id.userActionOptionPayNow -> UserAction.PAY_NOW 44 | else -> { 45 | throw IllegalArgumentException( 46 | "Expected one of the following ids: ${R.id.userActionOptionContinue}, or " + 47 | "${R.id.userActionOptionPayNow} but was $selectedId" 48 | ) 49 | } 50 | } 51 | } 52 | 53 | private val selectedOrderIntent: OrderIntent 54 | get() { 55 | return when (val selectedId = selectOrderIntent.checkedRadioButtonId) { 56 | R.id.orderIntentOptionAuthorize -> OrderIntent.AUTHORIZE 57 | R.id.orderIntentOptionCapture -> OrderIntent.CAPTURE 58 | else -> { 59 | throw IllegalArgumentException( 60 | "Expected one of the following ids: ${R.id.orderIntentOptionAuthorize}, or " + 61 | "${R.id.orderIntentOptionCapture} but was $selectedId" 62 | ) 63 | } 64 | } 65 | } 66 | 67 | private val selectedCurrencyCode: CurrencyCode 68 | get() { 69 | return when (val selectedId = selectCurrencyCode.checkedRadioButtonId) { 70 | R.id.currencyCodeUsd -> CurrencyCode.USD 71 | R.id.currencyCodeEur -> CurrencyCode.EUR 72 | R.id.currencyCodeGbp -> CurrencyCode.GBP 73 | else -> { 74 | throw IllegalArgumentException( 75 | "Expected one of the following ids: ${R.id.currencyCodeUsd}, " + 76 | "${R.id.currencyCodeEur}, or ${R.id.currencyCodeGbp} but was $selectedId" 77 | ) 78 | } 79 | } 80 | } 81 | 82 | private val enteredAmount: String 83 | get() = totalAmountInput.editText!!.text.toString() 84 | 85 | private val uiScope = MainScope() 86 | 87 | override fun onCreate(savedInstanceState: Bundle?) { 88 | super.onCreate(savedInstanceState) 89 | setContentView(R.layout.activity_token_quick_start) 90 | 91 | totalAmountInput.editText?.addTextChangedListener { totalAmountInput.error = null } 92 | 93 | submitTokenButton.setOnClickListener { 94 | if (totalAmountInput.editText!!.text.isEmpty()) { 95 | totalAmountInput.error = getString(R.string.token_quick_start_activity_total_amount_required) 96 | return@setOnClickListener 97 | } 98 | 99 | startCheckout() 100 | } 101 | } 102 | 103 | private fun startCheckout() { 104 | checkoutSdk.startCheckout( 105 | createOrder = CreateOrder { createOrderActions -> 106 | uiScope.launch { 107 | val createdOrder = createOrder() 108 | createdOrder?.let { createOrderActions.set(createdOrder.id) } 109 | } 110 | }, 111 | // This sample app has not obtained consent from the buyer to collect location data. 112 | // This flag enables PayPal to collect necessary information required for Fraud Detection and Risk Management 113 | hasUserLocationConsent = false 114 | ) 115 | } 116 | 117 | private suspend fun createOrder(): CreatedOrder? { 118 | val orderRequest = createOrderRequest() 119 | return try { 120 | orderRepository.create(orderRequest) 121 | } catch (ex: IOException) { 122 | Log.w(tag, "Attempt to create order failed with the following message: ${ex.message}") 123 | null 124 | } 125 | } 126 | 127 | private fun createOrderRequest(): OrderRequest { 128 | return OrderRequest( 129 | intent = selectedOrderIntent.name, 130 | applicationContext = ApplicationContextRequest( 131 | userAction = selectedUserAction.name 132 | ), 133 | purchaseUnits = listOf( 134 | PurchaseUnitRequest( 135 | amount = AmountRequest( 136 | value = enteredAmount, 137 | currencyCode = selectedCurrencyCode.name 138 | ) 139 | ) 140 | ) 141 | ).also { Log.i(tag, "OrderRequest: $it") } 142 | } 143 | 144 | override fun onStop() { 145 | super.onStop() 146 | uiScope.coroutineContext.cancelChildren() 147 | } 148 | 149 | companion object { 150 | fun startIntent(context: Context): Intent { 151 | return Intent(context, TokenQuickStartActivity::class.java) 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /quickstart-kotlin/src/main/java/com/paypal/checkoutsamples/token/repository/AuthTokenRepository.kt: -------------------------------------------------------------------------------- 1 | package com.paypal.checkoutsamples.token.repository 2 | 3 | import android.util.Log 4 | import kotlinx.coroutines.CoroutineDispatcher 5 | import kotlinx.coroutines.Dispatchers 6 | import kotlinx.coroutines.withContext 7 | import java.util.Date 8 | 9 | /** 10 | * AuthTokenRepository provides a way to retrieve a valid [AuthToken]. If you encounter errors with 11 | * this repository please ensure you have a valid client id and secret set in QuickStartConstants. 12 | */ 13 | class AuthTokenRepository( 14 | private val checkoutApi: CheckoutApi, 15 | private val dispatcher: CoroutineDispatcher = Dispatchers.IO 16 | ) { 17 | private val tag = this::class.java.toString() 18 | 19 | /** 20 | * Retrieves a valid [AuthToken]. If one does not exist or has expired a new one will be created 21 | * and returned. 22 | */ 23 | suspend fun retrieve(): AuthToken { 24 | val currentAuthToken = authToken 25 | return if (currentAuthToken == null || currentAuthToken.expiresAt < Date()) { 26 | Log.i(tag, "Creating a new OAuth Token...") 27 | val oAuthToken = withContext(dispatcher) { 28 | checkoutApi.postOAuthToken() 29 | } 30 | Log.i(tag, "New token created...") 31 | Log.d(tag, "Token: $oAuthToken") 32 | 33 | val expiresAt: Long = (oAuthToken.expiresIn * expirationFactor).toLong() 34 | AuthToken( 35 | accessToken = oAuthToken.accessToken, 36 | expiresAt = Date().add(expiresAt) 37 | ) 38 | .also { authToken = it } 39 | .also { Log.d(tag, "New token cached: $it") } 40 | } else { 41 | Log.i(tag, "Valid token exists, returning to caller.") 42 | currentAuthToken 43 | } 44 | } 45 | 46 | private fun Date.add(seconds: Long): Date = apply { time += seconds } 47 | 48 | companion object { 49 | /** 50 | * The expirationFactor is applied to our OAuth Token expiration time. The primary goal is 51 | * to avoid using a token just as it's expiring, so instead of using the full time returned 52 | * by the API we only use 95% of that time to avoid making requests with an expired token. 53 | */ 54 | private const val expirationFactor = 0.95 55 | 56 | private var authToken: AuthToken? = null 57 | } 58 | } 59 | 60 | data class AuthToken( 61 | val accessToken: String, 62 | val expiresAt: Date 63 | ) 64 | -------------------------------------------------------------------------------- /quickstart-kotlin/src/main/java/com/paypal/checkoutsamples/token/repository/CheckoutApi.kt: -------------------------------------------------------------------------------- 1 | package com.paypal.checkoutsamples.token.repository 2 | 3 | import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory 4 | import com.paypal.checkoutsamples.PAYPAL_CLIENT_ID 5 | import com.paypal.checkoutsamples.PAYPAL_SECRET 6 | import com.paypal.checkoutsamples.token.repository.request.OrderRequest 7 | import com.paypal.checkoutsamples.token.repository.response.OAuthTokenResponse 8 | import com.paypal.checkoutsamples.token.repository.response.OrderResponse 9 | import kotlinx.serialization.ExperimentalSerializationApi 10 | import kotlinx.serialization.json.Json 11 | import okhttp3.Credentials 12 | import okhttp3.MediaType.Companion.toMediaType 13 | import retrofit2.Retrofit 14 | import retrofit2.http.Body 15 | import retrofit2.http.Field 16 | import retrofit2.http.FormUrlEncoded 17 | import retrofit2.http.Header 18 | import retrofit2.http.Headers 19 | import retrofit2.http.POST 20 | 21 | /** 22 | * CheckoutApi includes the necessary endpoints for creating an order and receiving an EC token that 23 | * can be used for launching the Checkout SDK pay sheet with a token. 24 | * 25 | * Note: when implementing your own integration, you should avoid creating the OAuth Token within your 26 | * Android app as the creating an OAuth Token in these examples require passing your app secret. 27 | */ 28 | interface CheckoutApi { 29 | 30 | /** 31 | * Creates a new OAuth Token that can be used for subsequent API requests. 32 | * 33 | * @see payPalAuthorization 34 | */ 35 | @Headers("Accept: application/json", "Accept-Language: en_US") 36 | @FormUrlEncoded 37 | @POST("/v1/oauth2/token") 38 | suspend fun postOAuthToken( 39 | @Header("Authorization") authorization: String = payPalAuthorization, 40 | @Field("grant_type") grantType: String = "client_credentials" 41 | ): OAuthTokenResponse 42 | 43 | /** 44 | * Creates a new Order, the subsequent id ([OrderResponse.id]) can then be used to start a new 45 | * pay sheet instance. 46 | */ 47 | @Headers("Accept: application/json") 48 | @POST("/v2/checkout/orders") 49 | suspend fun postCheckoutOrder( 50 | @Header("Authorization") authorization: String, 51 | @Body orderRequest: OrderRequest 52 | ): OrderResponse 53 | 54 | companion object { 55 | 56 | /** 57 | * Valid credentials for creating a server <-> server auth token require a valid client id 58 | * as the user and a corresponding secret for the password. This should be encoded in Base64. 59 | * 60 | * The proper format prior to encoding is as follows: clientId:secret 61 | * 62 | * OkHttp provides a convenient "Basic" function which handles the heavy lifting for us. 63 | */ 64 | val payPalAuthorization: String = Credentials.basic(PAYPAL_CLIENT_ID, PAYPAL_SECRET) 65 | 66 | /** 67 | * Provides an easy way to instantiate a [CheckoutApi] as you normally would if this were a 68 | * class instead of an interface. 69 | * 70 | * Example: val checkoutApi = CheckoutApi() 71 | */ 72 | @ExperimentalSerializationApi 73 | operator fun invoke(): CheckoutApi { 74 | val json = Json { 75 | ignoreUnknownKeys = true 76 | } 77 | val retrofit = Retrofit.Builder() 78 | .baseUrl("https://api.sandbox.paypal.com/") 79 | .addConverterFactory( 80 | json.asConverterFactory("application/json".toMediaType()) 81 | ) 82 | .build() 83 | 84 | return retrofit.create(CheckoutApi::class.java) 85 | } 86 | } 87 | } 88 | 89 | /** 90 | * Updates a [String] to be a valid Bearer token (assuming the String itself is a valid OAuth Token). 91 | */ 92 | val String.asBearer: String 93 | get() = "Bearer $this" 94 | -------------------------------------------------------------------------------- /quickstart-kotlin/src/main/java/com/paypal/checkoutsamples/token/repository/OrderRepository.kt: -------------------------------------------------------------------------------- 1 | package com.paypal.checkoutsamples.token.repository 2 | 3 | import android.util.Log 4 | import com.paypal.checkoutsamples.token.repository.request.OrderRequest 5 | import com.paypal.checkoutsamples.token.repository.response.OrderResponse 6 | import kotlinx.coroutines.CoroutineDispatcher 7 | import kotlinx.coroutines.Dispatchers 8 | import kotlinx.coroutines.withContext 9 | import retrofit2.HttpException 10 | 11 | /** 12 | * OrderRepository provides an entry point to create new orders. The main purpose with regards to 13 | * this sample app is being able to get an Order ID (EC Token) which can then be provided to the 14 | * Checkout SDK to launch a pay sheet. 15 | */ 16 | class OrderRepository( 17 | private val checkoutApi: CheckoutApi, 18 | private val authTokenRepository: AuthTokenRepository = AuthTokenRepository(checkoutApi), 19 | private val dispatcher: CoroutineDispatcher = Dispatchers.IO 20 | ) { 21 | private val tag = javaClass.simpleName 22 | 23 | /** 24 | * Creates a new Order ([CreatedOrder]) given a [CreateOrderRequest]. 25 | */ 26 | suspend fun create(request: CreateOrderRequest): CreatedOrder { 27 | Log.d(tag, "Create order request: $request") 28 | val token = authTokenRepository.retrieve() 29 | return try { 30 | withContext(dispatcher) { 31 | checkoutApi.postCheckoutOrder( 32 | authorization = token.accessToken.asBearer, 33 | orderRequest = request 34 | ) 35 | } 36 | } catch (ex: HttpException) { 37 | Log.w(tag, "Unable to create order with token: $token") 38 | Log.e(tag, "Could not create order: $ex") 39 | throw ex 40 | } 41 | } 42 | } 43 | 44 | typealias CreateOrderRequest = OrderRequest 45 | typealias CreatedOrder = OrderResponse 46 | -------------------------------------------------------------------------------- /quickstart-kotlin/src/main/java/com/paypal/checkoutsamples/token/repository/request/OrderRequest.kt: -------------------------------------------------------------------------------- 1 | package com.paypal.checkoutsamples.token.repository.request 2 | 3 | import com.paypal.checkout.createorder.CurrencyCode 4 | import kotlinx.serialization.SerialName 5 | import kotlinx.serialization.Serializable 6 | 7 | /** 8 | * OrderRequest is used for creating a new Order with the v2 Order's API. 9 | * 10 | * @see https://developer.paypal.com/docs/api/orders/v2/#definition-order_request 11 | * 12 | * @property intent describes how the order should be handled, should it be captured so the checkout is 13 | * completely shortly after the pay sheet is complete or authorized. 14 | * @property applicationContext provides details about application being used for placing the order, of 15 | * note is the user action which determines whether or not we show a cart total. 16 | * @property purchaseUnits contains details about the items that are part of the order. 17 | */ 18 | @Serializable 19 | data class OrderRequest( 20 | @SerialName("intent") 21 | val intent: String, 22 | @SerialName("application_context") 23 | val applicationContext: ApplicationContextRequest, 24 | @SerialName("purchase_units") 25 | val purchaseUnits: List 26 | ) 27 | 28 | /** 29 | * ApplicationContextRequest provides additional details about the application. For the purpose of this 30 | * sample we are only concerned with a subset of the parameters. 31 | * 32 | * @see https://developer.paypal.com/docs/api/orders/v2/#definition-order_application_context 33 | * 34 | * @property userAction determines whether or not the pay sheet will display the total order amount via 35 | * PAY_NOW being passed in. When CONTINUE is provided then the pay sheet will not display the total. 36 | */ 37 | @Serializable 38 | data class ApplicationContextRequest( 39 | @SerialName("user_action") 40 | val userAction: String, 41 | ) 42 | 43 | /** 44 | * PurchaseUnitRequest is used to provide item, payment, and shipping information. For the purpose of 45 | * this sample we are only concerned with a subset of the available parameters. 46 | * 47 | * @see https://developer.paypal.com/docs/api/orders/v2/#definition-purchase_unit_request 48 | * 49 | * @param amount is the total amount for the order. 50 | */ 51 | @Serializable 52 | data class PurchaseUnitRequest( 53 | @SerialName("amount") 54 | val amount: AmountRequest, 55 | ) 56 | 57 | /** 58 | * AmountRequest is used for outlining the amount of something (item, shipping, total, etc). 59 | * 60 | * @see https://developer.paypal.com/docs/api/orders/v2/#definition-order_request 61 | * @see [CurrencyCode] 62 | * 63 | * @property currencyCode defines what currency is being used for this order. 64 | * @property value defines how much of the amount is. 65 | * 66 | * Example, value = 100 + currencyCode = USD is how you would represent $100 67 | */ 68 | @Serializable 69 | data class AmountRequest( 70 | @SerialName("currency_code") 71 | val currencyCode: String, 72 | @SerialName("value") 73 | val value: String = "0.01" 74 | ) 75 | -------------------------------------------------------------------------------- /quickstart-kotlin/src/main/java/com/paypal/checkoutsamples/token/repository/response/OAuthTokenResponse.kt: -------------------------------------------------------------------------------- 1 | package com.paypal.checkoutsamples.token.repository.response 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | /** 7 | * OAuthTokenResponse is used for capturing an OAuth Token and the params available here are a subset 8 | * of the ones actually returned. 9 | * 10 | * @property accessToken should be used for other API requests, passing it in as "Bearer [accessToken]". 11 | * @property expiresIn provides the amount of time this token will be valid for to make it easier to 12 | * know when to re-authenticate. 13 | */ 14 | @Serializable 15 | data class OAuthTokenResponse( 16 | @SerialName("access_token") 17 | val accessToken: String, 18 | @SerialName("expires_in") 19 | val expiresIn: Long 20 | ) 21 | -------------------------------------------------------------------------------- /quickstart-kotlin/src/main/java/com/paypal/checkoutsamples/token/repository/response/OrderResponse.kt: -------------------------------------------------------------------------------- 1 | package com.paypal.checkoutsamples.token.repository.response 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | /** 6 | * OrderResponse is a partial response returned by /v2/checkout/orders 7 | * 8 | * @property id is the identifier for the Order, it is also referred to as a token or ec token. This 9 | * is used when creating a new pay sheet via the Checkout SDK. 10 | */ 11 | @Serializable 12 | data class OrderResponse( 13 | val id: String 14 | ) 15 | -------------------------------------------------------------------------------- /quickstart-kotlin/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /quickstart-kotlin/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /quickstart-kotlin/src/main/res/layout/activity_kotlin_quick_start.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 |