diff --git a/CHANGELOG.md b/CHANGELOG.md index 7baf399f7f..a4ab1eb987 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,11 @@ All notable changes to this project will be documented in this file. ## [0.75.0] ### Changed * ui: update fiserv 3D's flow. The user now has to enter additional information to add a credit card. - * instead of using the CreditCardInput it is now required to use the FiservInput instead. + * instead of using the `CreditCardInput` it is now required to use the `FiservInput` instead. +* ui: update datatrans 3D's flow. The user now has to enter additional information to add a credit card. + * the new flow can be integrated via the new `SnabbleUi.Event` `SHOW_DATATRANS_INPUT` + * u can directly integrate the `DatatransFragment` as navigation target for this event + ## [0.74.0] ### Added * core: add new and update existing user agent headers diff --git a/build.gradle.kts b/build.gradle.kts index c6697346d5..5f30647f8d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -33,6 +33,7 @@ allprojects { repositories { google() mavenCentral() + maven(url = "https://raw.githubusercontent.com/snabble/maven-repository/releases") maven(url = "https://datatrans.jfrog.io/artifactory/mobile-sdk/") maven(url = "https://jitpack.io") } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ef4fba0143..a3511088dd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -83,6 +83,7 @@ jakewhartonProcessPhoenix = "com.jakewharton:process-phoenix:2.1.2" picasso = "com.squareup.picasso:picasso:2.8" rekisoftLazyWorker = "eu.rekisoft.android.util:LazyWorker:2.1.0" relex-circleindicator = "me.relex:circleindicator:2.1.6" +snabble-phoneAuth-countryCodePicker = "io.snabble.phoneauth:countryCodePicker:3.2.2" squareup-okhttp3-loggingInterceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "com-squareup-okhttp3" } squareup-okhttp3-okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "com-squareup-okhttp3" } squareup-okhttp3-tls = { module = "com.squareup.okhttp3:okhttp-tls", version.ref = "com-squareup-okhttp3" } diff --git a/kotlin-sample/src/main/res/navigation/mobile_navigation.xml b/kotlin-sample/src/main/res/navigation/mobile_navigation.xml index dc745cb487..9161096493 100644 --- a/kotlin-sample/src/main/res/navigation/mobile_navigation.xml +++ b/kotlin-sample/src/main/res/navigation/mobile_navigation.xml @@ -166,7 +166,7 @@ + diff --git a/ui/src/main/assets/countriesAndStates.json b/ui/src/main/assets/countriesAndStates.json index d0d50f199e..5eadbbd79f 100644 --- a/ui/src/main/assets/countriesAndStates.json +++ b/ui/src/main/assets/countriesAndStates.json @@ -1,33 +1,43 @@ [ { - "code": "AF" + "code": "AF", + "numeric": "004" }, { - "code": "AL" + "code": "AL", + "numeric": "008" }, { - "code": "DZ" + "code": "DZ", + "numeric": "012" }, { - "code": "AS" + "code": "AS", + "numeric": "016" }, { - "code": "AD" + "code": "AD", + "numeric": "020" }, { - "code": "AO" + "code": "AO", + "numeric": "024" }, { - "code": "AI" + "code": "AI", + "numeric": "660" }, { - "code": "AQ" + "code": "AQ", + "numeric": "010" }, { - "code": "AG" + "code": "AG", + "numeric": "028" }, { "code": "AR", + "numeric": "032", "states": [ { "code" : "B", @@ -128,67 +138,88 @@ ] }, { - "code": "AM" + "code": "AM", + "numeric": "051" }, { - "code": "AW" + "code": "AW", + "numeric": "533" }, { - "code": "AU" + "code": "AU", + "numeric": "036" }, { - "code": "AT" + "code": "AT", + "numeric": "040" }, { - "code": "AZ" + "code": "AZ", + "numeric": "031" }, { - "code": "BS" + "code": "BS", + "numeric": "044" }, { - "code": "BH" + "code": "BH", + "numeric": "048" }, { - "code": "BD" + "code": "BD", + "numeric": "050" }, { - "code": "BB" + "code": "BB", + "numeric": "052" }, { - "code": "BY" + "code": "BY", + "numeric": "112" }, { - "code": "BE" + "code": "BE", + "numeric": "056" }, { - "code": "BZ" + "code": "BZ", + "numeric": "084" }, { - "code": "BJ" + "code": "BJ", + "numeric": "204" }, { - "code": "BM" + "code": "BM", + "numeric": "060" }, { - "code": "BT" + "code": "BT", + "numeric": "064" }, { - "code": "BO" + "code": "BO", + "numeric": "068" }, { - "code": "BQ" + "code": "BQ", + "numeric": "535" }, { - "code": "BA" + "code": "BA", + "numeric": "070" }, { - "code": "BW" + "code": "BW", + "numeric": "072" }, { - "code": "BV" + "code": "BV", + "numeric": "074" }, { "code": "BR", + "numeric": "076", "states": [ { "code" : "AC", @@ -301,28 +332,36 @@ ] }, { - "code": "IO" + "code": "IO", + "numeric": "086" }, { - "code": "BN" + "code": "BN", + "numeric": "096" }, { - "code": "BG" + "code": "BG", + "numeric": "100" }, { - "code": "BF" + "code": "BF", + "numeric": "854" }, { - "code": "BI" + "code": "BI", + "numeric": "108" }, { - "code": "KH" + "code": "KH", + "numeric": "116" }, { - "code": "CM" + "code": "CM", + "numeric": "120" }, { "code": "CA", + "numeric": "124", "states": [ { "code" : "AB", @@ -379,22 +418,28 @@ ] }, { - "code": "CV" + "code": "CV", + "numeric": "132" }, { - "code": "KY" + "code": "KY", + "numeric": "136" }, { - "code": "CF" + "code": "CF", + "numeric": "140" }, { - "code": "TD" + "code": "TD", + "numeric": "148" }, { - "code": "CL" + "code": "CL", + "numeric": "152" }, { "code": "CN", + "numeric": "156", "states": [ { "code" : "11", @@ -535,169 +580,224 @@ ] }, { - "code": "CW" + "code": "CW", + "numeric": "531" }, { - "code": "CX" + "code": "CX", + "numeric": "162" }, { - "code": "CC" + "code": "CC", + "numeric": "166" }, { - "code": "CO" + "code": "CO", + "numeric": "170" }, { - "code": "KM" + "code": "KM", + "numeric": "174" }, { - "code": "CG" + "code": "CG", + "numeric": "178" }, { - "code": "CK" + "code": "CK", + "numeric": "184" }, { - "code": "CR" + "code": "CR", + "numeric": "188" }, { - "code": "CI" + "code": "CI", + "numeric": "384" }, { - "code": "HR" + "code": "HR", + "numeric": "191" }, { - "code": "CU" + "code": "CU", + "numeric": "192" }, { - "code": "CY" + "code": "CY", + "numeric": "196" }, { - "code": "CZ" + "code": "CZ", + "numeric": "203" }, { - "code": "DK" + "code": "DK", + "numeric": "208" }, { - "code": "DJ" + "code": "DJ", + "numeric": "262" }, { - "code": "DM" + "code": "DM", + "numeric": "212" }, { - "code": "DO" + "code": "DO", + "numeric": "214" }, { - "code": "TL" + "code": "TL", + "numeric": "626" }, { - "code": "EC" + "code": "EC", + "numeric": "218" }, { - "code": "EG" + "code": "EG", + "numeric": "818" }, { - "code": "SV" + "code": "SV", + "numeric": "222" }, { - "code": "GQ" + "code": "GQ", + "numeric": "226" }, { - "code": "ER" + "code": "ER", + "numeric": "232" }, { - "code": "EE" + "code": "EE", + "numeric": "233" }, { - "code": "ET" + "code": "ET", + "numeric": "231" }, { - "code": "FK" + "code": "FK", + "numeric": "238" }, { - "code": "FO" + "code": "FO", + "numeric": "234" }, { - "code": "FJ" + "code": "FJ", + "numeric": "242" }, { - "code": "FI" + "code": "FI", + "numeric": "246" }, { - "code": "FR" + "code": "FR", + "numeric": "250" }, { - "code": "GF" + "code": "GF", + "numeric": "254" }, { - "code": "PF" + "code": "PF", + "numeric": "258" }, { - "code": "TF" + "code": "TF", + "numeric": "260" }, { - "code": "GA" + "code": "GA", + "numeric": "266" }, { - "code": "GM" + "code": "GM", + "numeric": "270" }, { - "code": "GE" + "code": "GE", + "numeric": "268" }, { - "code": "DE" + "code": "DE", + "numeric": "276" }, { - "code": "GH" + "code": "GH", + "numeric": "288" }, { - "code": "GI" + "code": "GI", + "numeric": "292" }, { - "code": "GR" + "code": "GR", + "numeric": "300" }, { - "code": "GL" + "code": "GL", + "numeric": "304" }, { - "code": "GD" + "code": "GD", + "numeric": "308" }, { - "code": "GP" + "code": "GP", + "numeric": "312" }, { - "code": "GU" + "code": "GU", + "numeric": "316" }, { - "code": "GT" + "code": "GT", + "numeric": "320" }, { - "code": "GN" + "code": "GN", + "numeric": "324" }, { - "code": "GW" + "code": "GW", + "numeric": "624" }, { - "code": "GY" + "code": "GY", + "numeric": "328" }, { - "code": "HT" + "code": "HT", + "numeric": "332" }, { - "code": "HM" + "code": "HM", + "numeric": "334" }, { - "code": "HN" + "code": "HN", + "numeric": "340" }, { - "code": "HK" + "code": "HK", + "numeric": "344" }, { - "code": "HU" + "code": "HU", + "numeric": "348" }, { - "code": "IS" + "code": "IS", + "numeric": "352" }, { "code": "IN", + "numeric": "356", "states": [ { "code" : "AN", @@ -847,6 +947,7 @@ }, { "code": "ID", + "numeric": "360", "states": [ { "code" : "AC", @@ -987,25 +1088,32 @@ ] }, { - "code": "IR" + "code": "IR", + "numeric": "364" }, { - "code": "IQ" + "code": "IQ", + "numeric": "368" }, { - "code": "IE" + "code": "IE", + "numeric": "372" }, { - "code": "IL" + "code": "IL", + "numeric": "376" }, { - "code": "IT" + "code": "IT", + "numeric": "380" }, { - "code": "JM" + "code": "JM", + "numeric": "388" }, { "code": "JP", + "numeric": "392", "states": [ { "code" : "23", @@ -1198,94 +1306,124 @@ ] }, { - "code": "JO" + "code": "JO", + "numeric": "400" }, { - "code": "KZ" + "code": "KZ", + "numeric": "398" }, { - "code": "KE" + "code": "KE", + "numeric": "404" }, { - "code": "KI" + "code": "KI", + "numeric": "296" }, { - "code": "KP" + "code": "KP", + "numeric": "408" }, { - "code": "KR" + "code": "KR", + "numeric": "410" }, { - "code": "KW" + "code": "KW", + "numeric": "414" }, { - "code": "KG" + "code": "KG", + "numeric": "417" }, { - "code": "LA" + "code": "LA", + "numeric": "418" }, { - "code": "LV" + "code": "LV", + "numeric": "428" }, { - "code": "LB" + "code": "LB", + "numeric": "422" }, { - "code": "LS" + "code": "LS", + "numeric": "426" }, { - "code": "LR" + "code": "LR", + "numeric": "430" }, { - "code": "LY" + "code": "LY", + "numeric": "434" }, { - "code": "LI" + "code": "LI", + "numeric": "438" }, { - "code": "LT" + "code": "LT", + "numeric": "440" }, { - "code": "LU" + "code": "LU", + "numeric": "442" }, { - "code": "MO" + "code": "MO", + "numeric": "446" }, { - "code": "MG" + "code": "MG", + "numeric": "450" }, { - "code": "MW" + "code": "MW", + "numeric": "454" }, { - "code": "MY" + "code": "MY", + "numeric": "458" }, { - "code": "MV" + "code": "MV", + "numeric": "462" }, { - "code": "ML" + "code": "ML", + "numeric": "466" }, { - "code": "MT" + "code": "MT", + "numeric": "470" }, { - "code": "MH" + "code": "MH", + "numeric": "584" }, { - "code": "MQ" + "code": "MQ", + "numeric": "474" }, { - "code": "MR" + "code": "MR", + "numeric": "478" }, { - "code": "MU" + "code": "MU", + "numeric": "480" }, { - "code": "YT" + "code": "YT", + "numeric": "175" }, { "code": "MX", + "numeric": "484", "states": [ { "code" : "AGU", @@ -1418,220 +1556,292 @@ ] }, { - "code": "FM" + "code": "FM", + "numeric": "583" }, { - "code": "MD" + "code": "MD", + "numeric": "498" }, { - "code": "MC" + "code": "MC", + "numeric": "492" }, { - "code": "MN" + "code": "MN", + "numeric": "496" }, { - "code": "MS" + "code": "MS", + "numeric": "500" }, { - "code": "MA" + "code": "MA", + "numeric": "504" }, { - "code": "MZ" + "code": "MZ", + "numeric": "508" }, { - "code": "MM" + "code": "MM", + "numeric": "104" }, { - "code": "NA" + "code": "NA", + "numeric": "516" }, { - "code": "NR" + "code": "NR", + "numeric": "520" }, { - "code": "NP" + "code": "NP", + "numeric": "524" }, { - "code": "NL" + "code": "NL", + "numeric": "528" }, { - "code": "NC" + "code": "NC", + "numeric": "540" }, { - "code": "NZ" + "code": "NZ", + "numeric": "554" }, { - "code": "NI" + "code": "NI", + "numeric": "558" }, { - "code": "NE" + "code": "NE", + "numeric": "562" }, { - "code": "NG" + "code": "NG", + "numeric": "566" }, { - "code": "NU" + "code": "NU", + "numeric": "570" }, { - "code": "NF" + "code": "NF", + "numeric": "574" }, { - "code": "MP" + "code": "MP", + "numeric": "580" }, { - "code": "NO" + "code": "NO", + "numeric": "578" }, { - "code": "OM" + "code": "OM", + "numeric": "512" }, { - "code": "PK" + "code": "PK", + "numeric": "586" }, { - "code": "PW" + "code": "PW", + "numeric": "585" }, { - "code": "PA" + "code": "PA", + "numeric": "591" }, { - "code": "PG" + "code": "PG", + "numeric": "598" }, { - "code": "PY" + "code": "PY", + "numeric": "600" }, { - "code": "PE" + "code": "PE", + "numeric": "604" }, { - "code": "PH" + "code": "PH", + "numeric": "608" }, { - "code": "PN" + "code": "PN", + "numeric": "612" }, { - "code": "PL" + "code": "PL", + "numeric": "616" }, { - "code": "PT" + "code": "PT", + "numeric": "620" }, { - "code": "PR" + "code": "PR", + "numeric": "630" }, { - "code": "QA" + "code": "QA", + "numeric": "634" }, { - "code": "MK" + "code": "MK", + "numeric": "807" }, { - "code": "RE" + "code": "RE", + "numeric": "638" }, { - "code": "RO" + "code": "RO", + "numeric": "642" }, { - "code": "RU" + "code": "RU", + "numeric": "643" }, { - "code": "RW" + "code": "RW", + "numeric": "646" }, { - "code": "KN" + "code": "KN", + "numeric": "659" }, { - "code": "LC" + "code": "LC", + "numeric": "662" }, { - "code": "VC" + "code": "VC", + "numeric": "670" }, { - "code": "WS" + "code": "WS", + "numeric": "882" }, { - "code": "SM" + "code": "SM", + "numeric": "674" }, { - "code": "ST" + "code": "ST", + "numeric": "678" }, { - "code": "SA" + "code": "SA", + "numeric": "682" }, { - "code": "SN" + "code": "SN", + "numeric": "686" }, { - "code": "SC" + "code": "SC", + "numeric": "690" }, { - "code": "SL" + "code": "SL", + "numeric": "694" }, { - "code": "SG" + "code": "SG", + "numeric": "702" }, { - "code": "SK" + "code": "SK", + "numeric": "703" }, { - "code": "SI" + "code": "SI", + "numeric": "705" }, { - "code": "SB" + "code": "SB", + "numeric": "090" }, { - "code": "SO" + "code": "SO", + "numeric": "706" }, { - "code": "SX" + "code": "SX", + "numeric": "534" }, { - "code": "ZA" + "code": "ZA", + "numeric": "710" }, { - "code": "GS" + "code": "GS", + "numeric": "239" }, { - "code": "ES" + "code": "ES", + "numeric": "724" }, { - "code": "LK" + "code": "LK", + "numeric": "144" }, { - "code": "SH" + "code": "SH", + "numeric": "654" }, { - "code": "PM" + "code": "PM", + "numeric": "666" }, { - "code": "SD" + "code": "SD", + "numeric": "729" }, { - "code": "SR" + "code": "SR", + "numeric": "740" }, { - "code": "SJ" + "code": "SJ", + "numeric": "744" }, { - "code": "SZ" + "code": "SZ", + "numeric": "748" }, { - "code": "SE" + "code": "SE", + "numeric": "752" }, { - "code": "CH" + "code": "CH", + "numeric": "756" }, { - "code": "SY" + "code": "SY", + "numeric": "760" }, { - "code": "TW" + "code": "TW", + "numeric": "158" }, { - "code": "TJ" + "code": "TJ", + "numeric": "762" }, { - "code": "TZ" + "code": "TZ", + "numeric": "834" }, { "code": "TH", + "numeric": "764", "states": [ { "code" : "37", @@ -1948,46 +2158,60 @@ ] }, { - "code": "TG" + "code": "TG", + "numeric": "768" }, { - "code": "TK" + "code": "TK", + "numeric": "772" }, { - "code": "TO" + "code": "TO", + "numeric": "776" }, { - "code": "TT" + "code": "TT", + "numeric": "780" }, { - "code": "TN" + "code": "TN", + "numeric": "788" }, { - "code": "TR" + "code": "TR", + "numeric": "792" }, { - "code": "TM" + "code": "TM", + "numeric": "795" }, { - "code": "TC" + "code": "TC", + "numeric": "796" }, { - "code": "TV" + "code": "TV", + "numeric": "798" }, { - "code": "UG" + "code": "UG", + "numeric": "800" }, { - "code": "UA" + "code": "UA", + "numeric": "804" }, { - "code": "AE" + "code": "AE", + "numeric": "784" }, { - "code": "GB" + "code": "GB", + "numeric": "826" }, { "code": "US", + "numeric": "840", "states": [ { "code" : "AL", @@ -2220,42 +2444,55 @@ ] }, { - "code": "UY" + "code": "UY", + "numeric": "858" }, { - "code": "UZ" + "code": "UZ", + "numeric": "860" }, { - "code": "VU" + "code": "VU", + "numeric": "548" }, { - "code": "VA" + "code": "VA", + "numeric": "336" }, { - "code": "VE" + "code": "VE", + "numeric": "862" }, { - "code": "VN" + "code": "VN", + "numeric": "704" }, { - "code": "VG" + "code": "VG", + "numeric": "092" }, { - "code": "VI" + "code": "VI", + "numeric": "850" }, { - "code": "WF" + "code": "WF", + "numeric": "876" }, { - "code": "YE" + "code": "YE", + "numeric": "887" }, { - "code": "CD" + "code": "CD", + "numeric": "180" }, { - "code": "ZM" + "code": "ZM", + "numeric": "894" }, { - "code": "ZW" + "code": "ZW", + "numeric": "716" } ] diff --git a/ui/src/main/java/io/snabble/sdk/ui/SnabbleUI.kt b/ui/src/main/java/io/snabble/sdk/ui/SnabbleUI.kt index b060d44cca..33273c9390 100644 --- a/ui/src/main/java/io/snabble/sdk/ui/SnabbleUI.kt +++ b/ui/src/main/java/io/snabble/sdk/ui/SnabbleUI.kt @@ -19,8 +19,8 @@ import io.snabble.sdk.ui.SnabbleUI.Event.SHOW_BARCODE_SEARCH import io.snabble.sdk.ui.SnabbleUI.Event.SHOW_CHECKOUT import io.snabble.sdk.ui.SnabbleUI.Event.SHOW_CHECKOUT_DONE import io.snabble.sdk.ui.SnabbleUI.Event.SHOW_COUPON_DETAILS -import io.snabble.sdk.ui.SnabbleUI.Event.SHOW_FISERV_INPUT import io.snabble.sdk.ui.SnabbleUI.Event.SHOW_EXTERNAL_BILLING +import io.snabble.sdk.ui.SnabbleUI.Event.SHOW_FISERV_INPUT import io.snabble.sdk.ui.SnabbleUI.Event.SHOW_GIROPAY_INPUT import io.snabble.sdk.ui.SnabbleUI.Event.SHOW_PAYMENT_CREDENTIALS_LIST import io.snabble.sdk.ui.SnabbleUI.Event.SHOW_PAYMENT_OPTIONS @@ -34,13 +34,14 @@ import io.snabble.sdk.ui.cart.deprecated.ShoppingCartActivity import io.snabble.sdk.ui.checkout.CheckoutActivity import io.snabble.sdk.ui.coupon.CouponDetailActivity import io.snabble.sdk.ui.payment.AgeVerificationInputActivity -import io.snabble.sdk.ui.payment.fiserv.FiservInputActivity import io.snabble.sdk.ui.payment.GiropayInputActivity import io.snabble.sdk.ui.payment.PaymentCredentialsListActivity import io.snabble.sdk.ui.payment.PaymentOptionsActivity import io.snabble.sdk.ui.payment.PayoneInputActivity import io.snabble.sdk.ui.payment.ProjectPaymentOptionsActivity import io.snabble.sdk.ui.payment.SEPACardInputActivity +import io.snabble.sdk.ui.payment.creditcard.datatrans.ui.DatatransActivity +import io.snabble.sdk.ui.payment.creditcard.fiserv.FiservInputActivity import io.snabble.sdk.ui.payment.externalbilling.ExternalBillingActivity import io.snabble.sdk.ui.payment.payone.sepa.form.PayoneSepaActivity import io.snabble.sdk.ui.scanner.SelfScanningActivity @@ -67,6 +68,7 @@ object SnabbleUI { SHOW_SEPA_CARD_INPUT, SHOW_PAYONE_SEPA, SHOW_FISERV_INPUT, + SHOW_DATATRANS_INPUT, SHOW_PAYONE_INPUT, SHOW_GIROPAY_INPUT, SHOW_EXTERNAL_BILLING, @@ -177,6 +179,9 @@ object SnabbleUI { SHOW_FISERV_INPUT -> startActivity(context, FiservInputActivity::class.java, args, canGoBack = false) + Event.SHOW_DATATRANS_INPUT -> + startActivity(context, DatatransActivity::class.java, args, canGoBack = false) + SHOW_PAYONE_INPUT -> startActivity(context, PayoneInputActivity::class.java, args, canGoBack = false) SHOW_GIROPAY_INPUT -> @@ -201,6 +206,7 @@ object SnabbleUI { NOT_CHECKED_IN, EXIT_TOKEN_AVAILABLE, null -> Unit + } } } diff --git a/ui/src/main/java/io/snabble/sdk/ui/payment/Datatrans.kt b/ui/src/main/java/io/snabble/sdk/ui/payment/Datatrans.kt deleted file mode 100644 index 73d64c2ffd..0000000000 --- a/ui/src/main/java/io/snabble/sdk/ui/payment/Datatrans.kt +++ /dev/null @@ -1,199 +0,0 @@ -package io.snabble.sdk.ui.payment - -import android.widget.Toast -import androidx.fragment.app.FragmentActivity -import androidx.lifecycle.DefaultLifecycleObserver -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleOwner -import ch.datatrans.payment.api.Transaction -import ch.datatrans.payment.api.TransactionListener -import ch.datatrans.payment.api.TransactionRegistry -import ch.datatrans.payment.api.TransactionSuccess -import ch.datatrans.payment.exception.TransactionException -import ch.datatrans.payment.paymentmethods.SavedCard -import ch.datatrans.payment.paymentmethods.SavedPostFinanceCard -import io.snabble.sdk.PaymentMethod -import io.snabble.sdk.Project -import io.snabble.sdk.Snabble -import io.snabble.sdk.payment.PaymentCredentials -import io.snabble.sdk.ui.Keyguard -import io.snabble.sdk.ui.R -import io.snabble.sdk.utils.Dispatch -import io.snabble.sdk.utils.GsonHolder -import io.snabble.sdk.utils.Logger -import io.snabble.sdk.utils.SimpleJsonCallback -import okhttp3.Callback -import okhttp3.MediaType.Companion.toMediaType -import okhttp3.Request -import okhttp3.RequestBody.Companion.toRequestBody -import java.util.* - -object Datatrans { - data class DatatransTokenizationRequest( - val paymentMethod: PaymentMethod, - val language: String = Locale.getDefault().language, - ) - - data class DatatransTokenizationResponse( - val mobileToken: String, - val isTesting: Boolean?, - ) - - @JvmStatic - fun registerCard(activity: FragmentActivity, project: Project, paymentMethod: PaymentMethod) { - val descriptor = project.paymentMethodDescriptors.find { it.paymentMethod == paymentMethod } - if (descriptor == null) { - project.events.logError("Datatrans Error: No payment descriptor") - Logger.e("Datatrans error: No payment method descriptor for $paymentMethod") - - Dispatch.mainThread { - showError(activity, paymentMethod) - } - return - } - - val url = descriptor.links?.get("tokenization") - if (url == null) { - project.events.logError("Datatrans Error: No tokenization url") - Logger.e("Datatrans error: No tokenization url") - - Dispatch.mainThread { - showError(activity, paymentMethod) - } - return - } - - val request: Request = Request.Builder() - .url(Snabble.absoluteUrl(url.href)) - .post( - GsonHolder.get().toJson( - DatatransTokenizationRequest(paymentMethod) - ).toRequestBody("application/json".toMediaType()) - ) - .build() - - project.okHttpClient.newCall(request).enqueue(object : - SimpleJsonCallback(DatatransTokenizationResponse::class.java), Callback { - override fun success(response: DatatransTokenizationResponse) { - startDatatransTransaction(activity, response, paymentMethod, project) - } - - override fun error(t: Throwable?) { - Dispatch.mainThread { - showError(activity, paymentMethod) - } - - project.events.logError("Datatrans Tokenization Error: " + t?.message) - Logger.e("Datatrans Tokenization Error: ${t?.message}") - } - }) - } - - private fun showError(activity: FragmentActivity, paymentMethod: PaymentMethod) { - if (!activity.isDestroyed) { - val err = when (paymentMethod) { - PaymentMethod.TWINT -> { - R.string.Snabble_Payment_Twint_error - } - - PaymentMethod.POST_FINANCE_CARD -> { - R.string.Snabble_Payment_PostFinanceCard_error - } - - else -> { - R.string.Snabble_Payment_CreditCard_error - } - } - - Toast.makeText(activity, err, Toast.LENGTH_LONG).show() - } - } - - private fun startDatatransTransaction( - activity: FragmentActivity, - tokenizationResponse: DatatransTokenizationResponse, - paymentMethod: PaymentMethod, - project: Project - ) { - val transaction = Transaction(tokenizationResponse.mobileToken) - transaction.listener = object : TransactionListener { - override fun onTransactionSuccess(result: TransactionSuccess) { - activity.runOnUiThreadWhenResumed { - val token = result.savedPaymentMethod - var month = "" - var year = "" - - when (token) { - is SavedPostFinanceCard -> { - token.cardExpiryDate?.let { - month = it.formattedMonth - year = it.formattedYear - } - } - - is SavedCard -> { - token.cardExpiryDate?.let { - month = it.formattedMonth - year = it.formattedYear - } - } - } - - if (token != null) { - Keyguard.unlock(activity, object : Keyguard.Callback { - override fun success() { - val store = Snabble.paymentCredentialsStore - val credentials = PaymentCredentials.fromDatatrans( - token.alias, - PaymentCredentials.Brand.fromPaymentMethod(paymentMethod), - result.savedPaymentMethod?.getDisplayTitle(activity), - month, - year, - project.id, - ) - store.add(credentials) - } - - override fun error() { - Toast.makeText(activity, R.string.Snabble_SEPA_encryptionError, Toast.LENGTH_LONG) - .show() - } - }) - } else { - Toast.makeText(activity, R.string.Snabble_SEPA_encryptionError, Toast.LENGTH_LONG).show() - } - } - } - - override fun onTransactionError(exception: TransactionException) { - activity.runOnUiThreadWhenResumed { - project.events.logError("Datatrans TransactionException: " + exception.message) - showError(activity, paymentMethod) - } - } - } - transaction.options.appCallbackScheme = "snabble" - transaction.options.isTesting = tokenizationResponse.isTesting ?: false - transaction.options.useCertificatePinning = true - TransactionRegistry.startTransaction(activity, transaction) - } -} - -fun FragmentActivity.runOnUiThreadWhenResumed(task: () -> Unit) { - Dispatch.mainThread { - if (lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) { - Dispatch.mainThread { - task() - } - } else { - lifecycle.addObserver(object : DefaultLifecycleObserver { - override fun onResume(owner: LifecycleOwner) { - Dispatch.mainThread { - task() - } - lifecycle.removeObserver(this) - } - }) - } - } -} diff --git a/ui/src/main/java/io/snabble/sdk/ui/payment/PaymentInputViewHelper.kt b/ui/src/main/java/io/snabble/sdk/ui/payment/PaymentInputViewHelper.kt index 60d42a5ff7..c5044ad4f3 100644 --- a/ui/src/main/java/io/snabble/sdk/ui/payment/PaymentInputViewHelper.kt +++ b/ui/src/main/java/io/snabble/sdk/ui/payment/PaymentInputViewHelper.kt @@ -10,8 +10,9 @@ import io.snabble.sdk.Snabble import io.snabble.sdk.payment.PaymentCredentials import io.snabble.sdk.ui.R import io.snabble.sdk.ui.SnabbleUI +import io.snabble.sdk.ui.payment.creditcard.datatrans.ui.DatatransFragment +import io.snabble.sdk.ui.payment.creditcard.fiserv.FiservInputView import io.snabble.sdk.ui.payment.externalbilling.ExternalBillingFragment.Companion.ARG_PROJECT_ID -import io.snabble.sdk.ui.payment.fiserv.FiservInputView import io.snabble.sdk.ui.utils.KeyguardUtils import io.snabble.sdk.ui.utils.UIUtils import io.snabble.sdk.utils.Logger @@ -36,13 +37,18 @@ object PaymentInputViewHelper { val args = Bundle() when { - useDatatrans -> Datatrans.registerCard(activity, project, paymentMethod) + useDatatrans -> { + args.putString(DatatransFragment.ARG_PROJECT_ID, projectId) + args.putSerializable(DatatransFragment.ARG_PAYMENT_TYPE, paymentMethod) + SnabbleUI.executeAction(context, SnabbleUI.Event.SHOW_DATATRANS_INPUT, args) + } usePayone -> Payone.registerCard(activity, project, paymentMethod, Snabble.formPrefillData) useFiserv -> { args.putString(FiservInputView.ARG_PROJECT_ID, projectId) args.putSerializable(FiservInputView.ARG_PAYMENT_TYPE, paymentMethod) SnabbleUI.executeAction(context, SnabbleUI.Event.SHOW_FISERV_INPUT, args) } + paymentMethod == PaymentMethod.EXTERNAL_BILLING -> { args.putString(ARG_PROJECT_ID, projectId) SnabbleUI.executeAction(context, SnabbleUI.Event.SHOW_EXTERNAL_BILLING, args) diff --git a/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/datatrans/data/DatatransRemoteDataSource.kt b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/datatrans/data/DatatransRemoteDataSource.kt new file mode 100644 index 0000000000..0b35034b24 --- /dev/null +++ b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/datatrans/data/DatatransRemoteDataSource.kt @@ -0,0 +1,44 @@ +package io.snabble.sdk.ui.payment.creditcard.datatrans.data + +import com.google.gson.Gson +import io.snabble.sdk.Snabble +import io.snabble.sdk.ui.payment.creditcard.datatrans.data.dto.AuthDataDto +import io.snabble.sdk.ui.payment.creditcard.datatrans.data.dto.CustomerDataDto +import io.snabble.sdk.ui.payment.creditcard.shared.getTokenizationUrlFor +import io.snabble.sdk.ui.payment.creditcard.shared.post +import io.snabble.sdk.utils.GsonHolder +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.Request +import okhttp3.RequestBody +import okhttp3.RequestBody.Companion.toRequestBody + +internal interface DatatransRemoteDataSource { + + suspend fun sendUserData(customerDataDto: CustomerDataDto): Result +} + +internal class DatatransRemoteDataSourceImpl( + private val snabble: Snabble = Snabble, + private val gson: Gson = GsonHolder.get(), +) : DatatransRemoteDataSource { + + override suspend fun sendUserData( + customerDataDto: CustomerDataDto, + ): Result { + val project = + snabble.checkedInProject.value ?: return Result.failure(Exception("Missing projectId")) + + val customerInfoPostUrl = + project.paymentMethodDescriptors.getTokenizationUrlFor(customerDataDto.paymentMethod) + ?: return Result.failure(Exception("Missing link to send customer info to")) + + val requestBody: RequestBody = + gson.toJson(customerDataDto).toRequestBody("application/json".toMediaType()) + val request: Request = Request.Builder() + .url(customerInfoPostUrl) + .post(requestBody) + .build() + + return project.okHttpClient.post(request, gson) + } +} diff --git a/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/datatrans/data/DatatransRepositoryImpl.kt b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/datatrans/data/DatatransRepositoryImpl.kt new file mode 100644 index 0000000000..c57084858d --- /dev/null +++ b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/datatrans/data/DatatransRepositoryImpl.kt @@ -0,0 +1,53 @@ +package io.snabble.sdk.ui.payment.creditcard.datatrans.data + +import io.snabble.sdk.PaymentMethod +import io.snabble.sdk.ui.payment.creditcard.datatrans.data.dto.AddressDto +import io.snabble.sdk.ui.payment.creditcard.datatrans.data.dto.AuthDataDto +import io.snabble.sdk.ui.payment.creditcard.datatrans.data.dto.CustomerDataDto +import io.snabble.sdk.ui.payment.creditcard.datatrans.data.dto.CustomerInfoDto +import io.snabble.sdk.ui.payment.creditcard.datatrans.data.dto.PhoneNumberDto +import io.snabble.sdk.ui.payment.creditcard.datatrans.domain.DatatransRepository +import io.snabble.sdk.ui.payment.creditcard.datatrans.domain.model.AuthData +import io.snabble.sdk.ui.payment.creditcard.shared.country.domain.models.CustomerInfo +import java.util.Locale + +internal class DatatransRepositoryImpl( + private val datatransRemoteDataSource: DatatransRemoteDataSource = DatatransRemoteDataSourceImpl(), +) : DatatransRepository { + + override suspend fun sendUserData( + customerInfo: CustomerInfo, + paymentMethod: PaymentMethod + ): Result { + return datatransRemoteDataSource.sendUserData(createTokenizationRequest(customerInfo, paymentMethod)) + .map { it.toResponse() } + } +} + +private fun createTokenizationRequest( + customerInfo: CustomerInfo, + paymentMethod: PaymentMethod +) = CustomerDataDto( + paymentMethod = paymentMethod, + language = Locale.getDefault().language, + cardOwner = customerInfo.toDto() +) + +private fun CustomerInfo.toDto() = CustomerInfoDto( + name = name, + email = email, + address = AddressDto( + street = address.street, + city = address.city, + country = address.country.numericCode, + state = address.state, + zip = address.zip + ), + phoneNumber = PhoneNumberDto( + intCallingCode = intCallingCode, + number = phoneNumber + ) +) + +private fun AuthDataDto.toResponse() = + AuthData(mobileToken = mobileToken, isTesting = isTesting) diff --git a/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/datatrans/data/dto/AuthDataDto.kt b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/datatrans/data/dto/AuthDataDto.kt new file mode 100644 index 0000000000..4d407f7869 --- /dev/null +++ b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/datatrans/data/dto/AuthDataDto.kt @@ -0,0 +1,8 @@ +package io.snabble.sdk.ui.payment.creditcard.datatrans.data.dto + +import com.google.gson.annotations.SerializedName + +internal data class AuthDataDto( + @SerializedName("mobileToken") val mobileToken: String, + @SerializedName("isTesting") val isTesting: Boolean = false, +) diff --git a/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/datatrans/data/dto/CustomerDataDto.kt b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/datatrans/data/dto/CustomerDataDto.kt new file mode 100644 index 0000000000..e7dd70e796 --- /dev/null +++ b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/datatrans/data/dto/CustomerDataDto.kt @@ -0,0 +1,11 @@ +package io.snabble.sdk.ui.payment.creditcard.datatrans.data.dto + +import com.google.gson.annotations.SerializedName +import io.snabble.sdk.PaymentMethod +import java.util.Locale + +internal data class CustomerDataDto( + @SerializedName("paymentMethod") val paymentMethod: PaymentMethod, + @SerializedName("language") val language: String = Locale.getDefault().language, + @SerializedName("cardOwner") val cardOwner: CustomerInfoDto +) diff --git a/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/datatrans/data/dto/CustomerInfoDto.kt b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/datatrans/data/dto/CustomerInfoDto.kt new file mode 100644 index 0000000000..f8107e3d81 --- /dev/null +++ b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/datatrans/data/dto/CustomerInfoDto.kt @@ -0,0 +1,23 @@ +package io.snabble.sdk.ui.payment.creditcard.datatrans.data.dto + +import com.google.gson.annotations.SerializedName + +internal data class CustomerInfoDto( + @SerializedName("name") val name: String, + @SerializedName("email") val email: String, + @SerializedName("address") val address: AddressDto, + @SerializedName("phoneNumber") val phoneNumber: PhoneNumberDto, +) + +internal data class AddressDto( + @SerializedName("street") val street: String, + @SerializedName("zip") val zip: String, + @SerializedName("city") val city: String, + @SerializedName("state") val state: String?, + @SerializedName("country") val country: String, +) + +internal data class PhoneNumberDto( + @SerializedName("subscriber") val number: String, + @SerializedName("countryCode") val intCallingCode: String, +) diff --git a/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/datatrans/domain/DatatransRepository.kt b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/datatrans/domain/DatatransRepository.kt new file mode 100644 index 0000000000..7b1e0d59c4 --- /dev/null +++ b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/datatrans/domain/DatatransRepository.kt @@ -0,0 +1,13 @@ +package io.snabble.sdk.ui.payment.creditcard.datatrans.domain + +import io.snabble.sdk.PaymentMethod +import io.snabble.sdk.ui.payment.creditcard.datatrans.domain.model.AuthData +import io.snabble.sdk.ui.payment.creditcard.shared.country.domain.models.CustomerInfo + +internal interface DatatransRepository { + + suspend fun sendUserData( + customerInfo: CustomerInfo, + paymentMethod: PaymentMethod + ): Result +} diff --git a/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/datatrans/domain/model/AuthData.kt b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/datatrans/domain/model/AuthData.kt new file mode 100644 index 0000000000..cdd2ddbd0c --- /dev/null +++ b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/datatrans/domain/model/AuthData.kt @@ -0,0 +1,6 @@ +package io.snabble.sdk.ui.payment.creditcard.datatrans.domain.model + +data class AuthData( + val mobileToken: String, + val isTesting: Boolean, +) diff --git a/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/datatrans/ui/DatatransActivity.kt b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/datatrans/ui/DatatransActivity.kt new file mode 100644 index 0000000000..1074c6ed0e --- /dev/null +++ b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/datatrans/ui/DatatransActivity.kt @@ -0,0 +1,16 @@ +package io.snabble.sdk.ui.payment.creditcard.datatrans.ui + +import androidx.fragment.app.Fragment +import io.snabble.sdk.ui.BaseFragmentActivity + +class DatatransActivity : BaseFragmentActivity() { + + override fun onCreateFragment(): Fragment = DatatransFragment() + .apply { arguments = intent.extras } + + companion object { + + const val ARG_PROJECT_ID = DatatransFragment.ARG_PROJECT_ID + const val ARG_PAYMENT_TYPE = DatatransFragment.ARG_PAYMENT_TYPE + } +} diff --git a/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/datatrans/ui/DatatransFragment.kt b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/datatrans/ui/DatatransFragment.kt new file mode 100644 index 0000000000..ff143f1669 --- /dev/null +++ b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/datatrans/ui/DatatransFragment.kt @@ -0,0 +1,117 @@ +package io.snabble.sdk.ui.payment.creditcard.datatrans.ui + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.ui.platform.ComposeView +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.flowWithLifecycle +import androidx.lifecycle.lifecycleScope +import ch.datatrans.payment.api.TransactionRegistry +import io.snabble.sdk.PaymentMethod +import io.snabble.sdk.ui.Keyguard +import io.snabble.sdk.ui.R +import io.snabble.sdk.ui.payment.PaymentMethodMetaDataHelper +import io.snabble.sdk.ui.payment.creditcard.shared.CustomerInfoInputScreen +import io.snabble.sdk.ui.utils.ThemeWrapper +import io.snabble.sdk.ui.utils.serializableExtra +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.launch + +class DatatransFragment : Fragment() { + + private val viewModel: DatatransViewModel by viewModels { DatatransViewModelFactory(requireContext()) } + + private lateinit var paymentMethod: PaymentMethod + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + paymentMethod = arguments?.serializableExtra(ARG_PAYMENT_TYPE) + ?: kotlin.run { activity?.onBackPressed(); return } + + (requireActivity() as? AppCompatActivity)?.supportActionBar?.title = + PaymentMethodMetaDataHelper(requireContext()).labelFor(paymentMethod) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View = + ComposeView(inflater.context).apply { + setContent { + val uiState: UiState = viewModel.uiState.collectAsStateWithLifecycle().value + + ThemeWrapper { + CustomerInfoInputScreen( + onErrorProcessed = { viewModel.errorHandled() }, + isLoading = uiState.isLoading, + onSendAction = { viewModel.sendUserData(it) }, + showError = uiState.showError, + countryItems = uiState.countryItems, + onBackNavigationClick = { activity?.onBackPressed() } + ) + } + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + handleEvents() + } + + private fun handleEvents() { + lifecycleScope.launch { + viewModel.event + .filterNotNull() + .flowWithLifecycle(lifecycle) + .collectLatest { event -> + when (event) { + Event.TransactionFailed -> { + val err = when (paymentMethod) { + PaymentMethod.TWINT -> R.string.Snabble_Payment_Twint_error + PaymentMethod.POST_FINANCE_CARD -> R.string.Snabble_Payment_PostFinanceCard_error + else -> R.string.Snabble_Payment_CreditCard_error + } + + Toast.makeText(activity, err, Toast.LENGTH_LONG).show() + } + + is Event.TransActionCreated -> { + activity?.let { TransactionRegistry.startTransaction(it, event.transaction) } + } + + is Event.TransActionSucceeded -> activity?.let { + Keyguard.unlock(it, object : Keyguard.Callback { + override fun success() { + viewModel.saveDatatransToken( + event.datatransToken, + event.datatransToken.token.getDisplayTitle(it) + ) + } + + override fun error() { + Toast + .makeText(activity, R.string.Snabble_SEPA_encryptionError, Toast.LENGTH_LONG) + .show() + + activity?.onBackPressed() + } + }) + } + + Event.Finish -> activity?.onBackPressed() + } + } + } + } + + companion object { + + const val ARG_PROJECT_ID: String = "projectId" + const val ARG_PAYMENT_TYPE: String = "paymentType" + } +} diff --git a/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/datatrans/ui/DatatransViewModel.kt b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/datatrans/ui/DatatransViewModel.kt new file mode 100644 index 0000000000..af1ed348d6 --- /dev/null +++ b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/datatrans/ui/DatatransViewModel.kt @@ -0,0 +1,128 @@ +package io.snabble.sdk.ui.payment.creditcard.datatrans.ui + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import ch.datatrans.payment.api.Transaction +import ch.datatrans.payment.api.TransactionListener +import ch.datatrans.payment.api.TransactionSuccess +import ch.datatrans.payment.exception.TransactionException +import ch.datatrans.payment.paymentmethods.CardExpiryDate +import ch.datatrans.payment.paymentmethods.SavedCard +import ch.datatrans.payment.paymentmethods.SavedPaymentMethod +import ch.datatrans.payment.paymentmethods.SavedPostFinanceCard +import io.snabble.sdk.PaymentMethod +import io.snabble.sdk.Snabble +import io.snabble.sdk.payment.PaymentCredentials +import io.snabble.sdk.ui.payment.creditcard.datatrans.domain.DatatransRepository +import io.snabble.sdk.ui.payment.creditcard.datatrans.ui.DatatransFragment.Companion.ARG_PAYMENT_TYPE +import io.snabble.sdk.ui.payment.creditcard.datatrans.ui.DatatransFragment.Companion.ARG_PROJECT_ID +import io.snabble.sdk.ui.payment.creditcard.shared.country.domain.CountryItemsRepository +import io.snabble.sdk.ui.payment.creditcard.shared.country.domain.models.CountryItem +import io.snabble.sdk.ui.payment.creditcard.shared.country.domain.models.CustomerInfo +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +internal class DatatransViewModel( + private val datatransRepository: DatatransRepository, + countryItemsRepo: CountryItemsRepository, + savedStateHandle: SavedStateHandle +) : ViewModel() { + + private val _uiState = + MutableStateFlow(UiState(countryItems = countryItemsRepo.loadCountryItems())) + val uiState: StateFlow = _uiState.asStateFlow() + + private val _event: MutableStateFlow = MutableStateFlow(null) + val event = _event.asStateFlow() + + private val paymentMethod = savedStateHandle.get(ARG_PAYMENT_TYPE) + private val projectId = savedStateHandle.get(ARG_PROJECT_ID) + + fun sendUserData(customerInfo: CustomerInfo) { + viewModelScope.launch { + paymentMethod ?: return@launch + datatransRepository.sendUserData(customerInfo, paymentMethod) + .onSuccess { info -> + _uiState.update { it.copy(isLoading = false) } + _event.update { Event.TransActionCreated(createTransaction(info.mobileToken, info.isTesting)) } + } + .onFailure { + _uiState.update { it.copy(isLoading = false, showError = true) } + } + } + } + + private fun createTransaction(token: String, isTesting: Boolean) = Transaction(token).apply { + listener = object : TransactionListener { + override fun onTransactionSuccess(result: TransactionSuccess) { + val datatransToken = when (val savedPaymentMethod = result.savedPaymentMethod) { + is SavedPostFinanceCard -> DatatransToken(savedPaymentMethod, savedPaymentMethod.cardExpiryDate) + + is SavedCard -> DatatransToken(savedPaymentMethod, savedPaymentMethod.cardExpiryDate) + + else -> { + if (savedPaymentMethod != null) DatatransToken(savedPaymentMethod) + null + } + } + when { + datatransToken != null -> _event.update { + Event.TransActionSucceeded(datatransToken) + } + + else -> _event.update { Event.TransactionFailed } + } + } + + override fun onTransactionError(exception: TransactionException) { + _event.update { Event.TransactionFailed } + } + } + options.appCallbackScheme = "snabble" + options.isTesting = isTesting + options.useCertificatePinning = true + } + + fun errorHandled() { + _uiState.update { it.copy(showError = false) } + } + + fun saveDatatransToken(datatransToken: DatatransToken, displayName: String) { + val credentials = PaymentCredentials.fromDatatrans( + datatransToken.token.alias, + PaymentCredentials.Brand.fromPaymentMethod(paymentMethod), + displayName, + datatransToken.expiryDate?.formattedMonth.orEmpty(), + datatransToken.expiryDate?.formattedYear.orEmpty(), + projectId, + ) + Snabble.paymentCredentialsStore.add(credentials) + _event.update { Event.Finish } + } +} + +internal data class UiState( + val isLoading: Boolean = false, + val countryItems: List, + val mobileToken: String? = null, + val showError: Boolean = false, + val isTesting: Boolean = false, + val transaction: Transaction? = null, + val datatransToken: DatatransToken? = null +) + +internal sealed interface Event { + data object TransactionFailed : Event + data class TransActionCreated(val transaction: Transaction) : Event + data class TransActionSucceeded(val datatransToken: DatatransToken) : Event + data object Finish : Event +} + +internal data class DatatransToken( + val token: SavedPaymentMethod, + val expiryDate: CardExpiryDate? = null +) diff --git a/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/datatrans/ui/DatatransViewModelFactory.kt b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/datatrans/ui/DatatransViewModelFactory.kt new file mode 100644 index 0000000000..d5ba9b86e8 --- /dev/null +++ b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/datatrans/ui/DatatransViewModelFactory.kt @@ -0,0 +1,33 @@ +package io.snabble.sdk.ui.payment.creditcard.datatrans.ui + +import android.content.Context +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.createSavedStateHandle +import androidx.lifecycle.viewmodel.CreationExtras +import io.snabble.sdk.ui.payment.creditcard.datatrans.data.DatatransRepositoryImpl +import io.snabble.sdk.ui.payment.creditcard.shared.country.data.CountryItemsRepositoryImpl +import io.snabble.sdk.ui.payment.creditcard.shared.country.data.source.LocalCountryItemsDataSourceImpl +import io.snabble.sdk.utils.GsonHolder + +internal class DatatransViewModelFactory(private val context: Context) : ViewModelProvider.Factory { + + override fun create(modelClass: Class, extras: CreationExtras): T { + if (!modelClass.isAssignableFrom(DatatransViewModel::class.java)) { + throw IllegalArgumentException("Unable to construct viewmodel") + } + + val savedStateHandle = extras.createSavedStateHandle() + @Suppress("UNCHECKED_CAST") + return DatatransViewModel( + datatransRepository = DatatransRepositoryImpl(), + countryItemsRepo = CountryItemsRepositoryImpl( + localCountryItemsDataSource = LocalCountryItemsDataSourceImpl( + assetManager = context.assets, + gson = GsonHolder.get() + ) + ), + savedStateHandle = savedStateHandle + ) as T + } +} diff --git a/ui/src/main/java/io/snabble/sdk/ui/payment/fiserv/FiservInputActivity.kt b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/fiserv/FiservInputActivity.kt similarity index 89% rename from ui/src/main/java/io/snabble/sdk/ui/payment/fiserv/FiservInputActivity.kt rename to ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/fiserv/FiservInputActivity.kt index c9f8a177e5..620244b0ca 100644 --- a/ui/src/main/java/io/snabble/sdk/ui/payment/fiserv/FiservInputActivity.kt +++ b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/fiserv/FiservInputActivity.kt @@ -1,4 +1,4 @@ -package io.snabble.sdk.ui.payment.fiserv +package io.snabble.sdk.ui.payment.creditcard.fiserv import androidx.fragment.app.Fragment import io.snabble.sdk.ui.BaseFragmentActivity diff --git a/ui/src/main/java/io/snabble/sdk/ui/payment/fiserv/FiservInputFragment.kt b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/fiserv/FiservInputFragment.kt similarity index 89% rename from ui/src/main/java/io/snabble/sdk/ui/payment/fiserv/FiservInputFragment.kt rename to ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/fiserv/FiservInputFragment.kt index 62f44d05b7..18b122c1a1 100644 --- a/ui/src/main/java/io/snabble/sdk/ui/payment/fiserv/FiservInputFragment.kt +++ b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/fiserv/FiservInputFragment.kt @@ -1,4 +1,4 @@ -package io.snabble.sdk.ui.payment.fiserv +package io.snabble.sdk.ui.payment.creditcard.fiserv import android.os.Bundle import android.view.LayoutInflater @@ -13,6 +13,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.snabble.sdk.PaymentMethod import io.snabble.sdk.Snabble import io.snabble.sdk.ui.payment.PaymentMethodMetaDataHelper +import io.snabble.sdk.ui.payment.creditcard.shared.CustomerInfoInputScreen import io.snabble.sdk.ui.utils.ThemeWrapper import io.snabble.sdk.ui.utils.serializableExtra @@ -25,8 +26,9 @@ class FiservInputFragment : Fragment() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - paymentMethod = arguments?.serializableExtra(FiservInputView.ARG_PAYMENT_TYPE) - ?: kotlin.run { activity?.onBackPressed(); return } + paymentMethod = + arguments?.serializableExtra(FiservInputView.ARG_PAYMENT_TYPE) + ?: kotlin.run { activity?.onBackPressed(); return } (requireActivity() as? AppCompatActivity)?.supportActionBar?.title = PaymentMethodMetaDataHelper(requireContext()).labelFor(paymentMethod) diff --git a/ui/src/main/java/io/snabble/sdk/ui/payment/fiserv/FiservInputView.java b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/fiserv/FiservInputView.java similarity index 98% rename from ui/src/main/java/io/snabble/sdk/ui/payment/fiserv/FiservInputView.java rename to ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/fiserv/FiservInputView.java index 87629bab09..49c172c1a7 100644 --- a/ui/src/main/java/io/snabble/sdk/ui/payment/fiserv/FiservInputView.java +++ b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/fiserv/FiservInputView.java @@ -1,4 +1,4 @@ -package io.snabble.sdk.ui.payment.fiserv; +package io.snabble.sdk.ui.payment.creditcard.fiserv; import android.annotation.SuppressLint; import android.app.Activity; @@ -36,8 +36,8 @@ import io.snabble.sdk.ui.Keyguard; import io.snabble.sdk.ui.R; import io.snabble.sdk.ui.SnabbleUI; -import io.snabble.sdk.ui.payment.creditcard.data.CreditCardInfo; -import io.snabble.sdk.ui.payment.creditcard.data.SnabbleCreditCardUrlCreator; +import io.snabble.sdk.ui.payment.creditcard.fiserv.data.CreditCardInfo; +import io.snabble.sdk.ui.payment.creditcard.fiserv.data.SnabbleCreditCardUrlCreator; import io.snabble.sdk.ui.telemetry.Telemetry; import io.snabble.sdk.ui.utils.UIUtils; import io.snabble.sdk.utils.Dispatch; diff --git a/ui/src/main/java/io/snabble/sdk/ui/payment/fiserv/FiservViewModel.kt b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/fiserv/FiservViewModel.kt similarity index 82% rename from ui/src/main/java/io/snabble/sdk/ui/payment/fiserv/FiservViewModel.kt rename to ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/fiserv/FiservViewModel.kt index 39ff761eeb..82afbb8075 100644 --- a/ui/src/main/java/io/snabble/sdk/ui/payment/fiserv/FiservViewModel.kt +++ b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/fiserv/FiservViewModel.kt @@ -1,13 +1,13 @@ -package io.snabble.sdk.ui.payment.fiserv +package io.snabble.sdk.ui.payment.creditcard.fiserv import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import io.snabble.sdk.PaymentMethod -import io.snabble.sdk.ui.payment.fiserv.domain.CountryItemsRepository -import io.snabble.sdk.ui.payment.fiserv.domain.CustomerInfo -import io.snabble.sdk.ui.payment.fiserv.domain.FiservRepository -import io.snabble.sdk.ui.payment.fiserv.domain.model.country.CountryItem +import io.snabble.sdk.ui.payment.creditcard.fiserv.domain.FiservRepository +import io.snabble.sdk.ui.payment.creditcard.shared.country.domain.CountryItemsRepository +import io.snabble.sdk.ui.payment.creditcard.shared.country.domain.models.CountryItem +import io.snabble.sdk.ui.payment.creditcard.shared.country.domain.models.CustomerInfo import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow diff --git a/ui/src/main/java/io/snabble/sdk/ui/payment/fiserv/FiservViewModelFactory.kt b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/fiserv/FiservViewModelFactory.kt similarity index 70% rename from ui/src/main/java/io/snabble/sdk/ui/payment/fiserv/FiservViewModelFactory.kt rename to ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/fiserv/FiservViewModelFactory.kt index d43bc4e93c..29524c6594 100644 --- a/ui/src/main/java/io/snabble/sdk/ui/payment/fiserv/FiservViewModelFactory.kt +++ b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/fiserv/FiservViewModelFactory.kt @@ -1,16 +1,16 @@ -package io.snabble.sdk.ui.payment.fiserv +package io.snabble.sdk.ui.payment.creditcard.fiserv import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.createSavedStateHandle import androidx.lifecycle.viewmodel.CreationExtras -import io.snabble.sdk.ui.payment.fiserv.data.CountryItemsRepositoryImpl -import io.snabble.sdk.ui.payment.fiserv.data.FiservRepositoryImpl -import io.snabble.sdk.ui.payment.fiserv.data.country.LocalCountryItemsDataSourceImpl +import io.snabble.sdk.ui.payment.creditcard.fiserv.data.FiservRepositoryImpl +import io.snabble.sdk.ui.payment.creditcard.shared.country.data.CountryItemsRepositoryImpl +import io.snabble.sdk.ui.payment.creditcard.shared.country.data.source.LocalCountryItemsDataSourceImpl import io.snabble.sdk.utils.GsonHolder -class FiservViewModelFactory(private val context: Context) : ViewModelProvider.Factory { +internal class FiservViewModelFactory(private val context: Context) : ViewModelProvider.Factory { override fun create(modelClass: Class, extras: CreationExtras): T { if (!modelClass.isAssignableFrom(FiservViewModel::class.java)) { diff --git a/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/data/CreditCardInfo.kt b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/fiserv/data/CreditCardInfo.kt similarity index 90% rename from ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/data/CreditCardInfo.kt rename to ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/fiserv/data/CreditCardInfo.kt index 20e7292884..573891bd0f 100644 --- a/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/data/CreditCardInfo.kt +++ b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/fiserv/data/CreditCardInfo.kt @@ -1,8 +1,7 @@ -package io.snabble.sdk.ui.payment.creditcard.data +package io.snabble.sdk.ui.payment.creditcard.fiserv.data import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json @Serializable diff --git a/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/data/CreditCardUrlBuilder.kt b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/fiserv/data/CreditCardUrlBuilder.kt similarity index 94% rename from ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/data/CreditCardUrlBuilder.kt rename to ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/fiserv/data/CreditCardUrlBuilder.kt index 2033b74c20..d95beaaf82 100644 --- a/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/data/CreditCardUrlBuilder.kt +++ b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/fiserv/data/CreditCardUrlBuilder.kt @@ -1,5 +1,6 @@ @file:JvmName("SnabbleCreditCardUrlCreator") -package io.snabble.sdk.ui.payment.creditcard.data + +package io.snabble.sdk.ui.payment.creditcard.fiserv.data import android.net.Uri import io.snabble.sdk.PaymentMethod diff --git a/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/fiserv/data/FiservRemoteDataSource.kt b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/fiserv/data/FiservRemoteDataSource.kt new file mode 100644 index 0000000000..527a9e4b8d --- /dev/null +++ b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/fiserv/data/FiservRemoteDataSource.kt @@ -0,0 +1,49 @@ +package io.snabble.sdk.ui.payment.creditcard.fiserv.data + +import com.google.gson.Gson +import io.snabble.sdk.PaymentMethod +import io.snabble.sdk.Snabble +import io.snabble.sdk.ui.payment.creditcard.fiserv.data.dto.AuthDataDto +import io.snabble.sdk.ui.payment.creditcard.fiserv.data.dto.CustomerInfoDto +import io.snabble.sdk.ui.payment.creditcard.shared.getTokenizationUrlFor +import io.snabble.sdk.ui.payment.creditcard.shared.post +import io.snabble.sdk.utils.GsonHolder +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.Request +import okhttp3.RequestBody +import okhttp3.RequestBody.Companion.toRequestBody + +internal interface FiservRemoteDataSource { + + suspend fun sendUserData( + customerInfo: CustomerInfoDto, + paymentMethod: PaymentMethod + ): Result +} + +internal class FiservRemoteDataSourceImpl( + private val snabble: Snabble = Snabble, + private val gson: Gson = GsonHolder.get(), +) : FiservRemoteDataSource { + + override suspend fun sendUserData( + customerInfo: CustomerInfoDto, + paymentMethod: PaymentMethod + ): Result { + val project = + snabble.checkedInProject.value ?: return Result.failure(Exception("Missing projectId")) + + val customerInfoPostUrl = + project.paymentMethodDescriptors.getTokenizationUrlFor(paymentMethod) + ?: return Result.failure(Exception("Missing link to send customer info to")) + + val requestBody: RequestBody = + gson.toJson(customerInfo).toRequestBody("application/json".toMediaType()) + val request: Request = Request.Builder() + .url(customerInfoPostUrl) + .post(requestBody) + .build() + + return project.okHttpClient.post(request, gson) + } +} diff --git a/ui/src/main/java/io/snabble/sdk/ui/payment/fiserv/data/FiservRepositoryImpl.kt b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/fiserv/data/FiservRepositoryImpl.kt similarity index 50% rename from ui/src/main/java/io/snabble/sdk/ui/payment/fiserv/data/FiservRepositoryImpl.kt rename to ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/fiserv/data/FiservRepositoryImpl.kt index ea5339e0fc..2d4cba6208 100644 --- a/ui/src/main/java/io/snabble/sdk/ui/payment/fiserv/data/FiservRepositoryImpl.kt +++ b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/fiserv/data/FiservRepositoryImpl.kt @@ -1,8 +1,11 @@ -package io.snabble.sdk.ui.payment.fiserv.data +package io.snabble.sdk.ui.payment.creditcard.fiserv.data import io.snabble.sdk.PaymentMethod -import io.snabble.sdk.ui.payment.fiserv.domain.CustomerInfo -import io.snabble.sdk.ui.payment.fiserv.domain.FiservRepository +import io.snabble.sdk.ui.payment.creditcard.fiserv.data.dto.AddressDto +import io.snabble.sdk.ui.payment.creditcard.fiserv.data.dto.CustomerInfoDto +import io.snabble.sdk.ui.payment.creditcard.fiserv.domain.FiservRepository +import io.snabble.sdk.ui.payment.creditcard.fiserv.domain.model.AuthData +import io.snabble.sdk.ui.payment.creditcard.shared.country.domain.models.CustomerInfo internal class FiservRepositoryImpl( private val remoteDataSource: FiservRemoteDataSource = FiservRemoteDataSourceImpl() @@ -11,26 +14,21 @@ internal class FiservRepositoryImpl( override suspend fun sendUserData( customerInfo: CustomerInfo, paymentMethod: PaymentMethod - ): Result = + ): Result = remoteDataSource .sendUserData(customerInfo.toDto(), paymentMethod) - .map { FiservCardRegisterUrls(it.links.formUrl.href, it.links.deleteUrl.href) } + .map { AuthData(it.links.formUrl.href, it.links.deleteUrl.href) } } private fun CustomerInfo.toDto() = CustomerInfoDto( name = name, - phoneNumber = phoneNumber, + phoneNumber = "$intCallingCode$phoneNumber", email = email, address = AddressDto( street = address.street, zip = address.zip, city = address.city, state = address.state.ifEmpty { null }, - country = address.country + country = address.country.code ), ) - -data class FiservCardRegisterUrls( - val formUrl: String, - val preAuthDeleteUrl: String -) diff --git a/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/fiserv/data/dto/AuthDataDto.kt b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/fiserv/data/dto/AuthDataDto.kt new file mode 100644 index 0000000000..c5110407d3 --- /dev/null +++ b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/fiserv/data/dto/AuthDataDto.kt @@ -0,0 +1,16 @@ +package io.snabble.sdk.ui.payment.creditcard.fiserv.data.dto + +import com.google.gson.annotations.SerializedName + +internal data class AuthDataDto( + @SerializedName("links") val links: LinksDto +) + +internal data class LinksDto( + @SerializedName("self") val deleteUrl: LinkDto, + @SerializedName("tokenizationForm") val formUrl: LinkDto +) + +internal data class LinkDto( + @SerializedName("href") val href: String +) diff --git a/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/fiserv/data/dto/CustomerInfoDto.kt b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/fiserv/data/dto/CustomerInfoDto.kt new file mode 100644 index 0000000000..a911cedc14 --- /dev/null +++ b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/fiserv/data/dto/CustomerInfoDto.kt @@ -0,0 +1,18 @@ +package io.snabble.sdk.ui.payment.creditcard.fiserv.data.dto + +import com.google.gson.annotations.SerializedName + +internal data class CustomerInfoDto( + @SerializedName("name") val name: String, + @SerializedName("phoneNumber") val phoneNumber: String, + @SerializedName("email") val email: String, + @SerializedName("address") val address: AddressDto, +) + +internal data class AddressDto( + @SerializedName("street") val street: String, + @SerializedName("zip") val zip: String, + @SerializedName("city") val city: String, + @SerializedName("state") val state: String?, + @SerializedName("country") val country: String +) diff --git a/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/fiserv/domain/FiservRepository.kt b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/fiserv/domain/FiservRepository.kt new file mode 100644 index 0000000000..c8da727488 --- /dev/null +++ b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/fiserv/domain/FiservRepository.kt @@ -0,0 +1,13 @@ +package io.snabble.sdk.ui.payment.creditcard.fiserv.domain + +import io.snabble.sdk.PaymentMethod +import io.snabble.sdk.ui.payment.creditcard.fiserv.domain.model.AuthData +import io.snabble.sdk.ui.payment.creditcard.shared.country.domain.models.CustomerInfo + +internal interface FiservRepository { + + suspend fun sendUserData( + customerInfo: CustomerInfo, + paymentMethod: PaymentMethod + ): Result +} diff --git a/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/fiserv/domain/model/AuthData.kt b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/fiserv/domain/model/AuthData.kt new file mode 100644 index 0000000000..fadbbe78a5 --- /dev/null +++ b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/fiserv/domain/model/AuthData.kt @@ -0,0 +1,6 @@ +package io.snabble.sdk.ui.payment.creditcard.fiserv.domain.model + +data class AuthData( + val formUrl: String, + val preAuthDeleteUrl: String +) diff --git a/ui/src/main/java/io/snabble/sdk/ui/payment/fiserv/CustomerInfoInputScreen.kt b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/shared/CustomerInfoInputScreen.kt similarity index 74% rename from ui/src/main/java/io/snabble/sdk/ui/payment/fiserv/CustomerInfoInputScreen.kt rename to ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/shared/CustomerInfoInputScreen.kt index 6757a2147e..9045bc2c23 100644 --- a/ui/src/main/java/io/snabble/sdk/ui/payment/fiserv/CustomerInfoInputScreen.kt +++ b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/shared/CustomerInfoInputScreen.kt @@ -1,4 +1,4 @@ -package io.snabble.sdk.ui.payment.fiserv +package io.snabble.sdk.ui.payment.creditcard.shared import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Arrangement @@ -23,15 +23,17 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardCapitalization -import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import io.snabble.sdk.ui.R import io.snabble.sdk.ui.cart.shoppingcart.utils.rememberTextFieldManager -import io.snabble.sdk.ui.payment.fiserv.domain.Address -import io.snabble.sdk.ui.payment.fiserv.domain.CustomerInfo -import io.snabble.sdk.ui.payment.fiserv.domain.model.country.CountryItem -import io.snabble.sdk.ui.payment.fiserv.widget.CountrySelectionMenu -import io.snabble.sdk.ui.payment.fiserv.widget.TextInput +import io.snabble.sdk.ui.payment.creditcard.shared.country.displayName +import io.snabble.sdk.ui.payment.creditcard.shared.country.domain.models.Address +import io.snabble.sdk.ui.payment.creditcard.shared.country.domain.models.CountryItem +import io.snabble.sdk.ui.payment.creditcard.shared.country.domain.models.CustomerInfo +import io.snabble.sdk.ui.payment.creditcard.shared.country.ui.CountrySelectionMenu +import io.snabble.sdk.ui.payment.creditcard.shared.widget.PhoneNumberInput +import io.snabble.sdk.ui.payment.creditcard.shared.widget.TextInput +import java.util.Locale @Composable internal fun CustomerInfoInputScreen( @@ -39,28 +41,38 @@ internal fun CustomerInfoInputScreen( onErrorProcessed: () -> Unit, showError: Boolean, isLoading: Boolean, - countryItems: List?, + countryItems: List, onBackNavigationClick: () -> Unit, ) { var name by remember { mutableStateOf("") } + var intCallingCode by remember { mutableStateOf("") } var phoneNumber by remember { mutableStateOf("") } var email by remember { mutableStateOf("") } var street by remember { mutableStateOf("") } var zip by remember { mutableStateOf("") } var city by remember { mutableStateOf("") } var state by remember { mutableStateOf("") } - var country by remember { mutableStateOf("") } + var country: CountryItem by remember { mutableStateOf(countryItems.loadDefaultCountry()) } val textFieldManager = rememberTextFieldManager() val isRequiredStateSet = - if (!countryItems?.firstOrNull { it.code == country }?.stateItems.isNullOrEmpty()) state.isNotEmpty() else true - val areRequiredFieldsSet = - listOf(name, phoneNumber, email, street, zip, city, country).all { it.isNotEmpty() } && isRequiredStateSet + if (!countryItems.firstOrNull { it.code == country.code }?.stateItems.isNullOrEmpty()) state.isNotEmpty() else true + val areRequiredFieldsSet = listOf( + name, + intCallingCode, + phoneNumber, + email, + street, + zip, + city, + country.code + ).all { it.isNotEmpty() } && isRequiredStateSet val createCustomerInfo: () -> CustomerInfo = { CustomerInfo( name = name, + intCallingCode = intCallingCode, phoneNumber = phoneNumber, email = email, address = Address( @@ -89,21 +101,13 @@ internal fun CustomerInfoInputScreen( onNext = { textFieldManager.moveFocusToNext() } ) ) - TextInput( - modifier = Modifier.fillMaxWidth(), - value = phoneNumber, - onValueChanged = { - phoneNumber = it - if (showError) onErrorProcessed() - }, - label = stringResource(R.string.Snabble_Payment_CustomerInfo_phoneNumber), - keyboardActions = KeyboardActions( - onNext = { textFieldManager.moveFocusToNext() } - ), - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Phone, - imeAction = ImeAction.Next - ) + PhoneNumberInput( + callingCode = intCallingCode, + onCallingCodeChanged = { callingCode -> intCallingCode = callingCode }, + phoneNumber = phoneNumber, + onPhoneNumberChanged = { number -> phoneNumber = number }, + onKeyboardAction = { textFieldManager.moveFocusToNext() }, + onErrorProcessed = onErrorProcessed, ) TextInput( modifier = Modifier.fillMaxWidth(), @@ -163,8 +167,8 @@ internal fun CustomerInfoInputScreen( countryItems = countryItems, selectedCountryCode = country, selectedStateCode = null, - onCountrySelected = { (_, countryCode), stateItem -> - country = countryCode + onCountrySelected = { countryItem, stateItem -> + country = countryItem state = stateItem?.code.orEmpty() if (showError) onErrorProcessed() } @@ -198,3 +202,13 @@ internal fun CustomerInfoInputScreen( } } } + +private fun List?.loadDefaultCountry(): CountryItem = + this?.firstOrNull { it.displayName == Locale.getDefault().country.displayName } + ?: this?.firstOrNull { it.code == Locale.GERMANY.displayCountry } + ?: CountryItem( + displayName = Locale.GERMANY.country.displayName, + code = Locale.GERMANY.country, + numericCode = "", + stateItems = null + ) diff --git a/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/shared/Extensions.kt b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/shared/Extensions.kt new file mode 100644 index 0000000000..4061601d52 --- /dev/null +++ b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/shared/Extensions.kt @@ -0,0 +1,60 @@ +package io.snabble.sdk.ui.payment.creditcard.shared + +import android.util.Log +import com.google.gson.Gson +import com.google.gson.JsonSyntaxException +import com.google.gson.reflect.TypeToken +import io.snabble.sdk.PaymentMethod +import io.snabble.sdk.PaymentMethodDescriptor +import io.snabble.sdk.Snabble +import okhttp3.Call +import okhttp3.Callback +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import java.io.IOException +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +internal fun List.getTokenizationUrlFor(paymentMethod: PaymentMethod): String? = + firstOrNull { it.paymentMethod == paymentMethod } + ?.links + ?.get("tokenization") + ?.href + ?.let(Snabble::absoluteUrl) + +internal suspend inline fun OkHttpClient.post(request: Request, gson: Gson) = suspendCoroutine> { + newCall(request).enqueue(object : Callback { + + override fun onResponse(call: Call, response: Response) { + when { + response.isSuccessful -> { + val body = response.body?.string() + val typeToken = object : TypeToken() {}.type + val data: T? = try { + gson.fromJson(body, typeToken) + } catch (e: JsonSyntaxException) { + Log.e("Payment", "Error parsing pre-registration response", e) + null + } + + val result = if (data == null) { + Result.failure(Exception("Missing content")) + } else { + Result.success(data) + } + it.resume(result) + } + + else -> { + response.body?.string() + it.resume(Result.failure(Exception(response.message))) + } + } + } + + override fun onFailure(call: Call, e: IOException) { + it.resume(Result.failure(e)) + } + }) +} diff --git a/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/shared/country/DisplayNameHelper.kt b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/shared/country/DisplayNameHelper.kt new file mode 100644 index 0000000000..b93608cf1c --- /dev/null +++ b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/shared/country/DisplayNameHelper.kt @@ -0,0 +1,6 @@ +package io.snabble.sdk.ui.payment.creditcard.shared.country + +import java.util.Locale + +internal val String.displayName: String + get() = Locale("", this).displayName diff --git a/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/shared/country/data/CountryItemsRepositoryImpl.kt b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/shared/country/data/CountryItemsRepositoryImpl.kt new file mode 100644 index 0000000000..9a82072ccd --- /dev/null +++ b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/shared/country/data/CountryItemsRepositoryImpl.kt @@ -0,0 +1,12 @@ +package io.snabble.sdk.ui.payment.creditcard.shared.country.data + +import io.snabble.sdk.ui.payment.creditcard.shared.country.data.source.LocalCountryItemsDataSource +import io.snabble.sdk.ui.payment.creditcard.shared.country.domain.CountryItemsRepository +import io.snabble.sdk.ui.payment.creditcard.shared.country.domain.models.CountryItem + +internal class CountryItemsRepositoryImpl( + private val localCountryItemsDataSource: LocalCountryItemsDataSource, +) : CountryItemsRepository { + + override fun loadCountryItems(): List = localCountryItemsDataSource.loadCountries() +} diff --git a/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/shared/country/data/dto/CountryDto.kt b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/shared/country/data/dto/CountryDto.kt new file mode 100644 index 0000000000..f5572377fd --- /dev/null +++ b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/shared/country/data/dto/CountryDto.kt @@ -0,0 +1,9 @@ +package io.snabble.sdk.ui.payment.creditcard.shared.country.data.dto + +import com.google.gson.annotations.SerializedName + +internal data class CountryDto( + @SerializedName("code") val countryCode: String, + @SerializedName("states") val states: List?, + @SerializedName("numeric") val numericCode: String, +) diff --git a/ui/src/main/java/io/snabble/sdk/ui/payment/fiserv/data/dto/country/StateDto.kt b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/shared/country/data/dto/StateDto.kt similarity index 70% rename from ui/src/main/java/io/snabble/sdk/ui/payment/fiserv/data/dto/country/StateDto.kt rename to ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/shared/country/data/dto/StateDto.kt index 333651b935..0bca4189aa 100644 --- a/ui/src/main/java/io/snabble/sdk/ui/payment/fiserv/data/dto/country/StateDto.kt +++ b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/shared/country/data/dto/StateDto.kt @@ -1,4 +1,4 @@ -package io.snabble.sdk.ui.payment.fiserv.data.dto.country +package io.snabble.sdk.ui.payment.creditcard.shared.country.data.dto import com.google.gson.annotations.SerializedName diff --git a/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/shared/country/data/source/LocalCountryItemsDataSource.kt b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/shared/country/data/source/LocalCountryItemsDataSource.kt new file mode 100644 index 0000000000..4abc40870c --- /dev/null +++ b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/shared/country/data/source/LocalCountryItemsDataSource.kt @@ -0,0 +1,8 @@ +package io.snabble.sdk.ui.payment.creditcard.shared.country.data.source + +import io.snabble.sdk.ui.payment.creditcard.shared.country.domain.models.CountryItem + +internal interface LocalCountryItemsDataSource { + + fun loadCountries(): List +} diff --git a/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/shared/country/data/source/LocalCountryItemsDataSourceImpl.kt b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/shared/country/data/source/LocalCountryItemsDataSourceImpl.kt new file mode 100644 index 0000000000..430dc9f693 --- /dev/null +++ b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/shared/country/data/source/LocalCountryItemsDataSourceImpl.kt @@ -0,0 +1,38 @@ +package io.snabble.sdk.ui.payment.creditcard.shared.country.data.source + +import android.content.res.AssetManager +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import io.snabble.sdk.ui.payment.creditcard.shared.country.data.dto.CountryDto +import io.snabble.sdk.ui.payment.creditcard.shared.country.displayName +import io.snabble.sdk.ui.payment.creditcard.shared.country.domain.models.CountryItem +import io.snabble.sdk.ui.payment.creditcard.shared.country.domain.models.StateItem +import java.io.InputStreamReader + +internal class LocalCountryItemsDataSourceImpl( + private val assetManager: AssetManager, + private val gson: Gson, +) : LocalCountryItemsDataSource { + + override fun loadCountries(): List { + val jsonFileReader: InputStreamReader = + assetManager.open(COUNTRIES_AND_STATES_FILE).reader() + val type = object : TypeToken>() {}.type + return gson + .fromJson>(jsonFileReader, type) + .map { + CountryItem( + displayName = it.countryCode.displayName, + code = it.countryCode, + numericCode = it.numericCode, + stateItems = it.states?.map(StateItem.Companion::from) + ) + } + .sortedBy { it.displayName } + } + + companion object { + + private const val COUNTRIES_AND_STATES_FILE = "countriesAndStates.json" + } +} diff --git a/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/shared/country/domain/CountryItemsRepository.kt b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/shared/country/domain/CountryItemsRepository.kt new file mode 100644 index 0000000000..4aa4ad7a10 --- /dev/null +++ b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/shared/country/domain/CountryItemsRepository.kt @@ -0,0 +1,8 @@ +package io.snabble.sdk.ui.payment.creditcard.shared.country.domain + +import io.snabble.sdk.ui.payment.creditcard.shared.country.domain.models.CountryItem + +internal interface CountryItemsRepository { + + fun loadCountryItems(): List +} diff --git a/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/shared/country/domain/models/CountryItem.kt b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/shared/country/domain/models/CountryItem.kt new file mode 100644 index 0000000000..8ddf8b128a --- /dev/null +++ b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/shared/country/domain/models/CountryItem.kt @@ -0,0 +1,8 @@ +package io.snabble.sdk.ui.payment.creditcard.shared.country.domain.models + +internal data class CountryItem( + val displayName: String, + val code: String, + val numericCode: String, + val stateItems: List? +) diff --git a/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/shared/country/domain/models/CustomerInfo.kt b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/shared/country/domain/models/CustomerInfo.kt new file mode 100644 index 0000000000..b301a6d149 --- /dev/null +++ b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/shared/country/domain/models/CustomerInfo.kt @@ -0,0 +1,17 @@ +package io.snabble.sdk.ui.payment.creditcard.shared.country.domain.models + +internal data class CustomerInfo( + val name: String, + val intCallingCode: String, + val phoneNumber: String, + val email: String, + val address: Address, +) + +internal data class Address( + val street: String, + val zip: String, + val city: String, + val state: String, + val country: CountryItem +) diff --git a/ui/src/main/java/io/snabble/sdk/ui/payment/fiserv/domain/model/country/StateItem.kt b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/shared/country/domain/models/StateItem.kt similarity index 62% rename from ui/src/main/java/io/snabble/sdk/ui/payment/fiserv/domain/model/country/StateItem.kt rename to ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/shared/country/domain/models/StateItem.kt index ea2fe103dd..15376650ea 100644 --- a/ui/src/main/java/io/snabble/sdk/ui/payment/fiserv/domain/model/country/StateItem.kt +++ b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/shared/country/domain/models/StateItem.kt @@ -1,8 +1,9 @@ -package io.snabble.sdk.ui.payment.fiserv.domain.model.country +package io.snabble.sdk.ui.payment.creditcard.shared.country.domain.models -import io.snabble.sdk.ui.payment.fiserv.data.dto.country.StateDto +import io.snabble.sdk.ui.payment.creditcard.shared.country.data.dto.StateDto internal data class StateItem(val displayName: String, val code: String) { + companion object { fun from(stateDto: StateDto) = StateItem( diff --git a/ui/src/main/java/io/snabble/sdk/ui/payment/fiserv/widget/CountrySelectionMenu.kt b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/shared/country/ui/CountrySelectionMenu.kt similarity index 76% rename from ui/src/main/java/io/snabble/sdk/ui/payment/fiserv/widget/CountrySelectionMenu.kt rename to ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/shared/country/ui/CountrySelectionMenu.kt index 73146a94c8..a63a569022 100644 --- a/ui/src/main/java/io/snabble/sdk/ui/payment/fiserv/widget/CountrySelectionMenu.kt +++ b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/shared/country/ui/CountrySelectionMenu.kt @@ -1,4 +1,4 @@ -package io.snabble.sdk.ui.payment.fiserv.widget +package io.snabble.sdk.ui.payment.creditcard.shared.country.ui import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.MaterialTheme @@ -11,17 +11,15 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.sp -import io.snabble.sdk.ui.payment.fiserv.domain.model.country.StateItem import io.snabble.sdk.ui.R -import io.snabble.sdk.ui.payment.fiserv.data.country.displayName -import io.snabble.sdk.ui.payment.fiserv.domain.model.country.CountryItem -import java.util.Locale +import io.snabble.sdk.ui.payment.creditcard.shared.country.domain.models.CountryItem +import io.snabble.sdk.ui.payment.creditcard.shared.country.domain.models.StateItem @Composable internal fun CountrySelectionMenu( modifier: Modifier = Modifier, - countryItems: List?, - selectedCountryCode: String? = null, + countryItems: List, + selectedCountryCode: CountryItem, selectedStateCode: String? = null, onCountrySelected: (CountryItem, StateItem?) -> Unit, ) { @@ -31,13 +29,7 @@ internal fun CountrySelectionMenu( var showStateList by remember { mutableStateOf(false) } var dismissStateList by remember { mutableStateOf(true) } - var currentCountryItem by remember { - mutableStateOf( - selectedCountryCode?.let { countryCode -> countryItems?.firstOrNull { it.code == countryCode } } - ?: countryItems.loadDefaultCountry() - .also { country -> onCountrySelected(country, null) } - ) - } + var currentCountryItem by remember { mutableStateOf(selectedCountryCode) } var currentStateItem by remember { mutableStateOf( @@ -112,12 +104,3 @@ internal fun CountrySelectionMenu( } } } - -private fun List?.loadDefaultCountry(): CountryItem = - this?.firstOrNull { it.displayName == Locale.getDefault().country.displayName } - ?: this?.firstOrNull { it.code == Locale.GERMANY.displayCountry } - ?: CountryItem( - displayName = Locale.GERMANY.country.displayName, - code = Locale.GERMANY.country, - stateItems = null - ) diff --git a/ui/src/main/java/io/snabble/sdk/ui/payment/fiserv/widget/DropDownMenu.kt b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/shared/country/ui/DropDownMenu.kt similarity index 98% rename from ui/src/main/java/io/snabble/sdk/ui/payment/fiserv/widget/DropDownMenu.kt rename to ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/shared/country/ui/DropDownMenu.kt index 25a2c425cc..af9ce8093e 100644 --- a/ui/src/main/java/io/snabble/sdk/ui/payment/fiserv/widget/DropDownMenu.kt +++ b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/shared/country/ui/DropDownMenu.kt @@ -1,4 +1,4 @@ -package io.snabble.sdk.ui.payment.fiserv.widget +package io.snabble.sdk.ui.payment.creditcard.shared.country.ui import androidx.compose.foundation.background import androidx.compose.foundation.clickable diff --git a/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/shared/country/ui/OutlinedCountryCodeField.kt b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/shared/country/ui/OutlinedCountryCodeField.kt new file mode 100644 index 0000000000..e0da3a8f45 --- /dev/null +++ b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/shared/country/ui/OutlinedCountryCodeField.kt @@ -0,0 +1,49 @@ +package io.snabble.sdk.ui.payment.creditcard.shared.country.ui + +import androidx.compose.foundation.gestures.awaitEachGesture +import androidx.compose.foundation.gestures.awaitFirstDown +import androidx.compose.foundation.gestures.waitForUpOrCancellation +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.PointerEventPass +import androidx.compose.ui.input.pointer.pointerInput +import io.snabble.sdk.ui.payment.creditcard.shared.widget.defaultColors + +@Composable +internal fun OutlinedCountryCodeField( + modifier: Modifier = Modifier, + label: String, + countryCode: String, + onClick: () -> Unit = {}, + enabled: Boolean = true, + isError: Boolean = false, +) { + OutlinedTextField( + modifier = modifier + .fillMaxWidth() + .pointerInput(key1 = enabled) { + if (!enabled) return@pointerInput + awaitEachGesture { + awaitFirstDown(pass = PointerEventPass.Initial) + waitForUpOrCancellation(pass = PointerEventPass.Initial)?.let { + onClick() + } + } + }, + readOnly = true, + value = countryCode, + onValueChange = { }, + textStyle = MaterialTheme.typography.bodyLarge, + label = { Text(text = label) }, + maxLines = 1, + singleLine = true, + enabled = enabled, + isError = isError, + colors = TextFieldDefaults.defaultColors() + ) +} diff --git a/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/shared/widget/PhoneNumberInput.kt b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/shared/widget/PhoneNumberInput.kt new file mode 100644 index 0000000000..e07a34e93f --- /dev/null +++ b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/shared/widget/PhoneNumberInput.kt @@ -0,0 +1,137 @@ +package io.snabble.sdk.ui.payment.creditcard.shared.widget + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import io.snabble.countrycodepicker.ui.CountryCodePickerDialog +import io.snabble.sdk.ui.R +import io.snabble.sdk.ui.payment.creditcard.shared.country.ui.OutlinedCountryCodeField + +@Composable +internal fun PhoneNumberInput( + modifier: Modifier = Modifier, + callingCode: String, + onCallingCodeChanged: (String) -> Unit, + enableCallingCodePicking: Boolean = true, + phoneNumber: String, + onPhoneNumberChanged: (String) -> Unit, + enablePhoneNumberInput: Boolean = true, + readOnlyPhoneNumberInput: Boolean = false, + keyboardAction: ImeAction = ImeAction.Next, + onKeyboardAction: () -> Unit, + focusPhoneNumberInput: Boolean = false, + errorMessage: String? = null, + onErrorProcessed: () -> Unit, +) { + var countryCode by rememberSaveable { mutableStateOf("") } + var showCodePicker by rememberSaveable { mutableStateOf(false) } + + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + val phoneNumberFocusRequester = remember { FocusRequester() } + CountryCodePickerDialog( + initialCallingCode = callingCode, + showDialog = showCodePicker, + // Called initially, so that the country code should never be empty + onCountrySelected = { callingCode, flagEmoji -> + countryCode = "$flagEmoji $callingCode" + onCallingCodeChanged(callingCode) + showCodePicker = false + }, + onDismissRequest = { + showCodePicker = false + if (phoneNumber.isBlank()) phoneNumberFocusRequester.requestFocus() + } + ) + + val callingCodeFocusRequester = remember { FocusRequester() } + if (showCodePicker) callingCodeFocusRequester.requestFocus() + OutlinedCountryCodeField( + modifier = Modifier + .widthIn(max = 120.dp) + .focusRequester(callingCodeFocusRequester) + .onFocusChanged { if (errorMessage != null) onErrorProcessed() }, + label = stringResource(id = R.string.Snabble_Payment_CustomerInfo_intCallingCode), + countryCode = countryCode, + onClick = { showCodePicker = true }, + enabled = enableCallingCodePicking, + isError = errorMessage != null + ) + + OutlinedTextField( + modifier = Modifier + .fillMaxWidth() + .focusRequester(phoneNumberFocusRequester) + .onFocusChanged { + if (errorMessage != null) onErrorProcessed() + }, + value = phoneNumber, + onValueChange = { onPhoneNumberChanged(it) }, + textStyle = MaterialTheme.typography.bodyLarge, + label = { + Text( + text = stringResource(id = R.string.Snabble_Payment_CustomerInfo_phoneNumber), + fontSize = 17.sp, + ) + }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Phone, + imeAction = keyboardAction + ), + keyboardActions = KeyboardActions( + onSend = { + onKeyboardAction() + } + ), + isError = errorMessage != null, + maxLines = 1, + singleLine = true, + enabled = enablePhoneNumberInput, + readOnly = readOnlyPhoneNumberInput, + colors = TextFieldDefaults.defaultColors() + ) + if (focusPhoneNumberInput) phoneNumberFocusRequester.requestFocus() + } + if (errorMessage != null) { + Text( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 8.dp), + text = errorMessage, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.error + ) + } + } +} diff --git a/ui/src/main/java/io/snabble/sdk/ui/payment/fiserv/widget/TextInput.kt b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/shared/widget/TextInput.kt similarity index 96% rename from ui/src/main/java/io/snabble/sdk/ui/payment/fiserv/widget/TextInput.kt rename to ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/shared/widget/TextInput.kt index 5f5a40ccbc..0de64fa6f3 100644 --- a/ui/src/main/java/io/snabble/sdk/ui/payment/fiserv/widget/TextInput.kt +++ b/ui/src/main/java/io/snabble/sdk/ui/payment/creditcard/shared/widget/TextInput.kt @@ -1,4 +1,4 @@ -package io.snabble.sdk.ui.payment.fiserv.widget +package io.snabble.sdk.ui.payment.creditcard.shared.widget import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -64,7 +64,7 @@ internal fun TextInput( } @Composable -private fun TextFieldDefaults.defaultColors() = colors( +internal fun TextFieldDefaults.defaultColors() = colors( focusedContainerColor = MaterialTheme.colorScheme.background, unfocusedContainerColor = MaterialTheme.colorScheme.background, focusedIndicatorColor = MaterialTheme.colorScheme.primary, diff --git a/ui/src/main/java/io/snabble/sdk/ui/payment/fiserv/data/CountryItemsRepositoryImpl.kt b/ui/src/main/java/io/snabble/sdk/ui/payment/fiserv/data/CountryItemsRepositoryImpl.kt deleted file mode 100644 index d0c153bb93..0000000000 --- a/ui/src/main/java/io/snabble/sdk/ui/payment/fiserv/data/CountryItemsRepositoryImpl.kt +++ /dev/null @@ -1,12 +0,0 @@ -package io.snabble.sdk.ui.payment.fiserv.data - -import io.snabble.sdk.ui.payment.fiserv.data.country.LocalCountryItemsDataSource -import io.snabble.sdk.ui.payment.fiserv.domain.CountryItemsRepository -import io.snabble.sdk.ui.payment.fiserv.domain.model.country.CountryItem - -internal class CountryItemsRepositoryImpl( - private val localCountryItemsDataSource: LocalCountryItemsDataSource, -) : CountryItemsRepository { - - override fun loadCountryItems(): List = localCountryItemsDataSource.loadCountries() -} diff --git a/ui/src/main/java/io/snabble/sdk/ui/payment/fiserv/data/CustomerInfoDto.kt b/ui/src/main/java/io/snabble/sdk/ui/payment/fiserv/data/CustomerInfoDto.kt deleted file mode 100644 index 312cd1c2cf..0000000000 --- a/ui/src/main/java/io/snabble/sdk/ui/payment/fiserv/data/CustomerInfoDto.kt +++ /dev/null @@ -1,21 +0,0 @@ -package io.snabble.sdk.ui.payment.fiserv.data - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -internal data class CustomerInfoDto( - @SerialName("name") val name: String, - @SerialName("phoneNumber") val phoneNumber: String, - @SerialName("email") val email: String, - @SerialName("address") val address: AddressDto, -) - -@Serializable -internal data class AddressDto( - @SerialName("street") val street: String, - @SerialName("zip") val zip: String, - @SerialName("city") val city: String, - @SerialName("state") val state: String?, - @SerialName("country") val country: String -) diff --git a/ui/src/main/java/io/snabble/sdk/ui/payment/fiserv/data/FiservRemoteDataSource.kt b/ui/src/main/java/io/snabble/sdk/ui/payment/fiserv/data/FiservRemoteDataSource.kt deleted file mode 100644 index f449ba715c..0000000000 --- a/ui/src/main/java/io/snabble/sdk/ui/payment/fiserv/data/FiservRemoteDataSource.kt +++ /dev/null @@ -1,99 +0,0 @@ -package io.snabble.sdk.ui.payment.fiserv.data - -import android.util.Log -import com.google.gson.Gson -import com.google.gson.JsonSyntaxException -import com.google.gson.annotations.SerializedName -import io.snabble.sdk.PaymentMethod -import io.snabble.sdk.Snabble -import io.snabble.sdk.utils.GsonHolder -import okhttp3.Call -import okhttp3.Callback -import okhttp3.MediaType.Companion.toMediaType -import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.RequestBody -import okhttp3.RequestBody.Companion.toRequestBody -import okhttp3.Response -import java.io.IOException -import kotlin.coroutines.resume -import kotlin.coroutines.suspendCoroutine - -internal interface FiservRemoteDataSource { - - suspend fun sendUserData(customerInfo: CustomerInfoDto, paymentMethod: PaymentMethod): Result -} - -internal class FiservRemoteDataSourceImpl( - private val snabble: Snabble = Snabble, - private val gson: Gson = GsonHolder.get(), -) : FiservRemoteDataSource { - - override suspend fun sendUserData( - customerInfo: CustomerInfoDto, - paymentMethod: PaymentMethod - ): Result { - val project = snabble.checkedInProject.value ?: return Result.failure(Exception("Missing projectId")) - - val customerInfoPostUrl = project.paymentMethodDescriptors - .firstOrNull { it.paymentMethod == paymentMethod } - ?.links - ?.get("tokenization") - ?.href - ?.let(snabble::absoluteUrl) - ?: return Result.failure(Exception("Missing link to send customer info to")) - - val requestBody: RequestBody = gson.toJson(customerInfo).toRequestBody("application/json".toMediaType()) - val request: Request = Request.Builder() - .url(customerInfoPostUrl) - .post(requestBody) - .build() - - return project.okHttpClient.post(request) - } - - private suspend fun OkHttpClient.post(request: Request) = suspendCoroutine> { - newCall(request).enqueue(object : Callback { - - override fun onResponse(call: Call, response: Response) { - when { - response.isSuccessful -> { - val body = response.body?.string() - val creditCardAuthData: CreditCardAuthData? = try { - gson.fromJson(body, CreditCardAuthData::class.java) - } catch (e: JsonSyntaxException) { - Log.e("Fiserv", "Error parsing pre-registration response", e) - null - } - - val result = if (creditCardAuthData == null) { - Result.failure(Exception("Missing content")) - } else { - Result.success(creditCardAuthData) - } - it.resume(result) - } - - else -> it.resume(Result.failure(Exception(response.message))) - } - } - - override fun onFailure(call: Call, e: IOException) { - it.resume(Result.failure(e)) - } - }) - } -} - -internal data class CreditCardAuthData( - @SerializedName("links") val links: Links -) - -internal data class Links( - @SerializedName("self") val deleteUrl: Link, - @SerializedName("tokenizationForm") val formUrl: Link -) - -internal data class Link( - @SerializedName("href") val href: String -) diff --git a/ui/src/main/java/io/snabble/sdk/ui/payment/fiserv/data/country/LocalCountryItemsDataSource.kt b/ui/src/main/java/io/snabble/sdk/ui/payment/fiserv/data/country/LocalCountryItemsDataSource.kt deleted file mode 100644 index 5f4b7d5eeb..0000000000 --- a/ui/src/main/java/io/snabble/sdk/ui/payment/fiserv/data/country/LocalCountryItemsDataSource.kt +++ /dev/null @@ -1,8 +0,0 @@ -package io.snabble.sdk.ui.payment.fiserv.data.country - -import io.snabble.sdk.ui.payment.fiserv.domain.model.country.CountryItem - -internal interface LocalCountryItemsDataSource { - - fun loadCountries(): List -} diff --git a/ui/src/main/java/io/snabble/sdk/ui/payment/fiserv/data/country/LocalCountryItemsDataSourceImpl.kt b/ui/src/main/java/io/snabble/sdk/ui/payment/fiserv/data/country/LocalCountryItemsDataSourceImpl.kt deleted file mode 100644 index 53c32201a4..0000000000 --- a/ui/src/main/java/io/snabble/sdk/ui/payment/fiserv/data/country/LocalCountryItemsDataSourceImpl.kt +++ /dev/null @@ -1,39 +0,0 @@ -package io.snabble.sdk.ui.payment.fiserv.data.country - -import android.content.res.AssetManager -import com.google.gson.Gson -import com.google.gson.reflect.TypeToken -import io.snabble.sdk.ui.payment.fiserv.domain.model.country.StateItem -import io.snabble.sdk.ui.payment.fiserv.data.dto.country.CountryDto -import io.snabble.sdk.ui.payment.fiserv.domain.model.country.CountryItem -import java.util.Locale - -internal class LocalCountryItemsDataSourceImpl( - private val assetManager: AssetManager, - private val gson: Gson, -) : LocalCountryItemsDataSource { - - override fun loadCountries(): List { - val typeToken = object : TypeToken>() {}.type - return gson.fromJson>( - assetManager.open(COUNTRIES_AND_STATES_FILE).reader(), - typeToken - ) - .map { (countryCode, states) -> - CountryItem( - displayName = countryCode.displayName, - code = countryCode, - stateItems = states?.map { StateItem.from(it) } - ) - } - .sortedBy { it.displayName } - } - - companion object { - - private const val COUNTRIES_AND_STATES_FILE = "countriesAndStates.json" - } -} - -val String.displayName: String - get() = Locale("", this).displayName diff --git a/ui/src/main/java/io/snabble/sdk/ui/payment/fiserv/data/dto/country/CountryDto.kt b/ui/src/main/java/io/snabble/sdk/ui/payment/fiserv/data/dto/country/CountryDto.kt deleted file mode 100644 index 7cb9c5b703..0000000000 --- a/ui/src/main/java/io/snabble/sdk/ui/payment/fiserv/data/dto/country/CountryDto.kt +++ /dev/null @@ -1,8 +0,0 @@ -package io.snabble.sdk.ui.payment.fiserv.data.dto.country - -import com.google.gson.annotations.SerializedName - -internal data class CountryDto( - @SerializedName("code") val countryCode: String, - @SerializedName("states") val states: List? = null, -) diff --git a/ui/src/main/java/io/snabble/sdk/ui/payment/fiserv/domain/CountryItemsRepository.kt b/ui/src/main/java/io/snabble/sdk/ui/payment/fiserv/domain/CountryItemsRepository.kt deleted file mode 100644 index bb81032f3d..0000000000 --- a/ui/src/main/java/io/snabble/sdk/ui/payment/fiserv/domain/CountryItemsRepository.kt +++ /dev/null @@ -1,8 +0,0 @@ -package io.snabble.sdk.ui.payment.fiserv.domain - -import io.snabble.sdk.ui.payment.fiserv.domain.model.country.CountryItem - -internal interface CountryItemsRepository { - - fun loadCountryItems(): List -} diff --git a/ui/src/main/java/io/snabble/sdk/ui/payment/fiserv/domain/CustomerInfo.kt b/ui/src/main/java/io/snabble/sdk/ui/payment/fiserv/domain/CustomerInfo.kt deleted file mode 100644 index 8afc10340e..0000000000 --- a/ui/src/main/java/io/snabble/sdk/ui/payment/fiserv/domain/CustomerInfo.kt +++ /dev/null @@ -1,16 +0,0 @@ -package io.snabble.sdk.ui.payment.fiserv.domain - -data class CustomerInfo( - val name: String, - val phoneNumber: String, - val email: String, - val address: Address, -) - -data class Address( - val street: String, - val zip: String, - val city: String, - val state: String, - val country: String -) diff --git a/ui/src/main/java/io/snabble/sdk/ui/payment/fiserv/domain/FiservRepository.kt b/ui/src/main/java/io/snabble/sdk/ui/payment/fiserv/domain/FiservRepository.kt deleted file mode 100644 index 7bd9a5129e..0000000000 --- a/ui/src/main/java/io/snabble/sdk/ui/payment/fiserv/domain/FiservRepository.kt +++ /dev/null @@ -1,9 +0,0 @@ -package io.snabble.sdk.ui.payment.fiserv.domain - -import io.snabble.sdk.PaymentMethod -import io.snabble.sdk.ui.payment.fiserv.data.FiservCardRegisterUrls - -internal interface FiservRepository { - - suspend fun sendUserData(customerInfo: CustomerInfo, paymentMethod: PaymentMethod): Result -} diff --git a/ui/src/main/java/io/snabble/sdk/ui/payment/fiserv/domain/model/country/CountryItem.kt b/ui/src/main/java/io/snabble/sdk/ui/payment/fiserv/domain/model/country/CountryItem.kt deleted file mode 100644 index c29020cb6c..0000000000 --- a/ui/src/main/java/io/snabble/sdk/ui/payment/fiserv/domain/model/country/CountryItem.kt +++ /dev/null @@ -1,3 +0,0 @@ -package io.snabble.sdk.ui.payment.fiserv.domain.model.country - -internal data class CountryItem(val displayName: String, val code: String, val stateItems: List?) diff --git a/ui/src/main/res/layout/snabble_fragment_cardinput_fiserv.xml b/ui/src/main/res/layout/snabble_fragment_cardinput_fiserv.xml index e28d2e0d23..55647e6014 100644 --- a/ui/src/main/res/layout/snabble_fragment_cardinput_fiserv.xml +++ b/ui/src/main/res/layout/snabble_fragment_cardinput_fiserv.xml @@ -1,5 +1,5 @@ - diff --git a/ui/src/main/res/values-de/strings.xml b/ui/src/main/res/values-de/strings.xml index 63495323eb..e1e5cfa5c3 100644 --- a/ui/src/main/res/values-de/strings.xml +++ b/ui/src/main/res/values-de/strings.xml @@ -106,6 +106,7 @@ E-Mail-Adresse Bitte überprüfe deine Eingaben und versuche es erneut Vor- und Nachname + Ländercode Weiter Telefonnummer Staat diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml index e085da7df9..1a6803750b 100644 --- a/ui/src/main/res/values/strings.xml +++ b/ui/src/main/res/values/strings.xml @@ -106,6 +106,7 @@ Email Please check your entries and try again Full name + Country code Continue Phone number State