diff --git a/.editorconfig b/.editorconfig index 4c697b24..dbe658a6 100644 --- a/.editorconfig +++ b/.editorconfig @@ -4,7 +4,7 @@ root = true charset = utf-8 end_of_line = lf indent_style = space -indent_size = 4 +indent_size = 2 insert_final_newline = true trim_trailing_whitespace = true diff --git a/afterpay/build.gradle.kts b/afterpay/build.gradle.kts index ae9ed0f9..67180521 100644 --- a/afterpay/build.gradle.kts +++ b/afterpay/build.gradle.kts @@ -14,79 +14,79 @@ * limitations under the License. */ plugins { - alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.parcelize) - alias(libs.plugins.vanniktech.maven.publish) - alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.parcelize) + alias(libs.plugins.vanniktech.maven.publish) + alias(libs.plugins.kotlin.serialization) } java { - toolchain { - languageVersion.set(JavaLanguageVersion.of(libs.versions.java.get())) - } + toolchain { + languageVersion.set(JavaLanguageVersion.of(libs.versions.java.get())) + } } kotlin { - jvmToolchain { - languageVersion.set(JavaLanguageVersion.of(libs.versions.java.get())) - } + jvmToolchain { + languageVersion.set(JavaLanguageVersion.of(libs.versions.java.get())) + } } android { - compileSdk = libs.versions.compileSdkVersion.get().toInt() + compileSdk = libs.versions.compileSdkVersion.get().toInt() - defaultConfig { - minSdk = libs.versions.minSdk.get().toInt() - targetSdk = libs.versions.compileSdkVersion.get().toInt() + defaultConfig { + minSdk = libs.versions.minSdk.get().toInt() + targetSdk = libs.versions.compileSdkVersion.get().toInt() - val VERSION_NAME: String by project - buildConfigField("String", "AfterpayLibraryVersion", "\"$VERSION_NAME\"") + val VERSION_NAME: String by project + buildConfigField("String", "AfterpayLibraryVersion", "\"$VERSION_NAME\"") - consumerProguardFiles("consumer-rules.pro") - } + consumerProguardFiles("consumer-rules.pro") + } - buildFeatures { - buildConfig = true - } + buildFeatures { + buildConfig = true + } - buildTypes { - create("staging") { - initWith(getByName("debug")) - } + buildTypes { + create("staging") { + initWith(getByName("debug")) } + } - compileOptions { - isCoreLibraryDesugaringEnabled = true - } + compileOptions { + isCoreLibraryDesugaringEnabled = true + } - namespace = "com.afterpay.android" + namespace = "com.afterpay.android" } dependencies { - implementation(libs.kotlinCoroutinesAndroid) - implementation(libs.kotlinSerializationJson) - implementation(libs.kotlinCoroutinesJdk8) - implementation(libs.androidxLifecycleRuntimeKtx) + implementation(libs.kotlinCoroutinesAndroid) + implementation(libs.kotlinSerializationJson) + implementation(libs.kotlinCoroutinesJdk8) + implementation(libs.androidxLifecycleRuntimeKtx) - implementation(libs.androidxCoreKtx) - implementation(libs.androidxAppcompat) + implementation(libs.androidxCoreKtx) + implementation(libs.androidxAppcompat) - testImplementation(libs.junit) - coreLibraryDesugaring(libs.androidToolsDesugarJdk) - testImplementation(libs.kotlinCoroutinesTest) - testImplementation(libs.mockK) + testImplementation(libs.junit) + coreLibraryDesugaring(libs.androidToolsDesugarJdk) + testImplementation(libs.kotlinCoroutinesTest) + testImplementation(libs.mockK) } tasks.withType { - val version = project.version.toString() - onlyIf { !version.endsWith("SNAPSHOT") } + val version = project.version.toString() + onlyIf { !version.endsWith("SNAPSHOT") } } signing { - useInMemoryPgpKeys( - findProperty("signingKeyId").toString(), - findProperty("signingKey").toString(), - findProperty("signingPassword").toString(), - ) + useInMemoryPgpKeys( + findProperty("signingKeyId").toString(), + findProperty("signingKey").toString(), + findProperty("signingPassword").toString(), + ) } diff --git a/afterpay/src/main/kotlin/com/afterpay/android/Afterpay.kt b/afterpay/src/main/kotlin/com/afterpay/android/Afterpay.kt index 15fed300..464f94f4 100644 --- a/afterpay/src/main/kotlin/com/afterpay/android/Afterpay.kt +++ b/afterpay/src/main/kotlin/com/afterpay/android/Afterpay.kt @@ -70,507 +70,507 @@ import kotlin.Result.Companion.success import kotlin.properties.Delegates.observable object Afterpay { - internal var configuration by observable(initialValue = null) { _, old, new -> - if (new != old) { - ConfigurationObservable.configurationChanged(new) - } + internal var configuration by observable(initialValue = null) { _, old, new -> + if (new != old) { + ConfigurationObservable.configurationChanged(new) } - private set + } + private set - internal val locale: Locale - get() = configuration?.locale ?: Locales.EN_US + internal val locale: Locale + get() = configuration?.locale ?: Locales.EN_US - internal val brand: Brand - get() = Brand.forLocale(locale) + internal val brand: Brand + get() = Brand.forLocale(locale) - internal val language: Locale? - get() = getRegionLanguage(locale, configuration?.consumerLocale ?: Locale.getDefault()) + internal val language: Locale? + get() = getRegionLanguage(locale, configuration?.consumerLocale ?: Locale.getDefault()) - internal val enabled: Boolean - get() = language != null && configuration?.locale != null + internal val enabled: Boolean + get() = language != null && configuration?.locale != null - internal val strings: AfterpayString - get() = AfterpayString.forLocale() + internal val strings: AfterpayString + get() = AfterpayString.forLocale() - internal val drawables: AfterpayDrawable - get() = AfterpayDrawable.forLocale() + internal val drawables: AfterpayDrawable + get() = AfterpayDrawable.forLocale() - internal var checkoutV2Handler: AfterpayCheckoutV2Handler? = null - private set + internal var checkoutV2Handler: AfterpayCheckoutV2Handler? = null + private set - val environment: AfterpayEnvironment? - get() = configuration?.environment + val environment: AfterpayEnvironment? + get() = configuration?.environment - /** - * Returns an [Intent] for the given [context] and [checkoutUrl] that can be passed to - * [startActivityForResult][android.app.Activity.startActivityForResult] to initiate the - * Afterpay checkout. - */ - @JvmStatic - fun createCheckoutIntent(context: Context, checkoutUrl: String, loadRedirectUrls: Boolean = false): Intent { - val url = if (enabled) { - checkoutUrl - } else { - "LANGUAGE_NOT_SUPPORTED" - } - return Intent(context, AfterpayCheckoutActivity::class.java) - .putCheckoutUrlExtra(url) - .putCheckoutShouldLoadRedirectUrls(loadRedirectUrls) + /** + * Returns an [Intent] for the given [context] and [checkoutUrl] that can be passed to + * [startActivityForResult][android.app.Activity.startActivityForResult] to initiate the + * Afterpay checkout. + */ + @JvmStatic + fun createCheckoutIntent(context: Context, checkoutUrl: String, loadRedirectUrls: Boolean = false): Intent { + val url = if (enabled) { + checkoutUrl + } else { + "LANGUAGE_NOT_SUPPORTED" } - - /** - * Returns an [Intent] for the given [context] and [options] that can be passed to - * [startActivityForResult][android.app.Activity.startActivityForResult] to initiate the - * Afterpay checkout. - */ - @JvmStatic - fun createCheckoutV2Intent( - context: Context, - options: AfterpayCheckoutV2Options = AfterpayCheckoutV2Options(), - ): Intent = Intent(context, AfterpayCheckoutV2Activity::class.java) - .putCheckoutV2OptionsExtra(options) - - /** - * Signs an Afterpay Cash App order for the relevant [token] and calls - * calls [complete] when done. This method should be called prior to calling - * createCustomerRequest on the Cash App Pay Kit SDK - */ - @JvmStatic - @WorkerThread - suspend fun signCashAppOrderToken( - token: String, - complete: (CashAppSignOrderResult) -> Unit, - ) { - AfterpayCashAppCheckout.performSignPaymentRequest(token, complete) + return Intent(context, AfterpayCheckoutActivity::class.java) + .putCheckoutUrlExtra(url) + .putCheckoutShouldLoadRedirectUrls(loadRedirectUrls) + } + + /** + * Returns an [Intent] for the given [context] and [options] that can be passed to + * [startActivityForResult][android.app.Activity.startActivityForResult] to initiate the + * Afterpay checkout. + */ + @JvmStatic + fun createCheckoutV2Intent( + context: Context, + options: AfterpayCheckoutV2Options = AfterpayCheckoutV2Options(), + ): Intent = Intent(context, AfterpayCheckoutV2Activity::class.java) + .putCheckoutV2OptionsExtra(options) + + /** + * Signs an Afterpay Cash App order for the relevant [token] and calls + * calls [complete] when done. This method should be called prior to calling + * createCustomerRequest on the Cash App Pay Kit SDK + */ + @JvmStatic + @WorkerThread + suspend fun signCashAppOrderToken( + token: String, + complete: (CashAppSignOrderResult) -> Unit, + ) { + AfterpayCashAppCheckout.performSignPaymentRequest(token, complete) + } + + /** + * Async version of the [signCashAppOrderToken] method. + * + * Signs an Afterpay Cash App order for the relevant [token] and calls + * [complete] when done. This method should be called prior to calling + * createCustomerRequest on the Cash App Pay Kit SDK + */ + @DelicateCoroutinesApi + @JvmStatic + fun signCashAppOrderTokenAsync( + token: String, + complete: (CashAppSignOrderResult) -> Unit, + ): CompletableFuture { + return GlobalScope.future { + signCashAppOrderToken(token, complete) } - - /** - * Async version of the [signCashAppOrderToken] method. - * - * Signs an Afterpay Cash App order for the relevant [token] and calls - * [complete] when done. This method should be called prior to calling - * createCustomerRequest on the Cash App Pay Kit SDK - */ - @DelicateCoroutinesApi - @JvmStatic - fun signCashAppOrderTokenAsync( - token: String, - complete: (CashAppSignOrderResult) -> Unit, - ): CompletableFuture { - return GlobalScope.future { - signCashAppOrderToken(token, complete) - } + } + + /** + * Validates the Cash App order for the relevant [jwt], [customerId] and [grantId] + * and calls [complete] once finished. This method should be called for a One Time payment + * once the Cash App order is in the approved state + */ + @JvmStatic + @WorkerThread + fun validateCashAppOrder( + jwt: String, + customerId: String, + grantId: String, + complete: (CashAppValidationResponse) -> Unit, + ) { + AfterpayCashAppCheckout.validatePayment(jwt, customerId, grantId, complete) + } + + /** + * Async version of the [validateCashAppOrder] method. + * + * Validates the Cash App order for the relevant [jwt], [customerId] and [grantId] + * and calls [complete] once finished. This method should be called for a One Time payment + * once the Cash App order is in the approved state + */ + @DelicateCoroutinesApi + @JvmStatic + fun validateCashAppOrderAsync( + jwt: String, + customerId: String, + grantId: String, + complete: (CashAppValidationResponse) -> Unit, + ): CompletableFuture { + return GlobalScope.future { + validateCashAppOrder(jwt, customerId, grantId, complete) } - - /** - * Validates the Cash App order for the relevant [jwt], [customerId] and [grantId] - * and calls [complete] once finished. This method should be called for a One Time payment - * once the Cash App order is in the approved state - */ - @JvmStatic - @WorkerThread - fun validateCashAppOrder( - jwt: String, - customerId: String, - grantId: String, - complete: (CashAppValidationResponse) -> Unit, - ) { - AfterpayCashAppCheckout.validatePayment(jwt, customerId, grantId, complete) + } + + /** + * Returns the [token][String] parsed from the given [intent] returned by a successful + * Afterpay checkout. + */ + @JvmStatic + fun parseCheckoutSuccessResponse(intent: Intent): String? = + intent.getOrderTokenExtra() + + /** + * Returns the [status][CancellationStatus] parsed from the given [intent] returned by a + * cancelled Afterpay checkout. + */ + @JvmStatic + fun parseCheckoutCancellationResponse(intent: Intent): CancellationStatus? = + intent.getCancellationStatusExtra() + + /** + * Sets the global checkout configuration comprising the [minimum order amount][minimumAmount], + * [maximum order amount][maximumAmount], [currency code in ISO 4217 format][currencyCode], + * [locale] for formatting of terms and conditions and currency, and the [environment] in which + * to launch the checkout. + * + * Results in a [NumberFormatException] if an amount is not a valid representation of a number + * or an [IllegalArgumentException] if the currency is not a valid ISO 4217 currency code, if + * the minimum and maximum amount isn't correctly ordered, or if the locale is not supported. + * + * Must be called from main thread + */ + @JvmStatic + fun setConfiguration( + minimumAmount: String?, + maximumAmount: String, + currencyCode: String, + locale: Locale, + environment: AfterpayEnvironment, + consumerLocale: Locale? = null, + ) { + configuration = Configuration( + minimumAmount = minimumAmount?.toBigDecimal(), + maximumAmount = maximumAmount.toBigDecimal(), + currency = Currency.getInstance(currencyCode), + locale = locale.clone() as Locale, + environment = environment, + consumerLocale = consumerLocale, + ).also { validateConfiguration(it) } + } + + private fun validateConfiguration(configuration: Configuration) { + require(configuration.maximumAmount >= BigDecimal.ZERO) { "Maximum order amount is invalid" } + configuration.minimumAmount?.let { minimumAmount -> + require(minimumAmount > BigDecimal.ZERO && minimumAmount < configuration.maximumAmount) { + "Minimum order amount is invalid" + } } - - /** - * Async version of the [validateCashAppOrder] method. - * - * Validates the Cash App order for the relevant [jwt], [customerId] and [grantId] - * and calls [complete] once finished. This method should be called for a One Time payment - * once the Cash App order is in the approved state - */ - @DelicateCoroutinesApi - @JvmStatic - fun validateCashAppOrderAsync( - jwt: String, - customerId: String, - grantId: String, - complete: (CashAppValidationResponse) -> Unit, - ): CompletableFuture { - return GlobalScope.future { - validateCashAppOrder(jwt, customerId, grantId, complete) - } + require(Locales.validSet.contains(configuration.locale)) { + val validCountries = Locales.validSet.joinToString(",") { it.country } + "Locale contains an unsupported country: ${configuration.locale.country}. Supported countries include: $validCountries" + } + } + + /** + * Sets the global [handler] used to provide callbacks for the v2 checkout. + */ + @JvmStatic + fun setCheckoutV2Handler(handler: AfterpayCheckoutV2Handler?) { + checkoutV2Handler = handler + } + + // region: V3 + /** + * Sets the global checkout configuration object. + * + * Results in a [NumberFormatException] if an amount is not a valid representation of a number + * or an [IllegalArgumentException] if the currency is not a valid ISO 4217 currency code, if + * the minimum and maximum amount isn't correctly ordered, or if the locale is not supported. + */ + @JvmStatic + fun setConfigurationV3(newConfiguration: Configuration) { + configuration = newConfiguration.also { validateConfiguration(it) } + } + + private var checkoutV3Configuration: CheckoutV3Configuration? = null + + /** + * Sets the collection of options and values required to interact with the Afterpay API. + */ + @JvmStatic + fun setCheckoutV3Configuration(configuration: CheckoutV3Configuration) { + checkoutV3Configuration = configuration + } + + /** + * Start checkout process by requesting data to pass to Cash App Pay SDK + * + * @param consumer information about customer + * @param orderTotal pricing information about this order + * @param items list of items in customer's cart + * @param configuration must be provided if not previously set via [setCheckoutV3Configuration] + */ + suspend fun beginCheckoutV3WithCashAppPay( + consumer: CheckoutV3Consumer, + orderTotal: OrderTotal, + items: Array = arrayOf(), + configuration: CheckoutV3Configuration? = checkoutV3Configuration, + ): Result { + requireNotNull(configuration) { + "`configuration` must be set via `setCheckoutV3Configuration` or passed into this function" + } + val checkoutRequest = CheckoutV3.Request.create( + consumer = consumer, + orderTotal = orderTotal, + items = items, + configuration = configuration, + isCashAppPay = true, + ) + + val checkoutResponseResult = runCatching { + val checkoutUrl = configuration.v3CheckoutUrl + val checkoutPayload = requireNotNull(Json.encodeToString(checkoutRequest)) + return@runCatching withContext(Dispatchers.IO) { + ApiV3.request( + checkoutUrl, + ApiV3.HttpVerb.POST, + checkoutPayload, + ) + }.getOrThrow() } - /** - * Returns the [token][String] parsed from the given [intent] returned by a successful - * Afterpay checkout. - */ - @JvmStatic - fun parseCheckoutSuccessResponse(intent: Intent): String? = - intent.getOrderTokenExtra() - - /** - * Returns the [status][CancellationStatus] parsed from the given [intent] returned by a - * cancelled Afterpay checkout. - */ - @JvmStatic - fun parseCheckoutCancellationResponse(intent: Intent): CancellationStatus? = - intent.getCancellationStatusExtra() - - /** - * Sets the global checkout configuration comprising the [minimum order amount][minimumAmount], - * [maximum order amount][maximumAmount], [currency code in ISO 4217 format][currencyCode], - * [locale] for formatting of terms and conditions and currency, and the [environment] in which - * to launch the checkout. - * - * Results in a [NumberFormatException] if an amount is not a valid representation of a number - * or an [IllegalArgumentException] if the currency is not a valid ISO 4217 currency code, if - * the minimum and maximum amount isn't correctly ordered, or if the locale is not supported. - * - * Must be called from main thread - */ - @JvmStatic - fun setConfiguration( - minimumAmount: String?, - maximumAmount: String, - currencyCode: String, - locale: Locale, - environment: AfterpayEnvironment, - consumerLocale: Locale? = null, - ) { - configuration = Configuration( - minimumAmount = minimumAmount?.toBigDecimal(), - maximumAmount = maximumAmount.toBigDecimal(), - currency = Currency.getInstance(currencyCode), - locale = locale.clone() as Locale, - environment = environment, - consumerLocale = consumerLocale, - ).also { validateConfiguration(it) } + // requesting checkout data failed + checkoutResponseResult.onFailure { error: Throwable -> + return failure(error) } - private fun validateConfiguration(configuration: Configuration) { - require(configuration.maximumAmount >= BigDecimal.ZERO) { "Maximum order amount is invalid" } - configuration.minimumAmount?.let { minimumAmount -> - require(minimumAmount > BigDecimal.ZERO && minimumAmount < configuration.maximumAmount) { - "Minimum order amount is invalid" + // requesting checkout data success: sign the checkout response token + checkoutResponseResult.onSuccess { result: CheckoutV3.Response -> + AfterpayCashAppCheckout.performSignPaymentRequest(result.token) + .let { cashAppSignOrderResult: CashAppSignOrderResult -> + return when (cashAppSignOrderResult) { + // signing failed + is Failure -> failure(cashAppSignOrderResult.error) + + // signing success + is Success -> { + // map Result to Result + success( + CheckoutV3CashAppPay( + token = result.token, + singleUseCardToken = result.singleUseCardToken, + amount = cashAppSignOrderResult.response.amount, + redirectUri = cashAppSignOrderResult.response.redirectUri, + merchantId = cashAppSignOrderResult.response.merchantId, + brandId = cashAppSignOrderResult.response.brandId, + jwt = cashAppSignOrderResult.response.jwt, + ), + ) } - } - require(Locales.validSet.contains(configuration.locale)) { - val validCountries = Locales.validSet.joinToString(",") { it.country } - "Locale contains an unsupported country: ${configuration.locale.country}. Supported countries include: $validCountries" + } } } - /** - * Sets the global [handler] used to provide callbacks for the v2 checkout. - */ - @JvmStatic - fun setCheckoutV2Handler(handler: AfterpayCheckoutV2Handler?) { - checkoutV2Handler = handler + // should never happen, compiler doesn't know success and failure are only options + throw IllegalStateException() + } + + /** + * Confirm that checkout was completed with Cash App Pay SDK + * + * @param customerId customer ID, received from Cash App Pay SDK + * @param grantId grant ID received from Cash App Pay SDK + * @param token token received from [beginCheckoutV3WithCashAppPay] + * @param singleUseCardToken singleUseCardToken received from [beginCheckoutV3WithCashAppPay] + * @param jwt JSON web token received from [beginCheckoutV3WithCashAppPay] + * @param configuration must be provided if not previously set via [setCheckoutV3Configuration] + */ + suspend fun confirmCheckoutV3WithCashAppPay( + customerId: String, + grantId: String, + token: String, + singleUseCardToken: String, + jwt: String, + configuration: CheckoutV3Configuration? = checkoutV3Configuration, + ): Result { + return runCatching { + requireNotNull(configuration) { + "`configuration` must be set via `setCheckoutV3Configuration` or passed into this function" + } + + val confirmUrl = configuration.v3CheckoutConfirmationUrl + + val request = CashAppPayRequest( + token = token, + singleUseCardToken = singleUseCardToken, + cashAppPspInfo = CashAppPayRequest.CashAppPspInfo( + externalCustomerId = customerId, + externalGrantId = grantId, + jwt = jwt, + ), + ) + + val response = withContext(Dispatchers.IO) { + ApiV3.request( + url = confirmUrl, + method = ApiV3.HttpVerb.POST, + body = request, + ) + }.getOrThrow() + + CheckoutV3Data( + cardDetails = response.paymentDetails.virtualCard + ?: response.paymentDetails.virtualCardToken!!, + cardValidUntilInternal = response.cardValidUntil, + tokens = CheckoutV3Tokens( + token = token, + singleUseCardToken = singleUseCardToken, + ppaConfirmToken = "", // this token is not used in Cash App flow + ), + ) } - - // region: V3 - /** - * Sets the global checkout configuration object. - * - * Results in a [NumberFormatException] if an amount is not a valid representation of a number - * or an [IllegalArgumentException] if the currency is not a valid ISO 4217 currency code, if - * the minimum and maximum amount isn't correctly ordered, or if the locale is not supported. - */ - @JvmStatic - fun setConfigurationV3(newConfiguration: Configuration) { - configuration = newConfiguration.also { validateConfiguration(it) } + } + + /** + * Returns an [Intent] for the given [context] and options that can be passed to + * [startActivityForResult][android.app.Activity.startActivityForResult] to initiate the + * Afterpay checkout. + */ + @JvmStatic + @JvmOverloads + fun createCheckoutV3Intent( + context: Context, + consumer: CheckoutV3Consumer, + orderTotal: OrderTotal, + items: Array = arrayOf(), + buyNow: Boolean, + configuration: CheckoutV3Configuration? = checkoutV3Configuration, + ): Intent { + requireNotNull(configuration) { + "`configuration` must be set via `setCheckoutV3Configuration` or passed into this function" } - - private var checkoutV3Configuration: CheckoutV3Configuration? = null - - /** - * Sets the collection of options and values required to interact with the Afterpay API. - */ - @JvmStatic - fun setCheckoutV3Configuration(configuration: CheckoutV3Configuration) { - checkoutV3Configuration = configuration + val checkoutRequest = CheckoutV3.Request.create( + consumer = consumer, + orderTotal = orderTotal, + items = items, + configuration = configuration, + isCashAppPay = false, + ) + val options = AfterpayCheckoutV3Options( + buyNow = buyNow, + checkoutPayload = Json.encodeToString(checkoutRequest), + checkoutUrl = configuration.v3CheckoutUrl, + confirmUrl = configuration.v3CheckoutConfirmationUrl, + ) + + return Intent(context, AfterpayCheckoutV3Activity::class.java) + .putCheckoutV3OptionsExtra(options) + } + + /** + * Updates the [merchantReference] corresponding to the checkout represented by the provided [tokens]. + */ + @JvmSynthetic + fun updateMerchantReferenceV3( + merchantReference: String, + tokens: CheckoutV3Tokens, + configuration: CheckoutV3Configuration? = checkoutV3Configuration, + ): Result { + requireNotNull(configuration) { + "`configuration` must be set via `setCheckoutV3Configuration` or passed into this function" } - /** - * Start checkout process by requesting data to pass to Cash App Pay SDK - * - * @param consumer information about customer - * @param orderTotal pricing information about this order - * @param items list of items in customer's cart - * @param configuration must be provided if not previously set via [setCheckoutV3Configuration] - */ - suspend fun beginCheckoutV3WithCashAppPay( - consumer: CheckoutV3Consumer, - orderTotal: OrderTotal, - items: Array = arrayOf(), - configuration: CheckoutV3Configuration? = checkoutV3Configuration, - ): Result { - requireNotNull(configuration) { - "`configuration` must be set via `setCheckoutV3Configuration` or passed into this function" - } - val checkoutRequest = CheckoutV3.Request.create( - consumer = consumer, - orderTotal = orderTotal, - items = items, - configuration = configuration, - isCashAppPay = true, + val payload = CheckoutV3.MerchantReferenceUpdate( + merchantReference, + token = tokens.token, + ppaConfirmToken = tokens.ppaConfirmToken, + singleUseCardToken = tokens.singleUseCardToken, + ) + + return ApiV3.requestUnit( + configuration.v3CheckoutUrl, + ApiV3.HttpVerb.PUT, + payload, + ) + } + + /** + * Updates the [merchantReference] corresponding to the checkout represented by the provided [tokens]. + */ + @DelicateCoroutinesApi + @JvmStatic + @JvmOverloads + fun updateMerchantReferenceV3Async( + merchantReference: String, + tokens: CheckoutV3Tokens, + configuration: CheckoutV3Configuration? = checkoutV3Configuration, + ): CompletableFuture { + return GlobalScope.future { + updateMerchantReferenceV3(merchantReference, tokens, configuration) + .fold( + onSuccess = { null }, + onFailure = { throw it }, ) - - val checkoutResponseResult = runCatching { - val checkoutUrl = configuration.v3CheckoutUrl - val checkoutPayload = requireNotNull(Json.encodeToString(checkoutRequest)) - return@runCatching withContext(Dispatchers.IO) { - ApiV3.request( - checkoutUrl, - ApiV3.HttpVerb.POST, - checkoutPayload, - ) - }.getOrThrow() - } - - // requesting checkout data failed - checkoutResponseResult.onFailure { error: Throwable -> - return failure(error) - } - - // requesting checkout data success: sign the checkout response token - checkoutResponseResult.onSuccess { result: CheckoutV3.Response -> - AfterpayCashAppCheckout.performSignPaymentRequest(result.token) - .let { cashAppSignOrderResult: CashAppSignOrderResult -> - return when (cashAppSignOrderResult) { - // signing failed - is Failure -> failure(cashAppSignOrderResult.error) - - // signing success - is Success -> { - // map Result to Result - success( - CheckoutV3CashAppPay( - token = result.token, - singleUseCardToken = result.singleUseCardToken, - amount = cashAppSignOrderResult.response.amount, - redirectUri = cashAppSignOrderResult.response.redirectUri, - merchantId = cashAppSignOrderResult.response.merchantId, - brandId = cashAppSignOrderResult.response.brandId, - jwt = cashAppSignOrderResult.response.jwt, - ), - ) - } - } - } - } - - // should never happen, compiler doesn't know success and failure are only options - throw IllegalStateException() - } - - /** - * Confirm that checkout was completed with Cash App Pay SDK - * - * @param customerId customer ID, received from Cash App Pay SDK - * @param grantId grant ID received from Cash App Pay SDK - * @param token token received from [beginCheckoutV3WithCashAppPay] - * @param singleUseCardToken singleUseCardToken received from [beginCheckoutV3WithCashAppPay] - * @param jwt JSON web token received from [beginCheckoutV3WithCashAppPay] - * @param configuration must be provided if not previously set via [setCheckoutV3Configuration] - */ - suspend fun confirmCheckoutV3WithCashAppPay( - customerId: String, - grantId: String, - token: String, - singleUseCardToken: String, - jwt: String, - configuration: CheckoutV3Configuration? = checkoutV3Configuration, - ): Result { - return runCatching { - requireNotNull(configuration) { - "`configuration` must be set via `setCheckoutV3Configuration` or passed into this function" - } - - val confirmUrl = configuration.v3CheckoutConfirmationUrl - - val request = CashAppPayRequest( - token = token, - singleUseCardToken = singleUseCardToken, - cashAppPspInfo = CashAppPayRequest.CashAppPspInfo( - externalCustomerId = customerId, - externalGrantId = grantId, - jwt = jwt, - ), - ) - - val response = withContext(Dispatchers.IO) { - ApiV3.request( - url = confirmUrl, - method = ApiV3.HttpVerb.POST, - body = request, - ) - }.getOrThrow() - - CheckoutV3Data( - cardDetails = response.paymentDetails.virtualCard - ?: response.paymentDetails.virtualCardToken!!, - cardValidUntilInternal = response.cardValidUntil, - tokens = CheckoutV3Tokens( - token = token, - singleUseCardToken = singleUseCardToken, - ppaConfirmToken = "", // this token is not used in Cash App flow - ), - ) - } } - - /** - * Returns an [Intent] for the given [context] and options that can be passed to - * [startActivityForResult][android.app.Activity.startActivityForResult] to initiate the - * Afterpay checkout. - */ - @JvmStatic - @JvmOverloads - fun createCheckoutV3Intent( - context: Context, - consumer: CheckoutV3Consumer, - orderTotal: OrderTotal, - items: Array = arrayOf(), - buyNow: Boolean, - configuration: CheckoutV3Configuration? = checkoutV3Configuration, - ): Intent { - requireNotNull(configuration) { - "`configuration` must be set via `setCheckoutV3Configuration` or passed into this function" - } - val checkoutRequest = CheckoutV3.Request.create( - consumer = consumer, - orderTotal = orderTotal, - items = items, - configuration = configuration, - isCashAppPay = false, - ) - val options = AfterpayCheckoutV3Options( - buyNow = buyNow, - checkoutPayload = Json.encodeToString(checkoutRequest), - checkoutUrl = configuration.v3CheckoutUrl, - confirmUrl = configuration.v3CheckoutConfirmationUrl, - ) - - return Intent(context, AfterpayCheckoutV3Activity::class.java) - .putCheckoutV3OptionsExtra(options) + } + + /** + * Returns the [Configuration] inclusive of minimum and maximum spend available. + */ + @JvmSynthetic + fun fetchMerchantConfigurationV3( + configuration: CheckoutV3Configuration? = checkoutV3Configuration, + ): Result { + requireNotNull(configuration) { + "`configuration` must be set via `setCheckoutV3Configuration` or passed into this function" } - /** - * Updates the [merchantReference] corresponding to the checkout represented by the provided [tokens]. - */ - @JvmSynthetic - fun updateMerchantReferenceV3( - merchantReference: String, - tokens: CheckoutV3Tokens, - configuration: CheckoutV3Configuration? = checkoutV3Configuration, - ): Result { - requireNotNull(configuration) { - "`configuration` must be set via `setCheckoutV3Configuration` or passed into this function" - } - - val payload = CheckoutV3.MerchantReferenceUpdate( - merchantReference, - token = tokens.token, - ppaConfirmToken = tokens.ppaConfirmToken, - singleUseCardToken = tokens.singleUseCardToken, - ) - - return ApiV3.requestUnit( - configuration.v3CheckoutUrl, - ApiV3.HttpVerb.PUT, - payload, + return ApiV3.get(configuration.v3ConfigurationUrl) + .map { + Configuration( + minimumAmount = it.minimumAmount.amount, + maximumAmount = it.maximumAmount.amount, + currency = Currency.getInstance(configuration.region.currencyCode), + locale = configuration.region.locale, + environment = configuration.environment, ) + } + } + + /** + * Returns the [Configuration] inclusive of minimum and maximum spend available. + */ + @DelicateCoroutinesApi + @JvmStatic + @JvmOverloads + fun fetchMerchantConfigurationV3Async( + configuration: CheckoutV3Configuration? = checkoutV3Configuration, + ): CompletableFuture { + requireNotNull(configuration) { + "`configuration` must be set via `setCheckoutV3Configuration` or passed into this function" } - /** - * Updates the [merchantReference] corresponding to the checkout represented by the provided [tokens]. - */ - @DelicateCoroutinesApi - @JvmStatic - @JvmOverloads - fun updateMerchantReferenceV3Async( - merchantReference: String, - tokens: CheckoutV3Tokens, - configuration: CheckoutV3Configuration? = checkoutV3Configuration, - ): CompletableFuture { - return GlobalScope.future { - updateMerchantReferenceV3(merchantReference, tokens, configuration) - .fold( - onSuccess = { null }, - onFailure = { throw it }, - ) + return GlobalScope.future { + val result = ApiV3.get(configuration.v3ConfigurationUrl) + .map { + Configuration( + minimumAmount = it.minimumAmount.amount, + maximumAmount = it.maximumAmount.amount, + currency = Currency.getInstance(configuration.region.currencyCode), + locale = configuration.region.locale, + environment = configuration.environment, + ) } - } - /** - * Returns the [Configuration] inclusive of minimum and maximum spend available. - */ - @JvmSynthetic - fun fetchMerchantConfigurationV3( - configuration: CheckoutV3Configuration? = checkoutV3Configuration, - ): Result { - requireNotNull(configuration) { - "`configuration` must be set via `setCheckoutV3Configuration` or passed into this function" - } - - return ApiV3.get(configuration.v3ConfigurationUrl) - .map { - Configuration( - minimumAmount = it.minimumAmount.amount, - maximumAmount = it.maximumAmount.amount, - currency = Currency.getInstance(configuration.region.currencyCode), - locale = configuration.region.locale, - environment = configuration.environment, - ) - } + result.fold( + onSuccess = { it }, + onFailure = { throw it }, + ) } - - /** - * Returns the [Configuration] inclusive of minimum and maximum spend available. - */ - @DelicateCoroutinesApi - @JvmStatic - @JvmOverloads - fun fetchMerchantConfigurationV3Async( - configuration: CheckoutV3Configuration? = checkoutV3Configuration, - ): CompletableFuture { - requireNotNull(configuration) { - "`configuration` must be set via `setCheckoutV3Configuration` or passed into this function" - } - - return GlobalScope.future { - val result = ApiV3.get(configuration.v3ConfigurationUrl) - .map { - Configuration( - minimumAmount = it.minimumAmount.amount, - maximumAmount = it.maximumAmount.amount, - currency = Currency.getInstance(configuration.region.currencyCode), - locale = configuration.region.locale, - environment = configuration.environment, - ) - } - - result.fold( - onSuccess = { it }, - onFailure = { throw it }, - ) - } + } + + /** + * Returns the [CheckoutV3Data] returned by a successful Afterpay checkout. + */ + @JvmStatic + fun parseCheckoutSuccessResponseV3(intent: Intent): CheckoutV3Data? = + intent.getResultDataExtra() + + /** + * Returns the [status][CancellationStatusV3] and [Exception] parsed from the given [intent] returned by a + * cancelled Afterpay checkout. + */ + @JvmStatic + fun parseCheckoutCancellationResponseV3(intent: Intent): Pair? = + intent.getCancellationStatusExtraV3()?.let { + Pair(it, intent.getCancellationStatusExtraErrorV3()) } - - /** - * Returns the [CheckoutV3Data] returned by a successful Afterpay checkout. - */ - @JvmStatic - fun parseCheckoutSuccessResponseV3(intent: Intent): CheckoutV3Data? = - intent.getResultDataExtra() - - /** - * Returns the [status][CancellationStatusV3] and [Exception] parsed from the given [intent] returned by a - * cancelled Afterpay checkout. - */ - @JvmStatic - fun parseCheckoutCancellationResponseV3(intent: Intent): Pair? = - intent.getCancellationStatusExtraV3()?.let { - Pair(it, intent.getCancellationStatusExtraErrorV3()) - } - // endregion: v3 + // endregion: v3 } diff --git a/afterpay/src/main/kotlin/com/afterpay/android/AfterpayCheckoutV2Handler.kt b/afterpay/src/main/kotlin/com/afterpay/android/AfterpayCheckoutV2Handler.kt index c2662887..dc304408 100644 --- a/afterpay/src/main/kotlin/com/afterpay/android/AfterpayCheckoutV2Handler.kt +++ b/afterpay/src/main/kotlin/com/afterpay/android/AfterpayCheckoutV2Handler.kt @@ -21,15 +21,15 @@ import com.afterpay.android.model.ShippingOptionUpdateResult import com.afterpay.android.model.ShippingOptionsResult interface AfterpayCheckoutV2Handler { - fun didCommenceCheckout(onTokenLoaded: (Result) -> Unit) + fun didCommenceCheckout(onTokenLoaded: (Result) -> Unit) - fun shippingAddressDidChange( - address: ShippingAddress, - onProvideShippingOptions: (ShippingOptionsResult) -> Unit, - ) + fun shippingAddressDidChange( + address: ShippingAddress, + onProvideShippingOptions: (ShippingOptionsResult) -> Unit, + ) - fun shippingOptionDidChange( - shippingOption: ShippingOption, - onProvideShippingOption: (ShippingOptionUpdateResult?) -> Unit, - ) + fun shippingOptionDidChange( + shippingOption: ShippingOption, + onProvideShippingOption: (ShippingOptionUpdateResult?) -> Unit, + ) } diff --git a/afterpay/src/main/kotlin/com/afterpay/android/AfterpayCheckoutV2Options.kt b/afterpay/src/main/kotlin/com/afterpay/android/AfterpayCheckoutV2Options.kt index 86101917..2308d620 100644 --- a/afterpay/src/main/kotlin/com/afterpay/android/AfterpayCheckoutV2Options.kt +++ b/afterpay/src/main/kotlin/com/afterpay/android/AfterpayCheckoutV2Options.kt @@ -19,36 +19,36 @@ import android.os.Parcel import android.os.Parcelable data class AfterpayCheckoutV2Options( - val pickup: Boolean? = null, - val buyNow: Boolean? = null, - val shippingOptionRequired: Boolean? = null, - val enableSingleShippingOptionUpdate: Boolean? = null, + val pickup: Boolean? = null, + val buyNow: Boolean? = null, + val shippingOptionRequired: Boolean? = null, + val enableSingleShippingOptionUpdate: Boolean? = null, ) : Parcelable { - constructor(parcel: Parcel) : this( - parcel.readValue(Boolean::class.java.classLoader) as? Boolean, - parcel.readValue(Boolean::class.java.classLoader) as? Boolean, - parcel.readValue(Boolean::class.java.classLoader) as? Boolean, - parcel.readValue(Boolean::class.java.classLoader) as? Boolean, - ) + constructor(parcel: Parcel) : this( + parcel.readValue(Boolean::class.java.classLoader) as? Boolean, + parcel.readValue(Boolean::class.java.classLoader) as? Boolean, + parcel.readValue(Boolean::class.java.classLoader) as? Boolean, + parcel.readValue(Boolean::class.java.classLoader) as? Boolean, + ) - override fun writeToParcel(parcel: Parcel, flags: Int) { - parcel.writeValue(pickup) - parcel.writeValue(buyNow) - parcel.writeValue(shippingOptionRequired) - parcel.writeValue(enableSingleShippingOptionUpdate) - } + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeValue(pickup) + parcel.writeValue(buyNow) + parcel.writeValue(shippingOptionRequired) + parcel.writeValue(enableSingleShippingOptionUpdate) + } - override fun describeContents(): Int { - return 0 - } + override fun describeContents(): Int { + return 0 + } - companion object CREATOR : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): AfterpayCheckoutV2Options { - return AfterpayCheckoutV2Options(parcel) - } + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): AfterpayCheckoutV2Options { + return AfterpayCheckoutV2Options(parcel) + } - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } + override fun newArray(size: Int): Array { + return arrayOfNulls(size) } + } } diff --git a/afterpay/src/main/kotlin/com/afterpay/android/AfterpayCheckoutV3Options.kt b/afterpay/src/main/kotlin/com/afterpay/android/AfterpayCheckoutV3Options.kt index 5998408d..6efc6f88 100644 --- a/afterpay/src/main/kotlin/com/afterpay/android/AfterpayCheckoutV3Options.kt +++ b/afterpay/src/main/kotlin/com/afterpay/android/AfterpayCheckoutV3Options.kt @@ -21,12 +21,12 @@ import java.net.URL @Parcelize data class AfterpayCheckoutV3Options( - val buyNow: Boolean? = null, - val checkoutPayload: String? = null, - val token: String? = null, - val ppaConfirmToken: String? = null, - val singleUseCardToken: String? = null, - val checkoutUrl: URL? = null, - val redirectUrl: URL? = null, - val confirmUrl: URL? = null, + val buyNow: Boolean? = null, + val checkoutPayload: String? = null, + val token: String? = null, + val ppaConfirmToken: String? = null, + val singleUseCardToken: String? = null, + val checkoutUrl: URL? = null, + val redirectUrl: URL? = null, + val confirmUrl: URL? = null, ) : Parcelable diff --git a/afterpay/src/main/kotlin/com/afterpay/android/AfterpayEnvironment.kt b/afterpay/src/main/kotlin/com/afterpay/android/AfterpayEnvironment.kt index 86e1e3aa..017e0a2c 100644 --- a/afterpay/src/main/kotlin/com/afterpay/android/AfterpayEnvironment.kt +++ b/afterpay/src/main/kotlin/com/afterpay/android/AfterpayEnvironment.kt @@ -22,22 +22,22 @@ const val API_PLUS_SANDBOX_BASE_URL = "https://api-plus.us-sandbox.afterpay.com" const val API_PLUS_PRODUCTION_BASE_URL = "https://api-plus.us.afterpay.com" enum class AfterpayEnvironment( - val payKitClientId: String, - val cashAppPaymentSigningUrl: URL, - val cashAppPaymentValidationUrl: URL, + val payKitClientId: String, + val cashAppPaymentSigningUrl: URL, + val cashAppPaymentValidationUrl: URL, ) { - SANDBOX( - payKitClientId = "CAS-CI_AFTERPAY", - cashAppPaymentSigningUrl = URL("$API_PLUS_SANDBOX_BASE_URL/v2/payments/sign-payment"), - cashAppPaymentValidationUrl = URL("$API_PLUS_SANDBOX_BASE_URL/v2/payments/validate-payment"), - ), + SANDBOX( + payKitClientId = "CAS-CI_AFTERPAY", + cashAppPaymentSigningUrl = URL("$API_PLUS_SANDBOX_BASE_URL/v2/payments/sign-payment"), + cashAppPaymentValidationUrl = URL("$API_PLUS_SANDBOX_BASE_URL/v2/payments/validate-payment"), + ), - PRODUCTION( - payKitClientId = "CA-CI_AFTERPAY", - cashAppPaymentSigningUrl = URL("$API_PLUS_PRODUCTION_BASE_URL/v2/payments/sign-payment"), - cashAppPaymentValidationUrl = URL("$API_PLUS_PRODUCTION_BASE_URL/v2/payments/validate-payment"), - ), - ; + PRODUCTION( + payKitClientId = "CA-CI_AFTERPAY", + cashAppPaymentSigningUrl = URL("$API_PLUS_PRODUCTION_BASE_URL/v2/payments/sign-payment"), + cashAppPaymentValidationUrl = URL("$API_PLUS_PRODUCTION_BASE_URL/v2/payments/validate-payment"), + ), + ; - override fun toString(): String = name.lowercase(Locale.ROOT) + override fun toString(): String = name.lowercase(Locale.ROOT) } diff --git a/afterpay/src/main/kotlin/com/afterpay/android/CancellationStatus.kt b/afterpay/src/main/kotlin/com/afterpay/android/CancellationStatus.kt index 7a5adbbe..72d4113e 100644 --- a/afterpay/src/main/kotlin/com/afterpay/android/CancellationStatus.kt +++ b/afterpay/src/main/kotlin/com/afterpay/android/CancellationStatus.kt @@ -16,16 +16,16 @@ package com.afterpay.android enum class CancellationStatus { - USER_INITIATED, - NO_CHECKOUT_URL, - INVALID_CHECKOUT_URL, - NO_CHECKOUT_HANDLER, - NO_CONFIGURATION, - LANGUAGE_NOT_SUPPORTED, + USER_INITIATED, + NO_CHECKOUT_URL, + INVALID_CHECKOUT_URL, + NO_CHECKOUT_HANDLER, + NO_CONFIGURATION, + LANGUAGE_NOT_SUPPORTED, } enum class CancellationStatusV3 { - USER_INITIATED, - CONFIGURATION_ERROR, - REQUEST_ERROR, + USER_INITIATED, + CONFIGURATION_ERROR, + REQUEST_ERROR, } diff --git a/afterpay/src/main/kotlin/com/afterpay/android/cashapp/AfterpayCashApp.kt b/afterpay/src/main/kotlin/com/afterpay/android/cashapp/AfterpayCashApp.kt index 9e6aa42e..3ccd756a 100644 --- a/afterpay/src/main/kotlin/com/afterpay/android/cashapp/AfterpayCashApp.kt +++ b/afterpay/src/main/kotlin/com/afterpay/android/cashapp/AfterpayCashApp.kt @@ -16,9 +16,9 @@ package com.afterpay.android.cashapp data class AfterpayCashApp( - val amount: Double, - val redirectUri: String, - val merchantId: String, - val brandId: String, - val jwt: String, + val amount: Double, + val redirectUri: String, + val merchantId: String, + val brandId: String, + val jwt: String, ) diff --git a/afterpay/src/main/kotlin/com/afterpay/android/cashapp/AfterpayCashAppApi.kt b/afterpay/src/main/kotlin/com/afterpay/android/cashapp/AfterpayCashAppApi.kt index 1d9b7cec..b33906ac 100644 --- a/afterpay/src/main/kotlin/com/afterpay/android/cashapp/AfterpayCashAppApi.kt +++ b/afterpay/src/main/kotlin/com/afterpay/android/cashapp/AfterpayCashAppApi.kt @@ -27,70 +27,70 @@ import java.net.URL import javax.net.ssl.HttpsURLConnection internal object AfterpayCashAppApi { - private val json = Json { ignoreUnknownKeys = true } + private val json = Json { ignoreUnknownKeys = true } - internal inline fun cashRequest(url: URL, method: CashHttpVerb, body: B): Result { - val connection = url.openConnection() as HttpsURLConnection - return try { - configure(connection, method) - val payload = (body as? String) ?: json.encodeToString(body) + internal inline fun cashRequest(url: URL, method: CashHttpVerb, body: B): Result { + val connection = url.openConnection() as HttpsURLConnection + return try { + configure(connection, method) + val payload = (body as? String) ?: json.encodeToString(body) - OutputStreamWriter(connection.outputStream).use { writer -> - writer.write(payload) - writer.flush() - } + OutputStreamWriter(connection.outputStream).use { writer -> + writer.write(payload) + writer.flush() + } - if (connection.errorStream == null && connection.responseCode < HttpURLConnection.HTTP_BAD_REQUEST) { - connection.inputStream.bufferedReader().use { reader -> - val data = reader.readText() - val result = json.decodeFromString(data) - Result.success(result) - } - } else { - throw InvalidObjectException("Unexpected response code: ${connection.responseCode}.") - } - } catch (exception: Exception) { - Result.failure(exception) + if (connection.errorStream == null && connection.responseCode < HttpURLConnection.HTTP_BAD_REQUEST) { + connection.inputStream.bufferedReader().use { reader -> + val data = reader.readText() + val result = json.decodeFromString(data) + Result.success(result) } + } else { + throw InvalidObjectException("Unexpected response code: ${connection.responseCode}.") + } + } catch (exception: Exception) { + Result.failure(exception) } + } - private fun configure(connection: HttpsURLConnection, type: CashHttpVerb) { - connection.requestMethod = type.name - connection.setRequestProperty("${BuildConfig.AfterpayLibraryVersion}-android", "X-Afterpay-SDK") - when (type) { - CashHttpVerb.POST, CashHttpVerb.PUT -> { - connection.setRequestProperty("Content-Type", "application/json") - connection.setRequestProperty("Accept", "application/json") - } - else -> { } - } - when (type) { - CashHttpVerb.GET -> { - connection.doInput = true - connection.doOutput = false - } - CashHttpVerb.PUT -> { - connection.doInput = true - connection.doOutput = false - } - CashHttpVerb.POST -> { - connection.doInput = true - connection.doOutput = true - } - } + private fun configure(connection: HttpsURLConnection, type: CashHttpVerb) { + connection.requestMethod = type.name + connection.setRequestProperty("${BuildConfig.AfterpayLibraryVersion}-android", "X-Afterpay-SDK") + when (type) { + CashHttpVerb.POST, CashHttpVerb.PUT -> { + connection.setRequestProperty("Content-Type", "application/json") + connection.setRequestProperty("Accept", "application/json") + } + else -> { } } - - internal enum class CashHttpVerb { - POST, - PUT, - GET, + when (type) { + CashHttpVerb.GET -> { + connection.doInput = true + connection.doOutput = false + } + CashHttpVerb.PUT -> { + connection.doInput = true + connection.doOutput = false + } + CashHttpVerb.POST -> { + connection.doInput = true + connection.doOutput = true + } } + } + + internal enum class CashHttpVerb { + POST, + PUT, + GET, + } - @Serializable - internal data class ApiErrorCashApp( - val errorCode: String, - val errorId: String, - val message: String, - val httpStatusCode: Int, - ) + @Serializable + internal data class ApiErrorCashApp( + val errorCode: String, + val errorId: String, + val message: String, + val httpStatusCode: Int, + ) } diff --git a/afterpay/src/main/kotlin/com/afterpay/android/cashapp/AfterpayCashAppCheckout.kt b/afterpay/src/main/kotlin/com/afterpay/android/cashapp/AfterpayCashAppCheckout.kt index e15e0f9d..9e4b2be8 100644 --- a/afterpay/src/main/kotlin/com/afterpay/android/cashapp/AfterpayCashAppCheckout.kt +++ b/afterpay/src/main/kotlin/com/afterpay/android/cashapp/AfterpayCashAppCheckout.kt @@ -23,127 +23,127 @@ import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json sealed class CashAppSignOrderResult { - data class Success(val response: AfterpayCashApp) : CashAppSignOrderResult() - data class Failure(val error: Throwable) : CashAppSignOrderResult() + data class Success(val response: AfterpayCashApp) : CashAppSignOrderResult() + data class Failure(val error: Throwable) : CashAppSignOrderResult() } sealed class CashAppValidationResponse { - data class Success(val response: AfterpayCashAppValidationResponse) : CashAppValidationResponse() - data class Failure(val error: Throwable) : CashAppValidationResponse() + data class Success(val response: AfterpayCashAppValidationResponse) : CashAppValidationResponse() + data class Failure(val error: Throwable) : CashAppValidationResponse() } object AfterpayCashAppCheckout { - suspend fun performSignPaymentRequest(token: String): CashAppSignOrderResult { - runCatching { - signPayment(token) - .let { result: Result -> - result.onSuccess { response -> - AfterpayCashAppJwt.decode(response.jwtToken) - .onSuccess { jwtBody -> - val cashApp = AfterpayCashApp( - amount = jwtBody.amount.amount.toDouble(), - redirectUri = jwtBody.redirectUrl, - merchantId = jwtBody.externalMerchantId, - brandId = response.externalBrandId, - jwt = response.jwtToken, - ) + suspend fun performSignPaymentRequest(token: String): CashAppSignOrderResult { + runCatching { + signPayment(token) + .let { result: Result -> + result.onSuccess { response -> + AfterpayCashAppJwt.decode(response.jwtToken) + .onSuccess { jwtBody -> + val cashApp = AfterpayCashApp( + amount = jwtBody.amount.amount.toDouble(), + redirectUri = jwtBody.redirectUrl, + merchantId = jwtBody.externalMerchantId, + brandId = response.externalBrandId, + jwt = response.jwtToken, + ) - return CashAppSignOrderResult.Success(cashApp) - } - .onFailure { - return CashAppSignOrderResult.Failure(it) - } - } - .onFailure { - return CashAppSignOrderResult.Failure(it) - } - } + return CashAppSignOrderResult.Success(cashApp) + } + .onFailure { + return CashAppSignOrderResult.Failure(it) + } + } + .onFailure { + return CashAppSignOrderResult.Failure(it) + } } - // should never happen, compiler doesn't know success and failure are only options - throw IllegalStateException() } + // should never happen, compiler doesn't know success and failure are only options + throw IllegalStateException() + } - // TODO stop using this, no need for suspend *and* callback - suspend fun performSignPaymentRequest(token: String, complete: (CashAppSignOrderResult) -> Unit) { - runCatching { - signPayment(token) - .onSuccess { response -> - AfterpayCashAppJwt.decode(response.jwtToken) - .onSuccess { jwtBody -> - val cashApp = AfterpayCashApp( - amount = jwtBody.amount.amount.toDouble(), - redirectUri = jwtBody.redirectUrl, - merchantId = jwtBody.externalMerchantId, - brandId = response.externalBrandId, - jwt = response.jwtToken, - ) + // TODO stop using this, no need for suspend *and* callback + suspend fun performSignPaymentRequest(token: String, complete: (CashAppSignOrderResult) -> Unit) { + runCatching { + signPayment(token) + .onSuccess { response -> + AfterpayCashAppJwt.decode(response.jwtToken) + .onSuccess { jwtBody -> + val cashApp = AfterpayCashApp( + amount = jwtBody.amount.amount.toDouble(), + redirectUri = jwtBody.redirectUrl, + merchantId = jwtBody.externalMerchantId, + brandId = response.externalBrandId, + jwt = response.jwtToken, + ) - complete(CashAppSignOrderResult.Success(cashApp)) - } - .onFailure { - complete(CashAppSignOrderResult.Failure(it)) - } - } - .onFailure { - complete(CashAppSignOrderResult.Failure(it)) - } + complete(CashAppSignOrderResult.Success(cashApp)) + } + .onFailure { + complete(CashAppSignOrderResult.Failure(it)) + } + } + .onFailure { + complete(CashAppSignOrderResult.Failure(it)) } } + } - private suspend fun signPayment(token: String): Result { - return runCatching { - val url = Afterpay.environment?.cashAppPaymentSigningUrl ?: throw Exception("No signing url found") - val payload = """{ "token": "$token" }""" + private suspend fun signPayment(token: String): Result { + return runCatching { + val url = Afterpay.environment?.cashAppPaymentSigningUrl ?: throw Exception("No signing url found") + val payload = """{ "token": "$token" }""" - val response = withContext(Dispatchers.IO) { - AfterpayCashAppApi.cashRequest( - url = url, - method = AfterpayCashAppApi.CashHttpVerb.POST, - body = payload, - ) - }.getOrThrow() + val response = withContext(Dispatchers.IO) { + AfterpayCashAppApi.cashRequest( + url = url, + method = AfterpayCashAppApi.CashHttpVerb.POST, + body = payload, + ) + }.getOrThrow() - response - } + response } + } - fun validatePayment( - jwt: String, - customerId: String, - grantId: String, - complete: (validationResponse: CashAppValidationResponse) -> Unit, - ) { - return runBlocking { - Afterpay.environment?.cashAppPaymentValidationUrl?.let { url -> - val request = AfterpayCashAppValidationRequest( - jwt = jwt, - externalCustomerId = customerId, - externalGrantId = grantId, - ) - - val payload = Json.encodeToString(request) + fun validatePayment( + jwt: String, + customerId: String, + grantId: String, + complete: (validationResponse: CashAppValidationResponse) -> Unit, + ) { + return runBlocking { + Afterpay.environment?.cashAppPaymentValidationUrl?.let { url -> + val request = AfterpayCashAppValidationRequest( + jwt = jwt, + externalCustomerId = customerId, + externalGrantId = grantId, + ) - val response = withContext(Dispatchers.IO) { - AfterpayCashAppApi.cashRequest( - url = url, - method = AfterpayCashAppApi.CashHttpVerb.POST, - body = payload, - ) - } + val payload = Json.encodeToString(request) - response - .onSuccess { - when (it.status) { - "SUCCESS" -> complete(CashAppValidationResponse.Success(it)) - else -> complete(CashAppValidationResponse.Failure(Exception("status is ${it.status}"))) - } - } - .onFailure { - complete(CashAppValidationResponse.Failure(Exception(it.message))) - } + val response = withContext(Dispatchers.IO) { + AfterpayCashAppApi.cashRequest( + url = url, + method = AfterpayCashAppApi.CashHttpVerb.POST, + body = payload, + ) + } - Unit + response + .onSuccess { + when (it.status) { + "SUCCESS" -> complete(CashAppValidationResponse.Success(it)) + else -> complete(CashAppValidationResponse.Failure(Exception("status is ${it.status}"))) } - } ?: complete(CashAppValidationResponse.Failure(Exception("environment not set"))) - } + } + .onFailure { + complete(CashAppValidationResponse.Failure(Exception(it.message))) + } + + Unit + } + } ?: complete(CashAppValidationResponse.Failure(Exception("environment not set"))) + } } diff --git a/afterpay/src/main/kotlin/com/afterpay/android/cashapp/AfterpayCashAppJwt.kt b/afterpay/src/main/kotlin/com/afterpay/android/cashapp/AfterpayCashAppJwt.kt index 7cdacf6c..ff2f995c 100644 --- a/afterpay/src/main/kotlin/com/afterpay/android/cashapp/AfterpayCashAppJwt.kt +++ b/afterpay/src/main/kotlin/com/afterpay/android/cashapp/AfterpayCashAppJwt.kt @@ -21,53 +21,53 @@ import kotlinx.serialization.json.Json @Serializable data class AfterpayCashAppSigningResponse( - var externalBrandId: String, - var jwtToken: String, - var redirectUrl: String, + var externalBrandId: String, + var jwtToken: String, + var redirectUrl: String, ) @Serializable data class AfterpayCashAppValidationRequest( - val jwt: String, - val externalCustomerId: String, - val externalGrantId: String, + val jwt: String, + val externalCustomerId: String, + val externalGrantId: String, ) @Serializable data class AfterpayCashAppValidationResponse( - var cashAppTag: String, - var status: String, - var callbackBaseUrl: String, + var cashAppTag: String, + var status: String, + var callbackBaseUrl: String, ) @Serializable data class AfterpayCashAppJwt( - var amount: AfterpayCashAppAmount, - var token: String, - var externalMerchantId: String, - var redirectUrl: String, + var amount: AfterpayCashAppAmount, + var token: String, + var externalMerchantId: String, + var redirectUrl: String, ) { - companion object { - fun decode(jwt: String): Result { - return runCatching { - val split = jwt.split(".").toTypedArray() - val jwtBody = getJson(split[1]) + companion object { + fun decode(jwt: String): Result { + return runCatching { + val split = jwt.split(".").toTypedArray() + val jwtBody = getJson(split[1]) - val json = Json { ignoreUnknownKeys = true } - json.decodeFromString(jwtBody) - } - } + val json = Json { ignoreUnknownKeys = true } + json.decodeFromString(jwtBody) + } + } - private fun getJson(strEncoded: String): String { - val decodedBytes: ByteArray = Base64.decode(strEncoded, Base64.URL_SAFE) - return String(decodedBytes, Charsets.UTF_8) - } + private fun getJson(strEncoded: String): String { + val decodedBytes: ByteArray = Base64.decode(strEncoded, Base64.URL_SAFE) + return String(decodedBytes, Charsets.UTF_8) } + } } @Serializable data class AfterpayCashAppAmount( - var amount: String, - var currency: String, - var symbol: String, + var amount: String, + var currency: String, + var symbol: String, ) diff --git a/afterpay/src/main/kotlin/com/afterpay/android/internal/AfterpayCheckoutCompletion.kt b/afterpay/src/main/kotlin/com/afterpay/android/internal/AfterpayCheckoutCompletion.kt index 28ebd307..1feebed1 100644 --- a/afterpay/src/main/kotlin/com/afterpay/android/internal/AfterpayCheckoutCompletion.kt +++ b/afterpay/src/main/kotlin/com/afterpay/android/internal/AfterpayCheckoutCompletion.kt @@ -19,14 +19,14 @@ import kotlinx.serialization.Serializable @Serializable internal data class AfterpayCheckoutCompletion( - val status: Status, - val orderToken: String, + val status: Status, + val orderToken: String, ) { - @Suppress("UNUSED_PARAMETER") - @Serializable - internal enum class Status(statusString: String) { - SUCCESS("SUCCESS"), - CANCELLED("CANCELLED"), - } + @Suppress("UNUSED_PARAMETER") + @Serializable + internal enum class Status(statusString: String) { + SUCCESS("SUCCESS"), + CANCELLED("CANCELLED"), + } } diff --git a/afterpay/src/main/kotlin/com/afterpay/android/internal/AfterpayCheckoutMessage.kt b/afterpay/src/main/kotlin/com/afterpay/android/internal/AfterpayCheckoutMessage.kt index 0b4bd2ae..73263417 100644 --- a/afterpay/src/main/kotlin/com/afterpay/android/internal/AfterpayCheckoutMessage.kt +++ b/afterpay/src/main/kotlin/com/afterpay/android/internal/AfterpayCheckoutMessage.kt @@ -30,86 +30,86 @@ import kotlinx.serialization.Serializable @Serializable internal sealed class AfterpayCheckoutMessage { - abstract val meta: AfterpayCheckoutMessageMeta + abstract val meta: AfterpayCheckoutMessageMeta - @Serializable - internal data class AfterpayCheckoutMessageMeta(val requestId: String) + @Serializable + internal data class AfterpayCheckoutMessageMeta(val requestId: String) - companion object { + companion object { - fun fromShippingOptionsResult( - result: ShippingOptionsResult, - meta: AfterpayCheckoutMessageMeta, - ): AfterpayCheckoutMessage = when (result) { - is ShippingOptionsErrorResult -> CheckoutErrorMessage(meta, result.error.name) - is ShippingOptionsSuccessResult -> ShippingOptionsMessage(meta, result.shippingOptions) - } + fun fromShippingOptionsResult( + result: ShippingOptionsResult, + meta: AfterpayCheckoutMessageMeta, + ): AfterpayCheckoutMessage = when (result) { + is ShippingOptionsErrorResult -> CheckoutErrorMessage(meta, result.error.name) + is ShippingOptionsSuccessResult -> ShippingOptionsMessage(meta, result.shippingOptions) + } - fun fromShippingOptionUpdateResult( - result: ShippingOptionUpdateResult?, - meta: AfterpayCheckoutMessageMeta, - ): AfterpayCheckoutMessage = when (result) { - is ShippingOptionUpdateErrorResult -> CheckoutErrorMessage(meta, result.error.name) - is ShippingOptionUpdateSuccessResult -> ShippingOptionUpdateMessage( - meta, - result.shippingOptionUpdate, - ) - null -> EmptyPayloadMessage(meta) - } + fun fromShippingOptionUpdateResult( + result: ShippingOptionUpdateResult?, + meta: AfterpayCheckoutMessageMeta, + ): AfterpayCheckoutMessage = when (result) { + is ShippingOptionUpdateErrorResult -> CheckoutErrorMessage(meta, result.error.name) + is ShippingOptionUpdateSuccessResult -> ShippingOptionUpdateMessage( + meta, + result.shippingOptionUpdate, + ) + null -> EmptyPayloadMessage(meta) } + } } @Serializable @SerialName("onMessage") internal data class CheckoutLogMessage( - override val meta: AfterpayCheckoutMessageMeta, - val payload: CheckoutLog, + override val meta: AfterpayCheckoutMessageMeta, + val payload: CheckoutLog, ) : AfterpayCheckoutMessage() { - @Serializable - internal data class CheckoutLog( - val severity: String, - val message: String, - ) + @Serializable + internal data class CheckoutLog( + val severity: String, + val message: String, + ) } @Serializable @SerialName("onError") internal data class CheckoutErrorMessage( - override val meta: AfterpayCheckoutMessageMeta, - val error: String, + override val meta: AfterpayCheckoutMessageMeta, + val error: String, ) : AfterpayCheckoutMessage() @Serializable @SerialName("onShippingAddressChange") internal data class ShippingAddressMessage( - override val meta: AfterpayCheckoutMessageMeta, - val payload: ShippingAddress, + override val meta: AfterpayCheckoutMessageMeta, + val payload: ShippingAddress, ) : AfterpayCheckoutMessage() @Serializable @SerialName("onShippingOptionChange") internal data class ShippingOptionMessage( - override val meta: AfterpayCheckoutMessageMeta, - val payload: ShippingOption, + override val meta: AfterpayCheckoutMessageMeta, + val payload: ShippingOption, ) : AfterpayCheckoutMessage() @Serializable @SerialName("onShippingOptionUpdateChange") internal data class ShippingOptionUpdateMessage( - override val meta: AfterpayCheckoutMessageMeta, - val payload: ShippingOptionUpdate?, + override val meta: AfterpayCheckoutMessageMeta, + val payload: ShippingOptionUpdate?, ) : AfterpayCheckoutMessage() @Serializable @SerialName("onShippingOptionsChange") internal data class ShippingOptionsMessage( - override val meta: AfterpayCheckoutMessageMeta, - val payload: List, + override val meta: AfterpayCheckoutMessageMeta, + val payload: List, ) : AfterpayCheckoutMessage() @Serializable @SerialName("onEmptyPayload") internal data class EmptyPayloadMessage( - override val meta: AfterpayCheckoutMessageMeta, + override val meta: AfterpayCheckoutMessageMeta, ) : AfterpayCheckoutMessage() diff --git a/afterpay/src/main/kotlin/com/afterpay/android/internal/AfterpayCheckoutV2.kt b/afterpay/src/main/kotlin/com/afterpay/android/internal/AfterpayCheckoutV2.kt index cee63ec1..78c7e292 100644 --- a/afterpay/src/main/kotlin/com/afterpay/android/internal/AfterpayCheckoutV2.kt +++ b/afterpay/src/main/kotlin/com/afterpay/android/internal/AfterpayCheckoutV2.kt @@ -23,29 +23,29 @@ import kotlinx.serialization.Serializable @Serializable internal data class AfterpayCheckoutV2( - val token: String, - val locale: String, - val environment: String, - val version: String, - val pickup: Boolean?, - val buyNow: Boolean?, - val shippingOptionRequired: Boolean?, - val checkoutRedesignForced: Boolean?, - val consumerLocale: String?, + val token: String, + val locale: String, + val environment: String, + val version: String, + val pickup: Boolean?, + val buyNow: Boolean?, + val shippingOptionRequired: Boolean?, + val checkoutRedesignForced: Boolean?, + val consumerLocale: String?, ) { - constructor( - token: String, - configuration: Configuration, - options: AfterpayCheckoutV2Options, - ) : this( - token = token, - locale = configuration.locale.toString(), - environment = configuration.environment.toString(), - version = "${BuildConfig.AfterpayLibraryVersion}-android", - pickup = options.pickup, - buyNow = options.buyNow, - shippingOptionRequired = options.shippingOptionRequired, - checkoutRedesignForced = options.enableSingleShippingOptionUpdate, - consumerLocale = Afterpay.language.toString(), - ) + constructor( + token: String, + configuration: Configuration, + options: AfterpayCheckoutV2Options, + ) : this( + token = token, + locale = configuration.locale.toString(), + environment = configuration.environment.toString(), + version = "${BuildConfig.AfterpayLibraryVersion}-android", + pickup = options.pickup, + buyNow = options.buyNow, + shippingOptionRequired = options.shippingOptionRequired, + checkoutRedesignForced = options.enableSingleShippingOptionUpdate, + consumerLocale = Afterpay.language.toString(), + ) } diff --git a/afterpay/src/main/kotlin/com/afterpay/android/internal/AfterpayDrawable.kt b/afterpay/src/main/kotlin/com/afterpay/android/internal/AfterpayDrawable.kt index 897e4a2e..00e45045 100644 --- a/afterpay/src/main/kotlin/com/afterpay/android/internal/AfterpayDrawable.kt +++ b/afterpay/src/main/kotlin/com/afterpay/android/internal/AfterpayDrawable.kt @@ -26,43 +26,43 @@ import com.afterpay.android.internal.Locales.EN_US import com.afterpay.android.internal.Locales.FR_CA private val localeLanguages = mapOf( - EN_AU to AfterpayDrawable.EN_AFTERPAY, - EN_GB to AfterpayDrawable.EN_CLEARPAY, - EN_NZ to AfterpayDrawable.EN_AFTERPAY, - EN_US to AfterpayDrawable.EN_AFTERPAY, - EN_CA to AfterpayDrawable.EN_AFTERPAY, - FR_CA to AfterpayDrawable.FR_CA, + EN_AU to AfterpayDrawable.EN_AFTERPAY, + EN_GB to AfterpayDrawable.EN_CLEARPAY, + EN_NZ to AfterpayDrawable.EN_AFTERPAY, + EN_US to AfterpayDrawable.EN_AFTERPAY, + EN_CA to AfterpayDrawable.EN_AFTERPAY, + FR_CA to AfterpayDrawable.FR_CA, ) internal enum class AfterpayDrawable( - @DrawableRes val buttonBuyNowForeground: Int, - @DrawableRes val buttonCheckoutForeground: Int, - @DrawableRes val buttonPayNowForeground: Int, - @DrawableRes val buttonPlaceOrderForeground: Int, + @DrawableRes val buttonBuyNowForeground: Int, + @DrawableRes val buttonCheckoutForeground: Int, + @DrawableRes val buttonPayNowForeground: Int, + @DrawableRes val buttonPlaceOrderForeground: Int, ) { - EN_AFTERPAY( - buttonBuyNowForeground = R.drawable.afterpay_button_buy_now_fg_en, - buttonCheckoutForeground = R.drawable.afterpay_button_checkout_fg_en, - buttonPayNowForeground = R.drawable.afterpay_button_pay_now_fg_en, - buttonPlaceOrderForeground = R.drawable.afterpay_button_place_order_fg_en, - ), - EN_CLEARPAY( - buttonBuyNowForeground = R.drawable.clearpay_button_buy_now_fg_en, - buttonCheckoutForeground = R.drawable.clearpay_button_checkout_fg_en, - buttonPayNowForeground = R.drawable.clearpay_button_pay_now_fg_en, - buttonPlaceOrderForeground = R.drawable.clearpay_button_place_order_fg_en, - ), - FR_CA( - buttonBuyNowForeground = R.drawable.afterpay_button_buy_now_fg_fr_ca, - buttonCheckoutForeground = R.drawable.afterpay_button_checkout_fg_fr_ca, - buttonPayNowForeground = R.drawable.afterpay_button_pay_now_fg_fr_ca, - buttonPlaceOrderForeground = R.drawable.afterpay_button_place_order_fg_fr_ca, - ), - ; + EN_AFTERPAY( + buttonBuyNowForeground = R.drawable.afterpay_button_buy_now_fg_en, + buttonCheckoutForeground = R.drawable.afterpay_button_checkout_fg_en, + buttonPayNowForeground = R.drawable.afterpay_button_pay_now_fg_en, + buttonPlaceOrderForeground = R.drawable.afterpay_button_place_order_fg_en, + ), + EN_CLEARPAY( + buttonBuyNowForeground = R.drawable.clearpay_button_buy_now_fg_en, + buttonCheckoutForeground = R.drawable.clearpay_button_checkout_fg_en, + buttonPayNowForeground = R.drawable.clearpay_button_pay_now_fg_en, + buttonPlaceOrderForeground = R.drawable.clearpay_button_place_order_fg_en, + ), + FR_CA( + buttonBuyNowForeground = R.drawable.afterpay_button_buy_now_fg_fr_ca, + buttonCheckoutForeground = R.drawable.afterpay_button_checkout_fg_fr_ca, + buttonPayNowForeground = R.drawable.afterpay_button_pay_now_fg_fr_ca, + buttonPlaceOrderForeground = R.drawable.afterpay_button_place_order_fg_fr_ca, + ), + ; - companion object { - fun forLocale(): AfterpayDrawable { - return localeLanguages[Afterpay.language] ?: EN_AFTERPAY - } + companion object { + fun forLocale(): AfterpayDrawable { + return localeLanguages[Afterpay.language] ?: EN_AFTERPAY } + } } diff --git a/afterpay/src/main/kotlin/com/afterpay/android/internal/AfterpayInfoActivity.kt b/afterpay/src/main/kotlin/com/afterpay/android/internal/AfterpayInfoActivity.kt index 6e665f0f..a504f160 100644 --- a/afterpay/src/main/kotlin/com/afterpay/android/internal/AfterpayInfoActivity.kt +++ b/afterpay/src/main/kotlin/com/afterpay/android/internal/AfterpayInfoActivity.kt @@ -23,27 +23,27 @@ import androidx.appcompat.app.AppCompatActivity import com.afterpay.android.R internal class AfterpayInfoActivity : AppCompatActivity() { - private lateinit var webView: WebView + private lateinit var webView: WebView - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_web_checkout) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_web_checkout) - window.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) + window.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) - webView = findViewById(R.id.afterpay_webView) - .setAfterpayUserAgentString() + webView = findViewById(R.id.afterpay_webView) + .setAfterpayUserAgentString() - loadUrl() - } + loadUrl() + } - private fun loadUrl() { - val url = intent.getInfoUrlExtra() ?: return dismiss() - webView.loadUrl(url) - } + private fun loadUrl() { + val url = intent.getInfoUrlExtra() ?: return dismiss() + webView.loadUrl(url) + } - private fun dismiss() { - setResult(Activity.RESULT_OK) - finish() - } + private fun dismiss() { + setResult(Activity.RESULT_OK) + finish() + } } diff --git a/afterpay/src/main/kotlin/com/afterpay/android/internal/AfterpayInfoSpan.kt b/afterpay/src/main/kotlin/com/afterpay/android/internal/AfterpayInfoSpan.kt index 172a3c2e..1b958c9c 100644 --- a/afterpay/src/main/kotlin/com/afterpay/android/internal/AfterpayInfoSpan.kt +++ b/afterpay/src/main/kotlin/com/afterpay/android/internal/AfterpayInfoSpan.kt @@ -21,24 +21,24 @@ import android.text.style.URLSpan import android.view.View internal class AfterpayInfoSpan(url: String) : URLSpan(url) { - private var underlined: Boolean = true + private var underlined: Boolean = true - constructor(url: String, underlined: Boolean) : this(url) { - this.underlined = underlined - } + constructor(url: String, underlined: Boolean) : this(url) { + this.underlined = underlined + } - override fun onClick(widget: View) { - val context = widget.context - val intent = Intent(context, AfterpayInfoActivity::class.java).putInfoUrlExtra(url) - if (intent.resolveActivity(context.packageManager) != null) { - context.startActivity(intent) - } else { - super.onClick(widget) - } + override fun onClick(widget: View) { + val context = widget.context + val intent = Intent(context, AfterpayInfoActivity::class.java).putInfoUrlExtra(url) + if (intent.resolveActivity(context.packageManager) != null) { + context.startActivity(intent) + } else { + super.onClick(widget) } + } - override fun updateDrawState(ds: TextPaint) { - super.updateDrawState(ds) - ds.isUnderlineText = this.underlined - } + override fun updateDrawState(ds: TextPaint) { + super.updateDrawState(ds) + ds.isUnderlineText = this.underlined + } } diff --git a/afterpay/src/main/kotlin/com/afterpay/android/internal/AfterpayInstalment.kt b/afterpay/src/main/kotlin/com/afterpay/android/internal/AfterpayInstalment.kt index 16a758e9..733d8f57 100644 --- a/afterpay/src/main/kotlin/com/afterpay/android/internal/AfterpayInstalment.kt +++ b/afterpay/src/main/kotlin/com/afterpay/android/internal/AfterpayInstalment.kt @@ -24,92 +24,92 @@ import java.util.Currency import java.util.Locale internal sealed class AfterpayInstalment { - data class Available( - val instalmentAmount: String, - ) : AfterpayInstalment() - - data class NotAvailable( - val minimumAmount: String?, - val maximumAmount: String, - ) : AfterpayInstalment() - - object NoConfiguration : AfterpayInstalment() - - companion object { - fun of(totalCost: BigDecimal, configuration: Configuration?, clientLocale: Locale): AfterpayInstalment { - if (configuration == null) { - return NoConfiguration - } - - val currencyLocales = Locales.validSet.filterTo(HashSet()) { - Currency.getInstance(it) == configuration.currency - } - - val currencyLocale: Locale = when { - currencyLocales.isEmpty() -> { - return NotAvailable( - minimumAmount = null, - maximumAmount = "0", - ) - } - currencyLocales.count() == 1 -> currencyLocales.first() - currencyLocales.contains(configuration.locale) -> configuration.locale - else -> Locales.validSet.first { Currency.getInstance(it) == configuration.currency } - } - - val localCurrency = Currency.getInstance(clientLocale) - val currencySymbol = configuration.currency.getSymbol(currencyLocale) - - val usCurrencySymbol = Currency.getInstance(Locales.EN_US).getSymbol(Locales.EN_US) - val gbCurrencySymbol = Currency.getInstance(Locales.EN_GB).getSymbol(Locales.EN_GB) - - val currencyFormatter = (NumberFormat.getCurrencyInstance(clientLocale) as DecimalFormat).apply { - this.currency = configuration.currency - } - - if (clientLocale == Locales.EN_US) { - currencyFormatter.apply { - decimalFormatSymbols = decimalFormatSymbols.apply { - this.currencySymbol = when (configuration.currency) { - Currency.getInstance(Locales.EN_AU) -> "A$" - Currency.getInstance(Locales.EN_NZ) -> "NZ$" - Currency.getInstance(Locales.EN_CA) -> "CA$" - Currency.getInstance(Locales.FR_CA) -> "CA$" - else -> currencySymbol - } - } - } - } else if (configuration.currency != localCurrency) { - currencyFormatter.apply { - decimalFormatSymbols = decimalFormatSymbols.apply { - this.currencySymbol = currencySymbol - } - - when (currencySymbol) { - usCurrencySymbol -> this.applyPattern("¤#,##0.00 ¤¤") - gbCurrencySymbol -> this.applyPattern("¤#,##0.00") - } - } - } - - val minimumAmount = configuration.minimumAmount ?: BigDecimal.ZERO - if (totalCost < minimumAmount || totalCost > configuration.maximumAmount) { - val currencyFormatterNoDecimals = currencyFormatter.clone() as DecimalFormat - currencyFormatterNoDecimals.maximumFractionDigits = 0 - - return NotAvailable( - minimumAmount = configuration.minimumAmount?.let(currencyFormatterNoDecimals::format), - maximumAmount = currencyFormatterNoDecimals.format(configuration.maximumAmount), - ) + data class Available( + val instalmentAmount: String, + ) : AfterpayInstalment() + + data class NotAvailable( + val minimumAmount: String?, + val maximumAmount: String, + ) : AfterpayInstalment() + + object NoConfiguration : AfterpayInstalment() + + companion object { + fun of(totalCost: BigDecimal, configuration: Configuration?, clientLocale: Locale): AfterpayInstalment { + if (configuration == null) { + return NoConfiguration + } + + val currencyLocales = Locales.validSet.filterTo(HashSet()) { + Currency.getInstance(it) == configuration.currency + } + + val currencyLocale: Locale = when { + currencyLocales.isEmpty() -> { + return NotAvailable( + minimumAmount = null, + maximumAmount = "0", + ) + } + currencyLocales.count() == 1 -> currencyLocales.first() + currencyLocales.contains(configuration.locale) -> configuration.locale + else -> Locales.validSet.first { Currency.getInstance(it) == configuration.currency } + } + + val localCurrency = Currency.getInstance(clientLocale) + val currencySymbol = configuration.currency.getSymbol(currencyLocale) + + val usCurrencySymbol = Currency.getInstance(Locales.EN_US).getSymbol(Locales.EN_US) + val gbCurrencySymbol = Currency.getInstance(Locales.EN_GB).getSymbol(Locales.EN_GB) + + val currencyFormatter = (NumberFormat.getCurrencyInstance(clientLocale) as DecimalFormat).apply { + this.currency = configuration.currency + } + + if (clientLocale == Locales.EN_US) { + currencyFormatter.apply { + decimalFormatSymbols = decimalFormatSymbols.apply { + this.currencySymbol = when (configuration.currency) { + Currency.getInstance(Locales.EN_AU) -> "A$" + Currency.getInstance(Locales.EN_NZ) -> "NZ$" + Currency.getInstance(Locales.EN_CA) -> "CA$" + Currency.getInstance(Locales.FR_CA) -> "CA$" + else -> currencySymbol } - - val numberOfInstalments = numberOfInstalments(configuration.currency).toBigDecimal() - val instalment = totalCost.divide(numberOfInstalments, 2, RoundingMode.HALF_EVEN) - return Available(instalmentAmount = currencyFormatter.format(instalment)) + } } - - fun numberOfInstalments(currency: Currency): Int { - return 4 + } else if (configuration.currency != localCurrency) { + currencyFormatter.apply { + decimalFormatSymbols = decimalFormatSymbols.apply { + this.currencySymbol = currencySymbol + } + + when (currencySymbol) { + usCurrencySymbol -> this.applyPattern("¤#,##0.00 ¤¤") + gbCurrencySymbol -> this.applyPattern("¤#,##0.00") + } } + } + + val minimumAmount = configuration.minimumAmount ?: BigDecimal.ZERO + if (totalCost < minimumAmount || totalCost > configuration.maximumAmount) { + val currencyFormatterNoDecimals = currencyFormatter.clone() as DecimalFormat + currencyFormatterNoDecimals.maximumFractionDigits = 0 + + return NotAvailable( + minimumAmount = configuration.minimumAmount?.let(currencyFormatterNoDecimals::format), + maximumAmount = currencyFormatterNoDecimals.format(configuration.maximumAmount), + ) + } + + val numberOfInstalments = numberOfInstalments(configuration.currency).toBigDecimal() + val instalment = totalCost.divide(numberOfInstalments, 2, RoundingMode.HALF_EVEN) + return Available(instalmentAmount = currencyFormatter.format(instalment)) + } + + fun numberOfInstalments(currency: Currency): Int { + return 4 } + } } diff --git a/afterpay/src/main/kotlin/com/afterpay/android/internal/AfterpayString.kt b/afterpay/src/main/kotlin/com/afterpay/android/internal/AfterpayString.kt index 2aa3c73a..7ac354dd 100644 --- a/afterpay/src/main/kotlin/com/afterpay/android/internal/AfterpayString.kt +++ b/afterpay/src/main/kotlin/com/afterpay/android/internal/AfterpayString.kt @@ -24,105 +24,105 @@ import com.afterpay.android.internal.Locales.EN_US import com.afterpay.android.internal.Locales.FR_CA private val localeLanguages = mapOf( - EN_AU to AfterpayString.EN, - EN_GB to AfterpayString.EN, - EN_NZ to AfterpayString.EN, - EN_US to AfterpayString.EN, - EN_CA to AfterpayString.EN, - FR_CA to AfterpayString.FR_CA, + EN_AU to AfterpayString.EN, + EN_GB to AfterpayString.EN, + EN_NZ to AfterpayString.EN, + EN_US to AfterpayString.EN, + EN_CA to AfterpayString.EN, + FR_CA to AfterpayString.FR_CA, ) internal enum class AfterpayString( - val breakdownLimit: String, - val breakdownLimitDescription: String, + val breakdownLimit: String, + val breakdownLimitDescription: String, - val introOrTitle: String, - val introOr: String, - val introInTitle: String, - val introIn: String, - val introPayTitle: String, - val introPay: String, - val introPayInTitle: String, - val introPayIn: String, - val introMakeTitle: String, - val introMake: String, + val introOrTitle: String, + val introOr: String, + val introInTitle: String, + val introIn: String, + val introPayTitle: String, + val introPay: String, + val introPayInTitle: String, + val introPayIn: String, + val introMakeTitle: String, + val introMake: String, - val noConfigurationDescription: String, - val noConfiguration: String, + val noConfigurationDescription: String, + val noConfiguration: String, - val loadErrorTitle: String, - val loadErrorRetry: String, - val loadErrorCancel: String, - val loadErrorMessage: String, + val loadErrorTitle: String, + val loadErrorRetry: String, + val loadErrorCancel: String, + val loadErrorMessage: String, - val paymentButtonContentDescription: String, + val paymentButtonContentDescription: String, - val priceBreakdownAvailable: String, - val priceBreakdownAvailableDescription: String, - val priceBreakdownWith: String, - val priceBreakdownInterestFree: String, - val priceBreakdownLinkLearnMore: String, - val priceBreakdownLinkMoreInfo: String, + val priceBreakdownAvailable: String, + val priceBreakdownAvailableDescription: String, + val priceBreakdownWith: String, + val priceBreakdownInterestFree: String, + val priceBreakdownLinkLearnMore: String, + val priceBreakdownLinkMoreInfo: String, ) { - EN( - breakdownLimit = "available for orders between %1\$s – %2\$s", - breakdownLimitDescription = "%1\$s available for orders between %2\$s – %3\$s", - introOrTitle = "Or", - introOr = "or", - introInTitle = "In", - introIn = "in", - introPayTitle = "Pay", - introPay = "pay", - introPayInTitle = "Pay in", - introPayIn = "pay in", - introMakeTitle = "Make", - introMake = "make", - noConfigurationDescription = "Or pay with %1\$s", - noConfiguration = "or pay with", - loadErrorTitle = "Error", - loadErrorRetry = "Retry", - loadErrorCancel = "Cancel", - loadErrorMessage = "Failed to load %1\$s checkout", - paymentButtonContentDescription = "Pay now with %1\$s", - priceBreakdownAvailable = "%1\$s %2\$s %3\$spayments of %4\$s %5\$s", - priceBreakdownAvailableDescription = "%1\$s %2\$s %3\$spayments of %4\$s %5\$s%6\$s", - priceBreakdownWith = "with ", - priceBreakdownInterestFree = "interest-free ", - priceBreakdownLinkLearnMore = "Learn More", - priceBreakdownLinkMoreInfo = "More Info", - ), - FR_CA( - breakdownLimit = "disponible pour les montants entre %1\$s – %2\$s", - breakdownLimitDescription = "%1\$s disponible pour les montants entre %2\$s – %3\$s", - introOrTitle = "Ou", - introOr = "ou", - introInTitle = "En", - introIn = "en", - introPayTitle = "Payez", - introPay = "payez", - introPayInTitle = "Payez en", - introPayIn = "payez en", - introMakeTitle = "Effectuez", - introMake = "effectuez", - noConfigurationDescription = "Ou payer avec %1\$s", - noConfiguration = "ou payer avec", - loadErrorTitle = "Erreur", - loadErrorRetry = "Retenter", - loadErrorCancel = "Annuler", - loadErrorMessage = "Échec du chargement de la caisse %1\$s", - paymentButtonContentDescription = "Payez maintenant avec %1\$s", - priceBreakdownAvailable = "%1\$s %2\$s paiements %3\$sde %4\$s %5\$s", - priceBreakdownAvailableDescription = "%1\$s %2\$s paiements %3\$sde %4\$s %5\$s%6\$s", - priceBreakdownWith = "avec ", - priceBreakdownInterestFree = "sans intérêts ", - priceBreakdownLinkLearnMore = "En savoir plus", - priceBreakdownLinkMoreInfo = "Plus d'infos", - ), - ; + EN( + breakdownLimit = "available for orders between %1\$s – %2\$s", + breakdownLimitDescription = "%1\$s available for orders between %2\$s – %3\$s", + introOrTitle = "Or", + introOr = "or", + introInTitle = "In", + introIn = "in", + introPayTitle = "Pay", + introPay = "pay", + introPayInTitle = "Pay in", + introPayIn = "pay in", + introMakeTitle = "Make", + introMake = "make", + noConfigurationDescription = "Or pay with %1\$s", + noConfiguration = "or pay with", + loadErrorTitle = "Error", + loadErrorRetry = "Retry", + loadErrorCancel = "Cancel", + loadErrorMessage = "Failed to load %1\$s checkout", + paymentButtonContentDescription = "Pay now with %1\$s", + priceBreakdownAvailable = "%1\$s %2\$s %3\$spayments of %4\$s %5\$s", + priceBreakdownAvailableDescription = "%1\$s %2\$s %3\$spayments of %4\$s %5\$s%6\$s", + priceBreakdownWith = "with ", + priceBreakdownInterestFree = "interest-free ", + priceBreakdownLinkLearnMore = "Learn More", + priceBreakdownLinkMoreInfo = "More Info", + ), + FR_CA( + breakdownLimit = "disponible pour les montants entre %1\$s – %2\$s", + breakdownLimitDescription = "%1\$s disponible pour les montants entre %2\$s – %3\$s", + introOrTitle = "Ou", + introOr = "ou", + introInTitle = "En", + introIn = "en", + introPayTitle = "Payez", + introPay = "payez", + introPayInTitle = "Payez en", + introPayIn = "payez en", + introMakeTitle = "Effectuez", + introMake = "effectuez", + noConfigurationDescription = "Ou payer avec %1\$s", + noConfiguration = "ou payer avec", + loadErrorTitle = "Erreur", + loadErrorRetry = "Retenter", + loadErrorCancel = "Annuler", + loadErrorMessage = "Échec du chargement de la caisse %1\$s", + paymentButtonContentDescription = "Payez maintenant avec %1\$s", + priceBreakdownAvailable = "%1\$s %2\$s paiements %3\$sde %4\$s %5\$s", + priceBreakdownAvailableDescription = "%1\$s %2\$s paiements %3\$sde %4\$s %5\$s%6\$s", + priceBreakdownWith = "avec ", + priceBreakdownInterestFree = "sans intérêts ", + priceBreakdownLinkLearnMore = "En savoir plus", + priceBreakdownLinkMoreInfo = "Plus d'infos", + ), + ; - companion object { - fun forLocale(): AfterpayString { - return localeLanguages[Afterpay.language] ?: EN - } + companion object { + fun forLocale(): AfterpayString { + return localeLanguages[Afterpay.language] ?: EN } + } } diff --git a/afterpay/src/main/kotlin/com/afterpay/android/internal/ApiV3.kt b/afterpay/src/main/kotlin/com/afterpay/android/internal/ApiV3.kt index fcd0676b..353c842b 100644 --- a/afterpay/src/main/kotlin/com/afterpay/android/internal/ApiV3.kt +++ b/afterpay/src/main/kotlin/com/afterpay/android/internal/ApiV3.kt @@ -26,126 +26,126 @@ import javax.net.ssl.HttpsURLConnection import kotlin.Exception internal object ApiV3 { - private val json = Json { ignoreUnknownKeys = true } + private val json = Json { ignoreUnknownKeys = true } - internal inline fun request(url: URL, method: HttpVerb, body: B): Result { - val connection = url.openConnection() as HttpsURLConnection - return try { - configure(connection, method) - val payload = (body as? String) ?: json.encodeToString(body) + internal inline fun request(url: URL, method: HttpVerb, body: B): Result { + val connection = url.openConnection() as HttpsURLConnection + return try { + configure(connection, method) + val payload = (body as? String) ?: json.encodeToString(body) - val outputStreamWriter = OutputStreamWriter(connection.outputStream) - outputStreamWriter.write(payload) - outputStreamWriter.flush() + val outputStreamWriter = OutputStreamWriter(connection.outputStream) + outputStreamWriter.write(payload) + outputStreamWriter.flush() - // TODO: Status code checking, error object decoding, bypass if return type is Unit - val data = connection.inputStream.bufferedReader().readText() - connection.inputStream.close() - val result = json.decodeFromString(data) - Result.success(result) - } catch (exception: Exception) { - try { - val data = connection.errorStream.bufferedReader().readText() - connection.errorStream.close() - val result = json.decodeFromString(data) - Result.failure(InvalidObjectException(result.message)) - } catch (_: Exception) { - Result.failure(exception) - } - } finally { - connection.disconnect() - } + // TODO: Status code checking, error object decoding, bypass if return type is Unit + val data = connection.inputStream.bufferedReader().readText() + connection.inputStream.close() + val result = json.decodeFromString(data) + Result.success(result) + } catch (exception: Exception) { + try { + val data = connection.errorStream.bufferedReader().readText() + connection.errorStream.close() + val result = json.decodeFromString(data) + Result.failure(InvalidObjectException(result.message)) + } catch (_: Exception) { + Result.failure(exception) + } + } finally { + connection.disconnect() } + } - internal inline fun requestUnit(url: URL, method: HttpVerb, body: B): Result { - val connection = url.openConnection() as HttpsURLConnection - return try { - configure(connection, method) - val payload = (body as? String) ?: json.encodeToString(body) + internal inline fun requestUnit(url: URL, method: HttpVerb, body: B): Result { + val connection = url.openConnection() as HttpsURLConnection + return try { + configure(connection, method) + val payload = (body as? String) ?: json.encodeToString(body) - val outputStreamWriter = OutputStreamWriter(connection.outputStream) - outputStreamWriter.write(payload) - outputStreamWriter.flush() + val outputStreamWriter = OutputStreamWriter(connection.outputStream) + outputStreamWriter.write(payload) + outputStreamWriter.flush() - if (connection.errorStream == null && connection.responseCode < 400) { - Result.success(Unit) - } else { - throw InvalidObjectException("Unexpected response code: ${connection.responseCode}") - } - } catch (exception: Exception) { - try { - val data = connection.errorStream.bufferedReader().readText() - connection.errorStream.close() - val result = json.decodeFromString(data) - Result.failure(InvalidObjectException(result.message)) - } catch (_: Exception) { - Result.failure(exception) - } - } finally { - connection.disconnect() - } + if (connection.errorStream == null && connection.responseCode < 400) { + Result.success(Unit) + } else { + throw InvalidObjectException("Unexpected response code: ${connection.responseCode}") + } + } catch (exception: Exception) { + try { + val data = connection.errorStream.bufferedReader().readText() + connection.errorStream.close() + val result = json.decodeFromString(data) + Result.failure(InvalidObjectException(result.message)) + } catch (_: Exception) { + Result.failure(exception) + } + } finally { + connection.disconnect() } + } - internal inline fun get(url: URL): Result { - val connection = url.openConnection() as HttpsURLConnection - return try { - configure(connection, HttpVerb.GET) + internal inline fun get(url: URL): Result { + val connection = url.openConnection() as HttpsURLConnection + return try { + configure(connection, HttpVerb.GET) - val data = connection.inputStream.bufferedReader().readText() - connection.inputStream.close() - val result = json.decodeFromString(data) - Result.success(result) - } catch (exception: Exception) { - try { - val data = connection.errorStream.bufferedReader().readText() - connection.errorStream.close() - val result = json.decodeFromString(data) - Result.failure(InvalidObjectException(result.message)) - } catch (_: Exception) { - Result.failure(exception) - } - } finally { - connection.disconnect() - } + val data = connection.inputStream.bufferedReader().readText() + connection.inputStream.close() + val result = json.decodeFromString(data) + Result.success(result) + } catch (exception: Exception) { + try { + val data = connection.errorStream.bufferedReader().readText() + connection.errorStream.close() + val result = json.decodeFromString(data) + Result.failure(InvalidObjectException(result.message)) + } catch (_: Exception) { + Result.failure(exception) + } + } finally { + connection.disconnect() } + } - internal enum class HttpVerb { - POST, - PUT, - GET, - } + internal enum class HttpVerb { + POST, + PUT, + GET, + } - private fun configure(connection: HttpsURLConnection, type: HttpVerb) { - connection.requestMethod = type.name - connection.setRequestProperty("${BuildConfig.AfterpayLibraryVersion}-android", "X-Afterpay-SDK") - when (type) { - HttpVerb.POST, HttpVerb.PUT -> { - connection.setRequestProperty("Content-Type", "application/json") - connection.setRequestProperty("Accept", "application/json") - } - else -> { } - } - when (type) { - HttpVerb.GET -> { - connection.doInput = true - connection.doOutput = false - } - HttpVerb.PUT -> { - connection.doInput = true - connection.doOutput = false // TODO: What? - } - HttpVerb.POST -> { - connection.doInput = true - connection.doOutput = true - } - } + private fun configure(connection: HttpsURLConnection, type: HttpVerb) { + connection.requestMethod = type.name + connection.setRequestProperty("${BuildConfig.AfterpayLibraryVersion}-android", "X-Afterpay-SDK") + when (type) { + HttpVerb.POST, HttpVerb.PUT -> { + connection.setRequestProperty("Content-Type", "application/json") + connection.setRequestProperty("Accept", "application/json") + } + else -> { } + } + when (type) { + HttpVerb.GET -> { + connection.doInput = true + connection.doOutput = false + } + HttpVerb.PUT -> { + connection.doInput = true + connection.doOutput = false // TODO: What? + } + HttpVerb.POST -> { + connection.doInput = true + connection.doOutput = true + } } + } - @Serializable - internal data class ApiErrorV3( - val errorCode: String, - val errorId: String, - val message: String, - val httpStatusCode: Int, - ) + @Serializable + internal data class ApiErrorV3( + val errorCode: String, + val errorId: String, + val message: String, + val httpStatusCode: Int, + ) } diff --git a/afterpay/src/main/kotlin/com/afterpay/android/internal/Brand.kt b/afterpay/src/main/kotlin/com/afterpay/android/internal/Brand.kt index a07b76f6..c75304b7 100644 --- a/afterpay/src/main/kotlin/com/afterpay/android/internal/Brand.kt +++ b/afterpay/src/main/kotlin/com/afterpay/android/internal/Brand.kt @@ -27,38 +27,38 @@ import com.afterpay.android.internal.Locales.FR_CA import java.util.Locale private val brandLocales = mapOf( - setOf(EN_AU, EN_CA, FR_CA, EN_NZ, EN_US) to Brand.AFTERPAY, - setOf(EN_GB) to Brand.CLEARPAY, + setOf(EN_AU, EN_CA, FR_CA, EN_NZ, EN_US) to Brand.AFTERPAY, + setOf(EN_GB) to Brand.CLEARPAY, ) internal enum class Brand( - @StringRes val title: Int, - @StringRes val description: Int, - @DrawableRes val badgeForeground: Int, - @DrawableRes val badgeForegroundCropped: Int, - @DrawableRes val lockup: Int, + @StringRes val title: Int, + @StringRes val description: Int, + @DrawableRes val badgeForeground: Int, + @DrawableRes val badgeForegroundCropped: Int, + @DrawableRes val lockup: Int, ) { - AFTERPAY( - title = R.string.afterpay_service_name, - description = R.string.afterpay_service_name_description, - badgeForeground = R.drawable.afterpay_badge_fg, - badgeForegroundCropped = R.drawable.afterpay_badge_fg_cropped, - lockup = R.drawable.afterpay_lockup, - ), + AFTERPAY( + title = R.string.afterpay_service_name, + description = R.string.afterpay_service_name_description, + badgeForeground = R.drawable.afterpay_badge_fg, + badgeForegroundCropped = R.drawable.afterpay_badge_fg_cropped, + lockup = R.drawable.afterpay_lockup, + ), - CLEARPAY( - title = R.string.clearpay_service_name, - description = R.string.clearpay_service_name_description, - badgeForeground = R.drawable.clearpay_badge_fg, - badgeForegroundCropped = R.drawable.clearpay_badge_fg_cropped, - lockup = R.drawable.clearpay_lockup, - ), - ; + CLEARPAY( + title = R.string.clearpay_service_name, + description = R.string.clearpay_service_name_description, + badgeForeground = R.drawable.clearpay_badge_fg, + badgeForegroundCropped = R.drawable.clearpay_badge_fg_cropped, + lockup = R.drawable.clearpay_lockup, + ), + ; - companion object { + companion object { - fun forLocale(locale: Locale): Brand = - brandLocales.entries.find { locale in it.key }?.value ?: AFTERPAY - } + fun forLocale(locale: Locale): Brand = + brandLocales.entries.find { locale in it.key }?.value ?: AFTERPAY + } } diff --git a/afterpay/src/main/kotlin/com/afterpay/android/internal/CheckoutV3.kt b/afterpay/src/main/kotlin/com/afterpay/android/internal/CheckoutV3.kt index daa4a332..9060930b 100644 --- a/afterpay/src/main/kotlin/com/afterpay/android/internal/CheckoutV3.kt +++ b/afterpay/src/main/kotlin/com/afterpay/android/internal/CheckoutV3.kt @@ -27,184 +27,184 @@ import kotlinx.serialization.Serializable import java.util.Currency internal object CheckoutV3 { - @Serializable - data class MerchantReferenceUpdate( - val merchantReference: String, - val token: String, - val singleUseCardToken: String, - val ppaConfirmToken: String, - ) + @Serializable + data class MerchantReferenceUpdate( + val merchantReference: String, + val token: String, + val singleUseCardToken: String, + val ppaConfirmToken: String, + ) - @Serializable - data class Response( - val token: String, - val confirmMustBeCalledBefore: String?, - val redirectCheckoutUrl: String, - val singleUseCardToken: String, - ) + @Serializable + data class Response( + val token: String, + val confirmMustBeCalledBefore: String?, + val redirectCheckoutUrl: String, + val singleUseCardToken: String, + ) - @Serializable - data class Request( - val shopDirectoryId: String, - val shopDirectoryMerchantId: String, + @Serializable + data class Request( + val shopDirectoryId: String, + val shopDirectoryMerchantId: String, - val amount: Money, - val shippingAmount: Money?, - val taxAmount: Money?, + val amount: Money, + val shippingAmount: Money?, + val taxAmount: Money?, - val items: List, - val consumer: Consumer, - val merchant: Merchant, - val shipping: Contact?, - val billing: Contact?, - val isCashAppPay: Boolean?, - ) { - companion object { - @JvmStatic - fun create( - consumer: CheckoutV3Consumer, - isCashAppPay: Boolean?, - orderTotal: OrderTotal, - items: Array, - configuration: CheckoutV3Configuration, - ): Request { - val currency = Currency.getInstance(configuration.region.currencyCode) + val items: List, + val consumer: Consumer, + val merchant: Merchant, + val shipping: Contact?, + val billing: Contact?, + val isCashAppPay: Boolean?, + ) { + companion object { + @JvmStatic + fun create( + consumer: CheckoutV3Consumer, + isCashAppPay: Boolean?, + orderTotal: OrderTotal, + items: Array, + configuration: CheckoutV3Configuration, + ): Request { + val currency = Currency.getInstance(configuration.region.currencyCode) + + return Request( + shopDirectoryId = configuration.shopDirectoryId, + shopDirectoryMerchantId = configuration.shopDirectoryMerchantId, + amount = Money( + orderTotal.total, + currency, + ), + shippingAmount = Money(orderTotal.shipping, currency), + taxAmount = Money(orderTotal.tax, currency), + items = items.map { Item.create(it, configuration.region) }, + consumer = Consumer( + email = consumer.email, + givenNames = consumer.givenNames, + surname = consumer.surname, + phoneNumber = consumer.phoneNumber, + ), + merchant = Merchant( + redirectConfirmUrl = "https://static.afterpay.com", + redirectCancelUrl = "https://static.afterpay.com", + ), + shipping = Contact.create(consumer.shippingInformation), + billing = Contact.create(consumer.billingInformation), + // server only handles true or null + isCashAppPay = isCashAppPay?.let { if (!it) null else true }, + ) + } + } + } + + @Serializable + data class Item( + val name: String, + val quantity: UInt, + val price: Money, + val sku: String?, + val pageUrl: String?, + val imageUrl: String?, + val categories: List>?, + val estimatedShipmentDate: String?, + ) { + companion object { + @JvmStatic + fun create(item: CheckoutV3Item, region: AfterpayRegion): Item { + val currency = Currency.getInstance(region.currencyCode) + return Item( + name = item.name, + quantity = item.quantity, + price = Money(item.price, currency), + sku = item.sku, + pageUrl = item.pageUrl.toString(), + imageUrl = item.imageUrl.toString(), + categories = item.categories, + estimatedShipmentDate = item.estimatedShipmentDate, + ) + } + } + } + + @Serializable + data class Merchant( + val redirectConfirmUrl: String, + val redirectCancelUrl: String, + ) - return Request( - shopDirectoryId = configuration.shopDirectoryId, - shopDirectoryMerchantId = configuration.shopDirectoryMerchantId, - amount = Money( - orderTotal.total, - currency, - ), - shippingAmount = Money(orderTotal.shipping, currency), - taxAmount = Money(orderTotal.tax, currency), - items = items.map { Item.create(it, configuration.region) }, - consumer = Consumer( - email = consumer.email, - givenNames = consumer.givenNames, - surname = consumer.surname, - phoneNumber = consumer.phoneNumber, - ), - merchant = Merchant( - redirectConfirmUrl = "https://static.afterpay.com", - redirectCancelUrl = "https://static.afterpay.com", - ), - shipping = Contact.create(consumer.shippingInformation), - billing = Contact.create(consumer.billingInformation), - // server only handles true or null - isCashAppPay = isCashAppPay?.let { if (!it) null else true }, - ) - } - } + @Serializable + data class Consumer( + val email: String, + val givenNames: String?, + val surname: String?, + val phoneNumber: String?, + ) + + @Serializable + data class Contact( + val name: String, + val line1: String, + val line2: String?, + val area1: String?, + val area2: String?, + val region: String?, + val postcode: String?, + val countryCode: String, + val phoneNumber: String?, + ) { + companion object { + @JvmStatic + fun create(contact: CheckoutV3Contact?): Contact? { + contact ?: return null + return Contact( + name = contact.name, + line1 = contact.line1, + line2 = contact.line2, + area1 = contact.area1, + area2 = contact.area2, + region = contact.region, + postcode = contact.postcode, + countryCode = contact.countryCode, + phoneNumber = contact.phoneNumber, + ) + } } + } + object Confirmation { @Serializable - data class Item( - val name: String, - val quantity: UInt, - val price: Money, - val sku: String?, - val pageUrl: String?, - val imageUrl: String?, - val categories: List>?, - val estimatedShipmentDate: String?, + data class CashAppPayRequest( + val token: String, + val singleUseCardToken: String, + val cashAppPspInfo: CashAppPspInfo, ) { - companion object { - @JvmStatic - fun create(item: CheckoutV3Item, region: AfterpayRegion): Item { - val currency = Currency.getInstance(region.currencyCode) - return Item( - name = item.name, - quantity = item.quantity, - price = Money(item.price, currency), - sku = item.sku, - pageUrl = item.pageUrl.toString(), - imageUrl = item.imageUrl.toString(), - categories = item.categories, - estimatedShipmentDate = item.estimatedShipmentDate, - ) - } - } + @Serializable + data class CashAppPspInfo( + val externalCustomerId: String, + val externalGrantId: String, + val jwt: String, + ) } @Serializable - data class Merchant( - val redirectConfirmUrl: String, - val redirectCancelUrl: String, + data class CashAppPayResponse( + val paymentDetails: PaymentDetails, + val cardValidUntil: String?, ) @Serializable - data class Consumer( - val email: String, - val givenNames: String?, - val surname: String?, - val phoneNumber: String?, + data class Response( + val paymentDetails: PaymentDetails, + val cardValidUntil: String?, + val authToken: String, ) @Serializable - data class Contact( - val name: String, - val line1: String, - val line2: String?, - val area1: String?, - val area2: String?, - val region: String?, - val postcode: String?, - val countryCode: String, - val phoneNumber: String?, - ) { - companion object { - @JvmStatic - fun create(contact: CheckoutV3Contact?): Contact? { - contact ?: return null - return Contact( - name = contact.name, - line1 = contact.line1, - line2 = contact.line2, - area1 = contact.area1, - area2 = contact.area2, - region = contact.region, - postcode = contact.postcode, - countryCode = contact.countryCode, - phoneNumber = contact.phoneNumber, - ) - } - } - } - - object Confirmation { - @Serializable - data class CashAppPayRequest( - val token: String, - val singleUseCardToken: String, - val cashAppPspInfo: CashAppPspInfo, - ) { - @Serializable - data class CashAppPspInfo( - val externalCustomerId: String, - val externalGrantId: String, - val jwt: String, - ) - } - - @Serializable - data class CashAppPayResponse( - val paymentDetails: PaymentDetails, - val cardValidUntil: String?, - ) - - @Serializable - data class Response( - val paymentDetails: PaymentDetails, - val cardValidUntil: String?, - val authToken: String, - ) - - @Serializable - data class PaymentDetails( - val virtualCard: VirtualCard.Card? = null, - val virtualCardToken: VirtualCard.TokenizedCard? = null, - ) - } + data class PaymentDetails( + val virtualCard: VirtualCard.Card? = null, + val virtualCardToken: VirtualCard.TokenizedCard? = null, + ) + } } diff --git a/afterpay/src/main/kotlin/com/afterpay/android/internal/CheckoutV3ViewModel.kt b/afterpay/src/main/kotlin/com/afterpay/android/internal/CheckoutV3ViewModel.kt index 0faf0ba1..ccbb6ae3 100644 --- a/afterpay/src/main/kotlin/com/afterpay/android/internal/CheckoutV3ViewModel.kt +++ b/afterpay/src/main/kotlin/com/afterpay/android/internal/CheckoutV3ViewModel.kt @@ -26,53 +26,53 @@ import java.net.URL class CheckoutV3ViewModel(private var options: AfterpayCheckoutV3Options) : ViewModel() { - suspend fun performCheckoutRequest(): Result { - return runCatching { - val checkoutUrl = requireNotNull(options.checkoutUrl) - val checkoutPayload = requireNotNull(options.checkoutPayload) + suspend fun performCheckoutRequest(): Result { + return runCatching { + val checkoutUrl = requireNotNull(options.checkoutUrl) + val checkoutPayload = requireNotNull(options.checkoutPayload) - val response = withContext(Dispatchers.IO) { - ApiV3.request(checkoutUrl, ApiV3.HttpVerb.POST, checkoutPayload) - }.getOrThrow() + val response = withContext(Dispatchers.IO) { + ApiV3.request(checkoutUrl, ApiV3.HttpVerb.POST, checkoutPayload) + }.getOrThrow() - val builder = Uri.parse(response.redirectCheckoutUrl) - .buildUpon() - .appendQueryParameter("buyNow", options.buyNow.toString()) - .build() - val url = URL(builder.toString()) + val builder = Uri.parse(response.redirectCheckoutUrl) + .buildUpon() + .appendQueryParameter("buyNow", options.buyNow.toString()) + .build() + val url = URL(builder.toString()) - options = options.copy( - redirectUrl = url, - singleUseCardToken = response.singleUseCardToken, - token = response.token, - ) - url - } + options = options.copy( + redirectUrl = url, + singleUseCardToken = response.singleUseCardToken, + token = response.token, + ) + url } + } - suspend fun performConfirmationRequest(ppaConfirmToken: String): Result { - return runCatching { - val confirmationUrl = requireNotNull(options.confirmUrl) + suspend fun performConfirmationRequest(ppaConfirmToken: String): Result { + return runCatching { + val confirmationUrl = requireNotNull(options.confirmUrl) - val tokens = CheckoutV3Tokens( - token = requireNotNull(options.token), - singleUseCardToken = requireNotNull(options.singleUseCardToken), - ppaConfirmToken = ppaConfirmToken, - ) + val tokens = CheckoutV3Tokens( + token = requireNotNull(options.token), + singleUseCardToken = requireNotNull(options.singleUseCardToken), + ppaConfirmToken = ppaConfirmToken, + ) - val response = withContext(Dispatchers.IO) { - ApiV3.request( - confirmationUrl, - ApiV3.HttpVerb.POST, - tokens, - ) - }.getOrThrow() + val response = withContext(Dispatchers.IO) { + ApiV3.request( + confirmationUrl, + ApiV3.HttpVerb.POST, + tokens, + ) + }.getOrThrow() - CheckoutV3Data( - cardDetails = response.paymentDetails.virtualCard ?: response.paymentDetails.virtualCardToken!!, - cardValidUntilInternal = response.cardValidUntil, - tokens = tokens, - ) - } + CheckoutV3Data( + cardDetails = response.paymentDetails.virtualCard ?: response.paymentDetails.virtualCardToken!!, + cardValidUntilInternal = response.cardValidUntil, + tokens = tokens, + ) } + } } diff --git a/afterpay/src/main/kotlin/com/afterpay/android/internal/ConfigurationObservable.kt b/afterpay/src/main/kotlin/com/afterpay/android/internal/ConfigurationObservable.kt index 8ebee634..9ae61fcf 100644 --- a/afterpay/src/main/kotlin/com/afterpay/android/internal/ConfigurationObservable.kt +++ b/afterpay/src/main/kotlin/com/afterpay/android/internal/ConfigurationObservable.kt @@ -19,9 +19,9 @@ import com.afterpay.android.model.Configuration import java.util.Observable internal object ConfigurationObservable : Observable() { - fun configurationChanged(configuration: Configuration?) { - setChanged() - notifyObservers(configuration) - clearChanged() - } + fun configurationChanged(configuration: Configuration?) { + setChanged() + notifyObservers(configuration) + clearChanged() + } } diff --git a/afterpay/src/main/kotlin/com/afterpay/android/internal/Html.kt b/afterpay/src/main/kotlin/com/afterpay/android/internal/Html.kt index 1d0ba6bc..0dd18a1a 100644 --- a/afterpay/src/main/kotlin/com/afterpay/android/internal/Html.kt +++ b/afterpay/src/main/kotlin/com/afterpay/android/internal/Html.kt @@ -16,8 +16,8 @@ package com.afterpay.android.internal object Html { - internal const val LOADING: String = - """ + internal const val LOADING: String = + """ diff --git a/afterpay/src/main/kotlin/com/afterpay/android/internal/Intent.kt b/afterpay/src/main/kotlin/com/afterpay/android/internal/Intent.kt index 77dc7da9..2878d593 100644 --- a/afterpay/src/main/kotlin/com/afterpay/android/internal/Intent.kt +++ b/afterpay/src/main/kotlin/com/afterpay/android/internal/Intent.kt @@ -26,84 +26,84 @@ import kotlinx.serialization.json.Json import java.lang.Exception private object AfterpayIntent { - const val CHECKOUT_URL = "AFTERPAY_CHECKOUT_URL" - const val CHECKOUT_OPTIONS = "AFTERPAY_CHECKOUT_OPTIONS" - const val INFO_URL = "AFTERPAY_INFO_URL" - const val ORDER_TOKEN = "AFTERPAY_ORDER_TOKEN" - const val CANCELLATION_STATUS = "AFTERPAY_CANCELLATION_STATUS" - const val CANCELLATION_ERROR = "AFTERPAY_CANCELLATION_ERROR" - const val RESULT_DATA_V3 = "AFTERPAY_RESULT_DATA_V3" - const val SHOULD_LOAD_REDIRECT_URLS = "AFTERPAY_SHOULD_LOAD_REDIRECT_URLS" + const val CHECKOUT_URL = "AFTERPAY_CHECKOUT_URL" + const val CHECKOUT_OPTIONS = "AFTERPAY_CHECKOUT_OPTIONS" + const val INFO_URL = "AFTERPAY_INFO_URL" + const val ORDER_TOKEN = "AFTERPAY_ORDER_TOKEN" + const val CANCELLATION_STATUS = "AFTERPAY_CANCELLATION_STATUS" + const val CANCELLATION_ERROR = "AFTERPAY_CANCELLATION_ERROR" + const val RESULT_DATA_V3 = "AFTERPAY_RESULT_DATA_V3" + const val SHOULD_LOAD_REDIRECT_URLS = "AFTERPAY_SHOULD_LOAD_REDIRECT_URLS" } internal fun Intent.putCheckoutUrlExtra(url: String): Intent = - putExtra(AfterpayIntent.CHECKOUT_URL, url) + putExtra(AfterpayIntent.CHECKOUT_URL, url) internal fun Intent.getCheckoutUrlExtra(): String? = - getStringExtra(AfterpayIntent.CHECKOUT_URL) + getStringExtra(AfterpayIntent.CHECKOUT_URL) internal fun Intent.putCheckoutShouldLoadRedirectUrls(bool: Boolean): Intent = - putExtra(AfterpayIntent.SHOULD_LOAD_REDIRECT_URLS, bool) + putExtra(AfterpayIntent.SHOULD_LOAD_REDIRECT_URLS, bool) internal fun Intent.getCheckoutShouldLoadRedirectUrls(): Boolean = - getBooleanExtra(AfterpayIntent.SHOULD_LOAD_REDIRECT_URLS, false) + getBooleanExtra(AfterpayIntent.SHOULD_LOAD_REDIRECT_URLS, false) internal fun Intent.putCheckoutV2OptionsExtra(options: AfterpayCheckoutV2Options): Intent = - putExtra(AfterpayIntent.CHECKOUT_OPTIONS, options) + putExtra(AfterpayIntent.CHECKOUT_OPTIONS, options) internal fun Intent.getCheckoutV2OptionsExtra(): AfterpayCheckoutV2Options? = - getParcelableExtra(AfterpayIntent.CHECKOUT_OPTIONS) + getParcelableExtra(AfterpayIntent.CHECKOUT_OPTIONS) internal fun Intent.putCheckoutV3OptionsExtra(options: AfterpayCheckoutV3Options): Intent = - putExtra(AfterpayIntent.CHECKOUT_OPTIONS, options) + putExtra(AfterpayIntent.CHECKOUT_OPTIONS, options) internal fun Intent.getCheckoutV3OptionsExtra(): AfterpayCheckoutV3Options? = - getParcelableExtra(AfterpayIntent.CHECKOUT_OPTIONS) + getParcelableExtra(AfterpayIntent.CHECKOUT_OPTIONS) internal fun Intent.putOrderTokenExtra(token: String): Intent = - putExtra(AfterpayIntent.ORDER_TOKEN, token) + putExtra(AfterpayIntent.ORDER_TOKEN, token) internal fun Intent.getOrderTokenExtra(): String? = - getStringExtra(AfterpayIntent.ORDER_TOKEN) + getStringExtra(AfterpayIntent.ORDER_TOKEN) internal fun Intent.putResultDataV3(resultData: CheckoutV3Data): Intent { - val json = Json.encodeToString(resultData) - putExtra(AfterpayIntent.RESULT_DATA_V3, json) - return this + val json = Json.encodeToString(resultData) + putExtra(AfterpayIntent.RESULT_DATA_V3, json) + return this } internal fun Intent.getResultDataExtra(): CheckoutV3Data? { - val data = getStringExtra(AfterpayIntent.RESULT_DATA_V3) ?: return null - val json = Json { ignoreUnknownKeys = true } - return json.decodeFromString(data) + val data = getStringExtra(AfterpayIntent.RESULT_DATA_V3) ?: return null + val json = Json { ignoreUnknownKeys = true } + return json.decodeFromString(data) } internal fun Intent.putCancellationStatusExtra(status: CancellationStatus): Intent = - putExtra(AfterpayIntent.CANCELLATION_STATUS, status.name) + putExtra(AfterpayIntent.CANCELLATION_STATUS, status.name) internal fun Intent.getCancellationStatusExtra(): CancellationStatus? = try { - getStringExtra(AfterpayIntent.CANCELLATION_STATUS)?.let { enumValueOf(it) } + getStringExtra(AfterpayIntent.CANCELLATION_STATUS)?.let { enumValueOf(it) } } catch (_: Exception) { - null + null } internal fun Intent.putCancellationStatusExtraV3(status: CancellationStatusV3): Intent = - putExtra(AfterpayIntent.CANCELLATION_STATUS, status.name) + putExtra(AfterpayIntent.CANCELLATION_STATUS, status.name) internal fun Intent.getCancellationStatusExtraV3(): CancellationStatusV3? = try { - getStringExtra(AfterpayIntent.CANCELLATION_STATUS)?.let { enumValueOf(it) } + getStringExtra(AfterpayIntent.CANCELLATION_STATUS)?.let { enumValueOf(it) } } catch (_: Exception) { - null + null } internal fun Intent.putCancellationStatusExtraErrorV3(error: Exception): Intent = - putExtra(AfterpayIntent.CANCELLATION_ERROR, error) + putExtra(AfterpayIntent.CANCELLATION_ERROR, error) internal fun Intent.getCancellationStatusExtraErrorV3(): Exception? = - getSerializableExtra(AfterpayIntent.CANCELLATION_ERROR) as? Exception + getSerializableExtra(AfterpayIntent.CANCELLATION_ERROR) as? Exception internal fun Intent.putInfoUrlExtra(url: String): Intent = - putExtra(AfterpayIntent.INFO_URL, url) + putExtra(AfterpayIntent.INFO_URL, url) internal fun Intent.getInfoUrlExtra(): String? = - getStringExtra(AfterpayIntent.INFO_URL) + getStringExtra(AfterpayIntent.INFO_URL) diff --git a/afterpay/src/main/kotlin/com/afterpay/android/internal/Locales.kt b/afterpay/src/main/kotlin/com/afterpay/android/internal/Locales.kt index 03092270..9c2a9e22 100644 --- a/afterpay/src/main/kotlin/com/afterpay/android/internal/Locales.kt +++ b/afterpay/src/main/kotlin/com/afterpay/android/internal/Locales.kt @@ -18,33 +18,33 @@ package com.afterpay.android.internal import java.util.Locale private val validRegionLanguages = mapOf( - Locales.EN_AU.country to setOf(Locales.EN_AU), - Locales.EN_GB.country to setOf(Locales.EN_GB), - Locales.EN_NZ.country to setOf(Locales.EN_NZ), - Locales.EN_US.country to setOf(Locales.EN_US), - Locales.EN_CA.country to setOf(Locales.EN_CA, Locales.FR_CA), + Locales.EN_AU.country to setOf(Locales.EN_AU), + Locales.EN_GB.country to setOf(Locales.EN_GB), + Locales.EN_NZ.country to setOf(Locales.EN_NZ), + Locales.EN_US.country to setOf(Locales.EN_US), + Locales.EN_CA.country to setOf(Locales.EN_CA, Locales.FR_CA), ) internal object Locales { - val EN_AU: Locale = Locale("en", "AU") - val EN_CA: Locale = Locale.CANADA - val FR_CA: Locale = Locale.CANADA_FRENCH - val EN_NZ: Locale = Locale("en", "NZ") - val EN_GB: Locale = Locale.UK - val EN_US: Locale = Locale.US + val EN_AU: Locale = Locale("en", "AU") + val EN_CA: Locale = Locale.CANADA + val FR_CA: Locale = Locale.CANADA_FRENCH + val EN_NZ: Locale = Locale("en", "NZ") + val EN_GB: Locale = Locale.UK + val EN_US: Locale = Locale.US - val validSet = setOf( - EN_AU, - EN_CA, - FR_CA, - EN_GB, - EN_NZ, - EN_US, - ) + val validSet = setOf( + EN_AU, + EN_CA, + FR_CA, + EN_GB, + EN_NZ, + EN_US, + ) } internal fun getRegionLanguage(merchantLocale: Locale, consumerLocale: Locale): Locale? { - return validRegionLanguages[merchantLocale.country]?.find { - consumerLocale.language == it.language - } + return validRegionLanguages[merchantLocale.country]?.find { + consumerLocale.language == it.language + } } diff --git a/afterpay/src/main/kotlin/com/afterpay/android/internal/Resources.kt b/afterpay/src/main/kotlin/com/afterpay/android/internal/Resources.kt index 599837bd..45e2dd7a 100644 --- a/afterpay/src/main/kotlin/com/afterpay/android/internal/Resources.kt +++ b/afterpay/src/main/kotlin/com/afterpay/android/internal/Resources.kt @@ -30,44 +30,44 @@ import androidx.core.graphics.drawable.DrawableCompat import kotlin.math.roundToInt internal val Float.dp: Float - get() = TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, - this, - Resources.getSystem().displayMetrics, - ) + get() = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + this, + Resources.getSystem().displayMetrics, + ) internal val Int.dp: Int - get() = toFloat().dp.roundToInt() + get() = toFloat().dp.roundToInt() @ColorInt internal fun Context.resolveColorAttr(@AttrRes colorAttr: Int): Int { - val attribute = TypedValue().also { - theme.resolveAttribute(colorAttr, it, true) - } - val colorRes = if (attribute.resourceId != 0) attribute.resourceId else attribute.data - return ContextCompat.getColor(this, colorRes) + val attribute = TypedValue().also { + theme.resolveAttribute(colorAttr, it, true) + } + val colorRes = if (attribute.resourceId != 0) attribute.resourceId else attribute.data + return ContextCompat.getColor(this, colorRes) } @ColorInt internal fun Context.color(@ColorRes colorResId: Int): Int { - return ContextCompat.getColor(this, colorResId) + return ContextCompat.getColor(this, colorResId) } internal fun Context.coloredDrawable( - @DrawableRes drawableResId: Int, - @ColorRes colorResId: Int, + @DrawableRes drawableResId: Int, + @ColorRes colorResId: Int, ): Drawable = ContextCompat.getDrawable(this, drawableResId).let { - checkNotNull(it) { "Drawable resource not found" } - val wrappedDrawable = DrawableCompat.wrap(it) - DrawableCompat.setTint(wrappedDrawable, color(colorResId)) - return wrappedDrawable + checkNotNull(it) { "Drawable resource not found" } + val wrappedDrawable = DrawableCompat.wrap(it) + DrawableCompat.setTint(wrappedDrawable, color(colorResId)) + return wrappedDrawable } internal fun Context.rippleDrawable( - @ColorRes rippleColorResId: Int, - drawable: Drawable, + @ColorRes rippleColorResId: Int, + drawable: Drawable, ): Drawable = RippleDrawable( - ColorStateList.valueOf(color(rippleColorResId)), - drawable, - null, + ColorStateList.valueOf(color(rippleColorResId)), + drawable, + null, ) diff --git a/afterpay/src/main/kotlin/com/afterpay/android/internal/Serializers.kt b/afterpay/src/main/kotlin/com/afterpay/android/internal/Serializers.kt index 0a40d7a3..75057b87 100644 --- a/afterpay/src/main/kotlin/com/afterpay/android/internal/Serializers.kt +++ b/afterpay/src/main/kotlin/com/afterpay/android/internal/Serializers.kt @@ -32,44 +32,44 @@ import java.util.Currency internal object MoneyBigDecimalSerializer : KSerializer { - override val descriptor = PrimitiveSerialDescriptor( - serialName = "BigDecimal", - kind = PrimitiveKind.STRING, - ) + override val descriptor = PrimitiveSerialDescriptor( + serialName = "BigDecimal", + kind = PrimitiveKind.STRING, + ) - override fun deserialize(decoder: Decoder) = decoder.decodeString().toBigDecimal() + override fun deserialize(decoder: Decoder) = decoder.decodeString().toBigDecimal() - // Round to two decimals, as per ISO-4217, using banker's rounding - override fun serialize(encoder: Encoder, value: BigDecimal) { - return encoder.encodeString( - value.setScale(2, RoundingMode.HALF_EVEN).toPlainString(), - ) - } + // Round to two decimals, as per ISO-4217, using banker's rounding + override fun serialize(encoder: Encoder, value: BigDecimal) { + return encoder.encodeString( + value.setScale(2, RoundingMode.HALF_EVEN).toPlainString(), + ) + } } internal object CurrencySerializer : KSerializer { - override val descriptor = PrimitiveSerialDescriptor( - serialName = "Currency", - kind = PrimitiveKind.STRING, - ) + override val descriptor = PrimitiveSerialDescriptor( + serialName = "Currency", + kind = PrimitiveKind.STRING, + ) - override fun deserialize(decoder: Decoder): Currency = - Currency.getInstance(decoder.decodeString()) + override fun deserialize(decoder: Decoder): Currency = + Currency.getInstance(decoder.decodeString()) - override fun serialize(encoder: Encoder, value: Currency) = - encoder.encodeString(value.currencyCode) + override fun serialize(encoder: Encoder, value: Currency) = + encoder.encodeString(value.currencyCode) } internal object VirtualCardSerializer : JsonContentPolymorphicSerializer(VirtualCard::class) { - override fun selectDeserializer(element: JsonElement): DeserializationStrategy { - if (element.jsonObject.containsKey("cardToken")) { - return VirtualCard.TokenizedCard.serializer() - } - if (element.jsonObject.containsKey("cardNumber")) { - return VirtualCard.Card.serializer() - } - throw IllegalArgumentException("Unknown VirtualCard: JSON does not match any response type") + override fun selectDeserializer(element: JsonElement): DeserializationStrategy { + if (element.jsonObject.containsKey("cardToken")) { + return VirtualCard.TokenizedCard.serializer() + } + if (element.jsonObject.containsKey("cardNumber")) { + return VirtualCard.Card.serializer() } + throw IllegalArgumentException("Unknown VirtualCard: JSON does not match any response type") + } } diff --git a/afterpay/src/main/kotlin/com/afterpay/android/internal/WebView.kt b/afterpay/src/main/kotlin/com/afterpay/android/internal/WebView.kt index 936c67a0..3bbbc844 100644 --- a/afterpay/src/main/kotlin/com/afterpay/android/internal/WebView.kt +++ b/afterpay/src/main/kotlin/com/afterpay/android/internal/WebView.kt @@ -19,5 +19,5 @@ import android.webkit.WebView import com.afterpay.android.BuildConfig internal fun WebView.setAfterpayUserAgentString() = apply { - settings.userAgentString += " Afterpay-Android-SDK/${BuildConfig.AfterpayLibraryVersion}" + settings.userAgentString += " Afterpay-Android-SDK/${BuildConfig.AfterpayLibraryVersion}" } diff --git a/afterpay/src/main/kotlin/com/afterpay/android/model/AfterpayRegion.kt b/afterpay/src/main/kotlin/com/afterpay/android/model/AfterpayRegion.kt index 88f4a33f..85c35c01 100644 --- a/afterpay/src/main/kotlin/com/afterpay/android/model/AfterpayRegion.kt +++ b/afterpay/src/main/kotlin/com/afterpay/android/model/AfterpayRegion.kt @@ -19,6 +19,6 @@ import com.afterpay.android.internal.Locales import java.util.Locale enum class AfterpayRegion(val locale: Locale, val currencyCode: String) { - US(Locales.EN_US, "USD"), - CA(Locales.EN_CA, "CAD"), + US(Locales.EN_US, "USD"), + CA(Locales.EN_CA, "CAD"), } diff --git a/afterpay/src/main/kotlin/com/afterpay/android/model/CheckoutV3CashAppPay.kt b/afterpay/src/main/kotlin/com/afterpay/android/model/CheckoutV3CashAppPay.kt index 2fb902a6..adc2c730 100644 --- a/afterpay/src/main/kotlin/com/afterpay/android/model/CheckoutV3CashAppPay.kt +++ b/afterpay/src/main/kotlin/com/afterpay/android/model/CheckoutV3CashAppPay.kt @@ -16,11 +16,11 @@ package com.afterpay.android.model data class CheckoutV3CashAppPay( - val token: String, - val singleUseCardToken: String, - val amount: Double, - val redirectUri: String, - val merchantId: String, - val brandId: String, - val jwt: String, + val token: String, + val singleUseCardToken: String, + val amount: Double, + val redirectUri: String, + val merchantId: String, + val brandId: String, + val jwt: String, ) diff --git a/afterpay/src/main/kotlin/com/afterpay/android/model/CheckoutV3Configuration.kt b/afterpay/src/main/kotlin/com/afterpay/android/model/CheckoutV3Configuration.kt index 30aeedc3..277f4c07 100644 --- a/afterpay/src/main/kotlin/com/afterpay/android/model/CheckoutV3Configuration.kt +++ b/afterpay/src/main/kotlin/com/afterpay/android/model/CheckoutV3Configuration.kt @@ -20,50 +20,50 @@ import com.afterpay.android.AfterpayEnvironment import java.net.URL data class CheckoutV3Configuration( - val shopDirectoryMerchantId: String, - val region: AfterpayRegion, - val environment: AfterpayEnvironment, + val shopDirectoryMerchantId: String, + val region: AfterpayRegion, + val environment: AfterpayEnvironment, ) { - internal val shopDirectoryId: String - get() = when (region) { - AfterpayRegion.US -> when (environment) { - AfterpayEnvironment.SANDBOX -> "cd6b7914412b407d80aaf81d855d1105" - AfterpayEnvironment.PRODUCTION -> "e1e5632bebe64cee8e5daff8588e8f2f05ca4ed6ac524c76824c04e09033badc" - } - // Currently the same values as the US region - AfterpayRegion.CA -> when (environment) { - AfterpayEnvironment.SANDBOX -> "cd6b7914412b407d80aaf81d855d1105" - AfterpayEnvironment.PRODUCTION -> "e1e5632bebe64cee8e5daff8588e8f2f05ca4ed6ac524c76824c04e09033badc" - } - } + internal val shopDirectoryId: String + get() = when (region) { + AfterpayRegion.US -> when (environment) { + AfterpayEnvironment.SANDBOX -> "cd6b7914412b407d80aaf81d855d1105" + AfterpayEnvironment.PRODUCTION -> "e1e5632bebe64cee8e5daff8588e8f2f05ca4ed6ac524c76824c04e09033badc" + } + // Currently the same values as the US region + AfterpayRegion.CA -> when (environment) { + AfterpayEnvironment.SANDBOX -> "cd6b7914412b407d80aaf81d855d1105" + AfterpayEnvironment.PRODUCTION -> "e1e5632bebe64cee8e5daff8588e8f2f05ca4ed6ac524c76824c04e09033badc" + } + } - private val baseUrl: String - get() = when (region) { - AfterpayRegion.US -> when (environment) { - AfterpayEnvironment.SANDBOX -> "https://api-plus.us-sandbox.afterpay.com/v3/button" - AfterpayEnvironment.PRODUCTION -> "https://api-plus.us.afterpay.com/v3/button" - } - // Currently the same URLs as the US region - AfterpayRegion.CA -> when (environment) { - AfterpayEnvironment.SANDBOX -> "https://api-plus.us-sandbox.afterpay.com/v3/button" - AfterpayEnvironment.PRODUCTION -> "https://api-plus.us.afterpay.com/v3/button" - } - } + private val baseUrl: String + get() = when (region) { + AfterpayRegion.US -> when (environment) { + AfterpayEnvironment.SANDBOX -> "https://api-plus.us-sandbox.afterpay.com/v3/button" + AfterpayEnvironment.PRODUCTION -> "https://api-plus.us.afterpay.com/v3/button" + } + // Currently the same URLs as the US region + AfterpayRegion.CA -> when (environment) { + AfterpayEnvironment.SANDBOX -> "https://api-plus.us-sandbox.afterpay.com/v3/button" + AfterpayEnvironment.PRODUCTION -> "https://api-plus.us.afterpay.com/v3/button" + } + } - val v3CheckoutUrl: URL - get() = URL(baseUrl) + val v3CheckoutUrl: URL + get() = URL(baseUrl) - val v3CheckoutConfirmationUrl: URL - get() = URL("$baseUrl/confirm") + val v3CheckoutConfirmationUrl: URL + get() = URL("$baseUrl/confirm") - val v3ConfigurationUrl: URL - get() { - val url = "$baseUrl/merchant/config" - val builder = Uri.parse(url) - .buildUpon() - .appendQueryParameter("shopDirectoryId", shopDirectoryId) - .appendQueryParameter("shopDirectoryMerchantId", shopDirectoryMerchantId) - .build() - return URL(builder.toString()) - } + val v3ConfigurationUrl: URL + get() { + val url = "$baseUrl/merchant/config" + val builder = Uri.parse(url) + .buildUpon() + .appendQueryParameter("shopDirectoryId", shopDirectoryId) + .appendQueryParameter("shopDirectoryMerchantId", shopDirectoryMerchantId) + .build() + return URL(builder.toString()) + } } diff --git a/afterpay/src/main/kotlin/com/afterpay/android/model/CheckoutV3Consumer.kt b/afterpay/src/main/kotlin/com/afterpay/android/model/CheckoutV3Consumer.kt index 7529384b..7b330c65 100644 --- a/afterpay/src/main/kotlin/com/afterpay/android/model/CheckoutV3Consumer.kt +++ b/afterpay/src/main/kotlin/com/afterpay/android/model/CheckoutV3Consumer.kt @@ -16,21 +16,21 @@ package com.afterpay.android.model interface CheckoutV3Consumer { - /** The consumer’s email address. Limited to 128 characters. **/ - val email: String + /** The consumer’s email address. Limited to 128 characters. **/ + val email: String - /** The consumer’s first name and any middle names. Limited to 128 characters. **/ - val givenNames: String? + /** The consumer’s first name and any middle names. Limited to 128 characters. **/ + val givenNames: String? - /** The consumer’s last name. Limited to 128 characters. **/ - val surname: String? + /** The consumer’s last name. Limited to 128 characters. **/ + val surname: String? - /** The consumer’s phone number. Limited to 32 characters. **/ - val phoneNumber: String? + /** The consumer’s phone number. Limited to 32 characters. **/ + val phoneNumber: String? - /** The consumer's shipping information. **/ - val shippingInformation: CheckoutV3Contact? + /** The consumer's shipping information. **/ + val shippingInformation: CheckoutV3Contact? - /** The consumer's billing information. **/ - val billingInformation: CheckoutV3Contact? + /** The consumer's billing information. **/ + val billingInformation: CheckoutV3Contact? } diff --git a/afterpay/src/main/kotlin/com/afterpay/android/model/CheckoutV3Contact.kt b/afterpay/src/main/kotlin/com/afterpay/android/model/CheckoutV3Contact.kt index b4fa95fa..62089ea9 100644 --- a/afterpay/src/main/kotlin/com/afterpay/android/model/CheckoutV3Contact.kt +++ b/afterpay/src/main/kotlin/com/afterpay/android/model/CheckoutV3Contact.kt @@ -16,32 +16,32 @@ package com.afterpay.android.model interface CheckoutV3Contact { - /** Full name of contact. Limited to 255 characters */ - var name: String + /** Full name of contact. Limited to 255 characters */ + var name: String - /** First line of the address. Limited to 128 characters */ - var line1: String + /** First line of the address. Limited to 128 characters */ + var line1: String - /** Second line of the address. Limited to 128 characters. */ - var line2: String? + /** Second line of the address. Limited to 128 characters. */ + var line2: String? - /** Australian suburb, U.S. city, New Zealand town or city, U.K. Postal town. - * Maximum length is 128 characters. - */ - var area1: String? + /** Australian suburb, U.S. city, New Zealand town or city, U.K. Postal town. + * Maximum length is 128 characters. + */ + var area1: String? - /** New Zealand suburb, U.K. village or local area. Maximum length is 128 characters. */ - var area2: String? + /** New Zealand suburb, U.K. village or local area. Maximum length is 128 characters. */ + var area2: String? - /** U.S. state, Australian state, U.K. county, New Zealand region. Maximum length is 128 characters. */ - var region: String? + /** U.S. state, Australian state, U.K. county, New Zealand region. Maximum length is 128 characters. */ + var region: String? - /** The zip code or equivalent. Maximum length is 128 characters. */ - var postcode: String? + /** The zip code or equivalent. Maximum length is 128 characters. */ + var postcode: String? - /** The two-character ISO 3166-1 country code. */ - var countryCode: String + /** The two-character ISO 3166-1 country code. */ + var countryCode: String - /** The phone number, in E.123 format. Maximum length is 32 characters. */ - var phoneNumber: String? + /** The phone number, in E.123 format. Maximum length is 32 characters. */ + var phoneNumber: String? } diff --git a/afterpay/src/main/kotlin/com/afterpay/android/model/CheckoutV3Data.kt b/afterpay/src/main/kotlin/com/afterpay/android/model/CheckoutV3Data.kt index 88707aa5..abf547c3 100644 --- a/afterpay/src/main/kotlin/com/afterpay/android/model/CheckoutV3Data.kt +++ b/afterpay/src/main/kotlin/com/afterpay/android/model/CheckoutV3Data.kt @@ -26,39 +26,39 @@ import java.time.Instant /** Data returned from a successful V3 checkout */ @Serializable data class CheckoutV3Data( - /** The virtual card details */ - val cardDetails: VirtualCard, - /** The time before which an authorization needs to be made on the virtual card. */ - internal val cardValidUntilInternal: String?, - /** The collection of tokens required to update the merchant reference or cancel the virtual card */ - val tokens: CheckoutV3Tokens, + /** The virtual card details */ + val cardDetails: VirtualCard, + /** The time before which an authorization needs to be made on the virtual card. */ + internal val cardValidUntilInternal: String?, + /** The collection of tokens required to update the merchant reference or cancel the virtual card */ + val tokens: CheckoutV3Tokens, ) : Parcelable { - constructor(parcel: Parcel) : this( - cardDetails = parcel.readString()?.let { Json.decodeFromString(it) } ?: throw IllegalArgumentException("Missing Serialized value for `cardDetails`"), - cardValidUntilInternal = parcel.readString(), - tokens = parcel.readString()?.let { Json.decodeFromString(it) } ?: throw IllegalArgumentException("Missing Serialized value `tokens`"), - ) + constructor(parcel: Parcel) : this( + cardDetails = parcel.readString()?.let { Json.decodeFromString(it) } ?: throw IllegalArgumentException("Missing Serialized value for `cardDetails`"), + cardValidUntilInternal = parcel.readString(), + tokens = parcel.readString()?.let { Json.decodeFromString(it) } ?: throw IllegalArgumentException("Missing Serialized value `tokens`"), + ) - override fun writeToParcel(parcel: Parcel, flags: Int) { - parcel.writeString(Json.encodeToString(cardDetails)) - parcel.writeString(cardValidUntilInternal) - parcel.writeString(Json.encodeToString(tokens)) - } + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeString(Json.encodeToString(cardDetails)) + parcel.writeString(cardValidUntilInternal) + parcel.writeString(Json.encodeToString(tokens)) + } - override fun describeContents(): Int { - return 0 - } + override fun describeContents(): Int { + return 0 + } - companion object CREATOR : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): CheckoutV3Data { - return CheckoutV3Data(parcel) - } + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): CheckoutV3Data { + return CheckoutV3Data(parcel) + } - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } + override fun newArray(size: Int): Array { + return arrayOfNulls(size) } + } - val cardValidUntil: Instant? - get() = cardValidUntilInternal?.let { Instant.parse(it) } + val cardValidUntil: Instant? + get() = cardValidUntilInternal?.let { Instant.parse(it) } } diff --git a/afterpay/src/main/kotlin/com/afterpay/android/model/CheckoutV3Item.kt b/afterpay/src/main/kotlin/com/afterpay/android/model/CheckoutV3Item.kt index 87d74f0c..fdd2014b 100644 --- a/afterpay/src/main/kotlin/com/afterpay/android/model/CheckoutV3Item.kt +++ b/afterpay/src/main/kotlin/com/afterpay/android/model/CheckoutV3Item.kt @@ -19,31 +19,31 @@ import java.math.BigDecimal import java.net.URL interface CheckoutV3Item { - /** Product name. Limited to 255 characters. */ - val name: String + /** Product name. Limited to 255 characters. */ + val name: String - /** The quantity of the item, stored as a signed 32-bit integer. */ - val quantity: UInt + /** The quantity of the item, stored as a signed 32-bit integer. */ + val quantity: UInt - /** The unit price of the individual item. Must be a positive value. */ - val price: BigDecimal + /** The unit price of the individual item. Must be a positive value. */ + val price: BigDecimal - /** Product SKU. Limited to 128 characters. */ - val sku: String? + /** Product SKU. Limited to 128 characters. */ + val sku: String? - /** The canonical URL for the item's Product Detail Page. Limited to 2048 characters. */ - val pageUrl: URL? + /** The canonical URL for the item's Product Detail Page. Limited to 2048 characters. */ + val pageUrl: URL? - /** A URL for a web-optimised photo of the item, suitable for use directly as the src attribute of an img tag. - * Limited to 2048 characters. - */ - val imageUrl: URL? + /** A URL for a web-optimised photo of the item, suitable for use directly as the src attribute of an img tag. + * Limited to 2048 characters. + */ + val imageUrl: URL? - /** An array of arrays to accommodate multiple categories that might apply to the item. - * Each array contains comma separated strings with the left-most category being the top level category. - */ - val categories: List>? + /** An array of arrays to accommodate multiple categories that might apply to the item. + * Each array contains comma separated strings with the left-most category being the top level category. + */ + val categories: List>? - /** The estimated date when the order will be shipped. YYYY-MM or YYYY-MM-DD format. */ - val estimatedShipmentDate: String? + /** The estimated date when the order will be shipped. YYYY-MM or YYYY-MM-DD format. */ + val estimatedShipmentDate: String? } diff --git a/afterpay/src/main/kotlin/com/afterpay/android/model/CheckoutV3Tokens.kt b/afterpay/src/main/kotlin/com/afterpay/android/model/CheckoutV3Tokens.kt index 0a875d40..6536c75f 100644 --- a/afterpay/src/main/kotlin/com/afterpay/android/model/CheckoutV3Tokens.kt +++ b/afterpay/src/main/kotlin/com/afterpay/android/model/CheckoutV3Tokens.kt @@ -19,7 +19,7 @@ import kotlinx.serialization.Serializable @Serializable data class CheckoutV3Tokens( - val token: String, - val singleUseCardToken: String, - val ppaConfirmToken: String, + val token: String, + val singleUseCardToken: String, + val ppaConfirmToken: String, ) diff --git a/afterpay/src/main/kotlin/com/afterpay/android/model/Configuration.kt b/afterpay/src/main/kotlin/com/afterpay/android/model/Configuration.kt index 41621e19..4f4115c9 100644 --- a/afterpay/src/main/kotlin/com/afterpay/android/model/Configuration.kt +++ b/afterpay/src/main/kotlin/com/afterpay/android/model/Configuration.kt @@ -21,10 +21,10 @@ import java.util.Currency import java.util.Locale data class Configuration( - val minimumAmount: BigDecimal?, - val maximumAmount: BigDecimal, - val currency: Currency, - val locale: Locale, - val environment: AfterpayEnvironment, - val consumerLocale: Locale? = null, + val minimumAmount: BigDecimal?, + val maximumAmount: BigDecimal, + val currency: Currency, + val locale: Locale, + val environment: AfterpayEnvironment, + val consumerLocale: Locale? = null, ) diff --git a/afterpay/src/main/kotlin/com/afterpay/android/model/Consumer.kt b/afterpay/src/main/kotlin/com/afterpay/android/model/Consumer.kt index 6e225ed0..3ccec45e 100644 --- a/afterpay/src/main/kotlin/com/afterpay/android/model/Consumer.kt +++ b/afterpay/src/main/kotlin/com/afterpay/android/model/Consumer.kt @@ -19,10 +19,10 @@ package com.afterpay.android.model * A minimal implementation of [CheckoutV3Consumer] */ data class Consumer( - override var email: String, - override var givenNames: String? = null, - override var surname: String? = null, - override var phoneNumber: String? = null, - override var shippingInformation: CheckoutV3Contact? = null, - override var billingInformation: CheckoutV3Contact? = null, + override var email: String, + override var givenNames: String? = null, + override var surname: String? = null, + override var phoneNumber: String? = null, + override var shippingInformation: CheckoutV3Contact? = null, + override var billingInformation: CheckoutV3Contact? = null, ) : CheckoutV3Consumer diff --git a/afterpay/src/main/kotlin/com/afterpay/android/model/MerchantConfigurationV3.kt b/afterpay/src/main/kotlin/com/afterpay/android/model/MerchantConfigurationV3.kt index 5c4f3141..36a51cc5 100644 --- a/afterpay/src/main/kotlin/com/afterpay/android/model/MerchantConfigurationV3.kt +++ b/afterpay/src/main/kotlin/com/afterpay/android/model/MerchantConfigurationV3.kt @@ -19,6 +19,6 @@ import kotlinx.serialization.Serializable @Serializable data class MerchantConfigurationV3( - val minimumAmount: Money, - val maximumAmount: Money, + val minimumAmount: Money, + val maximumAmount: Money, ) diff --git a/afterpay/src/main/kotlin/com/afterpay/android/model/Money.kt b/afterpay/src/main/kotlin/com/afterpay/android/model/Money.kt index 35a72c71..b2d01e5f 100644 --- a/afterpay/src/main/kotlin/com/afterpay/android/model/Money.kt +++ b/afterpay/src/main/kotlin/com/afterpay/android/model/Money.kt @@ -23,6 +23,6 @@ import java.util.Currency @Serializable data class Money( - @Serializable(with = MoneyBigDecimalSerializer::class) val amount: BigDecimal, - @Serializable(with = CurrencySerializer::class) val currency: Currency, + @Serializable(with = MoneyBigDecimalSerializer::class) val amount: BigDecimal, + @Serializable(with = CurrencySerializer::class) val currency: Currency, ) diff --git a/afterpay/src/main/kotlin/com/afterpay/android/model/OrderTotal.kt b/afterpay/src/main/kotlin/com/afterpay/android/model/OrderTotal.kt index e6905009..b5fc3698 100644 --- a/afterpay/src/main/kotlin/com/afterpay/android/model/OrderTotal.kt +++ b/afterpay/src/main/kotlin/com/afterpay/android/model/OrderTotal.kt @@ -23,10 +23,10 @@ import java.math.BigDecimal * - Including the currency code as provided by [AfterpayRegion]. */ data class OrderTotal( - /** Amount to be charged to consumer, inclusive of [shipping] and [tax]. */ - val total: BigDecimal, - /** The shipping amount, included for fraud detection purposes. */ - val shipping: BigDecimal, - /** The tax amount, included for fraud detection purposes. */ - val tax: BigDecimal, + /** Amount to be charged to consumer, inclusive of [shipping] and [tax]. */ + val total: BigDecimal, + /** The shipping amount, included for fraud detection purposes. */ + val shipping: BigDecimal, + /** The tax amount, included for fraud detection purposes. */ + val tax: BigDecimal, ) diff --git a/afterpay/src/main/kotlin/com/afterpay/android/model/ShippingAddress.kt b/afterpay/src/main/kotlin/com/afterpay/android/model/ShippingAddress.kt index c6a5f162..48987ce6 100644 --- a/afterpay/src/main/kotlin/com/afterpay/android/model/ShippingAddress.kt +++ b/afterpay/src/main/kotlin/com/afterpay/android/model/ShippingAddress.kt @@ -19,12 +19,12 @@ import kotlinx.serialization.Serializable @Serializable data class ShippingAddress( - val name: String?, - val address1: String?, - val address2: String? = null, - val countryCode: String? = null, - val postcode: String?, - val phoneNumber: String? = null, - val state: String? = null, - val suburb: String? = null, + val name: String?, + val address1: String?, + val address2: String? = null, + val countryCode: String? = null, + val postcode: String?, + val phoneNumber: String? = null, + val state: String? = null, + val suburb: String? = null, ) diff --git a/afterpay/src/main/kotlin/com/afterpay/android/model/ShippingOption.kt b/afterpay/src/main/kotlin/com/afterpay/android/model/ShippingOption.kt index 2b0fd8ab..c81a6e9d 100644 --- a/afterpay/src/main/kotlin/com/afterpay/android/model/ShippingOption.kt +++ b/afterpay/src/main/kotlin/com/afterpay/android/model/ShippingOption.kt @@ -19,10 +19,10 @@ import kotlinx.serialization.Serializable @Serializable data class ShippingOption( - val id: String, - val name: String, - val description: String, - var shippingAmount: Money, - var orderAmount: Money, - var taxAmount: Money?, + val id: String, + val name: String, + val description: String, + var shippingAmount: Money, + var orderAmount: Money, + var taxAmount: Money?, ) diff --git a/afterpay/src/main/kotlin/com/afterpay/android/model/ShippingOptionUpdate.kt b/afterpay/src/main/kotlin/com/afterpay/android/model/ShippingOptionUpdate.kt index d035f1b8..f0573ab5 100644 --- a/afterpay/src/main/kotlin/com/afterpay/android/model/ShippingOptionUpdate.kt +++ b/afterpay/src/main/kotlin/com/afterpay/android/model/ShippingOptionUpdate.kt @@ -19,8 +19,8 @@ import kotlinx.serialization.Serializable @Serializable data class ShippingOptionUpdate( - val id: String, - var shippingAmount: Money, - var orderAmount: Money, - var taxAmount: Money?, + val id: String, + var shippingAmount: Money, + var orderAmount: Money, + var taxAmount: Money?, ) diff --git a/afterpay/src/main/kotlin/com/afterpay/android/model/ShippingOptionUpdateResult.kt b/afterpay/src/main/kotlin/com/afterpay/android/model/ShippingOptionUpdateResult.kt index 744aad17..3ba3eb1a 100644 --- a/afterpay/src/main/kotlin/com/afterpay/android/model/ShippingOptionUpdateResult.kt +++ b/afterpay/src/main/kotlin/com/afterpay/android/model/ShippingOptionUpdateResult.kt @@ -18,14 +18,14 @@ package com.afterpay.android.model sealed class ShippingOptionUpdateResult data class ShippingOptionUpdateSuccessResult( - val shippingOptionUpdate: ShippingOptionUpdate, + val shippingOptionUpdate: ShippingOptionUpdate, ) : ShippingOptionUpdateResult() data class ShippingOptionUpdateErrorResult( - val error: ShippingOptionUpdateError, + val error: ShippingOptionUpdateError, ) : ShippingOptionUpdateResult() enum class ShippingOptionUpdateError { - SERVICE_UNAVAILABLE, - BAD_RESPONSE, + SERVICE_UNAVAILABLE, + BAD_RESPONSE, } diff --git a/afterpay/src/main/kotlin/com/afterpay/android/model/ShippingOptionsResult.kt b/afterpay/src/main/kotlin/com/afterpay/android/model/ShippingOptionsResult.kt index bb679112..e94ea12a 100644 --- a/afterpay/src/main/kotlin/com/afterpay/android/model/ShippingOptionsResult.kt +++ b/afterpay/src/main/kotlin/com/afterpay/android/model/ShippingOptionsResult.kt @@ -18,16 +18,16 @@ package com.afterpay.android.model sealed class ShippingOptionsResult data class ShippingOptionsSuccessResult( - val shippingOptions: List, + val shippingOptions: List, ) : ShippingOptionsResult() data class ShippingOptionsErrorResult( - val error: ShippingOptionsError, + val error: ShippingOptionsError, ) : ShippingOptionsResult() enum class ShippingOptionsError { - SHIPPING_ADDRESS_UNRECOGNIZED, - SHIPPING_ADDRESS_UNSUPPORTED, - SERVICE_UNAVAILABLE, - BAD_RESPONSE, + SHIPPING_ADDRESS_UNRECOGNIZED, + SHIPPING_ADDRESS_UNSUPPORTED, + SERVICE_UNAVAILABLE, + BAD_RESPONSE, } diff --git a/afterpay/src/main/kotlin/com/afterpay/android/model/VirtualCard.kt b/afterpay/src/main/kotlin/com/afterpay/android/model/VirtualCard.kt index 5052b223..86be6435 100644 --- a/afterpay/src/main/kotlin/com/afterpay/android/model/VirtualCard.kt +++ b/afterpay/src/main/kotlin/com/afterpay/android/model/VirtualCard.kt @@ -21,34 +21,34 @@ import kotlinx.serialization.Transient @Serializable(with = VirtualCardSerializer::class) sealed class VirtualCard { - @Serializable - data class Card( - val cardType: String, - val cardNumber: String?, - val cvc: String, - private val expiry: String, - @Transient var expiryYear: Int = -1, - @Transient var expiryMonth: Int = -1, - ) : VirtualCard() { - init { - val components = expiry.split("-").map { it.toInt() } - expiryYear = components[0] - expiryMonth = components[1] - } + @Serializable + data class Card( + val cardType: String, + val cardNumber: String?, + val cvc: String, + private val expiry: String, + @Transient var expiryYear: Int = -1, + @Transient var expiryMonth: Int = -1, + ) : VirtualCard() { + init { + val components = expiry.split("-").map { it.toInt() } + expiryYear = components[0] + expiryMonth = components[1] } + } - @Serializable - data class TokenizedCard( - val paymentGateway: String, - val cardToken: String?, - private val expiry: String, - @Transient var expiryYear: Int = -1, - @Transient var expiryMonth: Int = -1, - ) : VirtualCard() { - init { - val components = expiry.split("-").map { it.toInt() } - expiryYear = components[0] - expiryMonth = components[1] - } + @Serializable + data class TokenizedCard( + val paymentGateway: String, + val cardToken: String?, + private val expiry: String, + @Transient var expiryYear: Int = -1, + @Transient var expiryMonth: Int = -1, + ) : VirtualCard() { + init { + val components = expiry.split("-").map { it.toInt() } + expiryYear = components[0] + expiryMonth = components[1] } + } } diff --git a/afterpay/src/main/kotlin/com/afterpay/android/view/AfterpayBadge.kt b/afterpay/src/main/kotlin/com/afterpay/android/view/AfterpayBadge.kt index 017ec8e5..0b46e75a 100644 --- a/afterpay/src/main/kotlin/com/afterpay/android/view/AfterpayBadge.kt +++ b/afterpay/src/main/kotlin/com/afterpay/android/view/AfterpayBadge.kt @@ -29,50 +29,50 @@ import com.afterpay.android.internal.dp private const val MIN_WIDTH: Int = 64 class AfterpayBadge @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, + context: Context, + attrs: AttributeSet? = null, ) : AppCompatImageView(context, attrs) { - var colorScheme: AfterpayColorScheme = AfterpayColorScheme.DEFAULT - set(value) { - field = value - update() - } + var colorScheme: AfterpayColorScheme = AfterpayColorScheme.DEFAULT + set(value) { + field = value + update() + } - init { - contentDescription = resources.getString(Afterpay.brand.title) - importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_YES - isFocusable = true - scaleType = FIT_CENTER - adjustViewBounds = true - minimumWidth = MIN_WIDTH.dp + init { + contentDescription = resources.getString(Afterpay.brand.title) + importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_YES + isFocusable = true + scaleType = FIT_CENTER + adjustViewBounds = true + minimumWidth = MIN_WIDTH.dp - context.theme.obtainStyledAttributes(attrs, R.styleable.Afterpay, 0, 0).use { attributes -> - colorScheme = AfterpayColorScheme.values()[ - attributes.getInteger( - R.styleable.Afterpay_afterpayColorScheme, - AfterpayColorScheme.DEFAULT.ordinal, - ), - ] - } + context.theme.obtainStyledAttributes(attrs, R.styleable.Afterpay, 0, 0).use { attributes -> + colorScheme = AfterpayColorScheme.values()[ + attributes.getInteger( + R.styleable.Afterpay_afterpayColorScheme, + AfterpayColorScheme.DEFAULT.ordinal, + ), + ] } + } - private fun update() { - visibility = if (!Afterpay.enabled) View.GONE else View.VISIBLE + private fun update() { + visibility = if (!Afterpay.enabled) View.GONE else View.VISIBLE - setImageDrawable( - context.coloredDrawable( - drawableResId = Afterpay.brand.badgeForeground, - colorResId = colorScheme.foregroundColorResId, - ), - ) + setImageDrawable( + context.coloredDrawable( + drawableResId = Afterpay.brand.badgeForeground, + colorResId = colorScheme.foregroundColorResId, + ), + ) - background = context.coloredDrawable( - R.drawable.afterpay_badge_bg, - colorScheme.backgroundColorResId, - ) + background = context.coloredDrawable( + R.drawable.afterpay_badge_bg, + colorScheme.backgroundColorResId, + ) - invalidate() - requestLayout() - } + invalidate() + requestLayout() + } } diff --git a/afterpay/src/main/kotlin/com/afterpay/android/view/AfterpayCheckoutActivity.kt b/afterpay/src/main/kotlin/com/afterpay/android/view/AfterpayCheckoutActivity.kt index 2d8265f0..381f2580 100644 --- a/afterpay/src/main/kotlin/com/afterpay/android/view/AfterpayCheckoutActivity.kt +++ b/afterpay/src/main/kotlin/com/afterpay/android/view/AfterpayCheckoutActivity.kt @@ -42,214 +42,214 @@ import com.afterpay.android.internal.setAfterpayUserAgentString internal class AfterpayCheckoutActivity : AppCompatActivity() { - private companion object { - - val validCheckoutUrls = listOf( - "portal.afterpay.com", - "portal.sandbox.afterpay.com", - "portal.clearpay.co.uk", - "portal.sandbox.clearpay.co.uk", - "checkout.clearpay.com", - "checkout.sandbox.clearpay.com", - ) + private companion object { + + val validCheckoutUrls = listOf( + "portal.afterpay.com", + "portal.sandbox.afterpay.com", + "portal.clearpay.co.uk", + "portal.sandbox.clearpay.co.uk", + "checkout.clearpay.com", + "checkout.sandbox.clearpay.com", + ) + } + + private lateinit var webView: WebView + + @SuppressLint("SetJavaScriptEnabled") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_web_checkout) + + window.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) + + webView = findViewById(R.id.afterpay_webView).apply { + setAfterpayUserAgentString() + settings.javaScriptEnabled = true + settings.setSupportMultipleWindows(true) + settings.setDomStorageEnabled(true) + webViewClient = AfterpayWebViewClient( + receivedError = ::handleError, + completed = ::finish, + shouldLoadRedirectUrls = intent.getCheckoutShouldLoadRedirectUrls(), + ) + webChromeClient = AfterpayWebChromeClient(openExternalLink = ::open) } - private lateinit var webView: WebView - - @SuppressLint("SetJavaScriptEnabled") - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_web_checkout) - - window.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) - - webView = findViewById(R.id.afterpay_webView).apply { - setAfterpayUserAgentString() - settings.javaScriptEnabled = true - settings.setSupportMultipleWindows(true) - settings.setDomStorageEnabled(true) - webViewClient = AfterpayWebViewClient( - receivedError = ::handleError, - completed = ::finish, - shouldLoadRedirectUrls = intent.getCheckoutShouldLoadRedirectUrls(), - ) - webChromeClient = AfterpayWebChromeClient(openExternalLink = ::open) - } - - val onBackPressedCallback: OnBackPressedCallback = object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - finish(CancellationStatus.USER_INITIATED) - } - } - onBackPressedDispatcher.addCallback(this, onBackPressedCallback) - - loadCheckoutUrl() + val onBackPressedCallback: OnBackPressedCallback = object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + finish(CancellationStatus.USER_INITIATED) + } } + onBackPressedDispatcher.addCallback(this, onBackPressedCallback) - override fun onDestroy() { - // Prevent WebView from leaking memory when the Activity is destroyed. - // The leak appears when enabling JavaScript and is fixed by disabling it. - webView.apply { - stopLoading() - settings.javaScriptEnabled = false - } + loadCheckoutUrl() + } - super.onDestroy() + override fun onDestroy() { + // Prevent WebView from leaking memory when the Activity is destroyed. + // The leak appears when enabling JavaScript and is fixed by disabling it. + webView.apply { + stopLoading() + settings.javaScriptEnabled = false } - private fun loadCheckoutUrl() { - val checkoutUrl = intent.getCheckoutUrlExtra() - ?: return finish(CancellationStatus.NO_CHECKOUT_URL) - - if (validCheckoutUrls.contains(Uri.parse(checkoutUrl).host)) { - webView.loadUrl(checkoutUrl) - } else if (checkoutUrl == "LANGUAGE_NOT_SUPPORTED") { - finish(CancellationStatus.LANGUAGE_NOT_SUPPORTED) - } else { - finish(CancellationStatus.INVALID_CHECKOUT_URL) - } - } + super.onDestroy() + } - private fun open(url: Uri) { - val intent = Intent(Intent.ACTION_VIEW, url) - if (intent.resolveActivity(packageManager) != null) { - startActivity(intent) - } - } + private fun loadCheckoutUrl() { + val checkoutUrl = intent.getCheckoutUrlExtra() + ?: return finish(CancellationStatus.NO_CHECKOUT_URL) - private fun handleError() { - // Clear default system error from the web view. - webView.loadUrl("about:blank") - - AlertDialog.Builder(this) - .setTitle(Afterpay.strings.loadErrorTitle) - .setMessage( - String.format( - Afterpay.strings.loadErrorMessage, - resources.getString(Afterpay.brand.title), - ), - ) - .setPositiveButton(Afterpay.strings.loadErrorRetry) { dialog, _ -> - loadCheckoutUrl() - dialog.dismiss() - } - .setNegativeButton(Afterpay.strings.loadErrorCancel) { dialog, _ -> - dialog.cancel() - } - .setOnCancelListener { - finish(CancellationStatus.USER_INITIATED) - } - .show() + if (validCheckoutUrls.contains(Uri.parse(checkoutUrl).host)) { + webView.loadUrl(checkoutUrl) + } else if (checkoutUrl == "LANGUAGE_NOT_SUPPORTED") { + finish(CancellationStatus.LANGUAGE_NOT_SUPPORTED) + } else { + finish(CancellationStatus.INVALID_CHECKOUT_URL) } + } - private fun finish(status: CheckoutStatus) { - when (status) { - is CheckoutStatus.Success -> { - setResult(Activity.RESULT_OK, Intent().putOrderTokenExtra(status.orderToken)) - finish() - } - CheckoutStatus.Cancelled -> { - finish(CancellationStatus.USER_INITIATED) - } - } + private fun open(url: Uri) { + val intent = Intent(Intent.ACTION_VIEW, url) + if (intent.resolveActivity(packageManager) != null) { + startActivity(intent) } - - private fun finish(status: CancellationStatus) { - setResult(Activity.RESULT_CANCELED, Intent().putCancellationStatusExtra(status)) + } + + private fun handleError() { + // Clear default system error from the web view. + webView.loadUrl("about:blank") + + AlertDialog.Builder(this) + .setTitle(Afterpay.strings.loadErrorTitle) + .setMessage( + String.format( + Afterpay.strings.loadErrorMessage, + resources.getString(Afterpay.brand.title), + ), + ) + .setPositiveButton(Afterpay.strings.loadErrorRetry) { dialog, _ -> + loadCheckoutUrl() + dialog.dismiss() + } + .setNegativeButton(Afterpay.strings.loadErrorCancel) { dialog, _ -> + dialog.cancel() + } + .setOnCancelListener { + finish(CancellationStatus.USER_INITIATED) + } + .show() + } + + private fun finish(status: CheckoutStatus) { + when (status) { + is CheckoutStatus.Success -> { + setResult(Activity.RESULT_OK, Intent().putOrderTokenExtra(status.orderToken)) finish() + } + CheckoutStatus.Cancelled -> { + finish(CancellationStatus.USER_INITIATED) + } } + } + + private fun finish(status: CancellationStatus) { + setResult(Activity.RESULT_CANCELED, Intent().putCancellationStatusExtra(status)) + finish() + } } private class AfterpayWebViewClient( - private val receivedError: () -> Unit, - private val completed: (CheckoutStatus) -> Unit, - private val shouldLoadRedirectUrls: Boolean, + private val receivedError: () -> Unit, + private val completed: (CheckoutStatus) -> Unit, + private val shouldLoadRedirectUrls: Boolean, ) : WebViewClient() { - override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean { - val url = request?.url ?: return false - val status = CheckoutStatus.fromUrl(url) - - return when { - status != null -> { - if (shouldLoadRedirectUrls) { - return false - } + override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean { + val url = request?.url ?: return false + val status = CheckoutStatus.fromUrl(url) + + return when { + status != null -> { + if (shouldLoadRedirectUrls) { + return false + } - completed(status) - true - } + completed(status) + true + } - else -> false - } + else -> false } + } - override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { - super.onPageStarted(view, url, favicon) + override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { + super.onPageStarted(view, url, favicon) - if (url.equals("about:blank")) { - return - } + if (url.equals("about:blank")) { + return + } - val uri = Uri.parse(url) - val status = CheckoutStatus.fromUrl(uri) + val uri = Uri.parse(url) + val status = CheckoutStatus.fromUrl(uri) - when { - status != null -> { - completed(status) - } + when { + status != null -> { + completed(status) + } - else -> {} - } + else -> {} } - - override fun onReceivedError( - view: WebView?, - request: WebResourceRequest?, - error: WebResourceError?, - ) { - if (request?.isForMainFrame == true) { - receivedError() - } + } + + override fun onReceivedError( + view: WebView?, + request: WebResourceRequest?, + error: WebResourceError?, + ) { + if (request?.isForMainFrame == true) { + receivedError() } + } } private class AfterpayWebChromeClient( - private val openExternalLink: (Uri) -> Unit, + private val openExternalLink: (Uri) -> Unit, ) : WebChromeClient() { - companion object { - const val URL_KEY = "url" - } - - override fun onCreateWindow( - view: WebView?, - isDialog: Boolean, - isUserGesture: Boolean, - resultMsg: Message?, - ): Boolean { - val hrefMessage = view?.handler?.obtainMessage() - view?.requestFocusNodeHref(hrefMessage) - - val url = hrefMessage?.data?.getString(URL_KEY) - url?.let { openExternalLink(Uri.parse(it)) } - - return false - } + companion object { + const val URL_KEY = "url" + } + + override fun onCreateWindow( + view: WebView?, + isDialog: Boolean, + isUserGesture: Boolean, + resultMsg: Message?, + ): Boolean { + val hrefMessage = view?.handler?.obtainMessage() + view?.requestFocusNodeHref(hrefMessage) + + val url = hrefMessage?.data?.getString(URL_KEY) + url?.let { openExternalLink(Uri.parse(it)) } + + return false + } } private sealed class CheckoutStatus { - data class Success(val orderToken: String) : CheckoutStatus() - object Cancelled : CheckoutStatus() - - companion object { - fun fromUrl(url: Uri): CheckoutStatus? { - return when (url.getQueryParameter("status")) { - "SUCCESS" -> { - val token = url.getQueryParameter("orderToken") ?: url.getQueryParameter("token") - token?.let(::Success) - } - "CANCELLED" -> Cancelled - else -> null - } + data class Success(val orderToken: String) : CheckoutStatus() + object Cancelled : CheckoutStatus() + + companion object { + fun fromUrl(url: Uri): CheckoutStatus? { + return when (url.getQueryParameter("status")) { + "SUCCESS" -> { + val token = url.getQueryParameter("orderToken") ?: url.getQueryParameter("token") + token?.let(::Success) } + "CANCELLED" -> Cancelled + else -> null + } } + } } diff --git a/afterpay/src/main/kotlin/com/afterpay/android/view/AfterpayCheckoutV2Activity.kt b/afterpay/src/main/kotlin/com/afterpay/android/view/AfterpayCheckoutV2Activity.kt index d009ce7f..4f062d10 100644 --- a/afterpay/src/main/kotlin/com/afterpay/android/view/AfterpayCheckoutV2Activity.kt +++ b/afterpay/src/main/kotlin/com/afterpay/android/view/AfterpayCheckoutV2Activity.kt @@ -61,304 +61,304 @@ import java.util.Locale internal class AfterpayCheckoutV2Activity : AppCompatActivity() { - private lateinit var bootstrapWebView: WebView - private lateinit var loadingWebView: WebView - private var checkoutWebView: WebView? = null + private lateinit var bootstrapWebView: WebView + private lateinit var loadingWebView: WebView + private var checkoutWebView: WebView? = null - private lateinit var bootstrapUrl: String + private lateinit var bootstrapUrl: String - @SuppressLint("SetJavaScriptEnabled") - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) + @SuppressLint("SetJavaScriptEnabled") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) - val onBackPressedCallback: OnBackPressedCallback = object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - finish(CancellationStatus.USER_INITIATED) - } - } - onBackPressedDispatcher.addCallback(this, onBackPressedCallback) - - bootstrapUrl = getString(R.string.afterpay_url_checkout_express) - - setContentView(R.layout.activity_express_web_checkout) - window.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) - - loadingWebView = findViewById(R.id.afterpay_loadingWebView).apply { - val htmlData = Base64.encodeToString(Html.LOADING.toByteArray(), Base64.NO_PADDING) - loadData(htmlData, "text/html", "base64") - } - - bootstrapWebView = findViewById(R.id.afterpay_webView) - - val activity = this - val frameLayout = findViewById(R.id.afterpay_webView_frame_layout) - - bootstrapWebView.apply { - setAfterpayUserAgentString() - settings.javaScriptEnabled = true - settings.javaScriptCanOpenWindowsAutomatically = true - settings.setSupportMultipleWindows(true) - - webViewClient = BootstrapWebViewClient(::loadCheckoutToken, ::handleBootstrapError) - webChromeClient = BootstrapWebChromeClient( - context = activity, - viewGroup = frameLayout, - onOpenWebView = { checkoutWebView = it }, - onPageFinished = { frameLayout.removeView(loadingWebView) }, - receivedError = ::handleCheckoutError, - openExternalLink = ::open, - ) - - val javascriptInterface = BootstrapJavascriptInterface( - activity = activity, - webView = this, - complete = ::finish, - cancel = ::finish, - ) - - addJavascriptInterface(javascriptInterface, "Android") - loadUrl(bootstrapUrl) - } + val onBackPressedCallback: OnBackPressedCallback = object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + finish(CancellationStatus.USER_INITIATED) + } } + onBackPressedDispatcher.addCallback(this, onBackPressedCallback) - override fun onDestroy() { - // Prevent WebView from leaking memory when the Activity is destroyed. - // The leak appears when enabling JavaScript and is fixed by disabling it. - bootstrapWebView.apply { - stopLoading() - settings.javaScriptEnabled = false - } + bootstrapUrl = getString(R.string.afterpay_url_checkout_express) - checkoutWebView?.apply { - stopLoading() - settings.javaScriptEnabled = false - } + setContentView(R.layout.activity_express_web_checkout) + window.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) - super.onDestroy() + loadingWebView = findViewById(R.id.afterpay_loadingWebView).apply { + val htmlData = Base64.encodeToString(Html.LOADING.toByteArray(), Base64.NO_PADDING) + loadData(htmlData, "text/html", "base64") } - private fun loadCheckoutToken() { - if (!Afterpay.enabled) { - return finish(LANGUAGE_NOT_SUPPORTED) - } - val handler = Afterpay.checkoutV2Handler ?: return finish(NO_CHECKOUT_HANDLER) - val configuration = - Afterpay.configuration ?: return finish(CancellationStatus.NO_CONFIGURATION) - val options = requireNotNull(intent.getCheckoutV2OptionsExtra()) - - handler.didCommenceCheckout { result -> - val token = result.getOrNull() ?: return@didCommenceCheckout handleCheckoutError() - val checkout = AfterpayCheckoutV2(token, configuration, options) - val checkoutJson = Json.encodeToString(checkout) - - runOnUiThread { - bootstrapWebView.evaluateJavascript("openCheckout('$checkoutJson');", null) - } - } + bootstrapWebView = findViewById(R.id.afterpay_webView) + + val activity = this + val frameLayout = findViewById(R.id.afterpay_webView_frame_layout) + + bootstrapWebView.apply { + setAfterpayUserAgentString() + settings.javaScriptEnabled = true + settings.javaScriptCanOpenWindowsAutomatically = true + settings.setSupportMultipleWindows(true) + + webViewClient = BootstrapWebViewClient(::loadCheckoutToken, ::handleBootstrapError) + webChromeClient = BootstrapWebChromeClient( + context = activity, + viewGroup = frameLayout, + onOpenWebView = { checkoutWebView = it }, + onPageFinished = { frameLayout.removeView(loadingWebView) }, + receivedError = ::handleCheckoutError, + openExternalLink = ::open, + ) + + val javascriptInterface = BootstrapJavascriptInterface( + activity = activity, + webView = this, + complete = ::finish, + cancel = ::finish, + ) + + addJavascriptInterface(javascriptInterface, "Android") + loadUrl(bootstrapUrl) } - - private fun open(url: Uri) { - val intent = Intent(Intent.ACTION_VIEW, url) - try { - startActivity(intent) - } catch (ex: ActivityNotFoundException) {} + } + + override fun onDestroy() { + // Prevent WebView from leaking memory when the Activity is destroyed. + // The leak appears when enabling JavaScript and is fixed by disabling it. + bootstrapWebView.apply { + stopLoading() + settings.javaScriptEnabled = false } - private fun errorAlert(retryAction: () -> Unit) = - AlertDialog.Builder(this) - .setTitle(Afterpay.strings.loadErrorTitle) - .setMessage( - String.format( - Afterpay.strings.loadErrorMessage, - resources.getString(Afterpay.brand.title), - ), - ) - .setPositiveButton(Afterpay.strings.loadErrorRetry) { dialog, _ -> - retryAction() - dialog.dismiss() - } - .setNegativeButton(Afterpay.strings.loadErrorCancel) { dialog, _ -> - dialog.cancel() - } - .setOnCancelListener { - finish(CancellationStatus.USER_INITIATED) - } - - private fun handleBootstrapError() { - errorAlert { bootstrapWebView.loadUrl(bootstrapUrl) }.show() + checkoutWebView?.apply { + stopLoading() + settings.javaScriptEnabled = false } - private fun handleCheckoutError() { - // Clear default system error from the web view. - checkoutWebView?.loadUrl("about:blank") + super.onDestroy() + } - errorAlert { loadCheckoutToken() }.show() + private fun loadCheckoutToken() { + if (!Afterpay.enabled) { + return finish(LANGUAGE_NOT_SUPPORTED) } - - private fun finish(completion: AfterpayCheckoutCompletion) { - when (completion.status) { - AfterpayCheckoutCompletion.Status.SUCCESS -> { - setResult(Activity.RESULT_OK, Intent().putOrderTokenExtra(completion.orderToken)) - finish() - } - AfterpayCheckoutCompletion.Status.CANCELLED -> { - finish(CancellationStatus.USER_INITIATED) - } - } + val handler = Afterpay.checkoutV2Handler ?: return finish(NO_CHECKOUT_HANDLER) + val configuration = + Afterpay.configuration ?: return finish(CancellationStatus.NO_CONFIGURATION) + val options = requireNotNull(intent.getCheckoutV2OptionsExtra()) + + handler.didCommenceCheckout { result -> + val token = result.getOrNull() ?: return@didCommenceCheckout handleCheckoutError() + val checkout = AfterpayCheckoutV2(token, configuration, options) + val checkoutJson = Json.encodeToString(checkout) + + runOnUiThread { + bootstrapWebView.evaluateJavascript("openCheckout('$checkoutJson');", null) + } } - - private fun finish(status: CancellationStatus) { - setResult(Activity.RESULT_CANCELED, Intent().putCancellationStatusExtra(status)) + } + + private fun open(url: Uri) { + val intent = Intent(Intent.ACTION_VIEW, url) + try { + startActivity(intent) + } catch (ex: ActivityNotFoundException) {} + } + + private fun errorAlert(retryAction: () -> Unit) = + AlertDialog.Builder(this) + .setTitle(Afterpay.strings.loadErrorTitle) + .setMessage( + String.format( + Afterpay.strings.loadErrorMessage, + resources.getString(Afterpay.brand.title), + ), + ) + .setPositiveButton(Afterpay.strings.loadErrorRetry) { dialog, _ -> + retryAction() + dialog.dismiss() + } + .setNegativeButton(Afterpay.strings.loadErrorCancel) { dialog, _ -> + dialog.cancel() + } + .setOnCancelListener { + finish(CancellationStatus.USER_INITIATED) + } + + private fun handleBootstrapError() { + errorAlert { bootstrapWebView.loadUrl(bootstrapUrl) }.show() + } + + private fun handleCheckoutError() { + // Clear default system error from the web view. + checkoutWebView?.loadUrl("about:blank") + + errorAlert { loadCheckoutToken() }.show() + } + + private fun finish(completion: AfterpayCheckoutCompletion) { + when (completion.status) { + AfterpayCheckoutCompletion.Status.SUCCESS -> { + setResult(Activity.RESULT_OK, Intent().putOrderTokenExtra(completion.orderToken)) finish() + } + AfterpayCheckoutCompletion.Status.CANCELLED -> { + finish(CancellationStatus.USER_INITIATED) + } } + } + + private fun finish(status: CancellationStatus) { + setResult(Activity.RESULT_CANCELED, Intent().putCancellationStatusExtra(status)) + finish() + } } private class BootstrapWebViewClient( - private val onPageFinished: () -> Unit, - private val receivedError: () -> Unit, + private val onPageFinished: () -> Unit, + private val receivedError: () -> Unit, ) : WebViewClient() { - override fun onPageFinished(view: WebView?, url: String?) { - onPageFinished() + override fun onPageFinished(view: WebView?, url: String?) { + onPageFinished() + } + + override fun onReceivedError( + view: WebView?, + request: WebResourceRequest?, + error: WebResourceError?, + ) { + if (request?.isForMainFrame == true) { + receivedError() } + } +} - override fun onReceivedError( +private class BootstrapWebChromeClient( + private val context: Context, + private val viewGroup: ViewGroup, + private val onOpenWebView: (WebView) -> Unit, + private val onPageFinished: () -> Unit, + private val receivedError: () -> Unit, + private val openExternalLink: (Uri) -> Unit, +) : WebChromeClient() { + companion object { + const val URL_KEY = "url" + } + + @SuppressLint("SetJavaScriptEnabled") + override fun onCreateWindow( + view: WebView?, + isDialog: Boolean, + isUserGesture: Boolean, + resultMsg: Message?, + ): Boolean { + val webView = WebView(context) + webView.setAfterpayUserAgentString() + webView.visibility = INVISIBLE + webView.settings.javaScriptEnabled = true + webView.settings.setSupportMultipleWindows(true) + webView.settings.domStorageEnabled = true + + webView.webViewClient = object : WebViewClient() { + override fun onPageFinished(view: WebView?, url: String?) { + webView.visibility = VISIBLE + onPageFinished() + } + + override fun onReceivedError( view: WebView?, request: WebResourceRequest?, error: WebResourceError?, - ) { + ) { if (request?.isForMainFrame == true) { - receivedError() + receivedError() } + } } -} -private class BootstrapWebChromeClient( - private val context: Context, - private val viewGroup: ViewGroup, - private val onOpenWebView: (WebView) -> Unit, - private val onPageFinished: () -> Unit, - private val receivedError: () -> Unit, - private val openExternalLink: (Uri) -> Unit, -) : WebChromeClient() { - companion object { - const val URL_KEY = "url" - } - - @SuppressLint("SetJavaScriptEnabled") - override fun onCreateWindow( + webView.webChromeClient = object : WebChromeClient() { + override fun onCreateWindow( view: WebView?, isDialog: Boolean, isUserGesture: Boolean, resultMsg: Message?, - ): Boolean { - val webView = WebView(context) - webView.setAfterpayUserAgentString() - webView.visibility = INVISIBLE - webView.settings.javaScriptEnabled = true - webView.settings.setSupportMultipleWindows(true) - webView.settings.domStorageEnabled = true - - webView.webViewClient = object : WebViewClient() { - override fun onPageFinished(view: WebView?, url: String?) { - webView.visibility = VISIBLE - onPageFinished() - } - - override fun onReceivedError( - view: WebView?, - request: WebResourceRequest?, - error: WebResourceError?, - ) { - if (request?.isForMainFrame == true) { - receivedError() - } - } - } + ): Boolean { + val hrefMessage = view?.handler?.obtainMessage() + view?.requestFocusNodeHref(hrefMessage) - webView.webChromeClient = object : WebChromeClient() { - override fun onCreateWindow( - view: WebView?, - isDialog: Boolean, - isUserGesture: Boolean, - resultMsg: Message?, - ): Boolean { - val hrefMessage = view?.handler?.obtainMessage() - view?.requestFocusNodeHref(hrefMessage) - - val url = hrefMessage?.data?.getString(URL_KEY) - url?.let { openExternalLink(Uri.parse(it)) } - - return false - } - } + val url = hrefMessage?.data?.getString(URL_KEY) + url?.let { openExternalLink(Uri.parse(it)) } - viewGroup.addView(webView) + return false + } + } - val transport = resultMsg?.obj as? WebViewTransport ?: return false - transport.webView = webView - resultMsg.sendToTarget() + viewGroup.addView(webView) - onOpenWebView(webView) + val transport = resultMsg?.obj as? WebViewTransport ?: return false + transport.webView = webView + resultMsg.sendToTarget() - return true - } + onOpenWebView(webView) + + return true + } } private class BootstrapJavascriptInterface( - private val activity: Activity, - private val webView: WebView, - private val complete: (AfterpayCheckoutCompletion) -> Unit, - private val cancel: (CancellationStatus) -> Unit, + private val activity: Activity, + private val webView: WebView, + private val complete: (AfterpayCheckoutCompletion) -> Unit, + private val cancel: (CancellationStatus) -> Unit, ) { - private val json = Json { ignoreUnknownKeys = true } - - @JavascriptInterface - fun postMessage(messageJson: String) { - runCatching { json.decodeFromString(messageJson) } - .onFailure { Log.d(javaClass.simpleName, it.toString()) } - .getOrNull() - ?.let { message -> - val handler = Afterpay.checkoutV2Handler ?: return cancel(NO_CHECKOUT_HANDLER) - - when (message) { - is CheckoutLogMessage -> Log.d( - javaClass.simpleName, - message.payload.run { "${severity.replaceFirstChar { it.uppercase(Locale.ROOT) }}: $message" }, - ) - - is ShippingAddressMessage -> handler.shippingAddressDidChange(message.payload) { - AfterpayCheckoutMessage - .fromShippingOptionsResult(it, message.meta) - .let { result -> - "postMessageToCheckout('${json.encodeToString(result)}');" - } - .also { javascript -> - activity.runOnUiThread { - webView.evaluateJavascript(javascript, null) - } - } - } - - is ShippingOptionMessage -> handler.shippingOptionDidChange(message.payload) { - AfterpayCheckoutMessage - .fromShippingOptionUpdateResult(it, message.meta) - .let { result -> - "postMessageToCheckout('${json.encodeToString(result)}');" - } - .also { javascript -> - activity.runOnUiThread { - webView.evaluateJavascript(javascript, null) - } - } - } - - else -> Unit + private val json = Json { ignoreUnknownKeys = true } + + @JavascriptInterface + fun postMessage(messageJson: String) { + runCatching { json.decodeFromString(messageJson) } + .onFailure { Log.d(javaClass.simpleName, it.toString()) } + .getOrNull() + ?.let { message -> + val handler = Afterpay.checkoutV2Handler ?: return cancel(NO_CHECKOUT_HANDLER) + + when (message) { + is CheckoutLogMessage -> Log.d( + javaClass.simpleName, + message.payload.run { "${severity.replaceFirstChar { it.uppercase(Locale.ROOT) }}: $message" }, + ) + + is ShippingAddressMessage -> handler.shippingAddressDidChange(message.payload) { + AfterpayCheckoutMessage + .fromShippingOptionsResult(it, message.meta) + .let { result -> + "postMessageToCheckout('${json.encodeToString(result)}');" + } + .also { javascript -> + activity.runOnUiThread { + webView.evaluateJavascript(javascript, null) } - } - ?: runCatching { json.decodeFromString(messageJson) } - .onFailure { Log.d(javaClass.simpleName, it.toString()) } - .getOrNull() - ?.let(complete) - } + } + } + + is ShippingOptionMessage -> handler.shippingOptionDidChange(message.payload) { + AfterpayCheckoutMessage + .fromShippingOptionUpdateResult(it, message.meta) + .let { result -> + "postMessageToCheckout('${json.encodeToString(result)}');" + } + .also { javascript -> + activity.runOnUiThread { + webView.evaluateJavascript(javascript, null) + } + } + } + + else -> Unit + } + } + ?: runCatching { json.decodeFromString(messageJson) } + .onFailure { Log.d(javaClass.simpleName, it.toString()) } + .getOrNull() + ?.let(complete) + } } diff --git a/afterpay/src/main/kotlin/com/afterpay/android/view/AfterpayCheckoutV3Activity.kt b/afterpay/src/main/kotlin/com/afterpay/android/view/AfterpayCheckoutV3Activity.kt index f1f233a7..cee8a94c 100644 --- a/afterpay/src/main/kotlin/com/afterpay/android/view/AfterpayCheckoutV3Activity.kt +++ b/afterpay/src/main/kotlin/com/afterpay/android/view/AfterpayCheckoutV3Activity.kt @@ -46,184 +46,184 @@ import java.lang.Exception internal class AfterpayCheckoutV3Activity : AppCompatActivity() { - private lateinit var webView: WebView - private lateinit var viewModel: CheckoutV3ViewModel - - @SuppressLint("SetJavaScriptEnabled") - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_web_checkout) - - window.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) - - viewModel = CheckoutV3ViewModel(requireNotNull(intent.getCheckoutV3OptionsExtra())) - webView = findViewById(R.id.afterpay_webView).apply { - setAfterpayUserAgentString() - settings.javaScriptEnabled = true - settings.setSupportMultipleWindows(true) - webViewClient = AfterpayWebViewClientV3( - receivedError = ::handleError, - received = ::received, - ) - webChromeClient = AfterpayWebChromeClientV3(openExternalLink = ::open) - val htmlData = Base64.encodeToString(Html.LOADING.toByteArray(), Base64.NO_PADDING) - loadData(htmlData, "text/html", "base64") - } - - lifecycleScope.launchWhenStarted { - viewModel.performCheckoutRequest() - .onSuccess { checkoutRedirectUrl -> - webView.loadUrl(checkoutRedirectUrl.toString()) - } - .onFailure { - received(CancellationStatusV3.REQUEST_ERROR, it as? Exception) - } - } + private lateinit var webView: WebView + private lateinit var viewModel: CheckoutV3ViewModel + + @SuppressLint("SetJavaScriptEnabled") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_web_checkout) + + window.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) + + viewModel = CheckoutV3ViewModel(requireNotNull(intent.getCheckoutV3OptionsExtra())) + webView = findViewById(R.id.afterpay_webView).apply { + setAfterpayUserAgentString() + settings.javaScriptEnabled = true + settings.setSupportMultipleWindows(true) + webViewClient = AfterpayWebViewClientV3( + receivedError = ::handleError, + received = ::received, + ) + webChromeClient = AfterpayWebChromeClientV3(openExternalLink = ::open) + val htmlData = Base64.encodeToString(Html.LOADING.toByteArray(), Base64.NO_PADDING) + loadData(htmlData, "text/html", "base64") } - override fun onDestroy() { - // Prevent WebView from leaking memory when the Activity is destroyed. - // The leak appears when enabling JavaScript and is fixed by disabling it. - webView.apply { - stopLoading() - settings.javaScriptEnabled = false + lifecycleScope.launchWhenStarted { + viewModel.performCheckoutRequest() + .onSuccess { checkoutRedirectUrl -> + webView.loadUrl(checkoutRedirectUrl.toString()) + } + .onFailure { + received(CancellationStatusV3.REQUEST_ERROR, it as? Exception) } - - super.onDestroy() } - - override fun onBackPressed() { - received(CancellationStatusV3.USER_INITIATED) + } + + override fun onDestroy() { + // Prevent WebView from leaking memory when the Activity is destroyed. + // The leak appears when enabling JavaScript and is fixed by disabling it. + webView.apply { + stopLoading() + settings.javaScriptEnabled = false } - private fun open(url: Uri) { - val intent = Intent(Intent.ACTION_VIEW, url) - if (intent.resolveActivity(packageManager) != null) { - startActivity(intent) - } - } + super.onDestroy() + } - private fun handleError() { - // Clear default system error from the web view. - webView.loadUrl("about:blank") - - AlertDialog.Builder(this) - .setTitle(Afterpay.strings.loadErrorTitle) - .setMessage(Afterpay.strings.loadErrorMessage) - .setPositiveButton(Afterpay.strings.loadErrorRetry) { dialog, _ -> - val options = intent.getCheckoutV3OptionsExtra() - val retryUrl = options?.redirectUrl ?: options?.checkoutUrl - retryUrl?.let { - webView.loadUrl(it.toString()) - } - dialog.dismiss() - } - .setNegativeButton(Afterpay.strings.loadErrorCancel) { dialog, _ -> - dialog.cancel() - } - .setOnCancelListener { - received(CancellationStatusV3.USER_INITIATED) - } - .show() - } + override fun onBackPressed() { + received(CancellationStatusV3.USER_INITIATED) + } - private fun received(status: CheckoutStatusV3) { - when (status) { - is CheckoutStatusV3.Success -> { - lifecycleScope.launch { - viewModel.performConfirmationRequest(status.ppaConfirmToken) - .onSuccess { - setResult(Activity.RESULT_OK, Intent().putResultDataV3(it)) - finish() - } - .onFailure { - received(CancellationStatusV3.REQUEST_ERROR, it as? Exception) - } - } + private fun open(url: Uri) { + val intent = Intent(Intent.ACTION_VIEW, url) + if (intent.resolveActivity(packageManager) != null) { + startActivity(intent) + } + } + + private fun handleError() { + // Clear default system error from the web view. + webView.loadUrl("about:blank") + + AlertDialog.Builder(this) + .setTitle(Afterpay.strings.loadErrorTitle) + .setMessage(Afterpay.strings.loadErrorMessage) + .setPositiveButton(Afterpay.strings.loadErrorRetry) { dialog, _ -> + val options = intent.getCheckoutV3OptionsExtra() + val retryUrl = options?.redirectUrl ?: options?.checkoutUrl + retryUrl?.let { + webView.loadUrl(it.toString()) + } + dialog.dismiss() + } + .setNegativeButton(Afterpay.strings.loadErrorCancel) { dialog, _ -> + dialog.cancel() + } + .setOnCancelListener { + received(CancellationStatusV3.USER_INITIATED) + } + .show() + } + + private fun received(status: CheckoutStatusV3) { + when (status) { + is CheckoutStatusV3.Success -> { + lifecycleScope.launch { + viewModel.performConfirmationRequest(status.ppaConfirmToken) + .onSuccess { + setResult(Activity.RESULT_OK, Intent().putResultDataV3(it)) + finish() } - CheckoutStatusV3.Cancelled -> { - received(CancellationStatusV3.USER_INITIATED) + .onFailure { + received(CancellationStatusV3.REQUEST_ERROR, it as? Exception) } } + } + CheckoutStatusV3.Cancelled -> { + received(CancellationStatusV3.USER_INITIATED) + } } + } - private fun received(status: CancellationStatusV3, exception: Exception? = null) { - val intent = Intent() - intent.putCancellationStatusExtraV3(status) - exception?.let { - intent.putCancellationStatusExtraErrorV3(it) - } - setResult(Activity.RESULT_CANCELED, intent) - finish() + private fun received(status: CancellationStatusV3, exception: Exception? = null) { + val intent = Intent() + intent.putCancellationStatusExtraV3(status) + exception?.let { + intent.putCancellationStatusExtraErrorV3(it) } + setResult(Activity.RESULT_CANCELED, intent) + finish() + } } private class AfterpayWebViewClientV3( - private val receivedError: () -> Unit, - private val received: (CheckoutStatusV3) -> Unit, + private val receivedError: () -> Unit, + private val received: (CheckoutStatusV3) -> Unit, ) : WebViewClient() { - override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean { - val url = request?.url ?: return false - val status = CheckoutStatusV3.fromUrl(url) - - return when { - status != null -> { - received(status) - true - } + override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean { + val url = request?.url ?: return false + val status = CheckoutStatusV3.fromUrl(url) - else -> false - } - } + return when { + status != null -> { + received(status) + true + } - override fun onReceivedError( - view: WebView?, - request: WebResourceRequest?, - error: WebResourceError?, - ) { - if (request?.isForMainFrame == true) { - receivedError() - } + else -> false } + } + + override fun onReceivedError( + view: WebView?, + request: WebResourceRequest?, + error: WebResourceError?, + ) { + if (request?.isForMainFrame == true) { + receivedError() + } + } } private class AfterpayWebChromeClientV3( - private val openExternalLink: (Uri) -> Unit, - private val URL_KEY: String = "url", + private val openExternalLink: (Uri) -> Unit, + private val URL_KEY: String = "url", ) : WebChromeClient() { - override fun onCreateWindow( - view: WebView?, - isDialog: Boolean, - isUserGesture: Boolean, - resultMsg: Message?, - ): Boolean { - val hrefMessage = view?.handler?.obtainMessage() - view?.requestFocusNodeHref(hrefMessage) + override fun onCreateWindow( + view: WebView?, + isDialog: Boolean, + isUserGesture: Boolean, + resultMsg: Message?, + ): Boolean { + val hrefMessage = view?.handler?.obtainMessage() + view?.requestFocusNodeHref(hrefMessage) - val url = hrefMessage?.data?.getString(URL_KEY) - url?.let { openExternalLink(Uri.parse(it)) } + val url = hrefMessage?.data?.getString(URL_KEY) + url?.let { openExternalLink(Uri.parse(it)) } - return false - } + return false + } } private sealed class CheckoutStatusV3 { - data class Success(val orderToken: String, val ppaConfirmToken: String) : CheckoutStatusV3() - object Cancelled : CheckoutStatusV3() - - companion object { - fun fromUrl(url: Uri): CheckoutStatusV3? = when (url.getQueryParameter("status")) { - "SUCCESS" -> { - val success = url.getQueryParameter("orderToken")?.let { token -> - url.getQueryParameter("ppaConfirmToken")?.let { confirmToken -> - Success(token, confirmToken) - } - } - success - } - "CANCELLED" -> Cancelled - else -> null + data class Success(val orderToken: String, val ppaConfirmToken: String) : CheckoutStatusV3() + object Cancelled : CheckoutStatusV3() + + companion object { + fun fromUrl(url: Uri): CheckoutStatusV3? = when (url.getQueryParameter("status")) { + "SUCCESS" -> { + val success = url.getQueryParameter("orderToken")?.let { token -> + url.getQueryParameter("ppaConfirmToken")?.let { confirmToken -> + Success(token, confirmToken) + } } + success + } + "CANCELLED" -> Cancelled + else -> null } + } } diff --git a/afterpay/src/main/kotlin/com/afterpay/android/view/AfterpayColorScheme.kt b/afterpay/src/main/kotlin/com/afterpay/android/view/AfterpayColorScheme.kt index b31ad1f4..f0363e45 100644 --- a/afterpay/src/main/kotlin/com/afterpay/android/view/AfterpayColorScheme.kt +++ b/afterpay/src/main/kotlin/com/afterpay/android/view/AfterpayColorScheme.kt @@ -19,30 +19,30 @@ import androidx.annotation.ColorRes import com.afterpay.android.R enum class AfterpayColorScheme( - @ColorRes val foregroundColorResId: Int, - @ColorRes val backgroundColorResId: Int, + @ColorRes val foregroundColorResId: Int, + @ColorRes val backgroundColorResId: Int, ) { - BLACK_ON_MINT( - foregroundColorResId = R.color.afterpay_black, - backgroundColorResId = R.color.afterpay_mint, - ), - MINT_ON_BLACK( - foregroundColorResId = R.color.afterpay_mint, - backgroundColorResId = R.color.afterpay_black, - ), - WHITE_ON_BLACK( - foregroundColorResId = R.color.afterpay_white, - backgroundColorResId = R.color.afterpay_black, - ), - BLACK_ON_WHITE( - foregroundColorResId = R.color.afterpay_black, - backgroundColorResId = R.color.afterpay_white, - ), - ; + BLACK_ON_MINT( + foregroundColorResId = R.color.afterpay_black, + backgroundColorResId = R.color.afterpay_mint, + ), + MINT_ON_BLACK( + foregroundColorResId = R.color.afterpay_mint, + backgroundColorResId = R.color.afterpay_black, + ), + WHITE_ON_BLACK( + foregroundColorResId = R.color.afterpay_white, + backgroundColorResId = R.color.afterpay_black, + ), + BLACK_ON_WHITE( + foregroundColorResId = R.color.afterpay_black, + backgroundColorResId = R.color.afterpay_white, + ), + ; - internal companion object { + internal companion object { - @JvmField - val DEFAULT = BLACK_ON_MINT - } + @JvmField + val DEFAULT = BLACK_ON_MINT + } } diff --git a/afterpay/src/main/kotlin/com/afterpay/android/view/AfterpayIntroText.kt b/afterpay/src/main/kotlin/com/afterpay/android/view/AfterpayIntroText.kt index 0f590bf3..8c1987a8 100644 --- a/afterpay/src/main/kotlin/com/afterpay/android/view/AfterpayIntroText.kt +++ b/afterpay/src/main/kotlin/com/afterpay/android/view/AfterpayIntroText.kt @@ -19,39 +19,39 @@ import com.afterpay.android.Afterpay import java.lang.Exception enum class AfterpayIntroText(val id: Int) { - EMPTY(1), - MAKE_TITLE(2), - MAKE(3), - PAY_TITLE(4), - PAY(5), - IN_TITLE(6), - IN(7), - OR_TITLE(8), - OR(9), - PAY_IN_TITLE(10), - PAY_IN(11), - ; + EMPTY(1), + MAKE_TITLE(2), + MAKE(3), + PAY_TITLE(4), + PAY(5), + IN_TITLE(6), + IN(7), + OR_TITLE(8), + OR(9), + PAY_IN_TITLE(10), + PAY_IN(11), + ; - internal companion object { + internal companion object { - @JvmField - val DEFAULT = OR + @JvmField + val DEFAULT = OR - fun fromId(id: Int): String { - return when (id) { - EMPTY.id -> "" - MAKE_TITLE.id -> Afterpay.strings.introMakeTitle - MAKE.id -> Afterpay.strings.introMake - PAY_TITLE.id -> Afterpay.strings.introPayTitle - PAY.id -> Afterpay.strings.introPay - IN_TITLE.id -> Afterpay.strings.introInTitle - IN.id -> Afterpay.strings.introIn - OR_TITLE.id -> Afterpay.strings.introOrTitle - OR.id -> Afterpay.strings.introOr - PAY_IN_TITLE.id -> Afterpay.strings.introPayInTitle - PAY_IN.id -> Afterpay.strings.introPayIn - else -> throw Exception("Invalid id `$id`, available ids are ${values().map { it.id }}") - } - } + fun fromId(id: Int): String { + return when (id) { + EMPTY.id -> "" + MAKE_TITLE.id -> Afterpay.strings.introMakeTitle + MAKE.id -> Afterpay.strings.introMake + PAY_TITLE.id -> Afterpay.strings.introPayTitle + PAY.id -> Afterpay.strings.introPay + IN_TITLE.id -> Afterpay.strings.introInTitle + IN.id -> Afterpay.strings.introIn + OR_TITLE.id -> Afterpay.strings.introOrTitle + OR.id -> Afterpay.strings.introOr + PAY_IN_TITLE.id -> Afterpay.strings.introPayInTitle + PAY_IN.id -> Afterpay.strings.introPayIn + else -> throw Exception("Invalid id `$id`, available ids are ${values().map { it.id }}") + } } + } } diff --git a/afterpay/src/main/kotlin/com/afterpay/android/view/AfterpayLockup.kt b/afterpay/src/main/kotlin/com/afterpay/android/view/AfterpayLockup.kt index 92f70558..0fcb2a44 100644 --- a/afterpay/src/main/kotlin/com/afterpay/android/view/AfterpayLockup.kt +++ b/afterpay/src/main/kotlin/com/afterpay/android/view/AfterpayLockup.kt @@ -29,45 +29,45 @@ import com.afterpay.android.internal.dp private const val MIN_WIDTH: Int = 64 class AfterpayLockup @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, + context: Context, + attrs: AttributeSet? = null, ) : AppCompatImageView(context, attrs) { - var colorScheme: AfterpayColorScheme = AfterpayColorScheme.DEFAULT - set(value) { - field = value - update() - } + var colorScheme: AfterpayColorScheme = AfterpayColorScheme.DEFAULT + set(value) { + field = value + update() + } - init { - contentDescription = resources.getString(Afterpay.brand.title) - importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_YES - isFocusable = true - scaleType = FIT_CENTER - adjustViewBounds = true - minimumWidth = MIN_WIDTH.dp + init { + contentDescription = resources.getString(Afterpay.brand.title) + importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_YES + isFocusable = true + scaleType = FIT_CENTER + adjustViewBounds = true + minimumWidth = MIN_WIDTH.dp - context.theme.obtainStyledAttributes(attrs, R.styleable.Afterpay, 0, 0).use { attributes -> - colorScheme = AfterpayColorScheme.values()[ - attributes.getInteger( - R.styleable.Afterpay_afterpayColorScheme, - AfterpayColorScheme.DEFAULT.ordinal, - ), - ] - } + context.theme.obtainStyledAttributes(attrs, R.styleable.Afterpay, 0, 0).use { attributes -> + colorScheme = AfterpayColorScheme.values()[ + attributes.getInteger( + R.styleable.Afterpay_afterpayColorScheme, + AfterpayColorScheme.DEFAULT.ordinal, + ), + ] } + } - private fun update() { - visibility = if (!Afterpay.enabled) View.GONE else View.VISIBLE + private fun update() { + visibility = if (!Afterpay.enabled) View.GONE else View.VISIBLE - setImageDrawable( - context.coloredDrawable( - drawableResId = Afterpay.brand.lockup, - colorResId = colorScheme.foregroundColorResId, - ), - ) + setImageDrawable( + context.coloredDrawable( + drawableResId = Afterpay.brand.lockup, + colorResId = colorScheme.foregroundColorResId, + ), + ) - invalidate() - requestLayout() - } + invalidate() + requestLayout() + } } diff --git a/afterpay/src/main/kotlin/com/afterpay/android/view/AfterpayLogoType.kt b/afterpay/src/main/kotlin/com/afterpay/android/view/AfterpayLogoType.kt index 9d1eb0cf..77123328 100644 --- a/afterpay/src/main/kotlin/com/afterpay/android/view/AfterpayLogoType.kt +++ b/afterpay/src/main/kotlin/com/afterpay/android/view/AfterpayLogoType.kt @@ -16,14 +16,14 @@ package com.afterpay.android.view enum class AfterpayLogoType(val fontHeightMultiplier: Double) { - BADGE(2.5), - LOCKUP(1.0), - COMPACT_BADGE(1.4), - ; + BADGE(2.5), + LOCKUP(1.0), + COMPACT_BADGE(1.4), + ; - internal companion object { + internal companion object { - @JvmField - val DEFAULT = BADGE - } + @JvmField + val DEFAULT = BADGE + } } diff --git a/afterpay/src/main/kotlin/com/afterpay/android/view/AfterpayModalLinkStyle.kt b/afterpay/src/main/kotlin/com/afterpay/android/view/AfterpayModalLinkStyle.kt index 10899830..2d7ecc5e 100644 --- a/afterpay/src/main/kotlin/com/afterpay/android/view/AfterpayModalLinkStyle.kt +++ b/afterpay/src/main/kotlin/com/afterpay/android/view/AfterpayModalLinkStyle.kt @@ -19,57 +19,57 @@ import com.afterpay.android.Afterpay import com.afterpay.android.R sealed class AfterpayModalLinkStyle(internal val config: ModalLinkConfig) { - object CircledInfoIcon : AfterpayModalLinkStyle( - ModalLinkConfig( - text = "\u24D8", - underlined = false, - ), - ) - object MoreInfoText : AfterpayModalLinkStyle( - ModalLinkConfig( - text = Afterpay.strings.priceBreakdownLinkMoreInfo, - ), - ) - object LearnMoreText : AfterpayModalLinkStyle( - ModalLinkConfig( - text = Afterpay.strings.priceBreakdownLinkLearnMore, - ), - ) - object CircledQuestionIcon : AfterpayModalLinkStyle( - ModalLinkConfig( - image = R.drawable.icon_circled_question, - imageRenderingMode = AfterpayImageRenderingMode.TEMPLATE, - ), - ) - object CircledLogo : AfterpayModalLinkStyle( - ModalLinkConfig( - image = R.drawable.afterpay_logo_small, - imageRenderingMode = AfterpayImageRenderingMode.ORIGINAL, - ), - ) - object Custom : AfterpayModalLinkStyle(ModalLinkConfig()) { - public fun setContent(content: CharSequence) { - config.customContent = content - } + object CircledInfoIcon : AfterpayModalLinkStyle( + ModalLinkConfig( + text = "\u24D8", + underlined = false, + ), + ) + object MoreInfoText : AfterpayModalLinkStyle( + ModalLinkConfig( + text = Afterpay.strings.priceBreakdownLinkMoreInfo, + ), + ) + object LearnMoreText : AfterpayModalLinkStyle( + ModalLinkConfig( + text = Afterpay.strings.priceBreakdownLinkLearnMore, + ), + ) + object CircledQuestionIcon : AfterpayModalLinkStyle( + ModalLinkConfig( + image = R.drawable.icon_circled_question, + imageRenderingMode = AfterpayImageRenderingMode.TEMPLATE, + ), + ) + object CircledLogo : AfterpayModalLinkStyle( + ModalLinkConfig( + image = R.drawable.afterpay_logo_small, + imageRenderingMode = AfterpayImageRenderingMode.ORIGINAL, + ), + ) + object Custom : AfterpayModalLinkStyle(ModalLinkConfig()) { + public fun setContent(content: CharSequence) { + config.customContent = content } - object None : AfterpayModalLinkStyle(ModalLinkConfig()) + } + object None : AfterpayModalLinkStyle(ModalLinkConfig()) - internal companion object { + internal companion object { - @JvmField - val DEFAULT = CircledInfoIcon - } + @JvmField + val DEFAULT = CircledInfoIcon + } } internal enum class AfterpayImageRenderingMode { - ORIGINAL, - TEMPLATE, + ORIGINAL, + TEMPLATE, } internal data class ModalLinkConfig( - val text: String? = null, - val image: Int? = null, - val imageRenderingMode: AfterpayImageRenderingMode? = null, - var customContent: CharSequence? = null, - val underlined: Boolean = true, + val text: String? = null, + val image: Int? = null, + val imageRenderingMode: AfterpayImageRenderingMode? = null, + var customContent: CharSequence? = null, + val underlined: Boolean = true, ) diff --git a/afterpay/src/main/kotlin/com/afterpay/android/view/AfterpayModalTheme.kt b/afterpay/src/main/kotlin/com/afterpay/android/view/AfterpayModalTheme.kt index 2b2a7490..f237d0b5 100644 --- a/afterpay/src/main/kotlin/com/afterpay/android/view/AfterpayModalTheme.kt +++ b/afterpay/src/main/kotlin/com/afterpay/android/view/AfterpayModalTheme.kt @@ -16,13 +16,13 @@ package com.afterpay.android.view enum class AfterpayModalTheme(val slug: String) { - MINT(""), - WHITE("-theme-white"), - ; + MINT(""), + WHITE("-theme-white"), + ; - internal companion object { + internal companion object { - @JvmField - val DEFAULT = AfterpayModalTheme.MINT - } + @JvmField + val DEFAULT = AfterpayModalTheme.MINT + } } diff --git a/afterpay/src/main/kotlin/com/afterpay/android/view/AfterpayMoreInfoOptions.kt b/afterpay/src/main/kotlin/com/afterpay/android/view/AfterpayMoreInfoOptions.kt index 2e35550f..fcbe02cf 100644 --- a/afterpay/src/main/kotlin/com/afterpay/android/view/AfterpayMoreInfoOptions.kt +++ b/afterpay/src/main/kotlin/com/afterpay/android/view/AfterpayMoreInfoOptions.kt @@ -19,53 +19,53 @@ import com.afterpay.android.Afterpay import com.afterpay.android.internal.Locales class AfterpayMoreInfoOptions { - internal var modalId: String? = null - internal var modalLinkStyle: AfterpayModalLinkStyle = AfterpayModalLinkStyle.DEFAULT - internal var modalTheme: AfterpayModalTheme = AfterpayModalTheme.DEFAULT - internal var isCbtEnabled: Boolean = false + internal var modalId: String? = null + internal var modalLinkStyle: AfterpayModalLinkStyle = AfterpayModalLinkStyle.DEFAULT + internal var modalTheme: AfterpayModalTheme = AfterpayModalTheme.DEFAULT + internal var isCbtEnabled: Boolean = false - /** - * Set up options for the more info link in AfterpayPriceBreakdown - * - * @param modalId the filename of a modal hosted on Afterpay static - */ - constructor( - modalId: String, - modalLinkStyle: AfterpayModalLinkStyle = AfterpayModalLinkStyle.DEFAULT, - ) { - this.modalId = modalId - this.modalLinkStyle = modalLinkStyle - } + /** + * Set up options for the more info link in AfterpayPriceBreakdown + * + * @param modalId the filename of a modal hosted on Afterpay static + */ + constructor( + modalId: String, + modalLinkStyle: AfterpayModalLinkStyle = AfterpayModalLinkStyle.DEFAULT, + ) { + this.modalId = modalId + this.modalLinkStyle = modalLinkStyle + } - /** - * Set up options for the more info link in AfterpayPriceBreakdown - * - * **Notes:** - * - Not all combinations of Locales and CBT are available. - * - * @param modalTheme the color theme used when displaying the modal - * @param isCbtEnabled whether to show the Cross Border Trade details in the modal - */ - constructor( - modalTheme: AfterpayModalTheme = AfterpayModalTheme.MINT, - isCbtEnabled: Boolean = false, - modalLinkStyle: AfterpayModalLinkStyle = AfterpayModalLinkStyle.DEFAULT, - ) { - this.modalTheme = modalTheme - this.isCbtEnabled = isCbtEnabled - this.modalLinkStyle = modalLinkStyle - } + /** + * Set up options for the more info link in AfterpayPriceBreakdown + * + * **Notes:** + * - Not all combinations of Locales and CBT are available. + * + * @param modalTheme the color theme used when displaying the modal + * @param isCbtEnabled whether to show the Cross Border Trade details in the modal + */ + constructor( + modalTheme: AfterpayModalTheme = AfterpayModalTheme.MINT, + isCbtEnabled: Boolean = false, + modalLinkStyle: AfterpayModalLinkStyle = AfterpayModalLinkStyle.DEFAULT, + ) { + this.modalTheme = modalTheme + this.isCbtEnabled = isCbtEnabled + this.modalLinkStyle = modalLinkStyle + } - internal fun modalFile(): String { - modalId?.let { - return "$it.html" - } + internal fun modalFile(): String { + modalId?.let { + return "$it.html" + } - val languageLocale = Afterpay.language ?: Locales.EN_GB - val locale = "${languageLocale.language}_${Afterpay.locale.country}" - val cbt = if (isCbtEnabled) "-cbt" else "" - val theme = modalTheme.slug + val languageLocale = Afterpay.language ?: Locales.EN_GB + val locale = "${languageLocale.language}_${Afterpay.locale.country}" + val cbt = if (isCbtEnabled) "-cbt" else "" + val theme = modalTheme.slug - return "$locale$theme$cbt.html" - } + return "$locale$theme$cbt.html" + } } diff --git a/afterpay/src/main/kotlin/com/afterpay/android/view/AfterpayPaymentButton.kt b/afterpay/src/main/kotlin/com/afterpay/android/view/AfterpayPaymentButton.kt index 2652e6fd..68b6c1c6 100644 --- a/afterpay/src/main/kotlin/com/afterpay/android/view/AfterpayPaymentButton.kt +++ b/afterpay/src/main/kotlin/com/afterpay/android/view/AfterpayPaymentButton.kt @@ -39,107 +39,107 @@ import java.util.Observer private const val PADDING: Int = 0 class AfterpayPaymentButton @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, + context: Context, + attrs: AttributeSet? = null, ) : AppCompatImageButton(context, attrs) { - var buttonText: ButtonText = ButtonText.DEFAULT - set(value) { - field = value - update() - } - - var colorScheme: AfterpayColorScheme = AfterpayColorScheme.DEFAULT - set(value) { - field = value - update() - } - - private val configurationObserver = Observer { _, _ -> - update() + var buttonText: ButtonText = ButtonText.DEFAULT + set(value) { + field = value + update() } - override fun onAttachedToWindow() { - super.onAttachedToWindow() - ConfigurationObservable.addObserver(configurationObserver) + var colorScheme: AfterpayColorScheme = AfterpayColorScheme.DEFAULT + set(value) { + field = value + update() } - override fun onDetachedFromWindow() { - super.onDetachedFromWindow() - ConfigurationObservable.deleteObserver(configurationObserver) + private val configurationObserver = Observer { _, _ -> + update() + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + ConfigurationObservable.addObserver(configurationObserver) + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + ConfigurationObservable.deleteObserver(configurationObserver) + } + + init { + contentDescription = String.format( + Afterpay.strings.paymentButtonContentDescription, + resources.getString(Afterpay.brand.description), + ) + scaleType = FIT_CENTER + adjustViewBounds = true + setPadding(PADDING.dp) + + context.theme.obtainStyledAttributes(attrs, R.styleable.Afterpay, 0, 0).use { attributes -> + buttonText = ButtonText.values()[ + attributes.getInteger( + R.styleable.Afterpay_afterpayButtonText, + ButtonText.DEFAULT.ordinal, + ), + ] + + colorScheme = values()[ + attributes.getInteger( + R.styleable.Afterpay_afterpayColorScheme, + AfterpayColorScheme.DEFAULT.ordinal, + ), + ] } - init { - contentDescription = String.format( - Afterpay.strings.paymentButtonContentDescription, - resources.getString(Afterpay.brand.description), - ) - scaleType = FIT_CENTER - adjustViewBounds = true - setPadding(PADDING.dp) - - context.theme.obtainStyledAttributes(attrs, R.styleable.Afterpay, 0, 0).use { attributes -> - buttonText = ButtonText.values()[ - attributes.getInteger( - R.styleable.Afterpay_afterpayButtonText, - ButtonText.DEFAULT.ordinal, - ), - ] - - colorScheme = values()[ - attributes.getInteger( - R.styleable.Afterpay_afterpayColorScheme, - AfterpayColorScheme.DEFAULT.ordinal, - ), - ] - } - - update() + update() + } + + private fun update() { + if (!Afterpay.enabled) { + visibility = View.GONE + } else { + visibility = View.VISIBLE } - private fun update() { - if (!Afterpay.enabled) { - visibility = View.GONE - } else { - visibility = View.VISIBLE - } - - setImageDrawable( - context.coloredDrawable( - drawableResId = buttonText.drawableResId, - colorResId = colorScheme.foregroundColorResId, - ), - ) - - val rippleColorResId = when (colorScheme) { - BLACK_ON_MINT, BLACK_ON_WHITE -> R.color.afterpay_ripple_light - MINT_ON_BLACK, WHITE_ON_BLACK -> R.color.afterpay_ripple_dark - } - - background = context.rippleDrawable( - rippleColorResId = rippleColorResId, - drawable = context.coloredDrawable( - drawableResId = R.drawable.afterpay_button_bg, - colorResId = colorScheme.backgroundColorResId, - ), - ) - - invalidate() - requestLayout() + setImageDrawable( + context.coloredDrawable( + drawableResId = buttonText.drawableResId, + colorResId = colorScheme.foregroundColorResId, + ), + ) + + val rippleColorResId = when (colorScheme) { + BLACK_ON_MINT, BLACK_ON_WHITE -> R.color.afterpay_ripple_light + MINT_ON_BLACK, WHITE_ON_BLACK -> R.color.afterpay_ripple_dark } - enum class ButtonText(@DrawableRes val drawableResId: Int) { + background = context.rippleDrawable( + rippleColorResId = rippleColorResId, + drawable = context.coloredDrawable( + drawableResId = R.drawable.afterpay_button_bg, + colorResId = colorScheme.backgroundColorResId, + ), + ) + + invalidate() + requestLayout() + } + + enum class ButtonText(@DrawableRes val drawableResId: Int) { - PAY_NOW(drawableResId = Afterpay.drawables.buttonPayNowForeground), - BUY_NOW(drawableResId = Afterpay.drawables.buttonBuyNowForeground), - CHECKOUT(drawableResId = Afterpay.drawables.buttonCheckoutForeground), - PLACE_ORDER(drawableResId = Afterpay.drawables.buttonPlaceOrderForeground), - ; + PAY_NOW(drawableResId = Afterpay.drawables.buttonPayNowForeground), + BUY_NOW(drawableResId = Afterpay.drawables.buttonBuyNowForeground), + CHECKOUT(drawableResId = Afterpay.drawables.buttonCheckoutForeground), + PLACE_ORDER(drawableResId = Afterpay.drawables.buttonPlaceOrderForeground), + ; - companion object { + companion object { - @JvmField - val DEFAULT = PAY_NOW - } + @JvmField + val DEFAULT = PAY_NOW } + } } diff --git a/afterpay/src/main/kotlin/com/afterpay/android/view/AfterpayPriceBreakdown.kt b/afterpay/src/main/kotlin/com/afterpay/android/view/AfterpayPriceBreakdown.kt index 8f225a0e..d4fdf9e1 100644 --- a/afterpay/src/main/kotlin/com/afterpay/android/view/AfterpayPriceBreakdown.kt +++ b/afterpay/src/main/kotlin/com/afterpay/android/view/AfterpayPriceBreakdown.kt @@ -46,389 +46,389 @@ import java.util.Currency import java.util.Observer class AfterpayPriceBreakdown @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, + context: Context, + attrs: AttributeSet? = null, ) : FrameLayout(context, attrs) { - private data class Content( - val text: String, - val description: String, - ) + private data class Content( + val text: String, + val description: String, + ) - var totalAmount: BigDecimal = BigDecimal.ZERO - set(value) { - field = value - updateText() - } - - var colorScheme: AfterpayColorScheme = AfterpayColorScheme.DEFAULT - set(value) { - field = value - updateText() - } - - var introText: AfterpayIntroText = AfterpayIntroText.DEFAULT - set(value) { - field = value - updateText() - } - - var showWithText: Boolean = true - set(value) { - field = value - updateText() - } - - var showInterestFreeText: Boolean = true - set(value) { - field = value - updateText() - } - - var logoType: AfterpayLogoType = AfterpayLogoType.DEFAULT - set(value) { - field = value - updateText() - } - - var moreInfoOptions: AfterpayMoreInfoOptions = AfterpayMoreInfoOptions() - set(value) { - field = value - updateText() - } - - private val textView: TextView = TextView(context).apply { - setTextColor(context.resolveColorAttr(android.R.attr.textColorPrimary)) - setLinkTextColor(context.resolveColorAttr(android.R.attr.textColorSecondary)) - setLineSpacing(0f, 1.2f) - textSize = 14f - movementMethod = LinkMovementMethod.getInstance() - importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_NO - layoutParams = LayoutParams(WRAP_CONTENT, WRAP_CONTENT) + var totalAmount: BigDecimal = BigDecimal.ZERO + set(value) { + field = value + updateText() } - // The terms and conditions are tied to the configured locale on the configuration - private val infoUrl: String - get() { - return "https://static.afterpay.com/modal/${moreInfoOptions.modalFile()}" - } - - init { - layoutParams = LayoutParams(WRAP_CONTENT, WRAP_CONTENT) - importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_YES - isFocusable = true - - addView(textView) - - context.theme.obtainStyledAttributes(attrs, R.styleable.Afterpay, 0, 0).use { attributes -> - colorScheme = AfterpayColorScheme.values()[ - attributes.getInteger( - R.styleable.Afterpay_afterpayColorScheme, - AfterpayColorScheme.DEFAULT.ordinal, - ), - ] - } + var colorScheme: AfterpayColorScheme = AfterpayColorScheme.DEFAULT + set(value) { + field = value + updateText() + } - updateText() + var introText: AfterpayIntroText = AfterpayIntroText.DEFAULT + set(value) { + field = value + updateText() } - private val configurationObserver = Observer { _, _ -> - updateText() + var showWithText: Boolean = true + set(value) { + field = value + updateText() } - override fun onAttachedToWindow() { - super.onAttachedToWindow() - ConfigurationObservable.addObserver(configurationObserver) + var showInterestFreeText: Boolean = true + set(value) { + field = value + updateText() } - override fun onDetachedFromWindow() { - super.onDetachedFromWindow() - ConfigurationObservable.deleteObserver(configurationObserver) + var logoType: AfterpayLogoType = AfterpayLogoType.DEFAULT + set(value) { + field = value + updateText() } - private fun updateText() { - visibility = if (!Afterpay.enabled) View.GONE else View.VISIBLE - - val drawable: Drawable = generateLogo() - val instalment = AfterpayInstalment.of(totalAmount, Afterpay.configuration, resources.configuration.locales[0]) - val content = generateContent(instalment) - - textView.apply { - text = SpannableStringBuilder().apply { - if (instalment is AfterpayInstalment.NotAvailable) { - append( - context.getString(Afterpay.brand.title), - CenteredImageSpan(drawable), - Spannable.SPAN_INCLUSIVE_EXCLUSIVE, - ) - append(" ") - append(content.text) - } else { - append(content.text) - append(" ") - append( - context.getString(Afterpay.brand.title), - CenteredImageSpan(drawable), - Spannable.SPAN_INCLUSIVE_EXCLUSIVE, - ) - } - - val linkStyle = moreInfoOptions.modalLinkStyle.config - - if (linkStyle.customContent != null) { - append(" ") - append( - linkStyle.customContent, - AfterpayInfoSpan(infoUrl, false), - Spannable.SPAN_INCLUSIVE_EXCLUSIVE, - ) - } else if (linkStyle.text != null) { - append(" ") - append( - linkStyle.text, - AfterpayInfoSpan(infoUrl, linkStyle.underlined), - Spannable.SPAN_INCLUSIVE_EXCLUSIVE, - ) - } else if (linkStyle.image != null && linkStyle.imageRenderingMode != null) { - append(" ") - - val imageDrawable = if (linkStyle.imageRenderingMode == AfterpayImageRenderingMode.TEMPLATE) { - val typedValue = TypedValue() - context.theme.resolveAttribute( - android.R.attr.textColorSecondary, - typedValue, - true, - ) - - context.coloredDrawable( - drawableResId = linkStyle.image, - colorResId = typedValue.resourceId, - ) - } else { - ResourcesCompat.getDrawable(resources, linkStyle.image, null) - } - - if (imageDrawable != null) { - imageDrawable.apply { - val aspectRatio = intrinsicWidth / intrinsicHeight.toFloat() - val drawableHeight = textView.paint.fontMetrics.run { descent - ascent } - val drawableWidth = drawableHeight * aspectRatio - setBounds(0, 0, drawableWidth.toInt(), drawableHeight.toInt()) - } - - val accessibleLinkString = Afterpay.strings.priceBreakdownLinkMoreInfo - append( - accessibleLinkString, - CenteredImageSpan(imageDrawable), - Spannable.SPAN_INCLUSIVE_EXCLUSIVE, - ) - - setSpan( - AfterpayInfoSpan(infoUrl), - this.length - accessibleLinkString.length, - this.length, - Spannable.SPAN_INCLUSIVE_EXCLUSIVE, - ) - } - } - } - contentDescription = content.description - } + var moreInfoOptions: AfterpayMoreInfoOptions = AfterpayMoreInfoOptions() + set(value) { + field = value + updateText() } - private fun getWidthToHeightRatioFromDrawableId(id: Int): Double { - val bmForeground = ResourcesCompat.getDrawable(resources, id, null)!! - val width = bmForeground.intrinsicWidth - val height = bmForeground.intrinsicHeight - return width.toDouble() / height.toDouble() + private val textView: TextView = TextView(context).apply { + setTextColor(context.resolveColorAttr(android.R.attr.textColorPrimary)) + setLinkTextColor(context.resolveColorAttr(android.R.attr.textColorSecondary)) + setLineSpacing(0f, 1.2f) + textSize = 14f + movementMethod = LinkMovementMethod.getInstance() + importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_NO + layoutParams = LayoutParams(WRAP_CONTENT, WRAP_CONTENT) + } + + // The terms and conditions are tied to the configured locale on the configuration + private val infoUrl: String + get() { + return "https://static.afterpay.com/modal/${moreInfoOptions.modalFile()}" } - private fun generateLogo(): Drawable { - val drawable = when (logoType) { - AfterpayLogoType.LOCKUP -> context.coloredDrawable( - drawableResId = Afterpay.brand.lockup, - colorResId = colorScheme.foregroundColorResId, - ) - AfterpayLogoType.COMPACT_BADGE -> { - val foreGround = Afterpay.brand.badgeForegroundCropped - val ratio = getWidthToHeightRatioFromDrawableId(foreGround) - - val badge = LayerDrawable( - arrayOf( - context.coloredDrawable( - drawableResId = R.drawable.afterpay_badge_narrow_bg, - colorResId = colorScheme.backgroundColorResId, - ), - - context.coloredDrawable( - drawableResId = foreGround, - colorResId = colorScheme.foregroundColorResId, - ), - ), - ) - - badge.setLayerSize( - 1, - (40 * ratio * logoType.fontHeightMultiplier).toInt(), - (40 * logoType.fontHeightMultiplier).toInt(), - ) - badge.setLayerGravity(1, Gravity.CENTER) - - badge - } - AfterpayLogoType.BADGE -> LayerDrawable( - arrayOf( - context.coloredDrawable( - drawableResId = R.drawable.afterpay_badge_bg, - colorResId = colorScheme.backgroundColorResId, - ), - - context.coloredDrawable( - drawableResId = Afterpay.brand.badgeForeground, - colorResId = colorScheme.foregroundColorResId, - ), - ), - ) - } + init { + layoutParams = LayoutParams(WRAP_CONTENT, WRAP_CONTENT) + importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_YES + isFocusable = true - drawable.apply { - val aspectRatio = intrinsicWidth / intrinsicHeight.toFloat() - val heightFactor = logoType.fontHeightMultiplier - val drawableHeight = textView.paint.fontMetrics.run { descent - ascent } * heightFactor - val drawableWidth = drawableHeight * aspectRatio - setBounds(0, 0, drawableWidth.toInt(), drawableHeight.toInt()) - } + addView(textView) - return drawable + context.theme.obtainStyledAttributes(attrs, R.styleable.Afterpay, 0, 0).use { attributes -> + colorScheme = AfterpayColorScheme.values()[ + attributes.getInteger( + R.styleable.Afterpay_afterpayColorScheme, + AfterpayColorScheme.DEFAULT.ordinal, + ), + ] } - private fun generateContent(afterpay: AfterpayInstalment): Content = when (afterpay) { - is AfterpayInstalment.Available -> { - val isUkLocale = Afterpay.configuration?.locale == Locales.EN_GB - val isGbpCurrency = Afterpay.configuration?.currency == Currency.getInstance(Locales.EN_GB) + updateText() + } + + private val configurationObserver = Observer { _, _ -> + updateText() + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + ConfigurationObservable.addObserver(configurationObserver) + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + ConfigurationObservable.deleteObserver(configurationObserver) + } + + private fun updateText() { + visibility = if (!Afterpay.enabled) View.GONE else View.VISIBLE + + val drawable: Drawable = generateLogo() + val instalment = AfterpayInstalment.of(totalAmount, Afterpay.configuration, resources.configuration.locales[0]) + val content = generateContent(instalment) + + textView.apply { + text = SpannableStringBuilder().apply { + if (instalment is AfterpayInstalment.NotAvailable) { + append( + context.getString(Afterpay.brand.title), + CenteredImageSpan(drawable), + Spannable.SPAN_INCLUSIVE_EXCLUSIVE, + ) + append(" ") + append(content.text) + } else { + append(content.text) + append(" ") + append( + context.getString(Afterpay.brand.title), + CenteredImageSpan(drawable), + Spannable.SPAN_INCLUSIVE_EXCLUSIVE, + ) + } - val withText: String = when { - showWithText -> Afterpay.strings.priceBreakdownWith - else -> "" - } + val linkStyle = moreInfoOptions.modalLinkStyle.config + + if (linkStyle.customContent != null) { + append(" ") + append( + linkStyle.customContent, + AfterpayInfoSpan(infoUrl, false), + Spannable.SPAN_INCLUSIVE_EXCLUSIVE, + ) + } else if (linkStyle.text != null) { + append(" ") + append( + linkStyle.text, + AfterpayInfoSpan(infoUrl, linkStyle.underlined), + Spannable.SPAN_INCLUSIVE_EXCLUSIVE, + ) + } else if (linkStyle.image != null && linkStyle.imageRenderingMode != null) { + append(" ") + + val imageDrawable = if (linkStyle.imageRenderingMode == AfterpayImageRenderingMode.TEMPLATE) { + val typedValue = TypedValue() + context.theme.resolveAttribute( + android.R.attr.textColorSecondary, + typedValue, + true, + ) - val interestFreeText: String = when { - isUkLocale || isGbpCurrency -> "" - showInterestFreeText -> Afterpay.strings.priceBreakdownInterestFree - else -> "" + context.coloredDrawable( + drawableResId = linkStyle.image, + colorResId = typedValue.resourceId, + ) + } else { + ResourcesCompat.getDrawable(resources, linkStyle.image, null) + } + + if (imageDrawable != null) { + imageDrawable.apply { + val aspectRatio = intrinsicWidth / intrinsicHeight.toFloat() + val drawableHeight = textView.paint.fontMetrics.run { descent - ascent } + val drawableWidth = drawableHeight * aspectRatio + setBounds(0, 0, drawableWidth.toInt(), drawableHeight.toInt()) } - val numberOfInstalments: Int = Afterpay.configuration?.let { - AfterpayInstalment.numberOfInstalments(it.currency) - } ?: 4 - - Content( - text = String.format( - Afterpay.strings.priceBreakdownAvailable, - AfterpayIntroText.fromId(introText.id), - numberOfInstalments.toString(), - interestFreeText, - afterpay.instalmentAmount, - withText, - ).trim(), - description = String.format( - Afterpay.strings.priceBreakdownAvailableDescription, - AfterpayIntroText.fromId(introText.id), - numberOfInstalments.toString(), - interestFreeText, - afterpay.instalmentAmount, - withText, - resources.getString(Afterpay.brand.description), - ).trim(), + val accessibleLinkString = Afterpay.strings.priceBreakdownLinkMoreInfo + append( + accessibleLinkString, + CenteredImageSpan(imageDrawable), + Spannable.SPAN_INCLUSIVE_EXCLUSIVE, ) - } - is AfterpayInstalment.NotAvailable -> - if (afterpay.minimumAmount != null) { - Content( - text = String.format( - Afterpay.strings.breakdownLimit, - afterpay.minimumAmount, - afterpay.maximumAmount, - ), - description = String.format( - Afterpay.strings.breakdownLimitDescription, - resources.getString(Afterpay.brand.description), - afterpay.minimumAmount, - afterpay.maximumAmount, - ), - ) - } else { - Content( - text = String.format( - Afterpay.strings.breakdownLimit, - "1", - afterpay.maximumAmount, - ), - description = String.format( - Afterpay.strings.breakdownLimitDescription, - resources.getString(Afterpay.brand.description), - "1", - afterpay.maximumAmount, - ), - ) - } - AfterpayInstalment.NoConfiguration -> - Content( - text = Afterpay.strings.noConfiguration, - description = String.format( - Afterpay.strings.noConfigurationDescription, - resources.getString(Afterpay.brand.description), - ), + + setSpan( + AfterpayInfoSpan(infoUrl), + this.length - accessibleLinkString.length, + this.length, + Spannable.SPAN_INCLUSIVE_EXCLUSIVE, ) + } + } + } + contentDescription = content.description + } + } + + private fun getWidthToHeightRatioFromDrawableId(id: Int): Double { + val bmForeground = ResourcesCompat.getDrawable(resources, id, null)!! + val width = bmForeground.intrinsicWidth + val height = bmForeground.intrinsicHeight + return width.toDouble() / height.toDouble() + } + + private fun generateLogo(): Drawable { + val drawable = when (logoType) { + AfterpayLogoType.LOCKUP -> context.coloredDrawable( + drawableResId = Afterpay.brand.lockup, + colorResId = colorScheme.foregroundColorResId, + ) + AfterpayLogoType.COMPACT_BADGE -> { + val foreGround = Afterpay.brand.badgeForegroundCropped + val ratio = getWidthToHeightRatioFromDrawableId(foreGround) + + val badge = LayerDrawable( + arrayOf( + context.coloredDrawable( + drawableResId = R.drawable.afterpay_badge_narrow_bg, + colorResId = colorScheme.backgroundColorResId, + ), + + context.coloredDrawable( + drawableResId = foreGround, + colorResId = colorScheme.foregroundColorResId, + ), + ), + ) + + badge.setLayerSize( + 1, + (40 * ratio * logoType.fontHeightMultiplier).toInt(), + (40 * logoType.fontHeightMultiplier).toInt(), + ) + badge.setLayerGravity(1, Gravity.CENTER) + + badge + } + AfterpayLogoType.BADGE -> LayerDrawable( + arrayOf( + context.coloredDrawable( + drawableResId = R.drawable.afterpay_badge_bg, + colorResId = colorScheme.backgroundColorResId, + ), + + context.coloredDrawable( + drawableResId = Afterpay.brand.badgeForeground, + colorResId = colorScheme.foregroundColorResId, + ), + ), + ) + } + + drawable.apply { + val aspectRatio = intrinsicWidth / intrinsicHeight.toFloat() + val heightFactor = logoType.fontHeightMultiplier + val drawableHeight = textView.paint.fontMetrics.run { descent - ascent } * heightFactor + val drawableWidth = drawableHeight * aspectRatio + setBounds(0, 0, drawableWidth.toInt(), drawableHeight.toInt()) } + + return drawable + } + + private fun generateContent(afterpay: AfterpayInstalment): Content = when (afterpay) { + is AfterpayInstalment.Available -> { + val isUkLocale = Afterpay.configuration?.locale == Locales.EN_GB + val isGbpCurrency = Afterpay.configuration?.currency == Currency.getInstance(Locales.EN_GB) + + val withText: String = when { + showWithText -> Afterpay.strings.priceBreakdownWith + else -> "" + } + + val interestFreeText: String = when { + isUkLocale || isGbpCurrency -> "" + showInterestFreeText -> Afterpay.strings.priceBreakdownInterestFree + else -> "" + } + + val numberOfInstalments: Int = Afterpay.configuration?.let { + AfterpayInstalment.numberOfInstalments(it.currency) + } ?: 4 + + Content( + text = String.format( + Afterpay.strings.priceBreakdownAvailable, + AfterpayIntroText.fromId(introText.id), + numberOfInstalments.toString(), + interestFreeText, + afterpay.instalmentAmount, + withText, + ).trim(), + description = String.format( + Afterpay.strings.priceBreakdownAvailableDescription, + AfterpayIntroText.fromId(introText.id), + numberOfInstalments.toString(), + interestFreeText, + afterpay.instalmentAmount, + withText, + resources.getString(Afterpay.brand.description), + ).trim(), + ) + } + is AfterpayInstalment.NotAvailable -> + if (afterpay.minimumAmount != null) { + Content( + text = String.format( + Afterpay.strings.breakdownLimit, + afterpay.minimumAmount, + afterpay.maximumAmount, + ), + description = String.format( + Afterpay.strings.breakdownLimitDescription, + resources.getString(Afterpay.brand.description), + afterpay.minimumAmount, + afterpay.maximumAmount, + ), + ) + } else { + Content( + text = String.format( + Afterpay.strings.breakdownLimit, + "1", + afterpay.maximumAmount, + ), + description = String.format( + Afterpay.strings.breakdownLimitDescription, + resources.getString(Afterpay.brand.description), + "1", + afterpay.maximumAmount, + ), + ) + } + AfterpayInstalment.NoConfiguration -> + Content( + text = Afterpay.strings.noConfiguration, + description = String.format( + Afterpay.strings.noConfigurationDescription, + resources.getString(Afterpay.brand.description), + ), + ) + } } /** * A vertically centered image span. */ private class CenteredImageSpan(drawable: Drawable) : ImageSpan(drawable) { - override fun getSize( - paint: Paint, - text: CharSequence, - start: Int, - end: Int, - fontMetricsInt: Paint.FontMetricsInt?, - ): Int { - val drawable = drawable - val bounds = drawable.bounds - fontMetricsInt?.let { - val paintFontMetrics = paint.fontMetricsInt - val fontHeight = paintFontMetrics.descent - paintFontMetrics.ascent - val drawableHeight = drawable.bounds.height() - - val centerY = paintFontMetrics.ascent + fontHeight / 2 - it.ascent = centerY - drawableHeight / 2 - it.top = it.ascent - it.bottom = centerY + drawableHeight / 2 - it.descent = it.bottom - } - return bounds.right - } - - override fun draw( - canvas: Canvas, - text: CharSequence, - start: Int, - end: Int, - x: Float, - top: Int, - y: Int, - bottom: Int, - paint: Paint, - ) { - val drawable = drawable - canvas.save() - val fmPaint = paint.fontMetricsInt - val fontHeight = fmPaint.descent - fmPaint.ascent - val centerY = y + fmPaint.descent - fontHeight / 2 - val translationY = centerY - drawable.bounds.height() / 2 - canvas.translate(x, translationY.toFloat()) - drawable.draw(canvas) - canvas.restore() + override fun getSize( + paint: Paint, + text: CharSequence, + start: Int, + end: Int, + fontMetricsInt: Paint.FontMetricsInt?, + ): Int { + val drawable = drawable + val bounds = drawable.bounds + fontMetricsInt?.let { + val paintFontMetrics = paint.fontMetricsInt + val fontHeight = paintFontMetrics.descent - paintFontMetrics.ascent + val drawableHeight = drawable.bounds.height() + + val centerY = paintFontMetrics.ascent + fontHeight / 2 + it.ascent = centerY - drawableHeight / 2 + it.top = it.ascent + it.bottom = centerY + drawableHeight / 2 + it.descent = it.bottom } + return bounds.right + } + + override fun draw( + canvas: Canvas, + text: CharSequence, + start: Int, + end: Int, + x: Float, + top: Int, + y: Int, + bottom: Int, + paint: Paint, + ) { + val drawable = drawable + canvas.save() + val fmPaint = paint.fontMetricsInt + val fontHeight = fmPaint.descent - fmPaint.ascent + val centerY = y + fmPaint.descent - fontHeight / 2 + val translationY = centerY - drawable.bounds.height() / 2 + canvas.translate(x, translationY.toFloat()) + drawable.draw(canvas) + canvas.restore() + } } diff --git a/afterpay/src/main/kotlin/com/afterpay/android/view/AfterpayWidgetView.kt b/afterpay/src/main/kotlin/com/afterpay/android/view/AfterpayWidgetView.kt index 8ff13ff0..de79a187 100644 --- a/afterpay/src/main/kotlin/com/afterpay/android/view/AfterpayWidgetView.kt +++ b/afterpay/src/main/kotlin/com/afterpay/android/view/AfterpayWidgetView.kt @@ -45,248 +45,248 @@ import java.io.IOException import java.math.BigDecimal class AfterpayWidgetView @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0, + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, ) : WebView(context, attrs, defStyleAttr) { - private val configuration: Configuration - get() = checkNotNull(Afterpay.configuration) { "Afterpay configuration is not set" } - - private val json = Json { ignoreUnknownKeys = true } - - private lateinit var onUpdate: (Money, String?) -> Unit - private lateinit var onError: (String) -> Unit - - /** - * Initialises the Afterpay widget for the given [token] returned from a successful checkout. - * - * External links in the widget should be handled in [onExternalRequest]. [onUpdate] will be - * called for any change to the total (including the initial value) while [onError] is called - * if a problem has occurred and indicates that the order should not proceed. - * - * The Afterpay logo and heading are visible by default but can be hidden by setting [showLogo] - * and [showHeading] to false. - * - * Results in an [IllegalStateException] if the configuration has not been set. - */ - @JvmOverloads - fun init( - token: String, - onExternalRequest: (externalUrl: Uri) -> Unit, - onUpdate: (dueToday: Money, checksum: String?) -> Unit, - onError: (error: String) -> Unit, - showLogo: Boolean = true, - showHeading: Boolean = true, - ) { - if (!Afterpay.enabled) { - visibility = View.GONE - } else { - visibility = View.VISIBLE - } + private val configuration: Configuration + get() = checkNotNull(Afterpay.configuration) { "Afterpay configuration is not set" } + + private val json = Json { ignoreUnknownKeys = true } + + private lateinit var onUpdate: (Money, String?) -> Unit + private lateinit var onError: (String) -> Unit + + /** + * Initialises the Afterpay widget for the given [token] returned from a successful checkout. + * + * External links in the widget should be handled in [onExternalRequest]. [onUpdate] will be + * called for any change to the total (including the initial value) while [onError] is called + * if a problem has occurred and indicates that the order should not proceed. + * + * The Afterpay logo and heading are visible by default but can be hidden by setting [showLogo] + * and [showHeading] to false. + * + * Results in an [IllegalStateException] if the configuration has not been set. + */ + @JvmOverloads + fun init( + token: String, + onExternalRequest: (externalUrl: Uri) -> Unit, + onUpdate: (dueToday: Money, checksum: String?) -> Unit, + onError: (error: String) -> Unit, + showLogo: Boolean = true, + showHeading: Boolean = true, + ) { + if (!Afterpay.enabled) { + visibility = View.GONE + } else { + visibility = View.VISIBLE + } - check(token.isNotBlank()) { "Supplied token is empty" } - this.onUpdate = onUpdate - this.onError = onError - configureWebView(onExternalRequest, onError) { - loadWidget(""""$token"""", totalCost = null, showLogo, showHeading) - } + check(token.isNotBlank()) { "Supplied token is empty" } + this.onUpdate = onUpdate + this.onError = onError + configureWebView(onExternalRequest, onError) { + loadWidget(""""$token"""", totalCost = null, showLogo, showHeading) + } + } + + /** + * Initialises the Afterpay widget for the given [currency amount][totalCost]. + * + * External links in the widget should be handled in [onExternalRequest]. [onUpdate] will be + * called for any change to the total (including the initial value) while [onError] is called + * if a problem has occurred and indicates that the order should not proceed. + * + * The Afterpay logo and heading are visible by default but can be hidden by setting [showLogo] + * and [showHeading] to false. + * + * Results in an [IllegalStateException] if the configuration has not been set. + */ + @JvmOverloads + fun init( + totalCost: BigDecimal, + onExternalRequest: (externalUrl: Uri) -> Unit, + onUpdate: (dueToday: Money, checksum: String?) -> Unit, + onError: (error: String) -> Unit, + showLogo: Boolean = true, + showHeading: Boolean = true, + ) { + if (!Afterpay.enabled) { + visibility = View.GONE + } else { + visibility = View.VISIBLE } - /** - * Initialises the Afterpay widget for the given [currency amount][totalCost]. - * - * External links in the widget should be handled in [onExternalRequest]. [onUpdate] will be - * called for any change to the total (including the initial value) while [onError] is called - * if a problem has occurred and indicates that the order should not proceed. - * - * The Afterpay logo and heading are visible by default but can be hidden by setting [showLogo] - * and [showHeading] to false. - * - * Results in an [IllegalStateException] if the configuration has not been set. - */ - @JvmOverloads - fun init( - totalCost: BigDecimal, - onExternalRequest: (externalUrl: Uri) -> Unit, - onUpdate: (dueToday: Money, checksum: String?) -> Unit, - onError: (error: String) -> Unit, - showLogo: Boolean = true, - showHeading: Boolean = true, - ) { - if (!Afterpay.enabled) { - visibility = View.GONE - } else { - visibility = View.VISIBLE + this.onUpdate = onUpdate + this.onError = onError + configureWebView(onExternalRequest, onError) { + loadWidget(token = null, totalCost.toAmount(), showLogo, showHeading) + } + } + + override fun setWebViewClient(client: WebViewClient) = Unit + + override fun setWebChromeClient(client: WebChromeClient?) = Unit + + private fun configureWebView( + onExternalRequest: (Uri) -> Unit, + onError: (String) -> Unit, + onPageFinished: () -> Unit, + ) { + setAfterpayUserAgentString() + @SuppressLint("SetJavaScriptEnabled") + settings.javaScriptEnabled = true + settings.domStorageEnabled = true + settings.setSupportMultipleWindows(true) + addJavascriptInterface(this, "Android") + + super.setWebChromeClient( + object : WebChromeClient() { + + override fun onCreateWindow( + webView: WebView?, + isDialog: Boolean, + isUserGesture: Boolean, + resultMsg: Message?, + ): Boolean { + val message = webView?.handler?.obtainMessage() + webView?.requestFocusNodeHref(message) + message?.data?.getString("url")?.let { onExternalRequest(Uri.parse(it)) } + return false } + }, + ) - this.onUpdate = onUpdate - this.onError = onError - configureWebView(onExternalRequest, onError) { - loadWidget(token = null, totalCost.toAmount(), showLogo, showHeading) - } - } + super.setWebViewClient( + object : WebViewClient() { - override fun setWebViewClient(client: WebViewClient) = Unit - - override fun setWebChromeClient(client: WebChromeClient?) = Unit - - private fun configureWebView( - onExternalRequest: (Uri) -> Unit, - onError: (String) -> Unit, - onPageFinished: () -> Unit, - ) { - setAfterpayUserAgentString() - @SuppressLint("SetJavaScriptEnabled") - settings.javaScriptEnabled = true - settings.domStorageEnabled = true - settings.setSupportMultipleWindows(true) - addJavascriptInterface(this, "Android") - - super.setWebChromeClient( - object : WebChromeClient() { - - override fun onCreateWindow( - webView: WebView?, - isDialog: Boolean, - isUserGesture: Boolean, - resultMsg: Message?, - ): Boolean { - val message = webView?.handler?.obtainMessage() - webView?.requestFocusNodeHref(message) - message?.data?.getString("url")?.let { onExternalRequest(Uri.parse(it)) } - return false - } - }, - ) - - super.setWebViewClient( - object : WebViewClient() { - - override fun onPageFinished(view: WebView?, url: String?) { - super.onPageFinished(view, url) - onPageFinished() - } - - @RequiresApi(Build.VERSION_CODES.M) - override fun onReceivedError( - webView: WebView?, - request: WebResourceRequest?, - error: WebResourceError?, - ) { - checkNotNull(webView) { "A WebView was expected but not received" } - if (request?.isForMainFrame == true) { - onError(error?.description.toString().orDefaultError()) - } - } - - override fun onReceivedError( - webView: WebView?, - errorCode: Int, - description: String?, - failingUrl: String?, - ) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { - checkNotNull(webView) { "A WebView was expected but not received" } - onError(description.orDefaultError()) - } - } - }, - ) - - val widgetScriptUrl = context.resources.getString(R.string.afterpay_url_widget) - val bootstrapScriptUrl = context.resources.getString(R.string.afterpay_url_widget_bootstrap) - - CoroutineScope(Dispatchers.IO).launch { - try { - val html = context.assets.open("widget/index.html") - .bufferedReader() - .use { it.readText() } - .format(widgetScriptUrl, bootstrapScriptUrl) - - withContext(Dispatchers.Main.immediate) { - loadDataWithBaseURL(widgetScriptUrl, html, "text/html", "base64", null) - } - } catch (e: IOException) { - onError(e.message ?: "Failed to open widget bootstrap") - } + override fun onPageFinished(view: WebView?, url: String?) { + super.onPageFinished(view, url) + onPageFinished() } - } - private fun loadWidget( - token: String?, - totalCost: String?, - showLogo: Boolean, - showHeading: Boolean, - ) { - val style = "{ \"logo\": $showLogo, \"heading\": $showHeading }" - evaluateJavascript( - "createAfterpayWidget($token, $totalCost, \"${configuration.locale}\", $style);", - null, - ) - } + @RequiresApi(Build.VERSION_CODES.M) + override fun onReceivedError( + webView: WebView?, + request: WebResourceRequest?, + error: WebResourceError?, + ) { + checkNotNull(webView) { "A WebView was expected but not received" } + if (request?.isForMainFrame == true) { + onError(error?.description.toString().orDefaultError()) + } + } - /** - * Updates the Afterpay widget with the given [currency amount][totalCost]. - * - * Results in an [IllegalStateException] if the configuration has not been set. - */ - fun update(totalCost: BigDecimal) { - if (!Afterpay.enabled) { - visibility = View.GONE - } else { - visibility = View.VISIBLE + override fun onReceivedError( + webView: WebView?, + errorCode: Int, + description: String?, + failingUrl: String?, + ) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + checkNotNull(webView) { "A WebView was expected but not received" } + onError(description.orDefaultError()) + } } + }, + ) + + val widgetScriptUrl = context.resources.getString(R.string.afterpay_url_widget) + val bootstrapScriptUrl = context.resources.getString(R.string.afterpay_url_widget_bootstrap) + + CoroutineScope(Dispatchers.IO).launch { + try { + val html = context.assets.open("widget/index.html") + .bufferedReader() + .use { it.readText() } + .format(widgetScriptUrl, bootstrapScriptUrl) - evaluateJavascript( - "updateAmount(${totalCost.toAmount()});", - null, - ) + withContext(Dispatchers.Main.immediate) { + loadDataWithBaseURL(widgetScriptUrl, html, "text/html", "base64", null) + } + } catch (e: IOException) { + onError(e.message ?: "Failed to open widget bootstrap") + } + } + } + + private fun loadWidget( + token: String?, + totalCost: String?, + showLogo: Boolean, + showHeading: Boolean, + ) { + val style = "{ \"logo\": $showLogo, \"heading\": $showHeading }" + evaluateJavascript( + "createAfterpayWidget($token, $totalCost, \"${configuration.locale}\", $style);", + null, + ) + } + + /** + * Updates the Afterpay widget with the given [currency amount][totalCost]. + * + * Results in an [IllegalStateException] if the configuration has not been set. + */ + fun update(totalCost: BigDecimal) { + if (!Afterpay.enabled) { + visibility = View.GONE + } else { + visibility = View.VISIBLE } - private fun BigDecimal.toAmount(): String = - json.encodeToString(Money.serializer(), Money(this, configuration.currency)) + evaluateJavascript( + "updateAmount(${totalCost.toAmount()});", + null, + ) + } - @JavascriptInterface - fun postMessage(messageJson: String) { - if (messageJson.contains("resize")) return + private fun BigDecimal.toAmount(): String = + json.encodeToString(Money.serializer(), Money(this, configuration.currency)) - runCatching { - val event = json.decodeFromString(messageJson) + @JavascriptInterface + fun postMessage(messageJson: String) { + if (messageJson.contains("resize")) return - if (event.isValid && event.amountDueToday == null) { - error("Valid widget event does not contain amount due") - } + runCatching { + val event = json.decodeFromString(messageJson) - return@runCatching event - } - .onSuccess { - if (it.isValid) { - onUpdate(it.amountDueToday!!, it.paymentScheduleChecksum) - } else { - onError(it.error?.message.orDefaultError()) - } - } - .onFailure { onError(it.message.orDefaultError()) } + if (event.isValid && event.amountDueToday == null) { + error("Valid widget event does not contain amount due") + } + + return@runCatching event } + .onSuccess { + if (it.isValid) { + onUpdate(it.amountDueToday!!, it.paymentScheduleChecksum) + } else { + onError(it.error?.message.orDefaultError()) + } + } + .onFailure { onError(it.message.orDefaultError()) } + } - private fun String?.orDefaultError() = - takeUnless { it.isNullOrBlank() } ?: "An unknown error occurred" + private fun String?.orDefaultError() = + takeUnless { it.isNullOrBlank() } ?: "An unknown error occurred" + + @Serializable + private data class Event( + val isValid: Boolean, + val amountDueToday: Money? = null, + val paymentScheduleChecksum: String? = null, + val error: Error? = null, + ) { @Serializable - private data class Event( - val isValid: Boolean, - val amountDueToday: Money? = null, - val paymentScheduleChecksum: String? = null, - val error: Error? = null, - ) { - - @Serializable - data class Error( - val errorCode: String? = null, - val errorId: String? = null, - val message: String? = null, - val httpStatusCode: Int? = null, - ) - } + data class Error( + val errorCode: String? = null, + val errorId: String? = null, + val message: String? = null, + val httpStatusCode: Int? = null, + ) + } } diff --git a/afterpay/src/test/kotlin/com/afterpay/android/AfterpayCashAppApiTest.kt b/afterpay/src/test/kotlin/com/afterpay/android/AfterpayCashAppApiTest.kt index 31465839..0a9e7a8e 100644 --- a/afterpay/src/test/kotlin/com/afterpay/android/AfterpayCashAppApiTest.kt +++ b/afterpay/src/test/kotlin/com/afterpay/android/AfterpayCashAppApiTest.kt @@ -31,102 +31,102 @@ import java.net.URL import javax.net.ssl.HttpsURLConnection class AfterpayCashAppApiTest { - companion object { - private val connection = mockk() - - @BeforeClass - @JvmStatic - fun setup() { - every { connection.requestMethod = "POST" } returns Unit - every { connection.doInput = true } returns Unit - every { connection.doOutput = true } returns Unit - every { connection.setRequestProperty(any(), any()) } returns Unit - every { connection.outputStream } returns ByteArrayOutputStream() - } + companion object { + private val connection = mockk() + + @BeforeClass + @JvmStatic + fun setup() { + every { connection.requestMethod = "POST" } returns Unit + every { connection.doInput = true } returns Unit + every { connection.doOutput = true } returns Unit + every { connection.setRequestProperty(any(), any()) } returns Unit + every { connection.outputStream } returns ByteArrayOutputStream() } + } - @Test - fun `test cashRequest for 503 response`() { - every { connection.responseCode } returns 503 - every { connection.errorStream } returns ByteArrayInputStream(byteArrayOf()) + @Test + fun `test cashRequest for 503 response`() { + every { connection.responseCode } returns 503 + every { connection.errorStream } returns ByteArrayInputStream(byteArrayOf()) - val url = mockk() - every { url.openConnection() } returns connection - val requestBody = """{ "token": "123" }""" + val url = mockk() + every { url.openConnection() } returns connection + val requestBody = """{ "token": "123" }""" - val result = AfterpayCashAppApi.cashRequest(url, CashHttpVerb.POST, requestBody) + val result = AfterpayCashAppApi.cashRequest(url, CashHttpVerb.POST, requestBody) - assert(result.isFailure) - assertEquals("Unexpected response code: 503.", result.exceptionOrNull()?.message) - assert(result.exceptionOrNull() is InvalidObjectException) - } + assert(result.isFailure) + assertEquals("Unexpected response code: 503.", result.exceptionOrNull()?.message) + assert(result.exceptionOrNull() is InvalidObjectException) + } - @Test - fun `test cashRequest for 400 response`() { - every { connection.responseCode } returns 400 - every { connection.errorStream } returns ByteArrayInputStream(byteArrayOf()) + @Test + fun `test cashRequest for 400 response`() { + every { connection.responseCode } returns 400 + every { connection.errorStream } returns ByteArrayInputStream(byteArrayOf()) - val url = mockk() - every { url.openConnection() } returns connection - val requestBody = """{ "token": "123" }""" + val url = mockk() + every { url.openConnection() } returns connection + val requestBody = """{ "token": "123" }""" - val result = AfterpayCashAppApi.cashRequest(url, CashHttpVerb.POST, requestBody) + val result = AfterpayCashAppApi.cashRequest(url, CashHttpVerb.POST, requestBody) - assert(result.isFailure) - assertEquals("Unexpected response code: 400.", result.exceptionOrNull()?.message) - assert(result.exceptionOrNull() is InvalidObjectException) - } + assert(result.isFailure) + assertEquals("Unexpected response code: 400.", result.exceptionOrNull()?.message) + assert(result.exceptionOrNull() is InvalidObjectException) + } - @OptIn(ExperimentalSerializationApi::class) - @Test - fun `test cashRequest for invalid json response`() { - val responseBody = """ + @OptIn(ExperimentalSerializationApi::class) + @Test + fun `test cashRequest for invalid json response`() { + val responseBody = """ { "invalid" : "json" } - """.trimIndent() - val inputStream = ByteArrayInputStream(responseBody.toByteArray()) + """.trimIndent() + val inputStream = ByteArrayInputStream(responseBody.toByteArray()) - every { connection.responseCode } returns 200 - every { connection.inputStream } returns inputStream - every { connection.errorStream } returns null + every { connection.responseCode } returns 200 + every { connection.inputStream } returns inputStream + every { connection.errorStream } returns null - val url = mockk() - every { url.openConnection() } returns connection - val requestBody = """{ "token": "123" }""" + val url = mockk() + every { url.openConnection() } returns connection + val requestBody = """{ "token": "123" }""" - val result = AfterpayCashAppApi.cashRequest(url, CashHttpVerb.POST, requestBody) - val response = result.exceptionOrNull()!! + val result = AfterpayCashAppApi.cashRequest(url, CashHttpVerb.POST, requestBody) + val response = result.exceptionOrNull()!! - assert(result.isFailure) - assert(response is MissingFieldException) - } + assert(result.isFailure) + assert(response is MissingFieldException) + } - @Test - fun `test cashRequest for valid json response`() { - val responseBody = """ + @Test + fun `test cashRequest for valid json response`() { + val responseBody = """ { "jwtToken" : "abc123", "redirectUrl" : "https://example.com/some/path/confirm", "externalBrandId" : "BRAND_ABC" } - """.trimIndent() - val inputStream = ByteArrayInputStream(responseBody.toByteArray()) + """.trimIndent() + val inputStream = ByteArrayInputStream(responseBody.toByteArray()) - every { connection.responseCode } returns 200 - every { connection.inputStream } returns inputStream - every { connection.errorStream } returns null + every { connection.responseCode } returns 200 + every { connection.inputStream } returns inputStream + every { connection.errorStream } returns null - val url = mockk() - every { url.openConnection() } returns connection - val requestBody = """{ "token": "123" }""" + val url = mockk() + every { url.openConnection() } returns connection + val requestBody = """{ "token": "123" }""" - val result = AfterpayCashAppApi.cashRequest(url, CashHttpVerb.POST, requestBody) - val response = result.getOrNull()!! + val result = AfterpayCashAppApi.cashRequest(url, CashHttpVerb.POST, requestBody) + val response = result.getOrNull()!! - assert(result.isSuccess) - assertEquals("BRAND_ABC", response.externalBrandId) - assertEquals("abc123", response.jwtToken) - assertEquals("https://example.com/some/path/confirm", response.redirectUrl) - } + assert(result.isSuccess) + assertEquals("BRAND_ABC", response.externalBrandId) + assertEquals("abc123", response.jwtToken) + assertEquals("https://example.com/some/path/confirm", response.redirectUrl) + } } diff --git a/afterpay/src/test/kotlin/com/afterpay/android/AfterpayCashAppJwtTest.kt b/afterpay/src/test/kotlin/com/afterpay/android/AfterpayCashAppJwtTest.kt index 56517d55..56b89a3b 100644 --- a/afterpay/src/test/kotlin/com/afterpay/android/AfterpayCashAppJwtTest.kt +++ b/afterpay/src/test/kotlin/com/afterpay/android/AfterpayCashAppJwtTest.kt @@ -22,63 +22,63 @@ import org.junit.Assert.assertEquals import org.junit.Test class AfterpayCashAppJwtTest { - /** - * JWT was created with the following payload - * - * { - * "amount": { - * "amount": "80.8", - * "currency": "USD", - * "symbol": "$" - * }, - * "token": "123", - * "externalMerchantId": "merchant_abc123", - * "redirectUrl": "https://example.com" - * } - */ - @Suppress("ktlint:standard:max-line-length") - private val validJwtValidPayload = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6IjRjYmMwNzY4NGQxYzRlZmIzMGY2YjA1M2VhZjM1Zjc1In0.eyJhbW91bnQiOnsiYW1vdW50IjoiODAuOCIsImN1cnJlbmN5IjoiVVNEIiwic3ltYm9sIjoiJCJ9LCJ0b2tlbiI6IjEyMyIsImV4dGVybmFsTWVyY2hhbnRJZCI6Im1lcmNoYW50X2FiYzEyMyIsInJlZGlyZWN0VXJsIjoiaHR0cHM6Ly9leGFtcGxlLmNvbSJ9.OJo_w8zisdC5572lxfAP2TYf434G_MH9KqpO0nInTabhTvJXIrtfWJsW2Ic4YupN0BfiRKUMdxAAD9f3jtszHQ" + /** + * JWT was created with the following payload + * + * { + * "amount": { + * "amount": "80.8", + * "currency": "USD", + * "symbol": "$" + * }, + * "token": "123", + * "externalMerchantId": "merchant_abc123", + * "redirectUrl": "https://example.com" + * } + */ + @Suppress("ktlint:standard:max-line-length") + private val validJwtValidPayload = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6IjRjYmMwNzY4NGQxYzRlZmIzMGY2YjA1M2VhZjM1Zjc1In0.eyJhbW91bnQiOnsiYW1vdW50IjoiODAuOCIsImN1cnJlbmN5IjoiVVNEIiwic3ltYm9sIjoiJCJ9LCJ0b2tlbiI6IjEyMyIsImV4dGVybmFsTWVyY2hhbnRJZCI6Im1lcmNoYW50X2FiYzEyMyIsInJlZGlyZWN0VXJsIjoiaHR0cHM6Ly9leGFtcGxlLmNvbSJ9.OJo_w8zisdC5572lxfAP2TYf434G_MH9KqpO0nInTabhTvJXIrtfWJsW2Ic4YupN0BfiRKUMdxAAD9f3jtszHQ" - /** - * JWT was created randomly - */ - @Suppress("ktlint:standard:max-line-length") - private val invalidJwtRandomGenerated = "aw4j3kj32.2nkjgsjfkbr1kjbwekjbwejkbqjerbwrkb.ae4rargaggr" + /** + * JWT was created randomly + */ + @Suppress("ktlint:standard:max-line-length") + private val invalidJwtRandomGenerated = "aw4j3kj32.2nkjgsjfkbr1kjbwekjbwejkbqjerbwrkb.ae4rargaggr" - /** - * JWT was created with the same payload as [validJwtValidPayload] and then - * 2 characters were removed from the start and end of the body part - */ - @Suppress("ktlint:standard:max-line-length") - private val invalidJwtMissingChars = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6IjRjYmMwNzY4NGQxYzRlZmIzMGY2YjA1M2VhZjM1Zjc1In0.JhbW91bnQiOnsiYW1vdW50IjoiODAuOCIsImN1cnJlbmN5IjoiVVNEIiwic3ltYm9sIjoiJCJ9LCJ0b2tlbiI6IjEyMyIsImV4dGVybmFsTWVyY2hhbnRJZCI6Im1lcmNoYW50X2FiYzEyMyIsInJlZGlyZWN0VXJsIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS.OJo_w8zisdC5572lxfAP2TYf434G_MH9KqpO0nInTabhTvJXIrtfWJsW2Ic4YupN0BfiRKUMdxAAD9f3jtszHQ" + /** + * JWT was created with the same payload as [validJwtValidPayload] and then + * 2 characters were removed from the start and end of the body part + */ + @Suppress("ktlint:standard:max-line-length") + private val invalidJwtMissingChars = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6IjRjYmMwNzY4NGQxYzRlZmIzMGY2YjA1M2VhZjM1Zjc1In0.JhbW91bnQiOnsiYW1vdW50IjoiODAuOCIsImN1cnJlbmN5IjoiVVNEIiwic3ltYm9sIjoiJCJ9LCJ0b2tlbiI6IjEyMyIsImV4dGVybmFsTWVyY2hhbnRJZCI6Im1lcmNoYW50X2FiYzEyMyIsInJlZGlyZWN0VXJsIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS.OJo_w8zisdC5572lxfAP2TYf434G_MH9KqpO0nInTabhTvJXIrtfWJsW2Ic4YupN0BfiRKUMdxAAD9f3jtszHQ" - @Test - fun `Should decode valid jwt with valid payload`() { - val jwtResult = AfterpayCashAppJwt.decode(validJwtValidPayload) - val jwt = jwtResult.getOrNull()!! + @Test + fun `Should decode valid jwt with valid payload`() { + val jwtResult = AfterpayCashAppJwt.decode(validJwtValidPayload) + val jwt = jwtResult.getOrNull()!! - assertEquals(true, jwtResult.isSuccess) - assertEquals("80.8", jwt.amount.amount) - assertEquals("123", jwt.token) - assertEquals("merchant_abc123", jwt.externalMerchantId) - assertEquals("https://example.com", jwt.redirectUrl) - } + assertEquals(true, jwtResult.isSuccess) + assertEquals("80.8", jwt.amount.amount) + assertEquals("123", jwt.token) + assertEquals("merchant_abc123", jwt.externalMerchantId) + assertEquals("https://example.com", jwt.redirectUrl) + } - @Test - fun `Should fail to decode randomly generated invalid JWT`() { - val jwtResult = AfterpayCashAppJwt.decode(invalidJwtRandomGenerated) - val jwt = jwtResult.exceptionOrNull()!! + @Test + fun `Should fail to decode randomly generated invalid JWT`() { + val jwtResult = AfterpayCashAppJwt.decode(invalidJwtRandomGenerated) + val jwt = jwtResult.exceptionOrNull()!! - assertEquals(true, jwtResult.isFailure) - assertThat(jwt.message, containsString("Expected start of the object '{', but had 'EOF' instead")) - } + assertEquals(true, jwtResult.isFailure) + assertThat(jwt.message, containsString("Expected start of the object '{', but had 'EOF' instead")) + } - @Test - fun `Should fail to decode invalid JWT due to stripped chars`() { - val jwtResult = AfterpayCashAppJwt.decode(invalidJwtMissingChars) - val jwt = jwtResult.exceptionOrNull()!! + @Test + fun `Should fail to decode invalid JWT due to stripped chars`() { + val jwtResult = AfterpayCashAppJwt.decode(invalidJwtMissingChars) + val jwt = jwtResult.exceptionOrNull()!! - assertEquals(true, jwtResult.isFailure) - assertThat(jwt.message, containsString("Expected start of the object '{', but had 'EOF' instead")) - } + assertEquals(true, jwtResult.isFailure) + assertThat(jwt.message, containsString("Expected start of the object '{', but had 'EOF' instead")) + } } diff --git a/afterpay/src/test/kotlin/com/afterpay/android/AfterpayEnabled.kt b/afterpay/src/test/kotlin/com/afterpay/android/AfterpayEnabled.kt index 456ab913..b7c55253 100644 --- a/afterpay/src/test/kotlin/com/afterpay/android/AfterpayEnabled.kt +++ b/afterpay/src/test/kotlin/com/afterpay/android/AfterpayEnabled.kt @@ -21,58 +21,58 @@ import org.junit.Test import java.util.Locale class AfterpayEnabled { - private val environment = AfterpayEnvironment.SANDBOX + private val environment = AfterpayEnvironment.SANDBOX - private val validMerchantLocales: Array = arrayOf( - Locales.EN_AU, - Locales.EN_CA, - Locales.EN_GB, - Locales.EN_NZ, - Locales.EN_US, - Locales.FR_CA, - ) + private val validMerchantLocales: Array = arrayOf( + Locales.EN_AU, + Locales.EN_CA, + Locales.EN_GB, + Locales.EN_NZ, + Locales.EN_US, + Locales.FR_CA, + ) - @Test - fun `Afterpay is enabled for basic config and locale is English`() { - Afterpay.setConfiguration( - minimumAmount = "10.00", - maximumAmount = "100.00", - currencyCode = "AUD", - locale = Locale.US, - environment = environment, - consumerLocale = Locale.ENGLISH, - ) + @Test + fun `Afterpay is enabled for basic config and locale is English`() { + Afterpay.setConfiguration( + minimumAmount = "10.00", + maximumAmount = "100.00", + currencyCode = "AUD", + locale = Locale.US, + environment = environment, + consumerLocale = Locale.ENGLISH, + ) - Assert.assertEquals(true, Afterpay.enabled) - } + Assert.assertEquals(true, Afterpay.enabled) + } - @Test - fun `Afterpay is not enabled for basic config and language is not available for merchant country`() { - Afterpay.setConfiguration( - minimumAmount = "10.00", - maximumAmount = "100.00", - currencyCode = "AUD", - locale = Locale.US, - environment = environment, - consumerLocale = Locale.FRANCE, - ) + @Test + fun `Afterpay is not enabled for basic config and language is not available for merchant country`() { + Afterpay.setConfiguration( + minimumAmount = "10.00", + maximumAmount = "100.00", + currencyCode = "AUD", + locale = Locale.US, + environment = environment, + consumerLocale = Locale.FRANCE, + ) - Assert.assertEquals(false, Afterpay.enabled) - } + Assert.assertEquals(false, Afterpay.enabled) + } - @Test - fun `Afterpay is enabled for merchant locales`() { - for (locale in validMerchantLocales) { - Afterpay.setConfiguration( - minimumAmount = "10.00", - maximumAmount = "1000.00", - currencyCode = "USD", - locale = locale, - environment = environment, - consumerLocale = Locale.US, - ) + @Test + fun `Afterpay is enabled for merchant locales`() { + for (locale in validMerchantLocales) { + Afterpay.setConfiguration( + minimumAmount = "10.00", + maximumAmount = "1000.00", + currencyCode = "USD", + locale = locale, + environment = environment, + consumerLocale = Locale.US, + ) - Assert.assertEquals(true, Afterpay.enabled) - } + Assert.assertEquals(true, Afterpay.enabled) } + } } diff --git a/afterpay/src/test/kotlin/com/afterpay/android/AfterpayInstalmentLocaleTest.kt b/afterpay/src/test/kotlin/com/afterpay/android/AfterpayInstalmentLocaleTest.kt index fc506894..7800236a 100644 --- a/afterpay/src/test/kotlin/com/afterpay/android/AfterpayInstalmentLocaleTest.kt +++ b/afterpay/src/test/kotlin/com/afterpay/android/AfterpayInstalmentLocaleTest.kt @@ -25,291 +25,291 @@ import java.util.Currency import java.util.Locale class AfterpayInstalmentLocaleTest { - private val australianDollar: Currency = Currency.getInstance("AUD") - private val canadianDollar: Currency = Currency.getInstance("CAD") - private val poundSterling: Currency = Currency.getInstance("GBP") - private val newZealandDollar: Currency = Currency.getInstance("NZD") - private val unitedStatesDollar: Currency = Currency.getInstance("USD") - private val euro: Currency = Currency.getInstance("EUR") - - private val oneHundredAndTwenty = 120.toBigDecimal() - - private val itLocale = Locale.ITALY - private val frLocale = Locale.FRANCE - private val esLocale = Locale("es", "ES") - - @Test - fun `available instalment in en-AU locale`() { - val instalments = createAllAvailableInstalments(oneHundredAndTwenty, Locales.EN_AU) - - assertEquals("$30.00", instalments.aud.instalmentAmount) - assertEquals("$30.00 CAD", instalments.cad.instalmentAmount) - assertEquals("£30.00", instalments.gbp.instalmentAmount) - assertEquals("$30.00 NZD", instalments.nzd.instalmentAmount) - assertEquals("$30.00 USD", instalments.usd.instalmentAmount) - } - - @Test - fun `unavailable instalment in en-AU locale`() { - val instalments = createAllUnavailableInstalments(oneHundredAndTwenty, Locales.EN_AU) - - assertEquals("$10", instalments.aud.minimumAmount) - assertEquals("$10 CAD", instalments.cad.minimumAmount) - assertEquals("£10", instalments.gbp.minimumAmount) - assertEquals("$10 NZD", instalments.nzd.minimumAmount) - assertEquals("$10 USD", instalments.usd.minimumAmount) - assertEquals(null, instalments.eur.minimumAmount) - } - - @Test - fun `available instalment in en-CA locale`() { - val instalments = createAllAvailableInstalments(oneHundredAndTwenty, Locales.EN_CA) - - assertEquals("$30.00 AUD", instalments.aud.instalmentAmount) - assertEquals("$30.00", instalments.cad.instalmentAmount) - assertEquals("£30.00", instalments.gbp.instalmentAmount) - assertEquals("$30.00 NZD", instalments.nzd.instalmentAmount) - assertEquals("$30.00 USD", instalments.usd.instalmentAmount) - } - - @Test - fun `unavailable instalment in en-CA locale`() { - val instalments = createAllUnavailableInstalments(oneHundredAndTwenty, Locales.EN_CA) - - assertEquals("$10 AUD", instalments.aud.minimumAmount) - assertEquals("$10", instalments.cad.minimumAmount) - assertEquals("£10", instalments.gbp.minimumAmount) - assertEquals("$10 NZD", instalments.nzd.minimumAmount) - assertEquals("$10 USD", instalments.usd.minimumAmount) - assertEquals(null, instalments.eur.minimumAmount) - } - - @Test - fun `available instalment in fr-CA locale`() { - val instalments = createAllAvailableInstalments(oneHundredAndTwenty, Locales.FR_CA) - - assertEquals("$30,00 AUD", instalments.aud.instalmentAmount) - assertEquals("30,00 $", instalments.cad.instalmentAmount) - assertEquals("£30,00", instalments.gbp.instalmentAmount) - assertEquals("$30,00 NZD", instalments.nzd.instalmentAmount) - assertEquals("$30,00 USD", instalments.usd.instalmentAmount) - } - - @Test - fun `unavailable instalment in fr-CA locale`() { - val instalments = createAllUnavailableInstalments(oneHundredAndTwenty, Locales.FR_CA) - - assertEquals("$10 AUD", instalments.aud.minimumAmount) - assertEquals("10 $", instalments.cad.minimumAmount) - assertEquals("£10", instalments.gbp.minimumAmount) - assertEquals("$10 NZD", instalments.nzd.minimumAmount) - assertEquals("$10 USD", instalments.usd.minimumAmount) - assertEquals(null, instalments.eur.minimumAmount) - } - - @Test - fun `available instalment in en-GB locale`() { - val instalments = createAllAvailableInstalments(oneHundredAndTwenty, Locales.EN_GB) - - assertEquals("$30.00 AUD", instalments.aud.instalmentAmount) - assertEquals("$30.00 CAD", instalments.cad.instalmentAmount) - assertEquals("£30.00", instalments.gbp.instalmentAmount) - assertEquals("$30.00 NZD", instalments.nzd.instalmentAmount) - assertEquals("$30.00 USD", instalments.usd.instalmentAmount) - } - - @Test - fun `unavailable instalment in en-GB locale`() { - val instalments = createAllUnavailableInstalments(oneHundredAndTwenty, Locales.EN_GB) - - assertEquals("$10 AUD", instalments.aud.minimumAmount) - assertEquals("$10 CAD", instalments.cad.minimumAmount) - assertEquals("£10", instalments.gbp.minimumAmount) - assertEquals("$10 NZD", instalments.nzd.minimumAmount) - assertEquals("$10 USD", instalments.usd.minimumAmount) - assertEquals(null, instalments.eur.minimumAmount) - } - - @Test - fun `available instalment in en-NZ locale`() { - val instalments = createAllAvailableInstalments(oneHundredAndTwenty, Locales.EN_NZ) - - assertEquals("$30.00 AUD", instalments.aud.instalmentAmount) - assertEquals("$30.00 CAD", instalments.cad.instalmentAmount) - assertEquals("£30.00", instalments.gbp.instalmentAmount) - assertEquals("$30.00", instalments.nzd.instalmentAmount) - assertEquals("$30.00 USD", instalments.usd.instalmentAmount) - } - - @Test - fun `unavailable instalment in en-NZ locale`() { - val instalments = createAllUnavailableInstalments(oneHundredAndTwenty, Locales.EN_NZ) - - assertEquals("$10 AUD", instalments.aud.minimumAmount) - assertEquals("$10 CAD", instalments.cad.minimumAmount) - assertEquals("£10", instalments.gbp.minimumAmount) - assertEquals("$10", instalments.nzd.minimumAmount) - assertEquals("$10 USD", instalments.usd.minimumAmount) - assertEquals(null, instalments.eur.minimumAmount) - } - - @Test - fun `available instalment in en-US locale`() { - val instalments = createAllAvailableInstalments(oneHundredAndTwenty, Locales.EN_US) - - assertEquals("A$30.00", instalments.aud.instalmentAmount) - assertEquals("CA$30.00", instalments.cad.instalmentAmount) - assertEquals("£30.00", instalments.gbp.instalmentAmount) - assertEquals("NZ$30.00", instalments.nzd.instalmentAmount) - assertEquals("$30.00", instalments.usd.instalmentAmount) - } - - @Test - fun `unavailable instalment in en-US locale`() { - val instalments = createAllUnavailableInstalments(oneHundredAndTwenty, Locales.EN_US) - - assertEquals("A$10", instalments.aud.minimumAmount) - assertEquals("CA$10", instalments.cad.minimumAmount) - assertEquals("£10", instalments.gbp.minimumAmount) - assertEquals("NZ$10", instalments.nzd.minimumAmount) - assertEquals("$10", instalments.usd.minimumAmount) - assertEquals(null, instalments.eur.minimumAmount) - } - - @Test - fun `available instalment in it-IT locale`() { - val instalments = createAllAvailableInstalments(oneHundredAndTwenty, itLocale) - - assertEquals("$30,00 AUD", instalments.aud.instalmentAmount) - assertEquals("$30,00 CAD", instalments.cad.instalmentAmount) - assertEquals("£30,00", instalments.gbp.instalmentAmount) - assertEquals("$30,00 NZD", instalments.nzd.instalmentAmount) - assertEquals("$30,00 USD", instalments.usd.instalmentAmount) - } - - @Test - fun `unavailable instalment in it-IT locale`() { - val instalments = createAllUnavailableInstalments(oneHundredAndTwenty, itLocale) - - assertEquals("$10 AUD", instalments.aud.minimumAmount) - assertEquals("$10 CAD", instalments.cad.minimumAmount) - assertEquals("£10", instalments.gbp.minimumAmount) - assertEquals("$10 NZD", instalments.nzd.minimumAmount) - assertEquals("$10 USD", instalments.usd.minimumAmount) - assertEquals(null, instalments.eur.minimumAmount) - } - - @Test - fun `available instalment in fr-FR locale`() { - val instalments = createAllAvailableInstalments(oneHundredAndTwenty, frLocale) - - assertEquals("$30,00 AUD", instalments.aud.instalmentAmount) - assertEquals("$30,00 CAD", instalments.cad.instalmentAmount) - assertEquals("£30,00", instalments.gbp.instalmentAmount) - assertEquals("$30,00 NZD", instalments.nzd.instalmentAmount) - assertEquals("$30,00 USD", instalments.usd.instalmentAmount) - } - - @Test - fun `unavailable instalment in fr-FR locale`() { - val instalments = createAllUnavailableInstalments(oneHundredAndTwenty, frLocale) - - assertEquals("$10 AUD", instalments.aud.minimumAmount) - assertEquals("$10 CAD", instalments.cad.minimumAmount) - assertEquals("£10", instalments.gbp.minimumAmount) - assertEquals("$10 NZD", instalments.nzd.minimumAmount) - assertEquals("$10 USD", instalments.usd.minimumAmount) - assertEquals(null, instalments.eur.minimumAmount) - } - - @Test - fun `available instalment in es-ES locale`() { - val instalments = createAllAvailableInstalments(oneHundredAndTwenty, esLocale) - - assertEquals("$30,00 AUD", instalments.aud.instalmentAmount) - assertEquals("$30,00 CAD", instalments.cad.instalmentAmount) - assertEquals("£30,00", instalments.gbp.instalmentAmount) - assertEquals("$30,00 NZD", instalments.nzd.instalmentAmount) - assertEquals("$30,00 USD", instalments.usd.instalmentAmount) - } - - @Test - fun `unavailable instalment in es-ES locale`() { - val instalments = createAllUnavailableInstalments(oneHundredAndTwenty, esLocale) - - assertEquals("$10 AUD", instalments.aud.minimumAmount) - assertEquals("$10 CAD", instalments.cad.minimumAmount) - assertEquals("£10", instalments.gbp.minimumAmount) - assertEquals("$10 NZD", instalments.nzd.minimumAmount) - assertEquals("$10 USD", instalments.usd.minimumAmount) - assertEquals(null, instalments.eur.minimumAmount) - } - - private data class AllAvailableInstallments( - val aud: AfterpayInstalment.Available, - val cad: AfterpayInstalment.Available, - val gbp: AfterpayInstalment.Available, - val nzd: AfterpayInstalment.Available, - val usd: AfterpayInstalment.Available, + private val australianDollar: Currency = Currency.getInstance("AUD") + private val canadianDollar: Currency = Currency.getInstance("CAD") + private val poundSterling: Currency = Currency.getInstance("GBP") + private val newZealandDollar: Currency = Currency.getInstance("NZD") + private val unitedStatesDollar: Currency = Currency.getInstance("USD") + private val euro: Currency = Currency.getInstance("EUR") + + private val oneHundredAndTwenty = 120.toBigDecimal() + + private val itLocale = Locale.ITALY + private val frLocale = Locale.FRANCE + private val esLocale = Locale("es", "ES") + + @Test + fun `available instalment in en-AU locale`() { + val instalments = createAllAvailableInstalments(oneHundredAndTwenty, Locales.EN_AU) + + assertEquals("$30.00", instalments.aud.instalmentAmount) + assertEquals("$30.00 CAD", instalments.cad.instalmentAmount) + assertEquals("£30.00", instalments.gbp.instalmentAmount) + assertEquals("$30.00 NZD", instalments.nzd.instalmentAmount) + assertEquals("$30.00 USD", instalments.usd.instalmentAmount) + } + + @Test + fun `unavailable instalment in en-AU locale`() { + val instalments = createAllUnavailableInstalments(oneHundredAndTwenty, Locales.EN_AU) + + assertEquals("$10", instalments.aud.minimumAmount) + assertEquals("$10 CAD", instalments.cad.minimumAmount) + assertEquals("£10", instalments.gbp.minimumAmount) + assertEquals("$10 NZD", instalments.nzd.minimumAmount) + assertEquals("$10 USD", instalments.usd.minimumAmount) + assertEquals(null, instalments.eur.minimumAmount) + } + + @Test + fun `available instalment in en-CA locale`() { + val instalments = createAllAvailableInstalments(oneHundredAndTwenty, Locales.EN_CA) + + assertEquals("$30.00 AUD", instalments.aud.instalmentAmount) + assertEquals("$30.00", instalments.cad.instalmentAmount) + assertEquals("£30.00", instalments.gbp.instalmentAmount) + assertEquals("$30.00 NZD", instalments.nzd.instalmentAmount) + assertEquals("$30.00 USD", instalments.usd.instalmentAmount) + } + + @Test + fun `unavailable instalment in en-CA locale`() { + val instalments = createAllUnavailableInstalments(oneHundredAndTwenty, Locales.EN_CA) + + assertEquals("$10 AUD", instalments.aud.minimumAmount) + assertEquals("$10", instalments.cad.minimumAmount) + assertEquals("£10", instalments.gbp.minimumAmount) + assertEquals("$10 NZD", instalments.nzd.minimumAmount) + assertEquals("$10 USD", instalments.usd.minimumAmount) + assertEquals(null, instalments.eur.minimumAmount) + } + + @Test + fun `available instalment in fr-CA locale`() { + val instalments = createAllAvailableInstalments(oneHundredAndTwenty, Locales.FR_CA) + + assertEquals("$30,00 AUD", instalments.aud.instalmentAmount) + assertEquals("30,00 $", instalments.cad.instalmentAmount) + assertEquals("£30,00", instalments.gbp.instalmentAmount) + assertEquals("$30,00 NZD", instalments.nzd.instalmentAmount) + assertEquals("$30,00 USD", instalments.usd.instalmentAmount) + } + + @Test + fun `unavailable instalment in fr-CA locale`() { + val instalments = createAllUnavailableInstalments(oneHundredAndTwenty, Locales.FR_CA) + + assertEquals("$10 AUD", instalments.aud.minimumAmount) + assertEquals("10 $", instalments.cad.minimumAmount) + assertEquals("£10", instalments.gbp.minimumAmount) + assertEquals("$10 NZD", instalments.nzd.minimumAmount) + assertEquals("$10 USD", instalments.usd.minimumAmount) + assertEquals(null, instalments.eur.minimumAmount) + } + + @Test + fun `available instalment in en-GB locale`() { + val instalments = createAllAvailableInstalments(oneHundredAndTwenty, Locales.EN_GB) + + assertEquals("$30.00 AUD", instalments.aud.instalmentAmount) + assertEquals("$30.00 CAD", instalments.cad.instalmentAmount) + assertEquals("£30.00", instalments.gbp.instalmentAmount) + assertEquals("$30.00 NZD", instalments.nzd.instalmentAmount) + assertEquals("$30.00 USD", instalments.usd.instalmentAmount) + } + + @Test + fun `unavailable instalment in en-GB locale`() { + val instalments = createAllUnavailableInstalments(oneHundredAndTwenty, Locales.EN_GB) + + assertEquals("$10 AUD", instalments.aud.minimumAmount) + assertEquals("$10 CAD", instalments.cad.minimumAmount) + assertEquals("£10", instalments.gbp.minimumAmount) + assertEquals("$10 NZD", instalments.nzd.minimumAmount) + assertEquals("$10 USD", instalments.usd.minimumAmount) + assertEquals(null, instalments.eur.minimumAmount) + } + + @Test + fun `available instalment in en-NZ locale`() { + val instalments = createAllAvailableInstalments(oneHundredAndTwenty, Locales.EN_NZ) + + assertEquals("$30.00 AUD", instalments.aud.instalmentAmount) + assertEquals("$30.00 CAD", instalments.cad.instalmentAmount) + assertEquals("£30.00", instalments.gbp.instalmentAmount) + assertEquals("$30.00", instalments.nzd.instalmentAmount) + assertEquals("$30.00 USD", instalments.usd.instalmentAmount) + } + + @Test + fun `unavailable instalment in en-NZ locale`() { + val instalments = createAllUnavailableInstalments(oneHundredAndTwenty, Locales.EN_NZ) + + assertEquals("$10 AUD", instalments.aud.minimumAmount) + assertEquals("$10 CAD", instalments.cad.minimumAmount) + assertEquals("£10", instalments.gbp.minimumAmount) + assertEquals("$10", instalments.nzd.minimumAmount) + assertEquals("$10 USD", instalments.usd.minimumAmount) + assertEquals(null, instalments.eur.minimumAmount) + } + + @Test + fun `available instalment in en-US locale`() { + val instalments = createAllAvailableInstalments(oneHundredAndTwenty, Locales.EN_US) + + assertEquals("A$30.00", instalments.aud.instalmentAmount) + assertEquals("CA$30.00", instalments.cad.instalmentAmount) + assertEquals("£30.00", instalments.gbp.instalmentAmount) + assertEquals("NZ$30.00", instalments.nzd.instalmentAmount) + assertEquals("$30.00", instalments.usd.instalmentAmount) + } + + @Test + fun `unavailable instalment in en-US locale`() { + val instalments = createAllUnavailableInstalments(oneHundredAndTwenty, Locales.EN_US) + + assertEquals("A$10", instalments.aud.minimumAmount) + assertEquals("CA$10", instalments.cad.minimumAmount) + assertEquals("£10", instalments.gbp.minimumAmount) + assertEquals("NZ$10", instalments.nzd.minimumAmount) + assertEquals("$10", instalments.usd.minimumAmount) + assertEquals(null, instalments.eur.minimumAmount) + } + + @Test + fun `available instalment in it-IT locale`() { + val instalments = createAllAvailableInstalments(oneHundredAndTwenty, itLocale) + + assertEquals("$30,00 AUD", instalments.aud.instalmentAmount) + assertEquals("$30,00 CAD", instalments.cad.instalmentAmount) + assertEquals("£30,00", instalments.gbp.instalmentAmount) + assertEquals("$30,00 NZD", instalments.nzd.instalmentAmount) + assertEquals("$30,00 USD", instalments.usd.instalmentAmount) + } + + @Test + fun `unavailable instalment in it-IT locale`() { + val instalments = createAllUnavailableInstalments(oneHundredAndTwenty, itLocale) + + assertEquals("$10 AUD", instalments.aud.minimumAmount) + assertEquals("$10 CAD", instalments.cad.minimumAmount) + assertEquals("£10", instalments.gbp.minimumAmount) + assertEquals("$10 NZD", instalments.nzd.minimumAmount) + assertEquals("$10 USD", instalments.usd.minimumAmount) + assertEquals(null, instalments.eur.minimumAmount) + } + + @Test + fun `available instalment in fr-FR locale`() { + val instalments = createAllAvailableInstalments(oneHundredAndTwenty, frLocale) + + assertEquals("$30,00 AUD", instalments.aud.instalmentAmount) + assertEquals("$30,00 CAD", instalments.cad.instalmentAmount) + assertEquals("£30,00", instalments.gbp.instalmentAmount) + assertEquals("$30,00 NZD", instalments.nzd.instalmentAmount) + assertEquals("$30,00 USD", instalments.usd.instalmentAmount) + } + + @Test + fun `unavailable instalment in fr-FR locale`() { + val instalments = createAllUnavailableInstalments(oneHundredAndTwenty, frLocale) + + assertEquals("$10 AUD", instalments.aud.minimumAmount) + assertEquals("$10 CAD", instalments.cad.minimumAmount) + assertEquals("£10", instalments.gbp.minimumAmount) + assertEquals("$10 NZD", instalments.nzd.minimumAmount) + assertEquals("$10 USD", instalments.usd.minimumAmount) + assertEquals(null, instalments.eur.minimumAmount) + } + + @Test + fun `available instalment in es-ES locale`() { + val instalments = createAllAvailableInstalments(oneHundredAndTwenty, esLocale) + + assertEquals("$30,00 AUD", instalments.aud.instalmentAmount) + assertEquals("$30,00 CAD", instalments.cad.instalmentAmount) + assertEquals("£30,00", instalments.gbp.instalmentAmount) + assertEquals("$30,00 NZD", instalments.nzd.instalmentAmount) + assertEquals("$30,00 USD", instalments.usd.instalmentAmount) + } + + @Test + fun `unavailable instalment in es-ES locale`() { + val instalments = createAllUnavailableInstalments(oneHundredAndTwenty, esLocale) + + assertEquals("$10 AUD", instalments.aud.minimumAmount) + assertEquals("$10 CAD", instalments.cad.minimumAmount) + assertEquals("£10", instalments.gbp.minimumAmount) + assertEquals("$10 NZD", instalments.nzd.minimumAmount) + assertEquals("$10 USD", instalments.usd.minimumAmount) + assertEquals(null, instalments.eur.minimumAmount) + } + + private data class AllAvailableInstallments( + val aud: AfterpayInstalment.Available, + val cad: AfterpayInstalment.Available, + val gbp: AfterpayInstalment.Available, + val nzd: AfterpayInstalment.Available, + val usd: AfterpayInstalment.Available, + ) + + private data class AllUnavailableInstallments( + val aud: AfterpayInstalment.NotAvailable, + val cad: AfterpayInstalment.NotAvailable, + val gbp: AfterpayInstalment.NotAvailable, + val nzd: AfterpayInstalment.NotAvailable, + val usd: AfterpayInstalment.NotAvailable, + val eur: AfterpayInstalment.NotAvailable, + ) + + private fun createAllAvailableInstalments(amount: BigDecimal, locale: Locale): AllAvailableInstallments { + return AllAvailableInstallments( + aud = availableInstalment(amount, australianDollar, locale), + cad = availableInstalment(amount, canadianDollar, locale), + gbp = availableInstalment(amount, poundSterling, locale), + nzd = availableInstalment(amount, newZealandDollar, locale), + usd = availableInstalment(amount, unitedStatesDollar, locale), ) - - private data class AllUnavailableInstallments( - val aud: AfterpayInstalment.NotAvailable, - val cad: AfterpayInstalment.NotAvailable, - val gbp: AfterpayInstalment.NotAvailable, - val nzd: AfterpayInstalment.NotAvailable, - val usd: AfterpayInstalment.NotAvailable, - val eur: AfterpayInstalment.NotAvailable, + } + + private fun createAllUnavailableInstalments(amount: BigDecimal, locale: Locale): AllUnavailableInstallments { + return AllUnavailableInstallments( + aud = unavailableInstalment(amount, australianDollar, locale), + cad = unavailableInstalment(amount, canadianDollar, locale), + gbp = unavailableInstalment(amount, poundSterling, locale), + nzd = unavailableInstalment(amount, newZealandDollar, locale), + usd = unavailableInstalment(amount, unitedStatesDollar, locale), + eur = unavailableInstalment(amount, euro, locale), ) - - private fun createAllAvailableInstalments(amount: BigDecimal, locale: Locale): AllAvailableInstallments { - return AllAvailableInstallments( - aud = availableInstalment(amount, australianDollar, locale), - cad = availableInstalment(amount, canadianDollar, locale), - gbp = availableInstalment(amount, poundSterling, locale), - nzd = availableInstalment(amount, newZealandDollar, locale), - usd = availableInstalment(amount, unitedStatesDollar, locale), - ) - } - - private fun createAllUnavailableInstalments(amount: BigDecimal, locale: Locale): AllUnavailableInstallments { - return AllUnavailableInstallments( - aud = unavailableInstalment(amount, australianDollar, locale), - cad = unavailableInstalment(amount, canadianDollar, locale), - gbp = unavailableInstalment(amount, poundSterling, locale), - nzd = unavailableInstalment(amount, newZealandDollar, locale), - usd = unavailableInstalment(amount, unitedStatesDollar, locale), - eur = unavailableInstalment(amount, euro, locale), - ) - } - - private fun availableInstalment( - amount: BigDecimal, - currency: Currency, - locale: Locale, - ): AfterpayInstalment.Available { - val configuration = Configuration( - 50.toBigDecimal(), - 1000.toBigDecimal(), - currency, - locale, - AfterpayEnvironment.SANDBOX, - ) - return AfterpayInstalment.of(amount, configuration, locale) as AfterpayInstalment.Available - } - - private fun unavailableInstalment( - amount: BigDecimal, - currency: Currency, - locale: Locale, - ): AfterpayInstalment.NotAvailable { - val configuration = Configuration( - 10.toBigDecimal(), - 20.toBigDecimal(), - currency, - locale, - AfterpayEnvironment.SANDBOX, - ) - return AfterpayInstalment.of(amount, configuration, locale) as AfterpayInstalment.NotAvailable - } + } + + private fun availableInstalment( + amount: BigDecimal, + currency: Currency, + locale: Locale, + ): AfterpayInstalment.Available { + val configuration = Configuration( + 50.toBigDecimal(), + 1000.toBigDecimal(), + currency, + locale, + AfterpayEnvironment.SANDBOX, + ) + return AfterpayInstalment.of(amount, configuration, locale) as AfterpayInstalment.Available + } + + private fun unavailableInstalment( + amount: BigDecimal, + currency: Currency, + locale: Locale, + ): AfterpayInstalment.NotAvailable { + val configuration = Configuration( + 10.toBigDecimal(), + 20.toBigDecimal(), + currency, + locale, + AfterpayEnvironment.SANDBOX, + ) + return AfterpayInstalment.of(amount, configuration, locale) as AfterpayInstalment.NotAvailable + } } diff --git a/afterpay/src/test/kotlin/com/afterpay/android/AfterpayInstalmentPriceTest.kt b/afterpay/src/test/kotlin/com/afterpay/android/AfterpayInstalmentPriceTest.kt index 7d10a226..67984d21 100644 --- a/afterpay/src/test/kotlin/com/afterpay/android/AfterpayInstalmentPriceTest.kt +++ b/afterpay/src/test/kotlin/com/afterpay/android/AfterpayInstalmentPriceTest.kt @@ -25,83 +25,83 @@ import java.util.Currency import java.util.Locale class AfterpayInstalmentPriceTest { - private val oneHundredAndTwentyOne = 121.toBigDecimal() + private val oneHundredAndTwentyOne = 121.toBigDecimal() - private val priceCasesDouble = mapOf( - 40.0 to "$10.00", - 40.2 to "$10.05", - 40.00 to "$10.00", - 40.01 to "$10.00", - 40.02 to "$10.00", - 40.03 to "$10.01", - 40.04 to "$10.01", - 40.009 to "$10.00", - 40.019 to "$10.00", - 40.3934567 to "$10.10", - ) + private val priceCasesDouble = mapOf( + 40.0 to "$10.00", + 40.2 to "$10.05", + 40.00 to "$10.00", + 40.01 to "$10.00", + 40.02 to "$10.00", + 40.03 to "$10.01", + 40.04 to "$10.01", + 40.009 to "$10.00", + 40.019 to "$10.00", + 40.3934567 to "$10.10", + ) - private val priceCasesInt = mapOf( - 40 to "$10.00", - 41 to "$10.25", - 100 to "$25.00", - 103 to "$25.75", - ) + private val priceCasesInt = mapOf( + 40 to "$10.00", + 41 to "$10.25", + 100 to "$25.00", + 103 to "$25.75", + ) - @Test - fun `available instalment double test cases display correctly`() { - priceCasesDouble.forEach { (amount, instalmentAmount) -> - val instalments = availableInstalment( - amount.toBigDecimal(), - Currency.getInstance("AUD"), - Locales.EN_AU, - ) - Assert.assertEquals(instalmentAmount, instalments.instalmentAmount) - } + @Test + fun `available instalment double test cases display correctly`() { + priceCasesDouble.forEach { (amount, instalmentAmount) -> + val instalments = availableInstalment( + amount.toBigDecimal(), + Currency.getInstance("AUD"), + Locales.EN_AU, + ) + Assert.assertEquals(instalmentAmount, instalments.instalmentAmount) } + } - @Test - fun `available instalment int test cases display correctly`() { - priceCasesInt.forEach { (amount, instalmentAmount) -> - val instalments = availableInstalment( - amount.toBigDecimal(), - Currency.getInstance("AUD"), - Locales.EN_AU, - ) - Assert.assertEquals(instalmentAmount, instalments.instalmentAmount) - } + @Test + fun `available instalment int test cases display correctly`() { + priceCasesInt.forEach { (amount, instalmentAmount) -> + val instalments = availableInstalment( + amount.toBigDecimal(), + Currency.getInstance("AUD"), + Locales.EN_AU, + ) + Assert.assertEquals(instalmentAmount, instalments.instalmentAmount) } + } - /** - * This test was added because when using the / character to divide a BigDecimal - * it uses the BigDecimal.div extension - * - * see here: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/java.math.-big-decimal/div.html - * - * Relevant section is: The scale of the result is the same as the scale of this (divident) - */ - @Test - fun `available instalment when amount is round and odd`() { - val instalments = availableInstalment( - oneHundredAndTwentyOne, - Currency.getInstance("AUD"), - Locales.EN_AU, - ) + /** + * This test was added because when using the / character to divide a BigDecimal + * it uses the BigDecimal.div extension + * + * see here: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/java.math.-big-decimal/div.html + * + * Relevant section is: The scale of the result is the same as the scale of this (divident) + */ + @Test + fun `available instalment when amount is round and odd`() { + val instalments = availableInstalment( + oneHundredAndTwentyOne, + Currency.getInstance("AUD"), + Locales.EN_AU, + ) - Assert.assertEquals("$30.25", instalments.instalmentAmount) - } + Assert.assertEquals("$30.25", instalments.instalmentAmount) + } - private fun availableInstalment( - amount: BigDecimal, - currency: Currency, - locale: Locale, - ): AfterpayInstalment.Available { - val configuration = Configuration( - 2.toBigDecimal(), - 1000.toBigDecimal(), - currency, - locale, - AfterpayEnvironment.SANDBOX, - ) - return AfterpayInstalment.of(amount, configuration, locale) as AfterpayInstalment.Available - } + private fun availableInstalment( + amount: BigDecimal, + currency: Currency, + locale: Locale, + ): AfterpayInstalment.Available { + val configuration = Configuration( + 2.toBigDecimal(), + 1000.toBigDecimal(), + currency, + locale, + AfterpayEnvironment.SANDBOX, + ) + return AfterpayInstalment.of(amount, configuration, locale) as AfterpayInstalment.Available + } } diff --git a/afterpay/src/test/kotlin/com/afterpay/android/AfterpayTest.kt b/afterpay/src/test/kotlin/com/afterpay/android/AfterpayTest.kt index aa559d3b..0e3a1df9 100644 --- a/afterpay/src/test/kotlin/com/afterpay/android/AfterpayTest.kt +++ b/afterpay/src/test/kotlin/com/afterpay/android/AfterpayTest.kt @@ -22,129 +22,129 @@ import java.util.Locale class AfterpayTest { - private val environment = AfterpayEnvironment.SANDBOX + private val environment = AfterpayEnvironment.SANDBOX - private val invalidMerchantLocales: Array = arrayOf( - Locale.ITALY, - Locale.FRANCE, - Locale("es", "ES"), - Locale.JAPAN, - ) + private val invalidMerchantLocales: Array = arrayOf( + Locale.ITALY, + Locale.FRANCE, + Locale("es", "ES"), + Locale.JAPAN, + ) - @Test - fun `setConfiguration does not throw for valid configuration`() { - Afterpay.setConfiguration( - minimumAmount = "10.00", - maximumAmount = "100.00", - currencyCode = "AUD", - locale = Locale.US, - environment = environment, - ) - } + @Test + fun `setConfiguration does not throw for valid configuration`() { + Afterpay.setConfiguration( + minimumAmount = "10.00", + maximumAmount = "100.00", + currencyCode = "AUD", + locale = Locale.US, + environment = environment, + ) + } - @Test - fun `setConfiguration does not throw for valid configuration with no minimum amount`() { - Afterpay.setConfiguration( - minimumAmount = null, - maximumAmount = "100.00", - currencyCode = "AUD", - locale = Locale.US, - environment = environment, - ) - } + @Test + fun `setConfiguration does not throw for valid configuration with no minimum amount`() { + Afterpay.setConfiguration( + minimumAmount = null, + maximumAmount = "100.00", + currencyCode = "AUD", + locale = Locale.US, + environment = environment, + ) + } - @Test - fun `setConfiguration throws for invalid currency code`() { - assertThrows(IllegalArgumentException::class.java) { - Afterpay.setConfiguration( - minimumAmount = "10.00", - maximumAmount = "100.00", - currencyCode = "foo", - locale = Locale.US, - environment = environment, - ) - } + @Test + fun `setConfiguration throws for invalid currency code`() { + assertThrows(IllegalArgumentException::class.java) { + Afterpay.setConfiguration( + minimumAmount = "10.00", + maximumAmount = "100.00", + currencyCode = "foo", + locale = Locale.US, + environment = environment, + ) } + } - @Test - fun `setConfiguration throws for invalid minimum order amount`() { - assertThrows(NumberFormatException::class.java) { - Afterpay.setConfiguration( - minimumAmount = "foo", - maximumAmount = "100.00", - currencyCode = "AUD", - locale = Locale.US, - environment = environment, - ) - } + @Test + fun `setConfiguration throws for invalid minimum order amount`() { + assertThrows(NumberFormatException::class.java) { + Afterpay.setConfiguration( + minimumAmount = "foo", + maximumAmount = "100.00", + currencyCode = "AUD", + locale = Locale.US, + environment = environment, + ) } + } - @Test - fun `setConfiguration throws for invalid maximum order amount`() { - assertThrows(NumberFormatException::class.java) { - Afterpay.setConfiguration( - minimumAmount = "10.00", - maximumAmount = "foo", - currencyCode = "AUD", - locale = Locale.US, - environment = environment, - ) - } + @Test + fun `setConfiguration throws for invalid maximum order amount`() { + assertThrows(NumberFormatException::class.java) { + Afterpay.setConfiguration( + minimumAmount = "10.00", + maximumAmount = "foo", + currencyCode = "AUD", + locale = Locale.US, + environment = environment, + ) } + } - @Test - fun `setConfiguration throws for minimum order amount less than zero`() { - assertThrows(IllegalArgumentException::class.java) { - Afterpay.setConfiguration( - minimumAmount = "-10.00", - maximumAmount = "100.00", - currencyCode = "AUD", - locale = Locale.US, - environment = environment, - ) - } + @Test + fun `setConfiguration throws for minimum order amount less than zero`() { + assertThrows(IllegalArgumentException::class.java) { + Afterpay.setConfiguration( + minimumAmount = "-10.00", + maximumAmount = "100.00", + currencyCode = "AUD", + locale = Locale.US, + environment = environment, + ) } + } - @Test - fun `setConfiguration throws for minimum order amount greater than maximum amount`() { - assertThrows(IllegalArgumentException::class.java) { - Afterpay.setConfiguration( - minimumAmount = "110.00", - maximumAmount = "100.00", - currencyCode = "AUD", - locale = Locale.US, - environment = environment, - ) - } + @Test + fun `setConfiguration throws for minimum order amount greater than maximum amount`() { + assertThrows(IllegalArgumentException::class.java) { + Afterpay.setConfiguration( + minimumAmount = "110.00", + maximumAmount = "100.00", + currencyCode = "AUD", + locale = Locale.US, + environment = environment, + ) } + } - @Test - fun `setConfiguration throws for maximum order amount less than zero`() { - assertThrows(IllegalArgumentException::class.java) { - Afterpay.setConfiguration( - minimumAmount = null, - maximumAmount = "-2.00", - currencyCode = "AUD", - locale = Locale.US, - environment = environment, - ) - } + @Test + fun `setConfiguration throws for maximum order amount less than zero`() { + assertThrows(IllegalArgumentException::class.java) { + Afterpay.setConfiguration( + minimumAmount = null, + maximumAmount = "-2.00", + currencyCode = "AUD", + locale = Locale.US, + environment = environment, + ) } + } - @Test - fun `setConfiguration throws for a locale not in the valid set`() { - assertThrows(IllegalArgumentException::class.java) { - for (locale in invalidMerchantLocales) { - Afterpay.setConfiguration( - minimumAmount = "10.00", - maximumAmount = "1000.00", - currencyCode = "USD", - locale = locale, - environment = environment, - ) + @Test + fun `setConfiguration throws for a locale not in the valid set`() { + assertThrows(IllegalArgumentException::class.java) { + for (locale in invalidMerchantLocales) { + Afterpay.setConfiguration( + minimumAmount = "10.00", + maximumAmount = "1000.00", + currencyCode = "USD", + locale = locale, + environment = environment, + ) - Assert.assertEquals(false, Afterpay.enabled) - } - } + Assert.assertEquals(false, Afterpay.enabled) + } } + } } diff --git a/afterpay/src/test/kotlin/com/afterpay/android/util/Base64.kt b/afterpay/src/test/kotlin/com/afterpay/android/util/Base64.kt index 6f7ef48d..8b8561b9 100644 --- a/afterpay/src/test/kotlin/com/afterpay/android/util/Base64.kt +++ b/afterpay/src/test/kotlin/com/afterpay/android/util/Base64.kt @@ -18,13 +18,13 @@ package android.util import java.util.Base64 object Base64 { - @JvmStatic - fun encodeToString(input: ByteArray?, flags: Int): String { - return Base64.getEncoder().encodeToString(input) - } + @JvmStatic + fun encodeToString(input: ByteArray?, flags: Int): String { + return Base64.getEncoder().encodeToString(input) + } - @JvmStatic - fun decode(str: String?, flags: Int): ByteArray { - return Base64.getDecoder().decode(str) - } + @JvmStatic + fun decode(str: String?, flags: Int): ByteArray { + return Base64.getDecoder().decode(str) + } } diff --git a/sample/build.gradle.kts b/sample/build.gradle.kts index 54bd719e..fa426d46 100644 --- a/sample/build.gradle.kts +++ b/sample/build.gradle.kts @@ -14,71 +14,71 @@ * limitations under the License. */ plugins { - alias(libs.plugins.android.application) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.secrets.gradle.plugin) + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.secrets.gradle.plugin) } java { - toolchain { - languageVersion.set(JavaLanguageVersion.of(libs.versions.java.get())) - } + toolchain { + languageVersion.set(JavaLanguageVersion.of(libs.versions.java.get())) + } } kotlin { - jvmToolchain { - languageVersion.set(JavaLanguageVersion.of(libs.versions.java.get())) - } + jvmToolchain { + languageVersion.set(JavaLanguageVersion.of(libs.versions.java.get())) + } } android { - namespace = "com.example" + namespace = "com.example" - compileSdk = libs.versions.exampleCompileSdk.get().toInt() + compileSdk = libs.versions.exampleCompileSdk.get().toInt() - buildFeatures { - buildConfig = true - viewBinding = true - } + buildFeatures { + buildConfig = true + viewBinding = true + } - compileOptions { - isCoreLibraryDesugaringEnabled = true - } + compileOptions { + isCoreLibraryDesugaringEnabled = true + } - defaultConfig { - applicationId = "com.afterpay.android.sample" - minSdk = libs.versions.minSdk.get().toInt() - targetSdk = libs.versions.exampleCompileSdk.get().toInt() + defaultConfig { + applicationId = "com.afterpay.android.sample" + minSdk = libs.versions.minSdk.get().toInt() + targetSdk = libs.versions.exampleCompileSdk.get().toInt() - versionCode = 1 - versionName = "1.0" - } + versionCode = 1 + versionName = "1.0" + } } dependencies { - // toggle between using Maven artifact and local module - implementation(projects.afterpay) - // implementation(libs.afterpay.android) + // toggle between using Maven artifact and local module + implementation(projects.afterpay) + // implementation(libs.afterpay.android) - implementation(libs.app.cash.paykit) + implementation(libs.app.cash.paykit) - implementation(libs.androidxAppcompat) - implementation(libs.androidxCoreKtx) - implementation(libs.androidxLifecycleRuntimeKtx) - implementation(libs.material) + implementation(libs.androidxAppcompat) + implementation(libs.androidxCoreKtx) + implementation(libs.androidxLifecycleRuntimeKtx) + implementation(libs.material) - /** - * Usage of retrofit / moshi is entirely preference to interact with - * sample Merchant API - */ - implementation(libs.moshi) - implementation(libs.moshi.kotlin) - implementation(libs.retrofit) - implementation(libs.retrofit.converter.moshi) + /** + * Usage of retrofit / moshi is entirely preference to interact with + * sample Merchant API + */ + implementation(libs.moshi) + implementation(libs.moshi.kotlin) + implementation(libs.retrofit) + implementation(libs.retrofit.converter.moshi) - coreLibraryDesugaring(libs.androidToolsDesugarJdk) + coreLibraryDesugaring(libs.androidToolsDesugarJdk) } secrets { - defaultPropertiesFileName = "local.defaults.properties" + defaultPropertiesFileName = "local.defaults.properties" } diff --git a/sample/src/main/java/com/example/AfterpayV2SampleActivity.kt b/sample/src/main/java/com/example/AfterpayV2SampleActivity.kt index 2ceb7c1f..8775d2a8 100644 --- a/sample/src/main/java/com/example/AfterpayV2SampleActivity.kt +++ b/sample/src/main/java/com/example/AfterpayV2SampleActivity.kt @@ -45,190 +45,190 @@ import java.util.Locale * Activity showing the Afterpay V2 checkout flow */ class AfterpayV2SampleActivity : AppCompatActivity() { - private lateinit var bindings: AfterpayV2LayoutBinding - - private val activityResultLauncher = - registerForActivityResult( - ActivityResultContracts.StartActivityForResult(), - ) { result: ActivityResult -> - result.data?.let { data -> - /** - * Step 6: Afterpay flow will complete and return result to your app. If checkout was - * successful you will receive an order token which you pass back to your server - * for final processing. - */ - when (result.resultCode) { - Activity.RESULT_OK -> { - val orderToken = Afterpay.parseCheckoutSuccessResponse(data) - val message = "Checkout Complete, Received order token: $orderToken" - Log.d(tag, message) - showToast(this, message) - } - - Activity.RESULT_CANCELED -> { - val status = Afterpay.parseCheckoutCancellationResponse(data) - showToast(this, "Checkout Cancelled: $status") - } - } - } - } - - private val checkoutHandler = - object : AfterpayCheckoutV2Handler { - override fun didCommenceCheckout(onTokenLoaded: (Result) -> Unit) { - Log.d(tag, "didCommenceCheckout") - /** - * Step 4: After starting Afterpay flow, Afterpay SDK will call back asking you to - * load your token. You will need to make another network request to your own server - * to fetch this token. - * - * You can fetch this token *before* customer clicks button and "have it ready". - * However you will need to re-request this token any time order amount changes. - * (e.g. customer adds items to cart) . - * - * Here in the sample app we request token from a sample merchant API / server. - * You must first have the sample server running: - * https://github.com/afterpay/sdk-example-server - */ - CoroutineScope(Dispatchers.IO).launch { - Log.d(tag, "Getting token from merchant server") - merchantApi() - .getToken( - GetTokenRequest( - email = customerEmail, - amount = "12.00", - // Our example server uses the same endpoint to get configuration for - // both V1 and V2. Server calls them "standard" and "express" respectively. - mode = CheckoutMode.STANDARD, - isCashAppPay = false, - ), - ) - /** - * Step 5: Pass that token back to Afterpay SDK via the supplied - * [onTokenLoaded] callback - */ - .onSuccess { response -> - // Because the example server uses the same endpoint for both V1 and V2 - // it returns more data than we need. Ideally your server would return - // only the token. (i.e. response.url is not needed here) - Log.d(tag, "Token received and loaded") - onTokenLoaded(Result.success(response.token)) - } - .onFailure { - // If fetching token failed for any reason you need to tell Afterpay - // so it can correctly return to your app - val msg = "Failed to fetch token" - Log.e(tag, msg) - onTokenLoaded(Result.failure(Throwable(msg))) - } - } - } - - override fun shippingAddressDidChange( - address: ShippingAddress, - onProvideShippingOptions: (ShippingOptionsResult) -> Unit, - ) { - TODO("Not yet implemented") - } - - override fun shippingOptionDidChange( - shippingOption: ShippingOption, - onProvideShippingOption: (ShippingOptionUpdateResult?) -> Unit, - ) { - TODO("Not yet implemented") - } - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - bindings = AfterpayV2LayoutBinding.inflate(LayoutInflater.from(this)) - val view = bindings.root - setContentView(view) + private lateinit var bindings: AfterpayV2LayoutBinding - bindings.afterpayButton.setOnClickListener { - /** - * Step 3: Once configuration is set, Afterpay SDK will enable the button, allowing - * customer to start checkout flow. - * - * Create [AfterpayCheckoutV2Options], create an [android.content.Intent], and start flow. - * - * Here we use Android's ActivityResult APIs but you can use older [startActivityForResult] - */ - val afterpayCheckoutV2Options = - AfterpayCheckoutV2Options( - pickup = bindings.pickup.isChecked, - buyNow = bindings.buyNow.isChecked, - shippingOptionRequired = bindings.shippingOptionRequiredCheckbox.isChecked, - enableSingleShippingOptionUpdate = true, - ) - - val intent = - Afterpay.createCheckoutV2Intent( - context = this@AfterpayV2SampleActivity, - options = afterpayCheckoutV2Options, - ) - - Log.d(tag, "Launching intent") - activityResultLauncher.launch(intent) + private val activityResultLauncher = + registerForActivityResult( + ActivityResultContracts.StartActivityForResult(), + ) { result: ActivityResult -> + result.data?.let { data -> + /** + * Step 6: Afterpay flow will complete and return result to your app. If checkout was + * successful you will receive an order token which you pass back to your server + * for final processing. + */ + when (result.resultCode) { + Activity.RESULT_OK -> { + val orderToken = Afterpay.parseCheckoutSuccessResponse(data) + val message = "Checkout Complete, Received order token: $orderToken" + Log.d(tag, message) + showToast(this, message) + } + + Activity.RESULT_CANCELED -> { + val status = Afterpay.parseCheckoutCancellationResponse(data) + showToast(this, "Checkout Cancelled: $status") + } } + } + } + private val checkoutHandler = + object : AfterpayCheckoutV2Handler { + override fun didCommenceCheckout(onTokenLoaded: (Result) -> Unit) { + Log.d(tag, "didCommenceCheckout") /** - * Step 0: Periodically check for new configurations from your own server. Doesn't need - * to happen before *every* transaction. Does need to happen before *first* transaction - * Use the tool of your choice to enable async non-main-thread network request - * (i.e. coroutines, rxjava, retrofit) + * Step 4: After starting Afterpay flow, Afterpay SDK will call back asking you to + * load your token. You will need to make another network request to your own server + * to fetch this token. * - * Here in the sample app we request configuration from a sample merchant API / server. + * You can fetch this token *before* customer clicks button and "have it ready". + * However you will need to re-request this token any time order amount changes. + * (e.g. customer adds items to cart) . + * + * Here in the sample app we request token from a sample merchant API / server. * You must first have the sample server running: * https://github.com/afterpay/sdk-example-server - * - * Using [lifecycleScope] is a quick-and-dirty example. Ideally you would not tie this network - * request to an Activity's lifecycle. */ - lifecycleScope.launch { - getConfiguration() + CoroutineScope(Dispatchers.IO).launch { + Log.d(tag, "Getting token from merchant server") + merchantApi() + .getToken( + GetTokenRequest( + email = customerEmail, + amount = "12.00", + // Our example server uses the same endpoint to get configuration for + // both V1 and V2. Server calls them "standard" and "express" respectively. + mode = CheckoutMode.STANDARD, + isCashAppPay = false, + ), + ) + /** + * Step 5: Pass that token back to Afterpay SDK via the supplied + * [onTokenLoaded] callback + */ + .onSuccess { response -> + // Because the example server uses the same endpoint for both V1 and V2 + // it returns more data than we need. Ideally your server would return + // only the token. (i.e. response.url is not needed here) + Log.d(tag, "Token received and loaded") + onTokenLoaded(Result.success(response.token)) + } + .onFailure { + // If fetching token failed for any reason you need to tell Afterpay + // so it can correctly return to your app + val msg = "Failed to fetch token" + Log.e(tag, msg) + onTokenLoaded(Result.failure(Throwable(msg))) + } } + } + + override fun shippingAddressDidChange( + address: ShippingAddress, + onProvideShippingOptions: (ShippingOptionsResult) -> Unit, + ) { + TODO("Not yet implemented") + } + + override fun shippingOptionDidChange( + shippingOption: ShippingOption, + onProvideShippingOption: (ShippingOptionUpdateResult?) -> Unit, + ) { + TODO("Not yet implemented") + } + } - /** - * Step 2: Set a handler which manages callbacks between your app and Afterpay SDK flow - */ - Afterpay.setCheckoutV2Handler(checkoutHandler) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + bindings = AfterpayV2LayoutBinding.inflate(LayoutInflater.from(this)) + val view = bindings.root + setContentView(view) + + bindings.afterpayButton.setOnClickListener { + /** + * Step 3: Once configuration is set, Afterpay SDK will enable the button, allowing + * customer to start checkout flow. + * + * Create [AfterpayCheckoutV2Options], create an [android.content.Intent], and start flow. + * + * Here we use Android's ActivityResult APIs but you can use older [startActivityForResult] + */ + val afterpayCheckoutV2Options = + AfterpayCheckoutV2Options( + pickup = bindings.pickup.isChecked, + buyNow = bindings.buyNow.isChecked, + shippingOptionRequired = bindings.shippingOptionRequiredCheckbox.isChecked, + enableSingleShippingOptionUpdate = true, + ) + + val intent = + Afterpay.createCheckoutV2Intent( + context = this@AfterpayV2SampleActivity, + options = afterpayCheckoutV2Options, + ) + + Log.d(tag, "Launching intent") + activityResultLauncher.launch(intent) } - private fun getConfiguration() { - CoroutineScope(Dispatchers.IO).launch { - merchantApi().getConfiguration().apply { - onFailure { error -> - val msg = "Failed to get configuration" - showToastFromBackground(this@AfterpayV2SampleActivity, msg) - Log.e(tag, msg, error) - } - - onSuccess { response: GetConfigurationResponse -> - withContext(Dispatchers.Main) { - Log.d(tag, "Fetched and setting configs") - /** - * Step 1: Pass configuration to Afterpay - * - * It is up to you to save this Configuration (e.g. local storage) to avoid repeat calls - * to fetch them. You will need to pass Configuration to Afterpay on each app - * restart (before first transaction) by calling .setConfiguration() - * - * Once this function is called, Afterpay will enable the button. - */ - Afterpay.setConfiguration( - minimumAmount = response.minimumAmount?.amount, - maximumAmount = response.maximumAmount.amount, - currencyCode = response.maximumAmount.currency, - locale = - Locale(response.locale.language, response.locale.country), - environment = AFTERPAY_ENVIRONMENT, - ) - } - } - } + /** + * Step 0: Periodically check for new configurations from your own server. Doesn't need + * to happen before *every* transaction. Does need to happen before *first* transaction + * Use the tool of your choice to enable async non-main-thread network request + * (i.e. coroutines, rxjava, retrofit) + * + * Here in the sample app we request configuration from a sample merchant API / server. + * You must first have the sample server running: + * https://github.com/afterpay/sdk-example-server + * + * Using [lifecycleScope] is a quick-and-dirty example. Ideally you would not tie this network + * request to an Activity's lifecycle. + */ + lifecycleScope.launch { + getConfiguration() + } + + /** + * Step 2: Set a handler which manages callbacks between your app and Afterpay SDK flow + */ + Afterpay.setCheckoutV2Handler(checkoutHandler) + } + + private fun getConfiguration() { + CoroutineScope(Dispatchers.IO).launch { + merchantApi().getConfiguration().apply { + onFailure { error -> + val msg = "Failed to get configuration" + showToastFromBackground(this@AfterpayV2SampleActivity, msg) + Log.e(tag, msg, error) + } + + onSuccess { response: GetConfigurationResponse -> + withContext(Dispatchers.Main) { + Log.d(tag, "Fetched and setting configs") + /** + * Step 1: Pass configuration to Afterpay + * + * It is up to you to save this Configuration (e.g. local storage) to avoid repeat calls + * to fetch them. You will need to pass Configuration to Afterpay on each app + * restart (before first transaction) by calling .setConfiguration() + * + * Once this function is called, Afterpay will enable the button. + */ + Afterpay.setConfiguration( + minimumAmount = response.minimumAmount?.amount, + maximumAmount = response.maximumAmount.amount, + currencyCode = response.maximumAmount.currency, + locale = + Locale(response.locale.language, response.locale.country), + environment = AFTERPAY_ENVIRONMENT, + ) + } } + } } + } } private val tag = AfterpayV2SampleActivity::class.java.simpleName diff --git a/sample/src/main/java/com/example/AfterpayV3SampleActivity.kt b/sample/src/main/java/com/example/AfterpayV3SampleActivity.kt index 60920dd3..88ba41b3 100644 --- a/sample/src/main/java/com/example/AfterpayV3SampleActivity.kt +++ b/sample/src/main/java/com/example/AfterpayV3SampleActivity.kt @@ -36,116 +36,116 @@ import kotlinx.coroutines.withContext * Activity showing the Afterpay V3 checkout flow */ class AfterpayV3SampleActivity : AppCompatActivity() { - private lateinit var bindings: AfterpayV3LayoutBinding + private lateinit var bindings: AfterpayV3LayoutBinding - private val activityResultLauncher = - registerForActivityResult( - ActivityResultContracts.StartActivityForResult(), - ) { result: ActivityResult -> - /** - * Step 3: Respond to intent result and show error if [android.app.Activity.RESULT_CANCELED] - * or signPayment with token if [android.app.Activity.RESULT_OK] - */ - val intent = result.data - checkNotNull(intent) - if (result.resultCode == RESULT_OK) { - Afterpay.parseCheckoutSuccessResponseV3(intent)?.let { - /** - * Step 4: Receive single-use card details. Pass these back to your server and on - * to your payment processor: - */ - it.cardDetails - it.tokens - it.cardValidUntil + private val activityResultLauncher = + registerForActivityResult( + ActivityResultContracts.StartActivityForResult(), + ) { result: ActivityResult -> + /** + * Step 3: Respond to intent result and show error if [android.app.Activity.RESULT_CANCELED] + * or signPayment with token if [android.app.Activity.RESULT_OK] + */ + val intent = result.data + checkNotNull(intent) + if (result.resultCode == RESULT_OK) { + Afterpay.parseCheckoutSuccessResponseV3(intent)?.let { + /** + * Step 4: Receive single-use card details. Pass these back to your server and on + * to your payment processor: + */ + it.cardDetails + it.tokens + it.cardValidUntil - showToast(this, "Payment details received. Ready to process payment") - } - } else if (result.resultCode == RESULT_CANCELED) { - Afterpay.parseCheckoutCancellationResponseV3(intent) - ?.let { - ( - cancellationStatusV3: CancellationStatusV3, - exception: Exception?, - ), - -> - Log.e(tag, cancellationStatusV3.toString(), exception) - } - } - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - /** - * The SDK is agnostic to the UI library of your choice: inflate XML, view binding, Compose, etc. - * Here we use view binding to keep it simple. - */ - bindings = AfterpayV3LayoutBinding.inflate(LayoutInflater.from(this)) - bindings.afterpayButton.apply { - setOnClickListener { view -> - /** - * Step 2: Once configuration is set, Afterpay SDK will automatically enable button, - * allowing customer to start checkout flow - * - * Create [CheckoutV3Configuration], create an [Intent], and start flow. - * - * Here we use Android's ActivityResult APIs but you can use older [startActivityForResult] - */ - val intent = - Afterpay.createCheckoutV3Intent( - context = view.context, - consumer = createConsumer(), - orderTotal = createOrderTotal(), - items = createItems(), - buyNow = true, - configuration = createCheckoutV3Configuration(), - ) - activityResultLauncher.launch(intent) - } + showToast(this, "Payment details received. Ready to process payment") } + } else if (result.resultCode == RESULT_CANCELED) { + Afterpay.parseCheckoutCancellationResponseV3(intent) + ?.let { + ( + cancellationStatusV3: CancellationStatusV3, + exception: Exception?, + ), + -> + Log.e(tag, cancellationStatusV3.toString(), exception) + } + } + } + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + /** + * The SDK is agnostic to the UI library of your choice: inflate XML, view binding, Compose, etc. + * Here we use view binding to keep it simple. + */ + bindings = AfterpayV3LayoutBinding.inflate(LayoutInflater.from(this)) + bindings.afterpayButton.apply { + setOnClickListener { view -> /** - * Step 0: Periodically check for new Merchant configurations. Doesn't need - * to happen before *every* transaction. Does need to happen before *first* transaction - * Use the tool of your choice to enable async non-main-thread network request (i.e. coroutines, rxjava) + * Step 2: Once configuration is set, Afterpay SDK will automatically enable button, + * allowing customer to start checkout flow * - * Using [lifecycleScope] is for simplicity. Ideally you would not tie this network - * request to an Activity's lifecycle. Use the networking, storage, lifecycle libraries of - * your choice. + * Create [CheckoutV3Configuration], create an [Intent], and start flow. + * + * Here we use Android's ActivityResult APIs but you can use older [startActivityForResult] */ - lifecycleScope.launch { - getMerchantConfig() - } + val intent = + Afterpay.createCheckoutV3Intent( + context = view.context, + consumer = createConsumer(), + orderTotal = createOrderTotal(), + items = createItems(), + buyNow = true, + configuration = createCheckoutV3Configuration(), + ) + activityResultLauncher.launch(intent) + } + } - val view = bindings.root - setContentView(view) + /** + * Step 0: Periodically check for new Merchant configurations. Doesn't need + * to happen before *every* transaction. Does need to happen before *first* transaction + * Use the tool of your choice to enable async non-main-thread network request (i.e. coroutines, rxjava) + * + * Using [lifecycleScope] is for simplicity. Ideally you would not tie this network + * request to an Activity's lifecycle. Use the networking, storage, lifecycle libraries of + * your choice. + */ + lifecycleScope.launch { + getMerchantConfig() } - private fun getMerchantConfig() = - CoroutineScope(Dispatchers.IO).launch { - Afterpay.fetchMerchantConfigurationV3(createCheckoutV3Configuration()) - .let { result: Result -> - withContext(Dispatchers.Main) { - result.onSuccess { merchantConfiguration: Configuration -> - /** - * Step 1: Pass configuration to Afterpay - * - * It is up to you to save this Configuration (e.g. local storage) to avoid repeat calls - * to fetch them. You will need to pass Configuration to Afterpay on each app - * restart (before first transaction) by calling .setConfiguration() - * - * Once this function is called, Afterpay will enable the button. - */ - Log.d(tag, "Fetched merchant configs") - Afterpay.setConfigurationV3(merchantConfiguration) - } - result.onFailure { - val msg = "Failed to fetch merchant configs" - Log.e(tag, msg, it) - showToastFromBackground(this@AfterpayV3SampleActivity, msg) - } - } - } + val view = bindings.root + setContentView(view) + } + + private fun getMerchantConfig() = + CoroutineScope(Dispatchers.IO).launch { + Afterpay.fetchMerchantConfigurationV3(createCheckoutV3Configuration()) + .let { result: Result -> + withContext(Dispatchers.Main) { + result.onSuccess { merchantConfiguration: Configuration -> + /** + * Step 1: Pass configuration to Afterpay + * + * It is up to you to save this Configuration (e.g. local storage) to avoid repeat calls + * to fetch them. You will need to pass Configuration to Afterpay on each app + * restart (before first transaction) by calling .setConfiguration() + * + * Once this function is called, Afterpay will enable the button. + */ + Log.d(tag, "Fetched merchant configs") + Afterpay.setConfigurationV3(merchantConfiguration) + } + result.onFailure { + val msg = "Failed to fetch merchant configs" + Log.e(tag, msg, it) + showToastFromBackground(this@AfterpayV3SampleActivity, msg) + } + } } + } } private val tag = AfterpayV3SampleActivity::class.java.simpleName diff --git a/sample/src/main/java/com/example/CashAppV3SampleActivity.kt b/sample/src/main/java/com/example/CashAppV3SampleActivity.kt index e5f022e0..38b5fa90 100644 --- a/sample/src/main/java/com/example/CashAppV3SampleActivity.kt +++ b/sample/src/main/java/com/example/CashAppV3SampleActivity.kt @@ -53,274 +53,274 @@ import kotlinx.coroutines.launch */ class CashAppV3SampleActivity : AppCompatActivity() { - private lateinit var bindings: CashAppV3LayoutBinding - private lateinit var cashAppPay: CashAppPay - - private var checkoutV3CashAppPay: CheckoutV3CashAppPay? = null - - private val cashAppPayListener = - object : CashAppPayListener { - override fun cashAppPayStateDidChange(newState: CashAppPayState) { - Log.d(tag, "cashAppPayStateDidChange: ${newState::class.java}") - when (newState) { - is ReadyToAuthorize -> { - /** - * Step 6: Cash App Pay SDK will response when an authorization attempt is ready - * We can now enable the button to let customer proceed with checkout - */ - lifecycleScope.launch { // jump back to UI thread to update UI - bindings.cashappPayButton.isEnabled = true - } - } - - Authorizing -> { - /** - * Step 8: Disable button while auth in process - */ - bindings.cashappPayButton.isEnabled = false - } - - is Approved -> { - /** - * Step 9: After successful approval, confirm checkout with Afterpay - */ - Log.d(tag, newState.responseData.toString()) - - newState.responseData.apply { - // optionally, retrieve customer's cash tag - val cashTag = customerProfile?.cashTag - showToast( - this@CashAppV3SampleActivity, - "Grant approved for customer: $cashTag", - ) - - grants?.get(0)?.let { grant: Grant -> - CoroutineScope(Dispatchers.IO).launch { - confirmCheckoutWithAfterpay( - grantId = grant.id, - customerId = grant.customerId, - ) - } - } - } - } - - is CashAppPayExceptionState -> { - showToast(this@CashAppV3SampleActivity, "Cash App Pay Exception") - Log.e(tag, "Cash App Pay Exception", newState.exception) - } - - Declined -> { - showToast(this@CashAppV3SampleActivity, "Payment Declined") - } - - NotStarted -> { - bindings.cashappPayButton.isEnabled = false - } - - CreatingCustomerRequest -> { - // Use this state to display loading status if desired. - } - - PollingTransactionStatus -> { - // Use this state to display loading status if desired. - } - - UpdatingCustomerRequest -> { - // Use this state to display loading status if desired. - } - - RetrievingExistingCustomerRequest -> { - // Use this state to display loading status if desired. - // Used only when retrieving existing customer request. Corner case (eg.: app killed) - } - - Refreshing -> { - // Optional. Use this state to display loading status if desired. - bindings.cashappPayButton.isEnabled = false - } - } - } - } + private lateinit var bindings: CashAppV3LayoutBinding + private lateinit var cashAppPay: CashAppPay - override fun onNewIntent(intent: Intent?) { - super.onNewIntent(intent) - /** - * If this activity was killed during Cash App flow, and recreated, then this intent - * can be used to start with an existing customer request. See - * Cash App Pay SDK for more information - * https://developers.cash.app/docs/api/technical-documentation/sdks/pay-kit/android-getting-started#start-with-an-existing-customer-request - * - * If this activity was not killed and instead just resumed then the [CashAppPayListener] - * above will continue to function and events should be handled there - */ - Log.d(tag, "onNewIntent $intent + ${intent?.extras}") - } + private var checkoutV3CashAppPay: CheckoutV3CashAppPay? = null - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - /** - * The SDK is agnostic to the UI library of your choice: inflate XML, view binding, Compose, etc. - * Here we use view binding to keep it simple. - */ - bindings = CashAppV3LayoutBinding.inflate(LayoutInflater.from(this)) - bindings.cashappPayButton.apply { - isEnabled = false - setOnClickListener { _ -> - /** - * Step 7: When customer clicks Cash App Pay button - * begin authorization. This will begin UI flow into Cash App - */ - cashAppPay.authorizeCustomerRequest() + private val cashAppPayListener = + object : CashAppPayListener { + override fun cashAppPayStateDidChange(newState: CashAppPayState) { + Log.d(tag, "cashAppPayStateDidChange: ${newState::class.java}") + when (newState) { + is ReadyToAuthorize -> { + /** + * Step 6: Cash App Pay SDK will response when an authorization attempt is ready + * We can now enable the button to let customer proceed with checkout + */ + lifecycleScope.launch { // jump back to UI thread to update UI + bindings.cashappPayButton.isEnabled = true } - } - - /** - * Step 0: Periodically check for new Merchant configurations. Doesn't need - * to happen before *every* transaction. Does need to happen before *first* transaction - * Use the tool of your choice to enable async non-main-thread network request (i.e. coroutines, rxjava) - * - * Using [lifecycleScope] is for simplicity. Ideally you would not tie this network - * request to an Activity's lifecycle. Use the networking, storage, lifecycle libraries of - * your choice. - */ - lifecycleScope.launch { - getMerchantConfig() - } + } - val view = bindings.root - setContentView(view) - } + Authorizing -> { + /** + * Step 8: Disable button while auth in process + */ + bindings.cashappPayButton.isEnabled = false + } - private fun getMerchantConfig() = - CoroutineScope(Dispatchers.IO).launch { - Afterpay.fetchMerchantConfigurationV3(createCheckoutV3Configuration()) - .let { result: Result -> - result.onSuccess { merchantConfiguration: Configuration -> - /** - * Step 1: Set configurations and being checkout process - * - * It is up to you to save this Configuration (e.g. local storage) to avoid repeat calls - * to fetch it. You will need to pass Configuration to Afterpay on each app - * restart (before first transaction) by calling .setConfigurationV3() - */ - Log.d(tag, "Fetched merchant configs") - Afterpay.setConfigurationV3(merchantConfiguration) - initializeCashAppSDK() - beginCheckout() - } - - result.onFailure { - showToastFromBackground( - this@CashAppV3SampleActivity, - "Failed to fetch merchant configs", - ) - } + is Approved -> { + /** + * Step 9: After successful approval, confirm checkout with Afterpay + */ + Log.d(tag, newState.responseData.toString()) + + newState.responseData.apply { + // optionally, retrieve customer's cash tag + val cashTag = customerProfile?.cashTag + showToast( + this@CashAppV3SampleActivity, + "Grant approved for customer: $cashTag", + ) + + grants?.get(0)?.let { grant: Grant -> + CoroutineScope(Dispatchers.IO).launch { + confirmCheckoutWithAfterpay( + grantId = grant.id, + customerId = grant.customerId, + ) } + } + } + } + + is CashAppPayExceptionState -> { + showToast(this@CashAppV3SampleActivity, "Cash App Pay Exception") + Log.e(tag, "Cash App Pay Exception", newState.exception) + } + + Declined -> { + showToast(this@CashAppV3SampleActivity, "Payment Declined") + } + + NotStarted -> { + bindings.cashappPayButton.isEnabled = false + } + + CreatingCustomerRequest -> { + // Use this state to display loading status if desired. + } + + PollingTransactionStatus -> { + // Use this state to display loading status if desired. + } + + UpdatingCustomerRequest -> { + // Use this state to display loading status if desired. + } + + RetrievingExistingCustomerRequest -> { + // Use this state to display loading status if desired. + // Used only when retrieving existing customer request. Corner case (eg.: app killed) + } + + Refreshing -> { + // Optional. Use this state to display loading status if desired. + bindings.cashappPayButton.isEnabled = false + } } + } + } - private fun initializeCashAppSDK() { + override fun onNewIntent(intent: Intent?) { + super.onNewIntent(intent) + /** + * If this activity was killed during Cash App flow, and recreated, then this intent + * can be used to start with an existing customer request. See + * Cash App Pay SDK for more information + * https://developers.cash.app/docs/api/technical-documentation/sdks/pay-kit/android-getting-started#start-with-an-existing-customer-request + * + * If this activity was not killed and instead just resumed then the [CashAppPayListener] + * above will continue to function and events should be handled there + */ + Log.d(tag, "onNewIntent $intent + ${intent?.extras}") + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + /** + * The SDK is agnostic to the UI library of your choice: inflate XML, view binding, Compose, etc. + * Here we use view binding to keep it simple. + */ + bindings = CashAppV3LayoutBinding.inflate(LayoutInflater.from(this)) + bindings.cashappPayButton.apply { + isEnabled = false + setOnClickListener { _ -> /** - * Step 2: create and register a state listener with Cash APp Pay SDK + * Step 7: When customer clicks Cash App Pay button + * begin authorization. This will begin UI flow into Cash App */ - Log.d(tag, "Initializing Cash App Pay SDK") - cashAppPay = CashAppPayFactory.createSandbox(AFTERPAY_ENVIRONMENT.payKitClientId) - cashAppPay.registerForStateUpdates(cashAppPayListener) + cashAppPay.authorizeCustomerRequest() + } } - private suspend fun beginCheckout() { - /** - * Step 3: Begin checkout process by requesting data from Afterpay, to be used later - * with Cash App Pay SDK - */ - Afterpay.beginCheckoutV3WithCashAppPay( - consumer = createConsumer(), - orderTotal = createOrderTotal(), - items = createItems(), - configuration = createCheckoutV3Configuration(), - ).let { result: Result -> - result.onSuccess { - /** - * Step 4: Store [CheckoutV3CashAppPay] for use later and create Customer Request - */ - checkoutV3CashAppPay = it - createCashAppPayCustomerRequest(it) - } + /** + * Step 0: Periodically check for new Merchant configurations. Doesn't need + * to happen before *every* transaction. Does need to happen before *first* transaction + * Use the tool of your choice to enable async non-main-thread network request (i.e. coroutines, rxjava) + * + * Using [lifecycleScope] is for simplicity. Ideally you would not tie this network + * request to an Activity's lifecycle. Use the networking, storage, lifecycle libraries of + * your choice. + */ + lifecycleScope.launch { + getMerchantConfig() + } - result.onFailure { error -> - val msg = "Failed to begin checkout" - Log.e(tag, msg, error) - showToastFromBackground(this, msg) - } + val view = bindings.root + setContentView(view) + } + + private fun getMerchantConfig() = + CoroutineScope(Dispatchers.IO).launch { + Afterpay.fetchMerchantConfigurationV3(createCheckoutV3Configuration()) + .let { result: Result -> + result.onSuccess { merchantConfiguration: Configuration -> + /** + * Step 1: Set configurations and being checkout process + * + * It is up to you to save this Configuration (e.g. local storage) to avoid repeat calls + * to fetch it. You will need to pass Configuration to Afterpay on each app + * restart (before first transaction) by calling .setConfigurationV3() + */ + Log.d(tag, "Fetched merchant configs") + Afterpay.setConfigurationV3(merchantConfiguration) + initializeCashAppSDK() + beginCheckout() + } + + result.onFailure { + showToastFromBackground( + this@CashAppV3SampleActivity, + "Failed to fetch merchant configs", + ) + } } } - private fun createCashAppPayCustomerRequest(checkoutV3CashAppPay: CheckoutV3CashAppPay) { + private fun initializeCashAppSDK() { + /** + * Step 2: create and register a state listener with Cash APp Pay SDK + */ + Log.d(tag, "Initializing Cash App Pay SDK") + cashAppPay = CashAppPayFactory.createSandbox(AFTERPAY_ENVIRONMENT.payKitClientId) + cashAppPay.registerForStateUpdates(cashAppPayListener) + } + + private suspend fun beginCheckout() { + /** + * Step 3: Begin checkout process by requesting data from Afterpay, to be used later + * with Cash App Pay SDK + */ + Afterpay.beginCheckoutV3WithCashAppPay( + consumer = createConsumer(), + orderTotal = createOrderTotal(), + items = createItems(), + configuration = createCheckoutV3Configuration(), + ).let { result: Result -> + result.onSuccess { /** - * Step 5: Create customer request with Cash App Pay SDK - * - * [redirectUri] defines where customer returns to after completing Cash App flow - * You are not required to use [CheckoutV3CashAppPay.redirectUri] . You can set your own - * value. Here in sample app we just redirect back to this Activity - * - * See intent filter in AndroidManifest.xml + * Step 4: Store [CheckoutV3CashAppPay] for use later and create Customer Request */ - val redirectUri = "example://example.com/" - val action = CashAppPayPaymentAction.OneTimeAction( - currency = USD, - amount = checkoutV3CashAppPay.amount.toInt(), - /** - * This is not the same merchant ID you set in [CheckoutV3Configuration]. - * This is a specific for use with Cash App Pay SDK. - */ - scopeId = checkoutV3CashAppPay.merchantId, - ) - - cashAppPay.createCustomerRequest( - action, - redirectUri = redirectUri, - ) + checkoutV3CashAppPay = it + createCashAppPayCustomerRequest(it) + } + + result.onFailure { error -> + val msg = "Failed to begin checkout" + Log.e(tag, msg, error) + showToastFromBackground(this, msg) + } } + } + + private fun createCashAppPayCustomerRequest(checkoutV3CashAppPay: CheckoutV3CashAppPay) { + /** + * Step 5: Create customer request with Cash App Pay SDK + * + * [redirectUri] defines where customer returns to after completing Cash App flow + * You are not required to use [CheckoutV3CashAppPay.redirectUri] . You can set your own + * value. Here in sample app we just redirect back to this Activity + * + * See intent filter in AndroidManifest.xml + */ + val redirectUri = "example://example.com/" + val action = CashAppPayPaymentAction.OneTimeAction( + currency = USD, + amount = checkoutV3CashAppPay.amount.toInt(), + /** + * This is not the same merchant ID you set in [CheckoutV3Configuration]. + * This is a specific for use with Cash App Pay SDK. + */ + scopeId = checkoutV3CashAppPay.merchantId, + ) + + cashAppPay.createCustomerRequest( + action, + redirectUri = redirectUri, + ) + } + + private suspend fun confirmCheckoutWithAfterpay( + grantId: String, + customerId: String, + ) { + requireNotNull(checkoutV3CashAppPay) + checkoutV3CashAppPay?.let { it -> + /** + * Step 10: confirm checkout with Afterpay by combining newly received [grantId] and + * [customerId] with previously-saved [CheckoutV3CashAppPay]. + */ + Afterpay.confirmCheckoutV3WithCashAppPay( + grantId = grantId, + customerId = customerId, + token = it.token, + singleUseCardToken = it.singleUseCardToken, + jwt = it.jwt, + configuration = createCheckoutV3Configuration(), + ).let { result: Result -> + + result.onSuccess { + /** + * Step 11: Receive single-use card details. Pass these back to your server + * and on to your payment processor: + */ + Log.d(tag, "Received card details, etc ${it.cardDetails}") + it.cardDetails + it.tokens + it.cardValidUntil + } - private suspend fun confirmCheckoutWithAfterpay( - grantId: String, - customerId: String, - ) { - requireNotNull(checkoutV3CashAppPay) - checkoutV3CashAppPay?.let { it -> - /** - * Step 10: confirm checkout with Afterpay by combining newly received [grantId] and - * [customerId] with previously-saved [CheckoutV3CashAppPay]. - */ - Afterpay.confirmCheckoutV3WithCashAppPay( - grantId = grantId, - customerId = customerId, - token = it.token, - singleUseCardToken = it.singleUseCardToken, - jwt = it.jwt, - configuration = createCheckoutV3Configuration(), - ).let { result: Result -> - - result.onSuccess { - /** - * Step 11: Receive single-use card details. Pass these back to your server - * and on to your payment processor: - */ - Log.d(tag, "Received card details, etc ${it.cardDetails}") - it.cardDetails - it.tokens - it.cardValidUntil - } - - result.onFailure { - showToastFromBackground( - this@CashAppV3SampleActivity, - "Failed to confirm payment with Afterpay", - ) - } - } + result.onFailure { + showToastFromBackground( + this@CashAppV3SampleActivity, + "Failed to confirm payment with Afterpay", + ) } + } } + } - private val tag = CashAppV3SampleActivity::class.java.simpleName + private val tag = CashAppV3SampleActivity::class.java.simpleName } diff --git a/sample/src/main/java/com/example/Common.kt b/sample/src/main/java/com/example/Common.kt index 1f12bd9e..d7c3b73b 100644 --- a/sample/src/main/java/com/example/Common.kt +++ b/sample/src/main/java/com/example/Common.kt @@ -37,9 +37,9 @@ val AFTERPAY_ENVIRONMENT = AfterpayEnvironment.SANDBOX const val MERCHANT_ID = BuildConfig.merchantId fun createCheckoutV3Configuration(): CheckoutV3Configuration { - return CheckoutV3Configuration( - shopDirectoryMerchantId = MERCHANT_ID, - region = AFTERPAY_REGION, - environment = AFTERPAY_ENVIRONMENT, - ) + return CheckoutV3Configuration( + shopDirectoryMerchantId = MERCHANT_ID, + region = AFTERPAY_REGION, + environment = AFTERPAY_ENVIRONMENT, + ) } diff --git a/sample/src/main/java/com/example/SampleData.kt b/sample/src/main/java/com/example/SampleData.kt index 9c115f8e..ee53c88e 100644 --- a/sample/src/main/java/com/example/SampleData.kt +++ b/sample/src/main/java/com/example/SampleData.kt @@ -27,119 +27,119 @@ import java.net.URL */ internal fun createItems(): Array { - return listOf( - createCheckoutItem(), - ).toTypedArray() + return listOf( + createCheckoutItem(), + ).toTypedArray() } internal fun createOrderTotal(): OrderTotal { - return OrderTotal( - total = BigDecimal(10.00), - shipping = BigDecimal(1.00), - tax = BigDecimal(2.34), - ) + return OrderTotal( + total = BigDecimal(10.00), + shipping = BigDecimal(1.00), + tax = BigDecimal(2.34), + ) } internal fun createCheckoutItem(): CheckoutV3Item { - return object : CheckoutV3Item { - override val categories: List>? - get() = emptyList() - override val estimatedShipmentDate: String? - get() = null - override val imageUrl: URL? - get() = null - override val name: String - get() = "a thing" - override val pageUrl: URL? - get() = null - override val price: BigDecimal - get() = BigDecimal(10.00) - override val quantity: UInt - get() = 1.toUInt() - override val sku: String? - get() = null - } + return object : CheckoutV3Item { + override val categories: List>? + get() = emptyList() + override val estimatedShipmentDate: String? + get() = null + override val imageUrl: URL? + get() = null + override val name: String + get() = "a thing" + override val pageUrl: URL? + get() = null + override val price: BigDecimal + get() = BigDecimal(10.00) + override val quantity: UInt + get() = 1.toUInt() + override val sku: String? + get() = null + } } internal fun createConsumer(): CheckoutV3Consumer { - return object : CheckoutV3Consumer { - override val billingInformation: CheckoutV3Contact? - get() = createBillingInfo() - override val email: String - get() = customerEmail - override val givenNames: String? - get() = "Bob" - override val phoneNumber: String? - get() = customerPhonenumber - override val shippingInformation: CheckoutV3Contact? - get() = createShippingInfo() - override val surname: String? - get() = "Smith" - } + return object : CheckoutV3Consumer { + override val billingInformation: CheckoutV3Contact? + get() = createBillingInfo() + override val email: String + get() = customerEmail + override val givenNames: String? + get() = "Bob" + override val phoneNumber: String? + get() = customerPhonenumber + override val shippingInformation: CheckoutV3Contact? + get() = createShippingInfo() + override val surname: String? + get() = "Smith" + } } internal fun createBillingInfo(): CheckoutV3Contact? { - return object : CheckoutV3Contact { - override var area1: String? - get() = null - set(value) {} - override var area2: String? - get() = null - set(value) {} - override var countryCode: String - get() = "US" - set(value) {} - override var line1: String - get() = "123 Main Street" - set(value) {} - override var line2: String? - get() = null - set(value) {} - override var name: String - get() = "Bob" - set(value) {} - override var phoneNumber: String? - get() = customerPhonenumber - set(value) {} - override var postcode: String? - get() = "post code" - set(value) {} - override var region: String? - get() = null - set(value) {} - } + return object : CheckoutV3Contact { + override var area1: String? + get() = null + set(value) {} + override var area2: String? + get() = null + set(value) {} + override var countryCode: String + get() = "US" + set(value) {} + override var line1: String + get() = "123 Main Street" + set(value) {} + override var line2: String? + get() = null + set(value) {} + override var name: String + get() = "Bob" + set(value) {} + override var phoneNumber: String? + get() = customerPhonenumber + set(value) {} + override var postcode: String? + get() = "post code" + set(value) {} + override var region: String? + get() = null + set(value) {} + } } internal fun createShippingInfo(): CheckoutV3Contact? { - return object : CheckoutV3Contact { - override var area1: String? - get() = null - set(value) {} - override var area2: String? - get() = null - set(value) {} - override var countryCode: String - get() = "US" - set(value) {} - override var line1: String - get() = "123 Main Street" - set(value) {} - override var line2: String? - get() = null - set(value) {} - override var name: String - get() = "Bob" - set(value) {} - override var phoneNumber: String? - get() = customerPhonenumber - set(value) {} - override var postcode: String? - get() = "post code" - set(value) {} - override var region: String? - get() = null - set(value) {} - } + return object : CheckoutV3Contact { + override var area1: String? + get() = null + set(value) {} + override var area2: String? + get() = null + set(value) {} + override var countryCode: String + get() = "US" + set(value) {} + override var line1: String + get() = "123 Main Street" + set(value) {} + override var line2: String? + get() = null + set(value) {} + override var name: String + get() = "Bob" + set(value) {} + override var phoneNumber: String? + get() = customerPhonenumber + set(value) {} + override var postcode: String? + get() = "post code" + set(value) {} + override var region: String? + get() = null + set(value) {} + } } val customerEmail = "example@squareup.com" diff --git a/sample/src/main/java/com/example/UiHelpers.kt b/sample/src/main/java/com/example/UiHelpers.kt index b44a0ffd..0d8cf9bf 100644 --- a/sample/src/main/java/com/example/UiHelpers.kt +++ b/sample/src/main/java/com/example/UiHelpers.kt @@ -21,21 +21,21 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext suspend fun showToastFromBackground( - context: Context, - message: String, + context: Context, + message: String, ) { - withContext(Dispatchers.Main) { - showToast(context, message) - } + withContext(Dispatchers.Main) { + showToast(context, message) + } } fun showToast( - context: Context, - message: String, + context: Context, + message: String, ) { - Toast.makeText( - context, - message, - Toast.LENGTH_SHORT, - ).show() + Toast.makeText( + context, + message, + Toast.LENGTH_SHORT, + ).show() } diff --git a/sample/src/main/java/com/example/api/GetConfigurationResponse.kt b/sample/src/main/java/com/example/api/GetConfigurationResponse.kt index 07133e3d..487f6536 100644 --- a/sample/src/main/java/com/example/api/GetConfigurationResponse.kt +++ b/sample/src/main/java/com/example/api/GetConfigurationResponse.kt @@ -19,18 +19,18 @@ package com.example.api * Required format to work with https://github.com/afterpay/sdk-example-server */ data class GetConfigurationResponse( - val minimumAmount: Money?, - val maximumAmount: Money, - val locale: Locale, + val minimumAmount: Money?, + val maximumAmount: Money, + val locale: Locale, ) { - data class Money( - val amount: String, - val currency: String, - ) + data class Money( + val amount: String, + val currency: String, + ) - data class Locale( - val identifier: String, - val language: String, - val country: String, - ) + data class Locale( + val identifier: String, + val language: String, + val country: String, + ) } diff --git a/sample/src/main/java/com/example/api/GetTokenRequest.kt b/sample/src/main/java/com/example/api/GetTokenRequest.kt index 6ca3f0ee..de2bf5ca 100644 --- a/sample/src/main/java/com/example/api/GetTokenRequest.kt +++ b/sample/src/main/java/com/example/api/GetTokenRequest.kt @@ -19,13 +19,13 @@ package com.example.api * Required format to work with https://github.com/afterpay/sdk-example-server */ data class GetTokenRequest( - val email: String, - val amount: String, - val mode: CheckoutMode, - val isCashAppPay: Boolean = false, + val email: String, + val amount: String, + val mode: CheckoutMode, + val isCashAppPay: Boolean = false, ) enum class CheckoutMode(val string: String) { - STANDARD("standard"), - EXPRESS("express"), + STANDARD("standard"), + EXPRESS("express"), } diff --git a/sample/src/main/java/com/example/api/GetTokenResponse.kt b/sample/src/main/java/com/example/api/GetTokenResponse.kt index cafaa21b..ebeed28a 100644 --- a/sample/src/main/java/com/example/api/GetTokenResponse.kt +++ b/sample/src/main/java/com/example/api/GetTokenResponse.kt @@ -19,6 +19,6 @@ package com.example.api * Required format to work with https://github.com/afterpay/sdk-example-server */ data class GetTokenResponse( - val url: String, - val token: String, + val url: String, + val token: String, ) diff --git a/sample/src/main/java/com/example/api/MerchantApi.kt b/sample/src/main/java/com/example/api/MerchantApi.kt index 94dd2663..bb1e4ac7 100644 --- a/sample/src/main/java/com/example/api/MerchantApi.kt +++ b/sample/src/main/java/com/example/api/MerchantApi.kt @@ -38,125 +38,125 @@ import java.lang.reflect.Type * most comfortable with. */ interface MerchantApi { - // TODO @jatwood handle success/error results, not just response + // TODO @jatwood handle success/error results, not just response - @GET("configuration") - suspend fun getConfiguration(): Result + @GET("configuration") + suspend fun getConfiguration(): Result - @POST("checkout") - suspend fun getToken( - @Body request: GetTokenRequest, - ): Result + @POST("checkout") + suspend fun getToken( + @Body request: GetTokenRequest, + ): Result } fun merchantApi() = - Retrofit.Builder() - .baseUrl(MERCHANT_API_BASE_URL) - .addConverterFactory( - MoshiConverterFactory.create( - Moshi.Builder() - .add(KotlinJsonAdapterFactory()) - .build(), - ), - ) - .addCallAdapterFactory(ResultCallAdapterFactory()) - .build() - .create(MerchantApi::class.java) + Retrofit.Builder() + .baseUrl(MERCHANT_API_BASE_URL) + .addConverterFactory( + MoshiConverterFactory.create( + Moshi.Builder() + .add(KotlinJsonAdapterFactory()) + .build(), + ), + ) + .addCallAdapterFactory(ResultCallAdapterFactory()) + .build() + .create(MerchantApi::class.java) const val MERCHANT_API_BASE_URL = "http://10.0.2.2:3000" // 10.0.2.2 if using emulator // Call adapter to get API to return kotlin.Result class ResultCallAdapterFactory : CallAdapter.Factory() { - override fun get( - returnType: Type, - annotations: Array, - retrofit: Retrofit, - ): CallAdapter<*, *>? { - if (getRawType(returnType) != Call::class.java || returnType !is ParameterizedType) { - return null - } - val upperBound = getParameterUpperBound(0, returnType) + override fun get( + returnType: Type, + annotations: Array, + retrofit: Retrofit, + ): CallAdapter<*, *>? { + if (getRawType(returnType) != Call::class.java || returnType !is ParameterizedType) { + return null + } + val upperBound = getParameterUpperBound(0, returnType) - return if (upperBound is ParameterizedType && upperBound.rawType == Result::class.java) { - object : CallAdapter>> { - override fun responseType(): Type = getParameterUpperBound(0, upperBound) + return if (upperBound is ParameterizedType && upperBound.rawType == Result::class.java) { + object : CallAdapter>> { + override fun responseType(): Type = getParameterUpperBound(0, upperBound) - override fun adapt(call: Call): Call> = - ResultCall(call) as Call> - } - } else { - null - } + override fun adapt(call: Call): Call> = + ResultCall(call) as Call> + } + } else { + null } + } } class ResultCall(val delegate: Call) : - Call> { - - override fun enqueue(callback: Callback>) { - delegate.enqueue( - object : Callback { - override fun onResponse(call: Call, response: Response) { - if (response.isSuccessful) { - callback.onResponse( - this@ResultCall, - Response.success( - response.code(), - Result.success(response.body()!!), - ), - ) - } else { - callback.onResponse( - this@ResultCall, - Response.success( - Result.failure( - HttpException(response), - ), - ), - ) - } - } - - override fun onFailure(call: Call, t: Throwable) { - val errorMessage = when (t) { - is IOException -> "No internet connection" - is HttpException -> "Something went wrong!" - else -> t.localizedMessage - } - callback.onResponse( - this@ResultCall, - Response.success(Result.failure(RuntimeException(errorMessage, t))), - ) - } - }, - ) - } + Call> { + + override fun enqueue(callback: Callback>) { + delegate.enqueue( + object : Callback { + override fun onResponse(call: Call, response: Response) { + if (response.isSuccessful) { + callback.onResponse( + this@ResultCall, + Response.success( + response.code(), + Result.success(response.body()!!), + ), + ) + } else { + callback.onResponse( + this@ResultCall, + Response.success( + Result.failure( + HttpException(response), + ), + ), + ) + } + } - override fun isExecuted(): Boolean { - return delegate.isExecuted - } + override fun onFailure(call: Call, t: Throwable) { + val errorMessage = when (t) { + is IOException -> "No internet connection" + is HttpException -> "Something went wrong!" + else -> t.localizedMessage + } + callback.onResponse( + this@ResultCall, + Response.success(Result.failure(RuntimeException(errorMessage, t))), + ) + } + }, + ) + } - override fun execute(): Response> { - return Response.success(Result.success(delegate.execute().body()!!)) - } + override fun isExecuted(): Boolean { + return delegate.isExecuted + } - override fun cancel() { - delegate.cancel() - } + override fun execute(): Response> { + return Response.success(Result.success(delegate.execute().body()!!)) + } - override fun isCanceled(): Boolean { - return delegate.isCanceled - } + override fun cancel() { + delegate.cancel() + } - override fun clone(): Call> { - return ResultCall(delegate.clone()) - } + override fun isCanceled(): Boolean { + return delegate.isCanceled + } - override fun request(): Request { - return delegate.request() - } + override fun clone(): Call> { + return ResultCall(delegate.clone()) + } - override fun timeout(): Timeout { - return delegate.timeout() - } + override fun request(): Request { + return delegate.request() + } + + override fun timeout(): Timeout { + return delegate.timeout() + } }