diff --git a/.gitignore b/.gitignore index d6679aa..55629f2 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ build/ *.apk *.pem -.kotlin \ No newline at end of file +.kotlin +kotlin-js-store/ \ No newline at end of file diff --git a/build.gradle b/build.gradle index 57f8631..8b8c809 100644 --- a/build.gradle +++ b/build.gradle @@ -16,6 +16,12 @@ plugins { alias(libs.plugins.nexusPublishingPlugin) + alias(libs.plugins.kotlin.multiplatform) apply false + alias(libs.plugins.androidLibrary) apply false + alias(libs.plugins.compose.compiler) apply false + alias(libs.plugins.compose.multiplatform) apply false + alias(libs.plugins.kotlin.android) apply false + alias(libs.plugins.androidApplication) apply false } apply from: "${rootDir}/scripts/publish-root.gradle" \ No newline at end of file diff --git a/compose-pay-button/build.gradle b/compose-pay-button/build.gradle index e1a4037..5d5df12 100644 --- a/compose-pay-button/build.gradle +++ b/compose-pay-button/build.gradle @@ -15,9 +15,10 @@ */ plugins { + alias(libs.plugins.kotlin.multiplatform) alias(libs.plugins.androidLibrary) - alias(libs.plugins.kotlin.android) alias(libs.plugins.compose.compiler) + alias(libs.plugins.compose.multiplatform) } ext { @@ -50,9 +51,6 @@ android { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } - kotlinOptions { - jvmTarget = '1.8' - } buildFeatures { compose true @@ -71,10 +69,50 @@ android { targetSdk 37 } } +kotlin { + androidTarget { + publishLibraryVariants("release") + } + js(IR) { + browser() + } + wasmJs { + browser() + } -dependencies { - implementation libs.playServices.wallet - implementation libs.compose.ui - implementation libs.compose.material - implementation libs.androidx.core + sourceSets { + commonMain { + dependencies { + implementation compose.runtime + implementation compose.ui + implementation compose.foundation + implementation compose.material + implementation compose.components.resources + } + } + androidMain { + dependencies { + implementation libs.playServices.wallet + implementation libs.androidx.core + } + } + jsMain { + dependencies { + implementation compose.ui + } + } + wasmJsMain { + dependencies { + implementation compose.ui + } + } + } +} + +tasks.register("printWasmJsClasspath") { + doLast { + configurations.getByName("wasmJsCompileClasspath").files.forEach { + println(it.absolutePath) + } + } } \ No newline at end of file diff --git a/compose-pay-button/src/main/java/com/google/pay/button/PayButton.kt b/compose-pay-button/src/androidMain/kotlin/com/google/pay/button/PayButton.kt similarity index 70% rename from compose-pay-button/src/main/java/com/google/pay/button/PayButton.kt rename to compose-pay-button/src/androidMain/kotlin/com/google/pay/button/PayButton.kt index f975490..1e5842c 100644 --- a/compose-pay-button/src/main/java/com/google/pay/button/PayButton.kt +++ b/compose-pay-button/src/androidMain/kotlin/com/google/pay/button/PayButton.kt @@ -24,46 +24,44 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import com.google.android.gms.wallet.button.ButtonConstants import com.google.android.gms.wallet.button.ButtonOptions import com.google.android.gms.wallet.button.PayButton as GmsPayButton -enum class ButtonTheme(val value: Int) { - Dark(ButtonConstants.ButtonTheme.DARK), - Light(ButtonConstants.ButtonTheme.LIGHT), -} +private const val FULL_ALPHA = 1f +private const val HALF_ALPHA = 0.5f -enum class ButtonType(val value: Int) { - Book(ButtonConstants.ButtonType.BOOK), - Buy(ButtonConstants.ButtonType.BUY), - Checkout(ButtonConstants.ButtonType.CHECKOUT), - Donate(ButtonConstants.ButtonType.DONATE), - Order(ButtonConstants.ButtonType.ORDER), - Pay(ButtonConstants.ButtonType.PAY), - Plain(ButtonConstants.ButtonType.PLAIN), - Subscribe(ButtonConstants.ButtonType.SUBSCRIBE), - PIX(ButtonConstants.ButtonType.PIX), - EWALLET(ButtonConstants.ButtonType.EWALLET) +private fun ButtonTheme.toAndroidValue(): Int = when (this) { + ButtonTheme.Dark -> ButtonConstants.ButtonTheme.DARK + ButtonTheme.Light -> ButtonConstants.ButtonTheme.LIGHT } -private const val FULL_ALPHA = 1f -private const val HALF_ALPHA = 0.5f +private fun ButtonType.toAndroidValue(): Int = when (this) { + ButtonType.Book -> ButtonConstants.ButtonType.BOOK + ButtonType.Buy -> ButtonConstants.ButtonType.BUY + ButtonType.Checkout -> ButtonConstants.ButtonType.CHECKOUT + ButtonType.Donate -> ButtonConstants.ButtonType.DONATE + ButtonType.Order -> ButtonConstants.ButtonType.ORDER + ButtonType.Pay -> ButtonConstants.ButtonType.PAY + ButtonType.Plain -> ButtonConstants.ButtonType.PLAIN + ButtonType.Subscribe -> ButtonConstants.ButtonType.SUBSCRIBE + ButtonType.PIX -> ButtonConstants.ButtonType.PIX + ButtonType.EWALLET -> ButtonConstants.ButtonType.EWALLET +} @Composable -fun PayButton( +actual fun PayButton( onClick: () -> Unit, allowedPaymentMethods: String, - modifier: Modifier = Modifier, - theme: ButtonTheme = ButtonTheme.Dark, - type: ButtonType = ButtonType.Buy, - radius: Dp = 100.dp, - enabled: Boolean = true, - onError: (Throwable) -> Unit = {}, - fallbackUi: @Composable (() -> Unit)? = null, + modifier: Modifier, + theme: ButtonTheme, + type: ButtonType, + radius: Dp, + enabled: Boolean, + onError: (Throwable) -> Unit, + fallbackUi: @Composable (() -> Unit)?, ) { - var showFallback by remember { mutableStateOf(false) } val radiusPixelValue = with(LocalDensity.current) { radius.toPx().toInt() } @@ -76,8 +74,8 @@ fun PayButton( kotlin.runCatching { this.initialize( ButtonOptions.newBuilder() - .setButtonTheme(theme.value) - .setButtonType(type.value) + .setButtonTheme(theme.toAndroidValue()) + .setButtonType(type.toAndroidValue()) .setCornerRadius(radiusPixelValue) .setAllowedPaymentMethods(allowedPaymentMethods) .build() diff --git a/compose-pay-button/src/commonMain/kotlin/com/google/pay/button/PayButton.kt b/compose-pay-button/src/commonMain/kotlin/com/google/pay/button/PayButton.kt new file mode 100644 index 0000000..524dcb4 --- /dev/null +++ b/compose-pay-button/src/commonMain/kotlin/com/google/pay/button/PayButton.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2023 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.pay.button + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +enum class ButtonTheme { + Dark, + Light, +} + +enum class ButtonType { + Book, + Buy, + Checkout, + Donate, + Order, + Pay, + Plain, + Subscribe, + PIX, + EWALLET +} + +@Composable +expect fun PayButton( + onClick: () -> Unit, + allowedPaymentMethods: String, + modifier: Modifier = Modifier, + theme: ButtonTheme = ButtonTheme.Dark, + type: ButtonType = ButtonType.Buy, + radius: Dp = 100.dp, + enabled: Boolean = true, + onError: (Throwable) -> Unit = {}, + fallbackUi: @Composable (() -> Unit)? = null, +) diff --git a/compose-pay-button/src/jsMain/kotlin/com/google/pay/button/PayButton.kt b/compose-pay-button/src/jsMain/kotlin/com/google/pay/button/PayButton.kt new file mode 100644 index 0000000..5ca7086 --- /dev/null +++ b/compose-pay-button/src/jsMain/kotlin/com/google/pay/button/PayButton.kt @@ -0,0 +1,171 @@ +/* + * Copyright 2023 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.pay.button + +import androidx.compose.foundation.layout.Box +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.layout.positionInWindow +import androidx.compose.ui.unit.Dp +import kotlinx.browser.document +import org.w3c.dom.HTMLDivElement +import org.w3c.dom.HTMLScriptElement + +private var isScriptLoading = false +private var isScriptLoaded = false +private val callbacks = mutableListOf<() -> Unit>() + +private fun loadScript(onLoad: () -> Unit, onError: (Throwable) -> Unit) { + if (isScriptLoaded) { + onLoad() + return + } + callbacks.add(onLoad) + if (isScriptLoading) return + isScriptLoading = true + + val script = document.createElement("script") as HTMLScriptElement + script.src = "https://pay.google.com/gp/p/js/pay.js" + script.async = true + script.onload = { + isScriptLoaded = true + isScriptLoading = false + callbacks.forEach { it() } + callbacks.clear() + } + script.onerror = { _, _, _, _, _ -> + isScriptLoading = false + onError(Exception("Failed to load Google Pay SDK")) + } + document.head?.appendChild(script) +} + +@Composable +actual fun PayButton( + onClick: () -> Unit, + allowedPaymentMethods: String, + modifier: Modifier, + theme: ButtonTheme, + type: ButtonType, + radius: Dp, + enabled: Boolean, + onError: (Throwable) -> Unit, + fallbackUi: @Composable (() -> Unit)?, +) { + var sdkState by remember { mutableStateOf(SdkState.Loading) } + val buttonContainer = remember { document.createElement("div") as HTMLDivElement } + + DisposableEffect(Unit) { + buttonContainer.style.position = "absolute" + buttonContainer.style.zIndex = "1000" + buttonContainer.style.display = "none" + document.body?.appendChild(buttonContainer) + + loadScript( + onLoad = { + sdkState = SdkState.Ready + }, + onError = { + sdkState = SdkState.Failed(it) + onError(it) + } + ) + + onDispose { + buttonContainer.remove() + } + } + + DisposableEffect(enabled) { + buttonContainer.style.opacity = if (enabled) "1.0" else "0.5" + buttonContainer.style.setProperty("pointer-events", if (enabled) "auto" else "none") + onDispose {} + } + + when (val state = sdkState) { + SdkState.Loading -> { + // Render nothing while loading + } + is SdkState.Failed -> { + fallbackUi?.invoke() + } + SdkState.Ready -> { + DisposableEffect(theme, type, radius, allowedPaymentMethods) { + buttonContainer.innerHTML = "" + + val buttonColor = when (theme) { + ButtonTheme.Dark -> "black" + ButtonTheme.Light -> "white" + } + val buttonType = when (type) { + ButtonType.Book -> "book" + ButtonType.Buy -> "buy" + ButtonType.Checkout -> "checkout" + ButtonType.Donate -> "donate" + ButtonType.Order -> "order" + ButtonType.Pay -> "pay" + ButtonType.Plain -> "plain" + ButtonType.Subscribe -> "subscribe" + else -> "buy" + } + + val radiusPx = radius.value.toString() + "px" + + val paymentsClient = js("new google.payments.api.PaymentsClient({environment: 'TEST'})") + val allowedPaymentMethodsParsed = js("JSON.parse(allowedPaymentMethods)") + val button = paymentsClient.createButton( + js("({ onClick: onClick, allowedPaymentMethods: allowedPaymentMethodsParsed, buttonColor: buttonColor, buttonType: buttonType, buttonSizeMode: 'fill' })") + ) + + val buttonElement = button.unsafeCast() + buttonElement.style.borderRadius = radiusPx + buttonElement.style.width = "100%" + buttonElement.style.height = "100%" + + buttonContainer.appendChild(buttonElement) + onDispose {} + } + + Box( + modifier = modifier + .onGloballyPositioned { coordinates -> + val position = coordinates.positionInWindow() + val size = coordinates.size + val density = kotlinx.browser.window.devicePixelRatio + + buttonContainer.style.left = "${position.x / density}px" + buttonContainer.style.top = "${position.y / density}px" + buttonContainer.style.width = "${size.width / density}px" + buttonContainer.style.height = "${size.height / density}px" + buttonContainer.style.display = "block" + } + ) + } + } +} + +private sealed interface SdkState { + object Loading : SdkState + class Failed(val error: Throwable) : SdkState + object Ready : SdkState +} diff --git a/compose-pay-button/src/wasmJsMain/kotlin/com/google/pay/button/PayButton.kt b/compose-pay-button/src/wasmJsMain/kotlin/com/google/pay/button/PayButton.kt new file mode 100644 index 0000000..f9e7ac1 --- /dev/null +++ b/compose-pay-button/src/wasmJsMain/kotlin/com/google/pay/button/PayButton.kt @@ -0,0 +1,196 @@ +/* + * Copyright 2023 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(kotlin.js.ExperimentalWasmJsInterop::class) + +package com.google.pay.button + +import androidx.compose.foundation.layout.Box +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.layout.positionInWindow +import androidx.compose.ui.unit.Dp +import kotlinx.browser.document +import org.w3c.dom.HTMLDivElement +import org.w3c.dom.HTMLScriptElement + +private var isScriptLoading = false +private var isScriptLoaded = false +private val callbacks = mutableListOf<() -> Unit>() + +private fun loadScript(onLoad: () -> Unit, onError: (Throwable) -> Unit) { + if (isScriptLoaded) { + onLoad() + return + } + callbacks.add(onLoad) + if (isScriptLoading) return + isScriptLoading = true + + val script = document.createElement("script") as HTMLScriptElement + script.src = "https://pay.google.com/gp/p/js/pay.js" + script.async = true + + script.addEventListener("load", { + isScriptLoaded = true + isScriptLoading = false + callbacks.forEach { it() } + callbacks.clear() + }) + + script.addEventListener("error", { + isScriptLoading = false + onError(Exception("Failed to load Google Pay SDK")) + }) + + document.head?.appendChild(script) +} + +@JsFun("""(onClick, allowedPaymentMethodsJson, buttonColor, buttonType, radiusPx) => { + const paymentsClient = new google.payments.api.PaymentsClient({environment: 'TEST'}); + const allowedPaymentMethods = JSON.parse(allowedPaymentMethodsJson); + const button = paymentsClient.createButton({ + onClick: onClick, + allowedPaymentMethods: allowedPaymentMethods, + buttonColor: buttonColor, + buttonType: buttonType, + buttonSizeMode: 'fill' + }); + button.style.borderRadius = radiusPx; + button.style.width = '100%'; + button.style.height = '100%'; + return button; +}""") +private external fun createGooglePayButton( + onClick: () -> Unit, + allowedPaymentMethodsJson: String, + buttonColor: String, + buttonType: String, + radiusPx: String +): org.w3c.dom.Element + +@Composable +actual fun PayButton( + onClick: () -> Unit, + allowedPaymentMethods: String, + modifier: Modifier, + theme: ButtonTheme, + type: ButtonType, + radius: Dp, + enabled: Boolean, + onError: (Throwable) -> Unit, + fallbackUi: @Composable (() -> Unit)?, +) { + var sdkState by remember { mutableStateOf(SdkState.Loading) } + val buttonContainer = remember { document.createElement("div") as HTMLDivElement } + + DisposableEffect(Unit) { + buttonContainer.style.position = "absolute" + buttonContainer.style.zIndex = "1000" + buttonContainer.style.display = "none" + document.body?.appendChild(buttonContainer) + + loadScript( + onLoad = { + sdkState = SdkState.Ready + }, + onError = { + sdkState = SdkState.Failed(it) + onError(it) + } + ) + + onDispose { + buttonContainer.remove() + } + } + + DisposableEffect(enabled) { + buttonContainer.style.opacity = if (enabled) "1.0" else "0.5" + buttonContainer.style.setProperty("pointer-events", if (enabled) "auto" else "none") + onDispose {} + } + + when (val state = sdkState) { + SdkState.Loading -> { + // Render nothing while loading + } + is SdkState.Failed -> { + fallbackUi?.invoke() + } + SdkState.Ready -> { + DisposableEffect(theme, type, radius, allowedPaymentMethods) { + buttonContainer.innerHTML = "" + + val buttonColor = when (theme) { + ButtonTheme.Dark -> "black" + ButtonTheme.Light -> "white" + } + val buttonType = when (type) { + ButtonType.Book -> "book" + ButtonType.Buy -> "buy" + ButtonType.Checkout -> "checkout" + ButtonType.Donate -> "donate" + ButtonType.Order -> "order" + ButtonType.Pay -> "pay" + ButtonType.Plain -> "plain" + ButtonType.Subscribe -> "subscribe" + else -> "buy" + } + + val radiusPx = radius.value.toString() + "px" + + val button = createGooglePayButton( + onClick = onClick, + allowedPaymentMethodsJson = allowedPaymentMethods, + buttonColor = buttonColor, + buttonType = buttonType, + radiusPx = radiusPx + ) + + buttonContainer.appendChild(button) + onDispose {} + } + + Box( + modifier = modifier + .onGloballyPositioned { coordinates -> + val position = coordinates.positionInWindow() + val size = coordinates.size + val density = kotlinx.browser.window.devicePixelRatio + + buttonContainer.style.left = "${position.x / density}px" + buttonContainer.style.top = "${position.y / density}px" + buttonContainer.style.width = "${size.width / density}px" + buttonContainer.style.height = "${size.height / density}px" + buttonContainer.style.display = "block" + } + ) + } + } +} + +private sealed interface SdkState { + object Loading : SdkState + class Failed(val error: Throwable) : SdkState + object Ready : SdkState +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 88e41d3..8edc882 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,4 +17,6 @@ nexusPublishingPlugin = "io.github.gradle-nexus.publish-plugin:2.0.0" androidApplication = "com.android.application:9.2.1" androidLibrary = "com.android.library:9.2.1" kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } -compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } \ No newline at end of file +kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } +compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +compose-multiplatform = { id = "org.jetbrains.compose", version = "1.7.3" } \ No newline at end of file diff --git a/scripts/publish-module.gradle b/scripts/publish-module.gradle index b6f033d..fd2d3c3 100644 --- a/scripts/publish-module.gradle +++ b/scripts/publish-module.gradle @@ -7,37 +7,69 @@ version = PUBLISH_VERSION afterEvaluate { publishing { publications { - release(MavenPublication) { - groupId PUBLISH_GROUP_ID - artifactId PUBLISH_ARTIFACT_ID - version PUBLISH_VERSION - - if (project.plugins.findPlugin("com.android.library")) { - from components.release - } else { - from components.java + if (project.plugins.hasPlugin("org.jetbrains.kotlin.multiplatform")) { + configureEach { publication -> + if (publication instanceof MavenPublication) { + publication.groupId = PUBLISH_GROUP_ID + publication.version = PUBLISH_VERSION + // artifactId is automatically set by KMP for target modules + publication.pom { + name = publication.name + description = 'Google Pay Button for Jetpack Compose' + url = 'https://github.com/google-pay/compose-pay-button' + licenses { + license { + name = 'Apache-2.0' + url = 'https://opensource.org/license/apache-2-0' + } + } + developers { + developer { + id = 'google' + name = 'Google' + } + } + scm { + connection = 'scm:git:github.com/google-pay/compose-pay-button.git' + developerConnection = 'scm:git:ssh://github.com/google-pay/compose-pay-button.git' + url = 'https://github.com/google-pay/compose-pay-button/tree/main' + } + } + } } + } else { + release(MavenPublication) { + groupId PUBLISH_GROUP_ID + artifactId PUBLISH_ARTIFACT_ID + version PUBLISH_VERSION - pom { - name = PUBLISH_ARTIFACT_ID - description = 'Google Pay Button for Jetpack Compose' - url = 'https://github.com/google-pay/compose-pay-button' - licenses { - license { - name = 'Apache-2.0' - url = 'https://opensource.org/license/apache-2-0' - } + if (project.plugins.findPlugin("com.android.library")) { + from components.release + } else { + from components.java } - developers { - developer { - id = 'google' - name = 'Google' + + pom { + name = PUBLISH_ARTIFACT_ID + description = 'Google Pay Button for Jetpack Compose' + url = 'https://github.com/google-pay/compose-pay-button' + licenses { + license { + name = 'Apache-2.0' + url = 'https://opensource.org/license/apache-2-0' + } + } + developers { + developer { + id = 'google' + name = 'Google' + } + } + scm { + connection = 'scm:git:github.com/google-pay/compose-pay-button.git' + developerConnection = 'scm:git:ssh://github.com/google-pay/compose-pay-button.git' + url = 'https://github.com/google-pay/compose-pay-button/tree/main' } - } - scm { - connection = 'scm:git:github.com/google-pay/compose-pay-button.git' - developerConnection = 'scm:git:ssh://github.com/google-pay/compose-pay-button.git' - url = 'https://github.com/google-pay/compose-pay-button/tree/main' } } } diff --git a/settings.gradle b/settings.gradle index be577ab..37fde5a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -22,12 +22,13 @@ pluginManagement { } } dependencyResolutionManagement { - repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { google() mavenCentral() } } -rootProject.name = "Compose Pay Button" +rootProject.name = "compose-pay-button-root" include ':app' include ':compose-pay-button' +include ':web-app' + diff --git a/web-app/build.gradle b/web-app/build.gradle new file mode 100644 index 0000000..079af76 --- /dev/null +++ b/web-app/build.gradle @@ -0,0 +1,40 @@ +/* + * Copyright 2024 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.compose.compiler) + alias(libs.plugins.compose.multiplatform) +} + +kotlin { + wasmJs { + browser() + binaries.executable() + } + + sourceSets { + commonMain { + dependencies { + implementation compose.runtime + implementation compose.ui + implementation compose.foundation + implementation compose.material + implementation project(":compose-pay-button") + } + } + } +} diff --git a/web-app/src/commonMain/kotlin/App.kt b/web-app/src/commonMain/kotlin/App.kt new file mode 100644 index 0000000..3bbe64c --- /dev/null +++ b/web-app/src/commonMain/kotlin/App.kt @@ -0,0 +1,189 @@ +/* + * Copyright 2024 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Card +import androidx.compose.material.Divider +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.google.pay.button.ButtonTheme +import com.google.pay.button.ButtonType +import com.google.pay.button.PayButton + +@Composable +fun App() { + var clickCount by remember { mutableStateOf(0) } + var statusText by remember { mutableStateOf("Ready to test") } + + val allowedPaymentMethods = """[ + { + "type": "CARD", + "parameters": { + "allowedAuthMethods": ["PAN_ONLY", "CRYPTOGRAM_3DS"], + "allowedCardNetworks": ["AMEX", "DISCOVER", "INTERAC", "JCB", "MASTERCARD", "VISA"] + } + } + ]""" + + val backgroundBrush = Brush.verticalGradient( + colors = listOf(Color(0xFFF5F7FA), Color(0xFFE4E8F0)) + ) + + MaterialTheme { + Box( + modifier = Modifier + .fillMaxSize() + .background(backgroundBrush), + contentAlignment = Alignment.Center + ) { + Card( + modifier = Modifier + .width(400.dp) + .padding(24.dp), + shape = RoundedCornerShape(16.dp), + elevation = 8.dp + ) { + Column( + modifier = Modifier.padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "Order Summary", + fontSize = 20.sp, + fontWeight = FontWeight.Bold, + color = Color(0xFF1E293B) + ) + + Spacer(modifier = Modifier.height(24.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = "Compose Pay Button Library", + color = Color(0xFF64748B), + fontSize = 14.sp + ) + Text( + text = "$10.00", + fontWeight = FontWeight.SemiBold, + color = Color(0xFF1E293B) + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + Divider(color = Color(0xFFE2E8F0), thickness = 1.dp) + Spacer(modifier = Modifier.height(16.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Total Amount", + fontWeight = FontWeight.Bold, + fontSize = 16.sp, + color = Color(0xFF1E293B) + ) + Text( + text = "$10.00", + fontWeight = FontWeight.Bold, + fontSize = 20.sp, + color = Color(0xFF10B981) + ) + } + + Spacer(modifier = Modifier.height(32.dp)) + + PayButton( + onClick = { + clickCount++ + statusText = "Payment successful! Count: $clickCount" + }, + allowedPaymentMethods = allowedPaymentMethods, + modifier = Modifier + .fillMaxWidth() + .height(48.dp), + theme = ButtonTheme.Dark, + type = ButtonType.Buy, + radius = 8.dp, + enabled = true, + onError = { error -> + statusText = "Error: ${error.message}" + } + ) + + Spacer(modifier = Modifier.height(24.dp)) + + Box( + modifier = Modifier + .fillMaxWidth() + .background( + color = if (clickCount > 0) Color(0xFFECFDF5) else Color(0xFFF1F5F9), + shape = RoundedCornerShape(8.dp) + ) + .padding(12.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = statusText, + color = if (clickCount > 0) Color(0xFF047857) else Color(0xFF475569), + fontSize = 13.sp, + fontWeight = FontWeight.Medium, + textAlign = TextAlign.Center + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = "Note: Clicking the button in this demo will trigger the callback but will not open the Google Pay sheet.", + fontSize = 11.sp, + color = Color(0xFF94A3B8), + textAlign = TextAlign.Center, + lineHeight = 16.sp + ) + } + } + } + } +} diff --git a/web-app/src/wasmJsMain/kotlin/main.kt b/web-app/src/wasmJsMain/kotlin/main.kt new file mode 100644 index 0000000..ad22215 --- /dev/null +++ b/web-app/src/wasmJsMain/kotlin/main.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2024 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.window.ComposeViewport + +@OptIn(ExperimentalComposeUiApi::class) +fun main() { + ComposeViewport(viewportContainerId = "compose-app") { + App() + } +} diff --git a/web-app/src/wasmJsMain/resources/index.html b/web-app/src/wasmJsMain/resources/index.html new file mode 100644 index 0000000..2f35b1e --- /dev/null +++ b/web-app/src/wasmJsMain/resources/index.html @@ -0,0 +1,20 @@ + + + + + Google Pay Compose Multiplatform WasmJS Sample + + + + +
+ +