Skip to content

Commit 1ae88e5

Browse files
committed
[Android] Add advanced present builder callbacks and migrate sample flow
1 parent 9e15892 commit 1ae88e5

13 files changed

Lines changed: 673 additions & 181 deletions

File tree

platforms/android/README.md

Lines changed: 107 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -113,10 +113,71 @@ function provided by the SDK:
113113
```kotlin
114114
fun presentCheckout() {
115115
val checkoutUrl = cart.checkoutUrl
116-
ShopifyCheckoutKit.present(checkoutUrl, context, checkoutEventProcessor)
116+
ShopifyCheckoutKit.present(checkoutUrl, context) {
117+
onComplete { event ->
118+
handleCompletedEvent(event)
119+
}
120+
onFail { error ->
121+
handleCheckoutError(error)
122+
}
123+
onCancel {
124+
resetCheckoutUi()
125+
}
126+
}
117127
}
118128
```
119129

130+
> [!NOTE]
131+
> Pass the standard `checkoutUrl` returned by Storefront API or a cart permalink.
132+
> Checkout Kit adds the required UCP query parameters automatically when it loads checkout,
133+
> so you do not need to rewrite the URL yourself.
134+
135+
If you also want typed Embedded Checkout Protocol (ECP) callbacks, connect a
136+
`CheckoutProtocol.Client` inside the same Kotlin builder:
137+
138+
```kotlin
139+
val checkoutProtocolClient = CheckoutProtocol.Client()
140+
.on(CheckoutProtocol.start) { checkout ->
141+
// Checkout is ready and interactive.
142+
}
143+
.on(CheckoutProtocol.complete) { checkout ->
144+
// Typed checkout payload emitted when checkout completes.
145+
}
146+
.on(CheckoutProtocol.windowOpen) { request ->
147+
startActivity(Intent(Intent.ACTION_VIEW, request.url))
148+
WindowOpenResult.Success
149+
}
150+
151+
ShopifyCheckoutKit.present(checkoutUrl, activity) {
152+
onComplete { event ->
153+
handleCompletedEvent(event)
154+
}
155+
connect(checkoutProtocolClient)
156+
}
157+
```
158+
159+
The Kotlin builder also supports optional browser/system callbacks:
160+
161+
```kotlin
162+
ShopifyCheckoutKit.present(checkoutUrl, activity) {
163+
onShowFileChooser { _, filePathCallback, fileChooserParams ->
164+
activity.onShowFileChooser(filePathCallback, fileChooserParams)
165+
}
166+
onGeolocationPermissionsShowPrompt { origin, callback ->
167+
activity.onGeolocationPermissionsShowPrompt(origin, callback)
168+
}
169+
onGeolocationPermissionsHidePrompt {
170+
activity.onGeolocationPermissionsHidePrompt()
171+
}
172+
onPermissionRequest { permissionRequest ->
173+
// Optional: grant, deny, or proxy requested web permissions.
174+
}
175+
}
176+
```
177+
178+
If you prefer a reusable object, or are integrating from Java, `DefaultCheckoutEventProcessor`
179+
remains available.
180+
120181
> [!TIP]
121182
> To help optimize and deliver the best experience the SDK also provides a
122183
> [preloading API](#preloading) that can be used to initialize the checkout session ahead of time.
@@ -317,9 +378,9 @@ The library will automatically invalidate/abort preload under the following cond
317378
318379
- Request results in network error or non 2XX server response code
319380
- The checkout has successfully completed, as indicated by the server response
320-
- When `ShopifyCheckoutSheet.configure` is called (e.g. with theming changes).
381+
- When `ShopifyCheckoutKit.configure` is called (e.g. with theming changes).
321382
322-
A preloaded checkout _is not_ automatically invalidated when checkout is closed. For example, if a buyer loads the checkout then exists, the preloaded checkout is retained and should be updated when cart contents change.
383+
A preloaded checkout _is not_ automatically invalidated when checkout is closed. For example, if a buyer loads the checkout then exits, the preloaded checkout is retained and should be updated when cart contents change.
323384
324385
#### Additional considerations for preloaded checkout
325386
@@ -332,41 +393,57 @@ A preloaded checkout _is not_ automatically invalidated when checkout is closed.
332393
333394
## Monitoring the lifecycle of a checkout session
334395
335-
Extend the `DefaultCheckoutEventProcessor` abstract class to register callbacks for key lifecycle events during the checkout session:
396+
For the common lifecycle path, register callbacks directly on `present(...)`:
336397
337398
```kotlin
338-
val processor = object : DefaultCheckoutEventProcessor(activity) {
339-
override fun onCheckoutCompleted(checkoutCompletedEvent: CheckoutCompletedEvent) {
399+
ShopifyCheckoutKit.present(checkoutUrl, context) {
400+
onComplete { checkoutCompletedEvent ->
340401
// Called when the checkout was completed successfully by the buyer.
341402
// Use this to update UI, reset cart state, etc.
342403
}
343404
344-
override fun onCheckoutCanceled() {
405+
onCancel {
345406
// Called when the checkout was canceled by the buyer.
346407
// Note: This will also be received after closing a completed checkout
347408
}
348409
349-
override fun onCheckoutFailed(error: CheckoutException) {
410+
onFail { error ->
350411
// Called when the checkout encountered an error and has been aborted.
351412
}
413+
}
414+
```
415+
416+
For browser/system integrations such as file chooser, geolocation, or permission requests,
417+
you can register handlers directly on the Kotlin builder:
418+
419+
```kotlin
420+
ShopifyCheckoutKit.present(checkoutUrl, activity) {
421+
onShowFileChooser { webView, filePathCallback, fileChooserParams ->
422+
// Return true if the host app handled the chooser request.
423+
false
424+
}
425+
426+
onGeolocationPermissionsShowPrompt { origin, callback ->
427+
// Called to tell the client to show a geolocation permissions prompt as a geolocation
428+
// request has been made. Invoked for example if a customer uses `Use my location`
429+
// for pickup points.
430+
}
431+
432+
onGeolocationPermissionsHidePrompt {
433+
// Called to tell the client to hide the geolocation permissions prompt.
434+
}
352435
353-
override fun onCheckoutLinkClicked(uri: Uri) {
354-
// Called when the buyer clicks a link within the checkout experience:
355-
// - email address (`mailto:`)
356-
// - telephone number (`tel:`)
357-
// - web (http:)
358-
// - deep link (e.g. myapp://checkout)
359-
// and is being directed outside the application.
360-
361-
// Note: to support deep links on Android 11+ using the `DefaultCheckoutEventProcessor`,
362-
// the client app should add a queries element in its manifest declaring which apps it should interact with.
363-
// See the MobileBuyIntegration sample's manifest for an example.
364-
// Queries reference - https://developer.android.com/guide/topics/manifest/queries-element
365-
366-
// If no app can be queried to deal with the link, the processor will log a warning:
367-
// `Unrecognized scheme for link clicked in checkout` along with the uri.
436+
onPermissionRequest { permissionRequest ->
437+
// Called when a web permission has been requested, e.g. to access the camera.
368438
}
439+
}
440+
```
441+
442+
If you prefer a reusable object, or are integrating from Java, extend
443+
`DefaultCheckoutEventProcessor` and pass it to the existing `present(...)` overload:
369444
445+
```kotlin
446+
val processor = object : DefaultCheckoutEventProcessor() {
370447
override fun onShowFileChooser(
371448
webView: WebView,
372449
filePathCallback: ValueCallback<Array<Uri>>,
@@ -392,10 +469,13 @@ val processor = object : DefaultCheckoutEventProcessor(activity) {
392469
// implement to grant/deny/request permissions.
393470
}
394471
}
472+
473+
ShopifyCheckoutKit.present(checkoutUrl, context, processor)
395474
```
396475
397476
> [!Note]
398-
> The `DefaultCheckoutEventProcessor` provides default implementations for current and future callback functions (such as `onLinkClicked()`), which can be overridden by clients wanting to change default behavior.
477+
> The `DefaultCheckoutEventProcessor` overload remains available for reusable or Java-facing
478+
> integrations and provides default implementations for optional browser/system callbacks.
399479
400480
### Error handling
401481
@@ -406,8 +486,8 @@ In the event of a checkout error occurring, the Checkout Kit _may_ attempt to re
406486
407487
There are some caveats to note when this scenario occurs:
408488
409-
1. The checkout experience may look different to buyers. Though the sheet kit will attempt to load any checkoput customizations for the storefront, there is no guarantee they will show in recovery mode.
410-
2. The `onCheckoutCompleted(checkoutCompletedEvent: CheckoutCompletedEvent)` will be emitted with partial data. Invocations will only received the order ID via `checkoutCompletedEvent.orderDetails.id`.
489+
1. The checkout experience may look different to buyers. Though the kit will attempt to load any checkout customizations for the storefront, there is no guarantee they will show in recovery mode.
490+
2. The `onCheckoutCompleted(checkoutCompletedEvent: CheckoutCompletedEvent)` callback will be emitted with partial data. Invocations will only receive the order ID via `checkoutCompletedEvent.orderDetails.id`.
411491
412492
Should you wish to opt-out of this fallback experience entirely, you can do so by overriding `shouldRecoverFromError`. Errors given to the `onCheckoutFailed(error: CheckoutException)` lifecycle method will contain an `isRecoverable` property by default indicating whether the request should be retried or not.
413493
@@ -492,7 +572,7 @@ classDiagram
492572
493573
Buyer-aware checkout experience reduces friction and increases conversion. Depending on the context
494574
of the buyer (guest or signed-in), knowledge of buyer preferences, or account/identity system, the
495-
application can use on of the following methods to initialize personalized and contextualized buyer
575+
application can use one of the following methods to initialize personalized and contextualized buyer
496576
experience.
497577
498578
### Cart: buyer bag, identity, and preferences

platforms/android/lib/api/lib.api

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -756,6 +756,17 @@ public final class com/shopify/checkoutkit/CheckoutLineItem$Companion {
756756
public final fun serializer ()Lkotlinx/serialization/KSerializer;
757757
}
758758

759+
public final class com/shopify/checkoutkit/CheckoutPresentation {
760+
public final fun connect (Lcom/shopify/checkoutkit/CheckoutCommunicationClient;)V
761+
public final fun onCancel (Lkotlin/jvm/functions/Function0;)V
762+
public final fun onComplete (Lkotlin/jvm/functions/Function1;)V
763+
public final fun onFail (Lkotlin/jvm/functions/Function1;)V
764+
public final fun onGeolocationPermissionsHidePrompt (Lkotlin/jvm/functions/Function0;)V
765+
public final fun onGeolocationPermissionsShowPrompt (Lkotlin/jvm/functions/Function2;)V
766+
public final fun onPermissionRequest (Lkotlin/jvm/functions/Function1;)V
767+
public final fun onShowFileChooser (Lkotlin/jvm/functions/Function3;)V
768+
}
769+
759770
public final class com/shopify/checkoutkit/CheckoutProtocol {
760771
public static final field INSTANCE Lcom/shopify/checkoutkit/CheckoutProtocol;
761772
public static final field specVersion Ljava/lang/String;
@@ -3927,6 +3938,7 @@ public final class com/shopify/checkoutkit/ShopifyCheckoutKit {
39273938
public static final fun preload (Ljava/lang/String;Landroidx/activity/ComponentActivity;)V
39283939
public static final fun present (Ljava/lang/String;Landroidx/activity/ComponentActivity;Lcom/shopify/checkoutkit/DefaultCheckoutEventProcessor;)Lcom/shopify/checkoutkit/CheckoutKitDialog;
39293940
public static final fun present (Ljava/lang/String;Landroidx/activity/ComponentActivity;Lcom/shopify/checkoutkit/DefaultCheckoutEventProcessor;Lcom/shopify/checkoutkit/CheckoutCommunicationClient;)Lcom/shopify/checkoutkit/CheckoutKitDialog;
3941+
public static final synthetic fun present (Ljava/lang/String;Landroidx/activity/ComponentActivity;Lkotlin/jvm/functions/Function1;)Lcom/shopify/checkoutkit/CheckoutKitDialog;
39303942
public static synthetic fun present$default (Ljava/lang/String;Landroidx/activity/ComponentActivity;Lcom/shopify/checkoutkit/DefaultCheckoutEventProcessor;Lcom/shopify/checkoutkit/CheckoutCommunicationClient;ILjava/lang/Object;)Lcom/shopify/checkoutkit/CheckoutKitDialog;
39313943
}
39323944

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
/*
2+
* MIT License
3+
*
4+
* Copyright 2023-present, Shopify Inc.
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in all
14+
* copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22+
*/
23+
package com.shopify.checkoutkit
24+
25+
import android.net.Uri
26+
import android.webkit.GeolocationPermissions
27+
import android.webkit.PermissionRequest
28+
import android.webkit.ValueCallback
29+
import android.webkit.WebChromeClient
30+
import android.webkit.WebView
31+
import com.shopify.checkoutkit.lifecycleevents.CheckoutCompletedEvent
32+
33+
/**
34+
* Kotlin-first builder for per-presentation checkout callbacks.
35+
*
36+
* Use through [ShopifyCheckoutKit.present].
37+
*/
38+
public class CheckoutPresentation internal constructor() {
39+
internal var onComplete: ((CheckoutCompletedEvent) -> Unit)? = null
40+
internal var onFail: ((CheckoutException) -> Unit)? = null
41+
internal var onCancel: (() -> Unit)? = null
42+
internal var onPermissionRequest: ((PermissionRequest) -> Unit)? = null
43+
internal var onShowFileChooser:
44+
((WebView, ValueCallback<Array<Uri>>, WebChromeClient.FileChooserParams) -> Boolean)? = null
45+
internal var onGeolocationPermissionsShowPrompt:
46+
((String, GeolocationPermissions.Callback) -> Unit)? = null
47+
internal var onGeolocationPermissionsHidePrompt: (() -> Unit)? = null
48+
internal var communicationClient: CheckoutCommunicationClient? = null
49+
50+
/**
51+
* Called when checkout completes successfully.
52+
*/
53+
public fun onComplete(handler: (CheckoutCompletedEvent) -> Unit) {
54+
onComplete = handler
55+
}
56+
57+
/**
58+
* Called when checkout fails.
59+
*/
60+
public fun onFail(handler: (CheckoutException) -> Unit) {
61+
onFail = handler
62+
}
63+
64+
/**
65+
* Called when checkout is canceled by the buyer.
66+
*/
67+
public fun onCancel(handler: () -> Unit) {
68+
onCancel = handler
69+
}
70+
71+
/**
72+
* Called when checkout requests a web permission, such as camera access.
73+
*/
74+
public fun onPermissionRequest(handler: (PermissionRequest) -> Unit) {
75+
onPermissionRequest = handler
76+
}
77+
78+
/**
79+
* Called when checkout requests that the host app present a file chooser.
80+
*/
81+
public fun onShowFileChooser(
82+
handler: (
83+
webView: WebView,
84+
filePathCallback: ValueCallback<Array<Uri>>,
85+
fileChooserParams: WebChromeClient.FileChooserParams,
86+
) -> Boolean,
87+
) {
88+
onShowFileChooser = handler
89+
}
90+
91+
/**
92+
* Called when checkout requests that the host app present a geolocation prompt.
93+
*/
94+
public fun onGeolocationPermissionsShowPrompt(
95+
handler: (origin: String, callback: GeolocationPermissions.Callback) -> Unit,
96+
) {
97+
onGeolocationPermissionsShowPrompt = handler
98+
}
99+
100+
/**
101+
* Called when checkout requests that any visible geolocation prompt be dismissed.
102+
*/
103+
public fun onGeolocationPermissionsHidePrompt(handler: () -> Unit) {
104+
onGeolocationPermissionsHidePrompt = handler
105+
}
106+
107+
/**
108+
* Connects a communication client for Embedded Checkout Protocol messages.
109+
*/
110+
public fun connect(client: CheckoutCommunicationClient?) {
111+
communicationClient = client
112+
}
113+
114+
internal fun buildEventProcessor(): DefaultCheckoutEventProcessor =
115+
object : DefaultCheckoutEventProcessor() {
116+
override fun onCheckoutCompleted(checkoutCompletedEvent: CheckoutCompletedEvent) {
117+
onComplete?.invoke(checkoutCompletedEvent)
118+
}
119+
120+
override fun onCheckoutFailed(error: CheckoutException) {
121+
onFail?.invoke(error)
122+
}
123+
124+
override fun onCheckoutCanceled() {
125+
onCancel?.invoke()
126+
}
127+
128+
override fun onPermissionRequest(permissionRequest: PermissionRequest) {
129+
onPermissionRequest?.invoke(permissionRequest)
130+
}
131+
132+
override fun onShowFileChooser(
133+
webView: WebView,
134+
filePathCallback: ValueCallback<Array<Uri>>,
135+
fileChooserParams: WebChromeClient.FileChooserParams,
136+
): Boolean {
137+
return onShowFileChooser?.invoke(webView, filePathCallback, fileChooserParams) ?: false
138+
}
139+
140+
override fun onGeolocationPermissionsShowPrompt(origin: String, callback: GeolocationPermissions.Callback) {
141+
onGeolocationPermissionsShowPrompt?.invoke(origin, callback)
142+
}
143+
144+
override fun onGeolocationPermissionsHidePrompt() {
145+
onGeolocationPermissionsHidePrompt?.invoke()
146+
}
147+
}
148+
}

0 commit comments

Comments
 (0)