Skip to content

Commit

Permalink
Implement onShowFileChooser, calling delegate (#124)
Browse files Browse the repository at this point in the history
  • Loading branch information
kiftio authored Sep 6, 2024
1 parent 85a9611 commit 7091edc
Show file tree
Hide file tree
Showing 17 changed files with 249 additions and 79 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## 3.1.0 September 6, 2024

- Implement and expose `onShowFileChooser()`, to support clients with checkouts that need to show a native file chooser. Example in the MobileBuyIntegration demo app.

## 3.0.4 August 7, 2024

- Update Web Pixel schema data classes.
Expand Down
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ your project:
#### Gradle

```groovy
implementation "com.shopify:checkout-sheet-kit:3.0.4"
implementation "com.shopify:checkout-sheet-kit:3.1.0"
```

#### Maven
Expand All @@ -33,7 +33,7 @@ implementation "com.shopify:checkout-sheet-kit:3.0.4"
<dependency>
<groupId>com.shopify</groupId>
<artifactId>checkout-sheet-kit</artifactId>
<version>3.0.4</version>
<version>3.1.0</version>
</dependency>
```

Expand Down Expand Up @@ -252,6 +252,16 @@ val processor = object : DefaultCheckoutEventProcessor(activity) {
// Use this to submit events to your analytics system, see below.
}
override fun onShowFileChooser(
webView: WebView,
filePathCallback: ValueCallback<Array<Uri>>,
fileChooserParams: FileChooserParams,
): Boolean {
// Called to tell the client to show a file chooser. This is called to handle HTML forms with 'file' input type,
// in response to the user pressing the "Select File" button.
// To cancel the request, call filePathCallback.onReceiveValue(null) and return true.
}
override fun onPermissionRequest(permissionRequest: PermissionRequest) {
// Called when a permission has been requested, e.g. to access the camera
// implement to grant/deny/request permissions.
Expand Down
2 changes: 1 addition & 1 deletion lib/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def resolveEnvVarValue(name, defaultValue) {
return rawValue ? rawValue : defaultValue
}

def versionName = resolveEnvVarValue("CHECKOUT_SHEET_KIT_VERSION", "3.0.4")
def versionName = resolveEnvVarValue("CHECKOUT_SHEET_KIT_VERSION", "3.1.0")

ext {
app_compat_version = '1.6.1'
Expand Down
11 changes: 11 additions & 0 deletions lib/src/main/java/com/shopify/checkoutsheetkit/BaseWebView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ package com.shopify.checkoutsheetkit
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Color.TRANSPARENT
import android.net.Uri
import android.os.Build
import android.util.AttributeSet
import android.view.KeyEvent
Expand All @@ -33,6 +34,7 @@ import android.view.ViewGroup.LayoutParams
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.webkit.PermissionRequest
import android.webkit.RenderProcessGoneDetail
import android.webkit.ValueCallback
import android.webkit.WebChromeClient
import android.webkit.WebResourceError
import android.webkit.WebResourceRequest
Expand Down Expand Up @@ -60,6 +62,7 @@ internal abstract class BaseWebView(context: Context, attributeSet: AttributeSet
settings.apply {
javaScriptEnabled = true
domStorageEnabled = true
allowContentAccess = true
}
webChromeClient = object: WebChromeClient() {
override fun onProgressChanged(view: WebView?, newProgress: Int) {
Expand All @@ -69,6 +72,14 @@ internal abstract class BaseWebView(context: Context, attributeSet: AttributeSet
override fun onPermissionRequest(request: PermissionRequest) {
getEventProcessor().onPermissionRequest(request)
}

override fun onShowFileChooser(
webView: WebView,
filePathCallback: ValueCallback<Array<Uri>>,
fileChooserParams: FileChooserParams,
): Boolean {
return getEventProcessor().onShowFileChooser(webView, filePathCallback, fileChooserParams)
}
}
isHorizontalScrollBarEnabled = false
requestDisallowInterceptTouchEvent(true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ import android.content.Context
import android.content.Intent
import android.net.Uri
import android.webkit.PermissionRequest
import android.webkit.ValueCallback
import android.webkit.WebChromeClient
import android.webkit.WebView
import com.shopify.checkoutsheetkit.lifecycleevents.CheckoutCompletedEvent
import com.shopify.checkoutsheetkit.pixelevents.PixelEvent

Expand Down Expand Up @@ -69,6 +72,16 @@ public interface CheckoutEventProcessor {
* and processed
*/
public fun onWebPixelEvent(event: PixelEvent)

/**
* Called when the client should show a file chooser. This is called to handle HTML forms with 'file' input type, in response to the
* user pressing the "Select File" button. To cancel the request, call filePathCallback.onReceiveValue(null) and return true.
*/
public fun onShowFileChooser(
webView: WebView,
filePathCallback: ValueCallback<Array<Uri>>,
fileChooserParams: WebChromeClient.FileChooserParams,
): Boolean
}

internal class NoopEventProcessor : CheckoutEventProcessor {
Expand All @@ -87,6 +100,14 @@ internal class NoopEventProcessor : CheckoutEventProcessor {
override fun onWebPixelEvent(event: PixelEvent) {/* noop */
}

override fun onShowFileChooser(
webView: WebView,
filePathCallback: ValueCallback<Array<Uri>>,
fileChooserParams: WebChromeClient.FileChooserParams,
): Boolean {
return false
}

override fun onPermissionRequest(permissionRequest: PermissionRequest) {/* noop */
}
}
Expand Down Expand Up @@ -118,6 +139,14 @@ public abstract class DefaultCheckoutEventProcessor @JvmOverloads constructor(
// no-op override to implement
}

override fun onShowFileChooser(
webView: WebView,
filePathCallback: ValueCallback<Array<Uri>>,
fileChooserParams: WebChromeClient.FileChooserParams,
): Boolean {
return false
}

private fun Context.launchEmailApp(to: String) {
val intent = Intent(Intent.ACTION_SEND)
intent.type = "vnd.android.cursor.item/email"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ import android.os.Looper
import android.view.View.INVISIBLE
import android.view.View.VISIBLE
import android.webkit.PermissionRequest
import android.webkit.ValueCallback
import android.webkit.WebChromeClient.FileChooserParams
import android.webkit.WebView
import com.shopify.checkoutsheetkit.lifecycleevents.CheckoutCompletedEvent
import com.shopify.checkoutsheetkit.pixelevents.PixelEvent

Expand Down Expand Up @@ -63,6 +66,14 @@ internal class CheckoutWebViewEventProcessor(
}
}

fun onShowFileChooser(
webView: WebView,
filePathCallback: ValueCallback<Array<Uri>>,
fileChooserParams: FileChooserParams,
): Boolean {
return eventProcessor.onShowFileChooser(webView, filePathCallback, fileChooserParams)
}

fun onPermissionRequest(permissionRequest: PermissionRequest) {
onMainThread {
eventProcessor.onPermissionRequest(permissionRequest)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,13 @@
package com.shopify.checkoutsheetkit

import android.graphics.Color
import android.net.Uri
import android.os.Looper
import android.view.View.VISIBLE
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.webkit.PermissionRequest
import android.webkit.ValueCallback
import android.webkit.WebChromeClient.FileChooserParams
import androidx.activity.ComponentActivity
import org.assertj.core.api.Assertions.assertThat
import org.junit.After
Expand Down Expand Up @@ -254,6 +257,21 @@ class CheckoutWebViewTest {
verify(webViewEventProcessor).onPermissionRequest(permissionRequest)
}

@Test
fun `calls processors onShowFileChooser when called on webChromeClient`() {
val view = CheckoutWebView.cacheableCheckoutView(URL, activity)
val webViewEventProcessor = mock<CheckoutWebViewEventProcessor>()
view.setEventProcessor(webViewEventProcessor)

val shadow = shadowOf(view)
val filePathCallback = mock<ValueCallback<Array<Uri>>>()
val fileChooserParams = mock<FileChooserParams>()

shadow.webChromeClient.onShowFileChooser(view, filePathCallback, fileChooserParams)

verify(webViewEventProcessor).onShowFileChooser(view, filePathCallback, fileChooserParams)
}

@Test
fun `should recover from errors`() {
Robolectric.buildActivity(ComponentActivity::class.java).use { activityController ->
Expand Down
2 changes: 1 addition & 1 deletion samples/MobileBuyIntegration/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ android {
applicationId "com.shopify.checkout_sdk_mobile_buy_integration_sample"
minSdk 23
targetSdk 34
versionCode 32
versionCode 33
versionName "0.0.${versionCode}"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
Expand Down
17 changes: 16 additions & 1 deletion samples/MobileBuyIntegration/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,13 @@
<uses-feature android:name="android.hardware.microphone" android:required="false"/>

<uses-permission android:name="android.permission.INTERNET" />

<!-- Optional: Example permissions required for certain payment methods provided via 3rd party apps -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" />

<application
android:name=".MobileBuyIntegration"
android:allowBackup="true"
Expand Down Expand Up @@ -35,5 +40,15 @@
android:name="android.app.lib_name"
android:value="" />
</activity>

<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* MIT License
*
* Copyright 2023-present, Shopify Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

package com.shopify.checkout_sdk_mobile_buy_integration_sample

import android.app.Activity.RESULT_OK
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Environment
import android.provider.MediaStore
import android.webkit.WebChromeClient.FileChooserParams
import androidx.activity.result.contract.ActivityResultContract
import androidx.core.content.FileProvider
import java.io.File
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale

/**
* For handling 3p apps that require a FileChooser / Camera in response to onShowFileChooser()
*/
class FileChooserResultContract : ActivityResultContract<FileChooserParams, Uri?>() {
private var cameraImageUri: Uri? = null

override fun createIntent(context: Context, input: FileChooserParams): Intent {
val fileChooserIntent = input.createIntent()
fileChooserIntent.addCategory(Intent.CATEGORY_OPENABLE)
var mimeType = if (input.acceptTypes == null) DEFAULT_MIME_TYPE else input.acceptTypes[0]
if (!ACCEPTABLE_MIME_TYPES.contains(mimeType)) {
mimeType = DEFAULT_MIME_TYPE
}
fileChooserIntent.setType(mimeType)

val photoFile = createImageFile(context)
cameraImageUri = FileProvider.getUriForFile(context, "${context.packageName}.provider", photoFile)
val cameraIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply {
putExtra(MediaStore.EXTRA_OUTPUT, cameraImageUri)
}

val chooserIntent = Intent.createChooser(fileChooserIntent, context.getText(R.string.filechooser_title))
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, arrayOf(cameraIntent))
return chooserIntent
}

override fun parseResult(resultCode: Int, intent: Intent?) : Uri? {
if (resultCode != RESULT_OK) {
return null
}

return intent?.data ?: cameraImageUri
}

private fun createImageFile(context: Context): File {
val timeStamp = SimpleDateFormat(DATE_FORMAT_PATTERN, Locale.US).format(Date())
val storageDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
return File.createTempFile("$IMG_FILE_PREFIX${timeStamp}_", IMG_FILE_SUFFIX, storageDir)
}

companion object {
private val ACCEPTABLE_MIME_TYPES = arrayListOf("image/*", "video/*")
private const val DEFAULT_MIME_TYPE = "*/*"
private const val DATE_FORMAT_PATTERN = "yyyyMMdd_HHmmss"
private const val IMG_FILE_PREFIX = "JPEG_"
private const val IMG_FILE_SUFFIX = ".jpg"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,59 @@
*/
package com.shopify.checkout_sdk_mobile_buy_integration_sample

import android.Manifest
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Bundle
import android.webkit.ValueCallback
import android.webkit.WebChromeClient.FileChooserParams
import android.webkit.WebView.setWebContentsDebuggingEnabled
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat

class MainActivity : ComponentActivity() {

private lateinit var requestPermissionLauncher: ActivityResultLauncher<String>
private lateinit var showFileChooserLauncher: ActivityResultLauncher<FileChooserParams>

private var filePathCallback: ValueCallback<Array<Uri>>? = null
private var fileChooserParams: FileChooserParams? = null

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setWebContentsDebuggingEnabled(true)
setContent {
CheckoutSdkApp()
}
requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
val fileChooserParams = this.fileChooserParams
if (isGranted && fileChooserParams != null) {
this.showFileChooserLauncher.launch(fileChooserParams)
this.fileChooserParams = null
}
// N.B. a file chooser intent (without camera) could be launched here if the permission was denied
}
showFileChooserLauncher = registerForActivityResult(FileChooserResultContract()) { uri: Uri? ->
filePathCallback?.onReceiveValue(if (uri != null) arrayOf(uri) else null)
filePathCallback = null
}
}

// Show a file chooser when prompted by the event processor
fun onShowFileChooser(filePathCallback: ValueCallback<Array<Uri>>, fileChooserParams: FileChooserParams): Boolean {
this.filePathCallback = filePathCallback
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
// Permissions not yet granted, request before launching chooser
this.fileChooserParams = fileChooserParams
requestPermissionLauncher.launch(Manifest.permission.CAMERA)
} else {
// Permissions already granted, launch chooser
showFileChooserLauncher.launch(fileChooserParams)
this.fileChooserParams = null
}
return true
}
}
Loading

0 comments on commit 7091edc

Please sign in to comment.