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 (22)
Showing
with 204 additions and 611 deletions
# FUTO TEMPORARY LICENSE
This license grants you the rights, and only the rights, set out below in respect of the source code provided. If you take advantage of these rights, you accept this license. If you do not accept the license, do not access the code.
Words used in the Terms of Service have the same meaning in this license. Where there is any inconsistency between this license and those Terms of Service, these terms prevail.
## Section 1: Definitions
- "code" means the source code made available from time, in our sole discretion, for access under this license. Reference to code in this license means the code and any part of it and any derivative of it.
- “compilation” means to compile the code from ‘source code’ to ‘machine code’.
- "defect" means a defect, bug, backdoor, security issue or other deficiency in the code.
- “non-commercial distribution” means distribution of the code or any compilation of the code, or of any other application or program containing the code or any compilation of the code, where such distribution is not intended for or directed towards commercial advantage or monetary compensation.
- "review" means to access, analyse, test and otherwise review the code as a reference, for the sole purpose of analysing it for defects.
- "you" means the licensee of rights set out in this license.
## Section 2: Grant of Rights
1. Subject to the terms of this license, we grant you a non-transferable, non-exclusive, worldwide, royalty-free license to access and use the code solely for the purposes of review, compilation and non-commercial distribution.
2. You may provide the code to anyone else and publish excerpts of it for the purposes of review, compilation and non-commercial distribution, provided that when you do so you make any recipient of the code aware of the terms of this license, they must agree to be bound by the terms of this license and you must attribute the code to the provider.
3. Other than in respect of those parts of the code that were developed by other parties and as specified strictly in accordance with the open source and other licenses under which those parts of the code have been made available, as set out on our website or in those items of code, you are not entitled to use or do anything with the code for any commercial or other purpose, other than review, compilation and non-commercial distribution in accordance with the terms of this license.
4. Subject to the terms of this license, you must at all times comply with and shall be bound by our Terms of Use, Privacy and Data Policy.
## Section 3: Limitations
1. This license does not grant you any rights to use the provider's name, logo, or trademarks and you must not in any way indicate you are authorised to speak on behalf of the provider.
2. If you issue proceedings in any jurisdiction against the provider because you consider the provider has infringed copyright or any patent right in respect of the code (including any joinder or counterclaim), your license to the code is automatically terminated.
3. THE CODE IS MADE AVAILABLE "AS-IS" AND WITHOUT ANY EXPRESS OR IMPLIED GUARANTEES AS TO FITNESS, MERCHANTABILITY, NON-INFRINGEMENT OR OTHERWISE. IT IS NOT BEING PROVIDED IN TRADE BUT ON A VOLUNTARY BASIS ON OUR PART AND IS NOT MADE AVAILABLE FOR ANY USE OUTSIDE THE TERMS OF THIS LICENSE. ANYONE ACCESSING THE CODE MUST ENSURE THEY HAVE THE REQUISITE EXPERTISE TO SECURE THEIR OWN SYSTEM AND DEVICES AND TO ACCESS AND USE THE CODE IN ACCORDANCE WITH THE TERMS OF THIS LICENSE. YOU BEAR THE RISK OF ACCESSING AND USING THE CODE. IN PARTICULAR, THE PROVIDER BEARS NO LIABILITY FOR ANY INTERFERENCE WITH OR ADVERSE EFFECT ON YOUR SYSTEM OR DEVICES AS A RESULT OF YOUR ACCESSING AND USING THE CODE IN ACCORDANCE WITH THE TERMS OF THIS LICENSE OR OTHERWISE.
## Section 4: Termination, suspension and variation
1. We may suspend, terminate or vary the terms of this license and any access to the code at any time, without notice, for any reason or no reason, in respect of any licensee, group of licensees or all licensees including as may be applicable any sub-licensees.
## Section 5: General
1. This license and its interpretation and operation are governed solely by the local law. You agree to submit to the exclusive jurisdiction of the local arbitral tribunals as further described in our Terms of Service and you agree not to raise any jurisdictional issue if we need to enforce an arbitral award or judgment in our jurisdiction or another country.
2. Questions and comments regarding this license are welcomed and should be addressed at https://chat.futo.org/login/.
Last updated 7 June 2023.
# 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,17 +31,14 @@ android {
}
dependencies {
implementation 'androidx.core:core-ktx:1.9.0'
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation "com.squareup.okhttp3:okhttp:4.10.0"
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'
annotationProcessor 'com.github.bumptech.glide:compiler:4.15.1'
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.github.bumptech.glide:glide:4.15.1'
implementation 'com.google.code.gson:gson:2.10.1' //TODO: Can we get rid of this?
implementation 'com.stripe:stripe-android:20.28.3'
implementation 'com.google.code.gson:gson:2.10.1'
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
package com.futo.futopay
import okio.ByteString.Companion.decodeBase64
import android.util.Base64
import java.security.KeyFactory
import java.security.PublicKey
import java.security.Signature
......@@ -11,7 +11,7 @@ class LicenseValidator(publicKey: String) {
init {
val keyFactory = KeyFactory.getInstance("RSA");
val publicKeySpec = X509EncodedKeySpec(publicKey.decodeBase64()!!.toByteArray());
val publicKeySpec = X509EncodedKeySpec(Base64.decode(publicKey, Base64.DEFAULT));
_publicPaymentKey = keyFactory.generatePublic(publicKeySpec);
}
......
......@@ -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(
......
......@@ -8,4 +8,5 @@ class PaymentIntentInfo(
val publishableKey: String,
val customer: String? = null,
val ephemeralKey: String? = null,
val purchaseId: String? = null
);
\ No newline at end of file
......@@ -4,8 +4,6 @@ import android.util.Log
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import com.stripe.android.PaymentConfiguration
import com.stripe.android.paymentsheet.PaymentSheet
import com.stripe.android.paymentsheet.PaymentSheetResult
......@@ -25,7 +23,9 @@ class PaymentManager {
private val _sheet: PaymentSheet;
private val _paymentState: PaymentState;
constructor(paymentState: PaymentState, fragment: Fragment, overlayContainer: ViewGroup, onCompleted: (success: Boolean, exception: Throwable?)->Unit) {
private var _lastPurchaseId: String? = null;
constructor(paymentState: PaymentState, fragment: Fragment, overlayContainer: ViewGroup, onCompleted: (success: Boolean, purchaseId: String?, exception: Throwable?)->Unit) {
_fragment = fragment;
_paymentState = paymentState;
_overlayContainer = overlayContainer;
......@@ -36,10 +36,10 @@ class PaymentManager {
}
is PaymentSheetResult.Failed -> {
onCompleted(false, paymentSheetResult.error)
onCompleted(false, null, paymentSheetResult.error)
}
is PaymentSheetResult.Completed -> {
onCompleted(true, null);
onCompleted(true, _lastPurchaseId,null);
}
}
};
......@@ -50,7 +50,7 @@ class PaymentManager {
scope.launch(Dispatchers.IO){
try{
val availableCurrencies = _paymentState.getAvailableCurrencies(productId);
val country = getCountryFromIP()?.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);
}
}
}
......@@ -77,6 +77,7 @@ class PaymentManager {
PaymentSheet.CustomerConfiguration(paymentIntentResult.customer, paymentIntentResult.ephemeralKey);
else null;
_lastPurchaseId = paymentIntentResult.purchaseId;
PaymentConfiguration.init(_fragment.requireContext(), paymentIntentResult.publishableKey);
......@@ -97,34 +98,14 @@ 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);
}
}
}
}
//TODO: Determine a good provider
private fun getCountryFromIP(): String? {
val urlString = "https://freeipapi.com/api/json"
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 ipInfoObj = JsonParser.parseString(json) as JsonObject;
if(ipInfoObj.has("countryCode"))
return ipInfoObj.get("countryCode").asString;
return null;
}
data class PaymentRequest(
val productId: String,
......
package com.futo.futopay
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import java.io.BufferedReader
import java.io.InputStreamReader
import java.net.HttpURLConnection
import java.net.URL
abstract class PaymentState {
val REGEX_KEY_FORMAT = Regex("[a-zA-Z0-9-]{4}-[a-zA-Z0-9-]{4}-[a-zA-Z0-9-]{4}-[a-zA-Z0-9-]{4}-[a-zA-Z0-9-]{4}-[a-zA-Z0-9-]{4}-[a-zA-Z0-9-]{4}-[a-zA-Z0-9-]{4}");
private val URL_BASE = "https://payment.grayjay.app";//"https://futopay.azurewebsites.net";
private val URL_BASE = if(!isTesting) "https://payment.grayjay.app" else "https://futopay-test.azurewebsites.net";
private val URL_STATIC_BASE = if(!isTesting) "https://spayment.grayjay.app" else "https://futopay-test.azurewebsites.net";
private val URL_PAYMENT_STRIPE_INTENT = "${URL_BASE}/api/v1/stripe/paymentintent/payment";
private val URL_PAYMENT_BREAKDOWN = "${URL_BASE}/api/v1/payment/breakdown";
private val URL_TIP_INTENT = "${URL_BASE}/api/v1/stripe/paymentintent/tip?amount=";
private val URL_LOCATION = "${URL_BASE}/api/v1/location";
private val URL_CURRENCIES = "${URL_BASE}/api/v1/payment/currencies";
private val URL_CURRENCIES = "${URL_STATIC_BASE}/api/v1/payment/currencies";
private val URL_PRICES = "${URL_STATIC_BASE}/api/v1/payment/prices";
private val URL_ACTIVATION_URL = "${URL_BASE}/api/v1/activate/";
private val URL_PAYMENT_STATUS = "${URL_BASE}/api/v1/payment/status/";
private val _currencyCache = HashMap<String, List<String>>();
private val _client = OkHttpClient()
private val _priceCache = HashMap<String, HashMap<String, Long>>();
private val _validator: LicenseValidator
var hasPaid: Boolean = false;
var hasPaidChanged = Event1<Boolean>();
protected open val isTesting get() = false;
constructor(validationPublicKey: String) {
_validator = LicenseValidator(validationPublicKey)
}
......@@ -50,9 +58,9 @@ abstract class PaymentState {
fun setPaymentLicenseKey(key: String): Boolean {
val activationKeyResponse = httpGET(URL_ACTIVATION_URL + key);
if(activationKeyResponse.isSuccessful)
return setPaymentLicenseUrl("${key}/${activationKeyResponse.body!!.string()}");
return setPaymentLicenseUrl("${key}/${activationKeyResponse.body!!}");
else
throw IllegalStateException("Request failed [${activationKeyResponse.code}]\n" + activationKeyResponse.body?.string());
throw IllegalStateException("Request failed [${activationKeyResponse.code}]\n" + activationKeyResponse.body);
}
fun setPaymentLicenseUrl(url: String): Boolean {
......@@ -91,16 +99,34 @@ abstract class PaymentState {
val url = URL_CURRENCIES + "?productId=" + productId;
val result = httpGET(url);
if(!result.isSuccessful)
throw IllegalStateException("Could not get currencies [${result.code}]:\n" + result.body?.string());
throw IllegalStateException("Could not get currencies [${result.code}]:\n" + result.body);
if(result.body == null)
throw IllegalStateException("Could not get currencies:\nEmpty response");
val listResult = _json.decodeFromString<List<String>>(result.body!!.string());
val listResult = _json.decodeFromString<List<String>>(result.body);
synchronized(_currencyCache) {
_currencyCache[productId] = listResult;
return _currencyCache[productId]!!;
}
}
fun getAvailableCurrencyPrices(productId: String): Map<String, Long> {
synchronized(_priceCache) {
if(_priceCache.containsKey(productId))
return _priceCache[productId]!!;
}
val url = URL_PRICES + "?productId=" + productId;
val result = httpGET(url);
if(!result.isSuccessful)
throw IllegalStateException("Could not get currencies [${result.code}]:\n" + result.body);
if(result.body == null)
throw IllegalStateException("Could not get currencies:\nEmpty response");
val listResult = _json.decodeFromString<HashMap<String, Long>>(result.body);
synchronized(_priceCache) {
_priceCache[productId] = listResult;
return _priceCache[productId]!!;
}
}
fun getPaymentBreakdown(productId: String, currency: String, country: String? = null, zipcode: String? = null): PaymentBreakdown {
val url = URL_PAYMENT_BREAKDOWN +
"?productId=" + productId +
......@@ -109,10 +135,10 @@ abstract class PaymentState {
(if(country != null && zipcode != null) "&zipcode=" + zipcode else "");
val result = httpGET(url);
if(!result.isSuccessful)
throw IllegalStateException("Could not get payment breakdown [${result.code}]:\n" + result.body?.string());
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!!.string());
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 +
......@@ -123,14 +149,64 @@ abstract class PaymentState {
(if(country != null && zipcode != null) "&zipcode=" + zipcode else "")
);
if(!result.isSuccessful)
throw IllegalStateException("Could not get payment intent:\n" + result.body?.string());
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!!.string());
return _json.decodeFromString(result.body);
}
private fun httpGET(url: String): Response {
val request = Request.Builder().url(url).build()
return _client.newCall(request).execute()
fun getPaymentStatus(purchaseId: String): PaymentStatus {
val result = httpGET(URL_PAYMENT_STATUS + purchaseId);
if(!result.isSuccessful)
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);
}
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 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;
}
catch(ex: Throwable) {
if(allowFail)
return null;
throw ex;
}
}
private fun httpGET(urlStr: String): HttpResp {
val url = URL(urlStr);
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()
return HttpResp(connection.responseCode, response.toString());
}
abstract fun savePaymentKey(licenseKey: String, licenseActivation: String);
abstract fun getPaymentKey(): Pair<String, String>;
......@@ -138,4 +214,12 @@ abstract class PaymentState {
companion object {
private val _json = Json { ignoreUnknownKeys = true };
}
private class HttpResp(
val code: Int,
val body: String?
)
{
val isSuccessful get() = code >= 200 && code < 300;
}
}
\ No newline at end of file
package com.futo.futopay
import kotlinx.serialization.Serializable
@Serializable
class PaymentStatus(
val status: Int,
val purchaseId: String? = null
);
\ No newline at end of file
......@@ -68,6 +68,11 @@ class SlideUpPayment : RelativeLayout {
if(items.size > 0) {
setItems(items.toList());
}
if(isFlagsInitialized())
CoroutineScope(Dispatchers.IO).launch {
initFlags(context);
};
}
companion object {
......@@ -165,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);
......@@ -176,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;
......@@ -189,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 ->
......@@ -225,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 ->
......@@ -262,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 {
......@@ -275,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
......