Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • videostreaming/futopayclientlibraries
  • alex/futopayclientlibraries
2 results
Show changes
Commits on Source (17)
Showing
with 157 additions and 611 deletions
# Default ignored files
/shelf/
/workspace.xml
PolycentricCore
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AndroidTestResultsUserPreferences">
<option name="androidTestResultsTableState">
<map>
<entry key="-2030737957">
<value>
<AndroidTestResultsTableState>
<option name="preferredColumnWidths">
<map>
<entry key="Duration" value="90" />
<entry key="Pixel_3a_API_33_x86_64" value="120" />
<entry key="Tests" value="360" />
</map>
</option>
</AndroidTestResultsTableState>
</value>
</entry>
<entry key="-1888365331">
<value>
<AndroidTestResultsTableState>
<option name="preferredColumnWidths">
<map>
<entry key="Duration" value="90" />
<entry key="R5CNC07TZCY" value="120" />
<entry key="Tests" value="360" />
<entry key="samsung&#10; SM-G998B" value="120" />
</map>
</option>
</AndroidTestResultsTableState>
</value>
</entry>
<entry key="-1766545009">
<value>
<AndroidTestResultsTableState>
<option name="preferredColumnWidths">
<map>
<entry key="Duration" value="90" />
<entry key="R5CNC07TZCY" value="120" />
<entry key="Tests" value="360" />
<entry key="samsung&#10; SM-G998B" value="120" />
</map>
</option>
</AndroidTestResultsTableState>
</value>
</entry>
<entry key="-1603504896">
<value>
<AndroidTestResultsTableState>
<option name="preferredColumnWidths">
<map>
<entry key="Duration" value="90" />
<entry key="R5CNC07TZCY" value="120" />
<entry key="Tests" value="360" />
<entry key="samsung&#10; SM-G998B" value="120" />
</map>
</option>
</AndroidTestResultsTableState>
</value>
</entry>
<entry key="-1533817792">
<value>
<AndroidTestResultsTableState>
<option name="preferredColumnWidths">
<map>
<entry key="Duration" value="90" />
<entry key="Pixel_3a_API_33_x86_64" value="120" />
<entry key="R5CNC07TZCY" value="120" />
<entry key="Tests" value="360" />
<entry key="samsung&#10; SM-G998B" value="120" />
</map>
</option>
</AndroidTestResultsTableState>
</value>
</entry>
<entry key="-1348472475">
<value>
<AndroidTestResultsTableState>
<option name="preferredColumnWidths">
<map>
<entry key="29211JEGR13699" value="120" />
<entry key="Duration" value="90" />
<entry key="Google&#10; Pixel 6a" value="120" />
<entry key="R5CNC07TZCY" value="120" />
<entry key="Tests" value="360" />
<entry key="samsung&#10; SM-G998B" value="120" />
</map>
</option>
</AndroidTestResultsTableState>
</value>
</entry>
<entry key="-1191746456">
<value>
<AndroidTestResultsTableState>
<option name="preferredColumnWidths">
<map>
<entry key="Duration" value="90" />
<entry key="R5CNC07TZCY" value="120" />
<entry key="Tests" value="360" />
<entry key="samsung&#10; SM-G998B" value="120" />
</map>
</option>
</AndroidTestResultsTableState>
</value>
</entry>
<entry key="-1089424001">
<value>
<AndroidTestResultsTableState>
<option name="preferredColumnWidths">
<map>
<entry key="Duration" value="90" />
<entry key="R5CNC07TZCY" value="120" />
<entry key="Tests" value="360" />
<entry key="samsung&#10; SM-G998B" value="120" />
</map>
</option>
</AndroidTestResultsTableState>
</value>
</entry>
<entry key="-1030942155">
<value>
<AndroidTestResultsTableState>
<option name="preferredColumnWidths">
<map>
<entry key="29211JEGR13699" value="120" />
<entry key="Duration" value="90" />
<entry key="Google&#10; Pixel 6a" value="120" />
<entry key="Pixel_3a_API_33_x86_64" value="120" />
<entry key="Pixel_C_API_33" value="120" />
<entry key="Tests" value="360" />
</map>
</option>
</AndroidTestResultsTableState>
</value>
</entry>
<entry key="-1013244967">
<value>
<AndroidTestResultsTableState>
<option name="preferredColumnWidths">
<map>
<entry key="Duration" value="90" />
<entry key="R5CNC07TZCY" value="120" />
<entry key="Tests" value="360" />
<entry key="samsung&#10; SM-G998B" value="120" />
</map>
</option>
</AndroidTestResultsTableState>
</value>
</entry>
<entry key="-998275601">
<value>
<AndroidTestResultsTableState>
<option name="preferredColumnWidths">
<map>
<entry key="29211JEGR13699" value="120" />
<entry key="Duration" value="90" />
<entry key="Google&#10; Pixel 6a" value="120" />
<entry key="Pixel_3a_API_33_x86_64" value="120" />
<entry key="Tests" value="360" />
</map>
</option>
</AndroidTestResultsTableState>
</value>
</entry>
<entry key="-693989101">
<value>
<AndroidTestResultsTableState>
<option name="preferredColumnWidths">
<map>
<entry key="Duration" value="90" />
<entry key="R5CNC07TZCY" value="120" />
<entry key="Tests" value="360" />
<entry key="samsung&#10; SM-G998B" value="120" />
</map>
</option>
</AndroidTestResultsTableState>
</value>
</entry>
<entry key="-483192549">
<value>
<AndroidTestResultsTableState>
<option name="preferredColumnWidths">
<map>
<entry key="Duration" value="90" />
<entry key="Pixel_3a_API_33_x86_64" value="120" />
<entry key="Tests" value="360" />
</map>
</option>
</AndroidTestResultsTableState>
</value>
</entry>
<entry key="-396783379">
<value>
<AndroidTestResultsTableState>
<option name="preferredColumnWidths">
<map>
<entry key="29211JEGR13699" value="120" />
<entry key="Duration" value="90" />
<entry key="Google&#10; Pixel 6a" value="120" />
<entry key="Pixel_3a_API_33_x86_64" value="120" />
<entry key="Pixel_C_API_33" value="120" />
<entry key="R5CNC07TZCY" value="120" />
<entry key="Tests" value="360" />
<entry key="samsung&#10; SM-G998B" value="120" />
</map>
</option>
</AndroidTestResultsTableState>
</value>
</entry>
<entry key="-285941453">
<value>
<AndroidTestResultsTableState>
<option name="preferredColumnWidths">
<map>
<entry key="Duration" value="90" />
<entry key="Pixel_3a_API_33_x86_64" value="120" />
<entry key="R5CNC07TZCY" value="120" />
<entry key="Tests" value="360" />
<entry key="samsung&#10; SM-G998B" value="120" />
</map>
</option>
</AndroidTestResultsTableState>
</value>
</entry>
<entry key="-43042216">
<value>
<AndroidTestResultsTableState>
<option name="preferredColumnWidths">
<map>
<entry key="Duration" value="90" />
<entry key="Pixel_3a_API_33_x86_64" value="120" />
<entry key="Tests" value="360" />
</map>
</option>
</AndroidTestResultsTableState>
</value>
</entry>
<entry key="143109477">
<value>
<AndroidTestResultsTableState>
<option name="preferredColumnWidths">
<map>
<entry key="29211JEGR13699" value="120" />
<entry key="Duration" value="90" />
<entry key="Google&#10; Pixel 6a" value="120" />
<entry key="R5CNC07TZCY" value="120" />
<entry key="Tests" value="360" />
<entry key="samsung&#10; SM-G998B" value="120" />
</map>
</option>
</AndroidTestResultsTableState>
</value>
</entry>
<entry key="217836404">
<value>
<AndroidTestResultsTableState>
<option name="preferredColumnWidths">
<map>
<entry key="Duration" value="90" />
<entry key="Pixel_3a_API_33_x86_64" value="120" />
<entry key="Tests" value="360" />
</map>
</option>
</AndroidTestResultsTableState>
</value>
</entry>
<entry key="309039273">
<value>
<AndroidTestResultsTableState>
<option name="preferredColumnWidths">
<map>
<entry key="Duration" value="90" />
<entry key="R5CNC07TZCY" value="120" />
<entry key="Tests" value="360" />
<entry key="samsung&#10; SM-G998B" value="120" />
</map>
</option>
</AndroidTestResultsTableState>
</value>
</entry>
<entry key="568179418">
<value>
<AndroidTestResultsTableState>
<option name="preferredColumnWidths">
<map>
<entry key="Duration" value="90" />
<entry key="R5CNC07TZCY" value="120" />
<entry key="Tests" value="360" />
<entry key="samsung&#10; SM-G998B" value="120" />
</map>
</option>
</AndroidTestResultsTableState>
</value>
</entry>
<entry key="728115857">
<value>
<AndroidTestResultsTableState>
<option name="preferredColumnWidths">
<map>
<entry key="Duration" value="90" />
<entry key="Pixel_3a_API_33_x86_64" value="120" />
<entry key="Tests" value="360" />
</map>
</option>
</AndroidTestResultsTableState>
</value>
</entry>
<entry key="762617096">
<value>
<AndroidTestResultsTableState>
<option name="preferredColumnWidths">
<map>
<entry key="29211JEGR13699" value="120" />
<entry key="Duration" value="90" />
<entry key="Google&#10; Pixel 6a" value="120" />
<entry key="R5CNC07TZCY" value="120" />
<entry key="Tests" value="360" />
<entry key="samsung&#10; SM-G998B" value="120" />
</map>
</option>
</AndroidTestResultsTableState>
</value>
</entry>
<entry key="1154603043">
<value>
<AndroidTestResultsTableState>
<option name="preferredColumnWidths">
<map>
<entry key="29211JEGR13699" value="120" />
<entry key="Duration" value="90" />
<entry key="Google&#10; Pixel 6a" value="120" />
<entry key="Tests" value="360" />
</map>
</option>
</AndroidTestResultsTableState>
</value>
</entry>
<entry key="1367930197">
<value>
<AndroidTestResultsTableState>
<option name="preferredColumnWidths">
<map>
<entry key="Duration" value="90" />
<entry key="Pixel_3a_API_33_x86_64" value="120" />
<entry key="Tests" value="360" />
</map>
</option>
</AndroidTestResultsTableState>
</value>
</entry>
<entry key="1392532197">
<value>
<AndroidTestResultsTableState>
<option name="preferredColumnWidths">
<map>
<entry key="Duration" value="90" />
<entry key="Pixel_3a_API_33_x86_64" value="120" />
<entry key="Tests" value="360" />
</map>
</option>
</AndroidTestResultsTableState>
</value>
</entry>
<entry key="1445499490">
<value>
<AndroidTestResultsTableState>
<option name="preferredColumnWidths">
<map>
<entry key="Duration" value="90" />
<entry key="Pixel_3a_API_33_x86_64" value="120" />
<entry key="Tests" value="360" />
</map>
</option>
</AndroidTestResultsTableState>
</value>
</entry>
<entry key="1489134328">
<value>
<AndroidTestResultsTableState>
<option name="preferredColumnWidths">
<map>
<entry key="29211JEGR13699" value="120" />
<entry key="Duration" value="90" />
<entry key="Google&#10; Pixel 6a" value="120" />
<entry key="Tests" value="360" />
</map>
</option>
</AndroidTestResultsTableState>
</value>
</entry>
<entry key="1814633309">
<value>
<AndroidTestResultsTableState>
<option name="preferredColumnWidths">
<map>
<entry key="Duration" value="90" />
<entry key="R5CNC07TZCY" value="120" />
<entry key="Tests" value="360" />
<entry key="samsung&#10; SM-G998B" value="120" />
</map>
</option>
</AndroidTestResultsTableState>
</value>
</entry>
<entry key="1823866028">
<value>
<AndroidTestResultsTableState>
<option name="preferredColumnWidths">
<map>
<entry key="Duration" value="90" />
<entry key="R5CNC07TZCY" value="120" />
<entry key="Tests" value="360" />
<entry key="samsung&#10; SM-G998B" value="120" />
</map>
</option>
</AndroidTestResultsTableState>
</value>
</entry>
<entry key="1824280393">
<value>
<AndroidTestResultsTableState>
<option name="preferredColumnWidths">
<map>
<entry key="Duration" value="90" />
<entry key="R5CNC07TZCY" value="120" />
<entry key="Tests" value="360" />
<entry key="samsung&#10; SM-G998B" value="120" />
</map>
</option>
</AndroidTestResultsTableState>
</value>
</entry>
<entry key="1956518097">
<value>
<AndroidTestResultsTableState>
<option name="preferredColumnWidths">
<map>
<entry key="29211JEGR13699" value="120" />
<entry key="Duration" value="90" />
<entry key="Google&#10; Pixel 6a" value="120" />
<entry key="Pixel_3a_API_33_x86_64" value="120" />
<entry key="R5CNC07TZCY" value="120" />
<entry key="Tests" value="360" />
<entry key="samsung&#10; SM-G998B" value="120" />
</map>
</option>
</AndroidTestResultsTableState>
</value>
</entry>
<entry key="2076253436">
<value>
<AndroidTestResultsTableState>
<option name="preferredColumnWidths">
<map>
<entry key="29211JEGR13699" value="120" />
<entry key="Duration" value="90" />
<entry key="Google&#10; Pixel 6a" value="120" />
<entry key="Tests" value="360" />
</map>
</option>
</AndroidTestResultsTableState>
</value>
</entry>
</map>
</option>
</component>
</project>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="17" />
</component>
</project>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetDropDown">
<targetSelectedWithDropDown>
<Target>
<type value="QUICK_BOOT_TARGET" />
<deviceKey>
<Key>
<type value="VIRTUAL_DEVICE_PATH" />
<value value="$USER_HOME$/.android/avd/Pixel_C_API_33.avd" />
</Key>
</deviceKey>
</Target>
</targetSelectedWithDropDown>
<timeTargetWasSelectedWithDropDown value="2023-08-03T14:42:06.113626110Z" />
</component>
</project>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="GRADLE" />
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="Embedded JDK" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinJpsPluginSettings">
<option name="version" value="1.7.20" />
</component>
</project>
\ No newline at end of file
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>
\ No newline at end of file
plugins {
id 'com.android.library'
id 'org.jetbrains.kotlin.android'
id 'org.jetbrains.kotlin.plugin.serialization' version '1.6.10'
id 'org.jetbrains.kotlin.plugin.serialization' version '1.9.21'
}
android {
namespace 'com.futo.futopay'
compileSdk 33
compileSdk 34
defaultConfig {
minSdk 26
targetSdk 32
targetSdk 34
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
......@@ -31,14 +31,14 @@ android {
}
dependencies {
implementation 'androidx.core:core-ktx:1.9.0'
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1"
implementation 'androidx.recyclerview:recyclerview:1.3.0'
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2"
implementation 'androidx.recyclerview:recyclerview:1.3.2'
implementation 'com.caverock:androidsvg-aar:1.4'
implementation 'com.google.code.gson:gson:2.10.1'
implementation 'com.stripe:stripe-android:20.28.3'
implementation 'com.stripe:stripe-android:20.35.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
......
package com.futo.futopay
import junit.framework.TestCase.assertEquals
import org.junit.Test
class LicenseValidatorTests {
@Test
fun testValidate() {
val validator = LicenseValidator("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqyDuxsRtD5gmBoLCNoZa" +
"XSRTwyUxgzcPHzLZkvomXVSQqzD+3aOKngcTKAZ83rm4GvoyMlBukxQMLShannSx" +
"k8GQGTCT7VStQKNc4lKVER5ASB6aEaypaFMIYI3rXN1xLF1LqY/j7cu5GgMsvAuU" +
"VYFBexYFF6xcC5JDBZW6Pw/KYoJm3rswFixjPMGESmZRFCjjdAkHk47BhRPFBlvz" +
"wv9Ez1stdHcTpa/odEXIeJWIsZk9DHtCNCZyt6B6FXojVzrXsF2TxCNHGcHhlX43" +
"ALgQikiRcof1FsxoewTQhjLwMiDqB02mHCdRxssdnW3xadqyK678kQKfoIB1KB2N" +
"/QIDAQAB")
assert(validator.validate("UX8Z-9D7F-8BPX-8Q9J-WD7W-2PH1-D55A-QUG5", "VjpUEu20t4yW_Qmc3_Gji8FXmBfa95OFNx15Zf7OCJFxw1Kq2XF_Rv0dg4EdWkGwVaHzulIsDz6_ndTEhTWKCPm7kh4kT2VAkf8LCEwLjp5w7sz46Lb1jltA2orDt6UH90uKlT516VmsTXlEd9kIQ72Yt3jqzyI8JIG7Q1P5zj_e-wQ8XOyAJNiRVz-Ot0Xe-3qtufyaIbpyVut-OCAPEpHVaVUNHCDM9EuintQxuSZe_zwOaoFIR1hLnTKuc-xizO_da5qhWKjfnd5Dl4xwH-1ff47s7ltUavYI4ovcIC4LpSV84_5VBAsUr84B8fwNqu-b6AWRL5N1wkollIBzAg"))
}
}
\ No newline at end of file
......@@ -6,12 +6,12 @@ class PaymentConfigurations {
companion object {
val PAYMENT_METHODS = listOf(
PaymentMethodDescriptor("stripe", R.drawable.stripe, "Standard Payment Methods",
"Stripe is a secure online payment service that accepts major credit cards, debit cards, and various localized payment methods."),
PaymentMethodDescriptor("_", R.drawable.ic_construction, "Mobile Carrier Methods",
"Under Construction", true),
PaymentMethodDescriptor("_", R.drawable.ic_construction, "Crypto Currency Payment",
"Under Construction", true)
PaymentMethodDescriptor("stripe", R.drawable.stripe, R.string.standard_payment_methods,
R.string.stripe_is_a_secure_online_payment_service_that_accepts_major_credit_cards_debit_cards_and_various_localized_payment_methods),
PaymentMethodDescriptor("_", R.drawable.ic_construction, R.string.mobile_carrier_methods,
R.string.under_construction, true),
PaymentMethodDescriptor("_", R.drawable.ic_construction, R.string.crypto_currency_payment,
R.string.under_construction, true)
);
val COUNTRIES = listOf(
......@@ -116,6 +116,7 @@ class PaymentConfigurations {
CountryDescriptor("QA", "Qatar", "قطر", "qar", "qa"),
CountryDescriptor("RO", "Romania", "România", "ron", "ro"),
CountryDescriptor("RS", "Serbia", "Србија", "rsd", "rs"),
CountryDescriptor("RU", "Russia", "Российская Федерация", "rub", "ru"),
CountryDescriptor("RW", "Rwanda", "Rwanda", "rwf", "rw"),
CountryDescriptor("SA", "Saudi Arabia", "المملكة العربية السعودية", "sar", "sa"),
CountryDescriptor("SE", "Sweden", "Sweden", "sek", "se"),
......@@ -214,6 +215,7 @@ class PaymentConfigurations {
CurrencyDescriptor("qar", "Qatari Rial", "ريال قطري", "ر.ق.", "qa"), //QA
CurrencyDescriptor("ron", "Romanian Leu", "leu românesc", "RON", "ro"), //RO
CurrencyDescriptor("rsd", "Serbian Dinar", "српски динар", "RSD", "rs"), //RS
CurrencyDescriptor("rub", "Russian Rubble", "Российский рубль", "RUB", "ru"), //RU
CurrencyDescriptor("rwf", "Rwandan Franc", "Rwandan Franc", "RF", "rw"), //RW
CurrencyDescriptor("sar", "Saudi Riyal", "ريال سعودي", "ر.س.", "sa"), //SA
CurrencyDescriptor("sek", "Swedish Krona", "Swedish Krona", "kr", "se"), //SE
......@@ -234,8 +236,8 @@ class PaymentConfigurations {
data class PaymentMethodDescriptor(
val id: String,
val image: Int,
val name: String,
val description: String,
val name: Int,
val description: Int,
val isDisabled: Boolean = false
);
data class CountryDescriptor(
......
......@@ -50,7 +50,7 @@ class PaymentManager {
scope.launch(Dispatchers.IO){
try{
val availableCurrencies = _paymentState.getAvailableCurrencies(productId);
val country = paymentState.getPaymentCountryFromIP()?.let { c -> PaymentConfigurations.COUNTRIES.find { it.id.equals(c, ignoreCase = true) } };
val country = paymentState.getPaymentCountryFromIP(true)?.let { c -> PaymentConfigurations.COUNTRIES.find { it.id.equals(c, ignoreCase = true) } };
withContext(Dispatchers.Main) {
SlideUpPayment.startPayment(paymentState, _overlayContainer, productId, country, availableCurrencies) { method, request ->
when(method) {
......@@ -62,7 +62,7 @@ class PaymentManager {
catch(ex: Throwable) {
Log.e(TAG, "startPayment failed", ex);
scope.launch(Dispatchers.Main){
UIDialogs.showGeneralErrorDialog(_fragment.requireContext(), "Failed to get required payment data", ex);
UIDialogs.showGeneralErrorDialog(_fragment.requireContext(), _overlayContainer.context.getString(R.string.failed_to_get_required_payment_data), ex);
}
}
}
......@@ -98,7 +98,8 @@ class PaymentManager {
catch(ex: Throwable) {
Log.e(TAG, "Payment failed: ${ex.message}", ex);
withContext(Dispatchers.Main) {
UIDialogs.showGeneralErrorDialog(_fragment.requireContext(), "Payment failed\nIf you are charged you should always receive the key in your mail.", ex);
val c = _fragment.requireContext();
UIDialogs.showGeneralErrorDialog(c, c.getString(R.string.payment_failed_if_you_are_charged_you_should_always_receive_the_key_in_your_mail), ex);
}
}
}
......
......@@ -103,7 +103,7 @@ abstract class PaymentState {
if(result.body == null)
throw IllegalStateException("Could not get currencies:\nEmpty response");
val listResult = _json.decodeFromString<List<String>>(result.body!!);
val listResult = _json.decodeFromString<List<String>>(result.body);
synchronized(_currencyCache) {
_currencyCache[productId] = listResult;
return _currencyCache[productId]!!;
......@@ -121,7 +121,7 @@ abstract class PaymentState {
if(result.body == null)
throw IllegalStateException("Could not get currencies:\nEmpty response");
val listResult = _json.decodeFromString<HashMap<String, Long>>(result.body!!);
val listResult = _json.decodeFromString<HashMap<String, Long>>(result.body);
synchronized(_priceCache) {
_priceCache[productId] = listResult;
return _priceCache[productId]!!;
......@@ -138,7 +138,7 @@ abstract class PaymentState {
throw IllegalStateException("Could not get payment breakdown [${result.code}]:\n" + result.body);
if(result.body == null)
throw IllegalStateException("Could not get payment breakdown:\nEmpty response");
return _json.decodeFromString(result.body!!);
return _json.decodeFromString(result.body);
}
fun getPaymentIntent(productId: String, currency: String, email: String, country: String? = null, zipcode: String? = null): PaymentIntentInfo {
val result = httpGET(URL_PAYMENT_STRIPE_INTENT +
......@@ -152,7 +152,7 @@ abstract class PaymentState {
throw IllegalStateException("Could not get payment intent:\n" + result.body);
if(result.body == null)
throw IllegalStateException("Could not get payment intent:\nEmpty response");
return _json.decodeFromString(result.body!!);
return _json.decodeFromString(result.body);
}
fun getPaymentStatus(purchaseId: String): PaymentStatus {
......@@ -161,30 +161,37 @@ abstract class PaymentState {
throw IllegalStateException("Could not get payment intent:\n" + result.body);
if(result.body == null)
throw IllegalStateException("Could not get payment intent:\nEmpty response");
return _json.decodeFromString(result.body!!);
return _json.decodeFromString(result.body);
}
fun getPaymentCountryFromIP(): String? {
val urlString = "https://freeipapi.com/api/json"
fun getPaymentCountryFromIP(allowFail: Boolean = false): String? {
try {
val urlString = "https://freeipapi.com/api/json"
val url = URL(urlString)
val connection = url.openConnection() as HttpURLConnection
connection.requestMethod = "GET"
val url = URL(urlString)
val connection = url.openConnection() as HttpURLConnection
connection.requestMethod = "GET"
val reader = BufferedReader(InputStreamReader(connection.inputStream))
val response = StringBuilder()
var line: String?
while (reader.readLine().also { line = it } != null) {
response.append(line)
}
reader.close()
val json = response.toString();
val reader = BufferedReader(InputStreamReader(connection.inputStream))
val response = StringBuilder()
var line: String?
while (reader.readLine().also { line = it } != null) {
response.append(line)
}
reader.close()
val json = response.toString();
val ipInfoObj = JsonParser.parseString(json) as JsonObject;
if(ipInfoObj.has("countryCode"))
return ipInfoObj.get("countryCode").asString;
return null;
val ipInfoObj = JsonParser.parseString(json) as JsonObject;
if (ipInfoObj.has("countryCode"))
return ipInfoObj.get("countryCode").asString;
return null;
}
catch(ex: Throwable) {
if(allowFail)
return null;
throw ex;
}
}
private fun httpGET(urlStr: String): HttpResp {
......
......@@ -170,7 +170,7 @@ class SlideUpPayment : RelativeLayout {
private fun requestPostalCode(paymentState: PaymentState, overlayContainer: ViewGroup) {
transitionTo {
SlideUpPayment(paymentState, overlayContainer.context, overlayContainer, "Postal code", null,
SlideUpPayment(paymentState, overlayContainer.context, overlayContainer, overlayContainer.context.getString(R.string.postal_code), null,
PaymentPostalCodeView(overlayContainer.context, _country?.id!!) {
_postalCode = it;
requestNext(paymentState, overlayContainer);
......@@ -181,7 +181,7 @@ class SlideUpPayment : RelativeLayout {
private fun requestPaymentMethod(paymentState: PaymentState, overlayContainer: ViewGroup) {
transitionTo {
SlideUpPayment(paymentState, overlayContainer.context, overlayContainer, "Payment using", null,
SlideUpPayment(paymentState, overlayContainer.context, overlayContainer, overlayContainer.context.getString(R.string.payment_using), null,
*PaymentConfigurations.PAYMENT_METHODS.map { method ->
PaymentMethodView(overlayContainer.context, method) {
_paymentMethod = method;
......@@ -194,7 +194,7 @@ class SlideUpPayment : RelativeLayout {
private fun requestCountry(paymentState: PaymentState, overlayContainer: ViewGroup) {
transitionTo {
SlideUpPayment(paymentState, overlayContainer.context, overlayContainer, "Country of residency", null).apply {
SlideUpPayment(paymentState, overlayContainer.context, overlayContainer, overlayContainer.context.getString(R.string.country_of_residency), null).apply {
setItems(PaymentConfigurations.COUNTRIES.sortedBy { i -> i.nameEnglish },
createView = { CountryView(overlayContainer.context) },
bindView = { view, value ->
......@@ -230,7 +230,7 @@ class SlideUpPayment : RelativeLayout {
Collections.swap(sortedCurrencies, 4, sortedCurrencies.indexOfFirst { it.id.equals("jpy", ignoreCase = true) });
transitionTo {
SlideUpPayment(paymentState, overlayContainer.context, overlayContainer, "Currency of payment", null).apply {
SlideUpPayment(paymentState, overlayContainer.context, overlayContainer, overlayContainer.context.getString(R.string.currency_of_payment), null).apply {
setItems(sortedCurrencies.filter { _currencies?.contains(it.id) ?: true },
createView = { CurrencyView(overlayContainer.context) },
bindView = { view, value ->
......@@ -267,7 +267,7 @@ class SlideUpPayment : RelativeLayout {
);
transitionTo {
SlideUpPayment(paymentState, overlayContainer.context, overlayContainer, "Checkout", null, paymentCheckoutView)
SlideUpPayment(paymentState, overlayContainer.context, overlayContainer, overlayContainer.context.getString(R.string.checkout), null, paymentCheckoutView)
}
CoroutineScope(Dispatchers.IO).launch {
......@@ -280,7 +280,7 @@ class SlideUpPayment : RelativeLayout {
catch (ex: Throwable) {
withContext(Dispatchers.Main) {
Log.e("SlideUpPayment", "Failed to obtain price breakdown", ex)
paymentCheckoutView.setError("Failed to obtain price breakdown\n(${ex.message})\n Try again later")
paymentCheckoutView.setError(overlayContainer.context.getString(R.string.failed_to_obtain_price_breakdown_exceptionmessage_try_again_later).replace("{exceptionMessage}", ex.message ?: ""))
}
}
}
......
......@@ -53,7 +53,6 @@ class UIDialogs {
val buttonView = TextView(context);
val dp10 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10f, resources.displayMetrics).toInt();
val dp28 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 28f, resources.displayMetrics).toInt();
val dp14 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 14.0f, resources.displayMetrics);
buttonView.layoutParams = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT
......
......@@ -3,8 +3,12 @@ package com.futo.futopay
import android.content.Context
import android.graphics.drawable.Drawable
import android.graphics.drawable.PictureDrawable
import android.icu.text.NumberFormat
import android.icu.util.Currency
import androidx.core.graphics.drawable.toBitmap
import com.caverock.androidsvg.SVG
import java.util.Locale
import kotlin.math.pow
private val _assetFlags = HashMap<String, Drawable>();
......@@ -38,6 +42,28 @@ fun initFlags(context: Context)
}
}
fun formatMoney(countryCode: String, currency: String, amount: Long): String {
fun getStripeDecimalPlaces(c: String): Int {
return when (c.uppercase()) {
"ISK", "HUF", "TWD", "UGX" -> 2
"BIF", "CLP", "DJF", "GNF", "JPY", "KMF", "KRW", "MGA", "PYG", "RWF", "VND", "VUV", "XAF", "XOF", "XPF" -> 0
"BHD", "JOD", "KWD", "OMR", "TND" -> 3
else -> 2 // default case for all other currencies
}
}
val decimalPlaces = getStripeDecimalPlaces(currency)
val adjustedAmount = amount / 10.0.pow(decimalPlaces.toDouble())
val locale = Locale(countryCode, currency)
val currencyInstance = Currency.getInstance(currency.uppercase())
val numberFormat = NumberFormat.getCurrencyInstance(locale).apply {
minimumFractionDigits = decimalPlaces
maximumFractionDigits = decimalPlaces
this.currency = currencyInstance
}
return numberFormat.format(adjustedAmount)
}
fun getCountryDrawable(context: Context, name: String): Drawable? {
val code = name.lowercase();
initFlags(context);
......
......@@ -3,6 +3,8 @@ package com.futo.futopay.views
import android.content.Context
import android.graphics.Color
import android.graphics.drawable.Animatable
import android.icu.text.NumberFormat
import android.icu.util.Currency
import android.text.Editable
import android.text.TextWatcher
import android.view.View
......@@ -15,6 +17,9 @@ import androidx.constraintlayout.widget.ConstraintLayout
import com.futo.futopay.PaymentBreakdown
import com.futo.futopay.PaymentConfigurations
import com.futo.futopay.R
import com.futo.futopay.formatMoney
import java.util.Locale
import kotlin.math.pow
class PaymentCheckoutView : ConstraintLayout {
private var _isValid = false;
......@@ -30,6 +35,7 @@ class PaymentCheckoutView : ConstraintLayout {
private val textTotal: TextView
private val textCountry: TextView
private val editEmail: EditText
private val editEmailConfirm: EditText
private val layoutBreakdown: LinearLayout
private val layoutLoader: FrameLayout
private val imageLoader: ImageView
......@@ -37,6 +43,7 @@ class PaymentCheckoutView : ConstraintLayout {
private val textEmailSubtext: TextView
private val textPostalCodeHeader: TextView
private val buttonChangePostalCode: FrameLayout
private val country: PaymentConfigurations.CountryDescriptor
constructor(context: Context, method: String, currency: PaymentConfigurations.CurrencyDescriptor, country: PaymentConfigurations.CountryDescriptor, postalCode: String?, onBuy: (String)->Unit, onChangeCountry: ()->Unit, onChangeCurrency: ()->Unit, onChangePostalCode: ()->Unit): super(context) {
inflate(context, R.layout.payment_checkout, this);
......@@ -54,11 +61,13 @@ class PaymentCheckoutView : ConstraintLayout {
textTotal = findViewById(R.id.text_total)
textCountry = findViewById(R.id.text_country)
editEmail = findViewById(R.id.edit_email)
editEmailConfirm = findViewById(R.id.edit_email_confirm)
layoutBreakdown = findViewById(R.id.layout_breakdown)
layoutLoader = findViewById(R.id.layout_loader)
textError = findViewById(R.id.text_error)
imageLoader = findViewById(R.id.image_loader)
textEmailSubtext = findViewById(R.id.text_email_subtext)
this.country = country
//findViewById<ImageView>(R.id.image_method).setImageResource(paymentMethod.image);
textMethod.text = method.replaceFirstChar { it.uppercase() }
......@@ -77,22 +86,18 @@ class PaymentCheckoutView : ConstraintLayout {
onChangePostalCode();
};
val emailRegex = """(?:[a-z0-9!#${'$'}%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#${'$'}%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])""".toRegex()
editEmail.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) = Unit
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
val text = editEmail.text.toString();
_isValid = emailRegex.matches(text);
if (_isValid) {
textEmailSubtext.setTextColor(Color.rgb(0x99, 0x99, 0x99));
textEmailSubtext.setText("Required to accurately calculate the applicable sales tax");
buttonPay.alpha = 1.0f;
} else {
textEmailSubtext.setTextColor(Color.rgb(0xFF, 0x00, 0x00));
textEmailSubtext.setText("Email is invalid");
buttonPay.alpha = 0.4f;
}
validateInput()
}
});
editEmailConfirm.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) = Unit
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
validateInput()
}
});
......@@ -109,6 +114,32 @@ class PaymentCheckoutView : ConstraintLayout {
setPaymentBreakdown(null);
}
private fun validateInput() {
val emailRegex = """(?:[a-zA-Z0-9!#${'$'}%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#${'$'}%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])""".toRegex()
val text = editEmail.text.toString()
if (!emailRegex.matches(text)) {
_isValid = false
textEmailSubtext.setTextColor(Color.rgb(0xFF, 0x00, 0x00))
textEmailSubtext.text = context.getString(R.string.email_is_invalid)
buttonPay.alpha = 0.4f;
return
}
if (editEmail.text.toString() != editEmailConfirm.text.toString()) {
_isValid = false
textEmailSubtext.setTextColor(Color.rgb(0xFF, 0x00, 0x00))
textEmailSubtext.text = context.getString(R.string.email_must_match)
buttonPay.alpha = 0.4f;
return
}
textEmailSubtext.setTextColor(Color.rgb(0x99, 0x99, 0x99));
textEmailSubtext.text = context.getString(R.string.required_to_accurately_calculate_the_applicable_sales_tax);
buttonPay.alpha = 1.0f;
_isValid = true
}
private fun setPostalCode(countryId: String, postalCode: String? = null) {
if (countryId.equals("us", ignoreCase = true) || countryId.equals("ca", ignoreCase = true)) {
textPostalCode.visibility = View.VISIBLE
......@@ -119,7 +150,7 @@ class PaymentCheckoutView : ConstraintLayout {
textPostalCode.text = postalCode.uppercase();
textPostalCode.setTextColor(Color.rgb(0xFF, 0xFF, 0xFF))
} else {
textPostalCode.text ="Missing";
textPostalCode.text =context.getString(R.string.missing);
textPostalCode.setTextColor(Color.rgb(0xFF, 0, 0))
}
} else {
......@@ -138,19 +169,16 @@ class PaymentCheckoutView : ConstraintLayout {
return;
}
val currency = PaymentConfigurations.CURRENCIES.find { it.id == paymentBreakdown.currency };
val symbol = currency?.symbol ?: "";
(imageLoader.drawable as Animatable?)?.stop()
layoutLoader.visibility = View.GONE
textError.visibility = View.GONE
layoutBreakdown.visibility = View.VISIBLE
textProduct.text = paymentBreakdown.productName;
textProductPrice.text = symbol + "%.2f".format((paymentBreakdown.productPrice).toDouble()/100);
textProductPrice.text = formatMoney(country.id, paymentBreakdown.currency, paymentBreakdown.productPrice)
textTaxPercentage.text = "%.2f".format(paymentBreakdown.taxPercentage);
textTax.text = symbol + "%.2f".format((paymentBreakdown.taxPrice).toDouble()/100);
textTotal.text = symbol + "%.2f".format((paymentBreakdown.totalPrice).toDouble()/100);
textCurrency.text = paymentBreakdown.currency.uppercase();
textTax.text = formatMoney(country.id, paymentBreakdown.currency, paymentBreakdown.taxPrice)
textTotal.text = formatMoney(country.id, paymentBreakdown.currency, paymentBreakdown.totalPrice)
textCurrency.text = paymentBreakdown.currency.uppercase()
}
fun setError(error: String) {
......
......@@ -29,8 +29,8 @@ class PaymentMethodView: ConstraintLayout {
fun bind(paymentMethod: PaymentConfigurations.PaymentMethodDescriptor, onClick: (String)->Unit) {
_image.setImageResource(paymentMethod.image);
_name.text = paymentMethod.name;
_description.text = paymentMethod.description;
_name.text = context.getString(paymentMethod.name);
_description.text = context.getString(paymentMethod.description);
if(!paymentMethod.isDisabled) {
setOnClickListener {
......
......@@ -34,20 +34,20 @@ class PaymentPostalCodeView : ConstraintLayout {
_isValid = regex?.matches(text) ?: true;
if (_isValid) {
textSubtext.setTextColor(Color.rgb(0x99, 0x99, 0x99));
textSubtext.setText("Required to accurately calculate the applicable sales tax");
textSubtext.text = context.getString(R.string.required_to_accurately_calculate_the_applicable_sales_tax);
buttonSubmit.alpha = 1.0f;
} else {
textSubtext.setTextColor(Color.rgb(0xFF, 0x00, 0x00));
textSubtext.setText("Value is invalid for ${countryId.uppercase()}");
textSubtext.text = context.getString(R.string.value_is_invalid_for) + countryId.uppercase();
buttonSubmit.alpha = 0.4f;
}
}
});
editPostalCode.hint = when (countryId) {
"us" -> "Enter ZIP code (e.g., 12345 or 12345-6789)"
"ca" -> "Enter Postal code (e.g., A1A 1A1)"
else -> "Enter Postal code"
"us" -> context.getString(R.string.enter_zip_code_e_g_12345_or_12345_6789)
"ca" -> context.getString(R.string.enter_postal_code_e_g_a1a_1a1)
else -> context.getString(R.string.enter_postal_code)
}
if (regex != null) {
......