diff --git a/app/build.gradle b/app/build.gradle index c37a4da509..f1fa3b08ef 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -6,13 +6,13 @@ apply plugin: 'com.google.firebase.crashlytics' apply from: '../dependencies.gradle' android { - compileSdkVersion 32 + compileSdkVersion 33 defaultConfig { applicationId "com.tangem.wallet" versionCode project.hasProperty('versionCode') ? project.property('versionCode') as int : 1 versionName project.hasProperty('versionName') ? project.property('versionName') : "1.0.0.-SNAPSHOT" minSdkVersion 21 - targetSdkVersion 32 + targetSdkVersion 33 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" buildConfigField 'Boolean', environmentConfig.testActionEnabled, 'false' @@ -83,11 +83,11 @@ dependencies { implementation implementation(project(path: ':network')) implementation implementation(project(path: ':common')) - implementation 'androidx.core:core-ktx:1.8.0' - implementation 'androidx.appcompat:appcompat:1.4.1' - implementation 'androidx.fragment:fragment-ktx:1.4.1' - implementation 'androidx.constraintlayout:constraintlayout:2.1.3' - implementation 'com.google.android.material:material:1.5.0' + implementation 'androidx.core:core-ktx:1.9.0' + implementation 'androidx.appcompat:appcompat:1.5.1' + implementation 'androidx.fragment:fragment-ktx:1.5.3' + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + implementation 'com.google.android.material:material:1.6.1' implementation 'com.google.android.play:core:1.10.3' implementation 'com.google.android.play:core-ktx:1.8.1' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' @@ -97,15 +97,15 @@ dependencies { implementation "com.tangem.tangem-sdk-kotlin:android:${versions.tangem_card_sdk}" // WebView - implementation "androidx.browser:browser:1.3.0" + implementation "androidx.browser:browser:1.4.0" implementation 'com.jakewharton.timber:timber:4.7.1' //lifecycle - implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.3.1" - implementation "androidx.lifecycle:lifecycle-common-java8:2.3.1" - implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1" - implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1" + implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.5.1" + implementation "androidx.lifecycle:lifecycle-common-java8:2.5.1" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1" + implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.5.1" implementation "org.rekotlin:rekotlin:1.0.4" @@ -176,6 +176,9 @@ dependencies { implementation 'com.github.kirich1409:viewbindingpropertydelegate-noreflection:1.5.6' + // Shared preferences (encrypted) + implementation 'at.favre.lib:armadillo:0.9.0' + testImplementation 'junit:junit:4.13.2' testImplementation "com.google.truth:truth:1.1.3" androidTestImplementation 'androidx.test.ext:junit:1.1.3' diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index 8a36217ba8..cf6ea50867 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit 8a36217ba816e1cc7cbc7290ffb948282ec15f6b +Subproject commit cf6ea50867477f97655da9da47ff94987e8f3354 diff --git a/app/src/main/assets/tos.html b/app/src/main/assets/tos.html new file mode 100644 index 0000000000..67c6eba9b4 --- /dev/null +++ b/app/src/main/assets/tos.html @@ -0,0 +1,1832 @@ + + + + + file_1666042066293 + + + +

+ SaltPay Oly Card Terms of Service +

+


+

We are delighted + that you (“you”, + “your”, or the “Cardholder”) have chosen to use the SaltPay Oly Card (the “Card”). + The terms + and conditions set out here (“Terms”) govern your use of your Card as operated by + SaltPay.

+

+ When you use the + Card, you also agree to + the SaltPay + General Terms + which are incorporated + into these Terms by + reference, including any amendments made to those from time to time. If you do not + understand any provisions of + these Terms, please contact us before using the Card. For information about how we treat + your personal data, + please see our Privacy + Policy.

+


+
    +
  1. + Definitions

    +

    + Affiliate: means any entity controlling, controlled by or under common control with that party, where “control” means the ownership of more than 50% of (i) the voting securities or (ii) an interest in the assets, profits, or earnings of an entity. +

    +

    + Blockchain Asset: means a digital data stored in and protected by the blockchain network, including but not limited to ownership right and value of cryptocurrency, tokenized assets, non-fungible asset. +

    +

    + Cardholder: a person who holds a SaltPay Oly Card. +

    +

    Private + Key: a secret cryptographic key which provides full control over a Blockchain Asset. +

    +

    + Public + Key: means a cryptographic key, which provides access to information about a Blockchain Asset, including but not limited to Blockchain Address. +

    +

    + SaltPay: Salt Pay IIB hf. +

    +

    SaltPay + Oly Card / Card: physical card which stores Private Key and Public Key.

    +

    + Services: enabling the use of the SaltPay Oly Card at physical point of sale terminals that are enabled for the acceptance of Visa Debit cards. +

    +

    + Tangem: means Tangem AG, a company incorporated in Switzerland with company number 1315048, whose address is at Baarerstrasse 10, 6300 Zug. +

    +

    + Tangem + Mobile Application: Third party mobile application, providing interoperability between SaltPay Oly Card and blockchain, and working on NFC-capable smartphones and tablets using Google Android and Apple iOS operating systems. +

    +

    + Tangem + Service: means the service provided by Tangem, which provides and administers the Tangem Mobile Application, which enables Cardholders to generate and store Public Keys and Private Keys in a hardware crypto wallet. +

    +

    Utorg + Service: means providing services of exchanging a virtual currency against a fiat currency. +

    +


  2. +
  3. + Important Legal + Stuff

    +

    + We think + it’s important that we highlight and clearly explain how SaltPay safeguards its + interests, while offering + its services to its Cardholders (you!) which includes limitation of liability and + disclaimers. The sections + set out below reflect those safeguards and how they might impact you. It’s therefore + important that you read + them carefully. If you’re unsure about any of them, please get in touch.

    +
      +
    1. + These + Terms of Service (the “Terms”) govern the use of SaltPay Oly Cards to + authorised employees of + SaltPay and its Affiliates.

    2. +
    3. + The Cards may be used for storage of Private Key and Public Key to the Cardholder’s + Blockchain Assets + and authentication of Card Transactions with the purpose of transfer of Blockchain + Assets for goods and + services purchasing through the Visa payments infrastructure.

      +


    4. +
    5. The Cardholder + agrees that these Terms are binding.

    6. +
    7. The + Cardholder shall be + the only person having physical access to the Card.

    8. +
    9. + The Cardholder acknowledges and agrees that SaltPay does not provide backup or + recovery of Private Key + and Public Key stored by the Card. The Cardholder creates the backup of the SaltPay + Card at the point at + which they activate the Card through the Tangem Mobile Application by linking their + Card with Tangem + Wallet.

      +


    10. +
    11. + The conditions of + use of the Tangem Wallet and Mobile Application are defined in the terms of service + for the Tangem + Wallet and Mobile Application, which will also be available upon downloading the + application.

      +


    12. +
    +
  4. +
  5. + SaltPay’s + obligations

    +
      +
    1. + SaltPay agrees to + provide the Cardholder with the Service in accordance with these Terms and within + the agreed scope.

      +
    2. +
    3. + Under the condition + that the Cardholder exercises their obligations set out under Clause 4 of these + Terms, SaltPay shall use + its best endeavours to ensure that the Card will function properly and without + restriction until the + date on which the Card expires.

    4. +
    5. + SaltPay shall ensure + that the Card prevents duplication of the Private Key, and that the Cardholder has + exclusive control + over the Blockchain Asset unless the contrary is imposed by specific blockchain + network rules e.g., two + or more private keys can be used to control the same Blockchain Asset.

      +


    6. +
    7. SaltPay + agrees that it will not keep any records of Cardholder information, save for the + Cardholder’s full name, + including but not limited to the amount of Blockchain Asset stored on the Card, the + Private Key or + personalised history of Card usage.

      +


    8. +
    9. For the + avoidance of doubt, SaltPay does not guarantee that the operation of Card and/or + Tangem Mobile + Application will be secure, accurate, complete, uninterrupted, without error or free + of viruses, worms, + other harmful components or other program limitations. SaltPay may, at its sole + discretion and without + obligation to do so, correct, modify, amend, enhance, improve and make any other + changes to Card.

      +


    10. +
    +
  6. +
  7. The + Cardholder’s + obligations

    +
      +
    1. + By + accepting these Terms, you represent and warrant that (a) you have the authority to + execute and perform + the obligations contained here; (b) you (and your employees, directors, contractors + and agents) will + comply with all law applicable to your business and the use of the Card; and (c) you + will not use the + Card, directly or indirectly, for any fraudulent or illegal undertaking.

    2. +
    3. + Upon receiving the Card, the Cardholder, if they choose to use the Tangem Service, + should download the + Tangem Mobile Application in order to determine the amount of Blockchain Asset + stored on the Card.

      +


    4. +
    5. + When the Cardholder + pays for goods or services using their SaltPay Oly Card through contactless + confirmation using NFC, they + confirm that there is a sufficient balance in the Tangem Wallet and thereby agree + that amount is + deducted from the account.

    6. +
    7. + The Cardholder shall at all times keep the Card and the means of access (backup card + and pin codes) to + the Card separate from each other.

    8. +
    9. + The Cardholder shall at all times be aware of where the Card is and regularly ensure + that it is still in + their possession. The Cardholder shall avoid even temporary possession of the Card + by any other

      +

      + person. If the Card is lost, stolen or destroyed, control over the corresponding + Blockchain Asset + may be permanently lost.

    10. +
    11. + The Cardholder shall treat the Card with care and protect the Card from mechanical + damage, high + temperatures, strong electromagnetic fields, and other harmful factors.

    12. +
    13. + In the event that the + Card is stolen or + lost, and/or if a Cardholder becomes aware of an unauthorised or incorrectly + initiated transaction, + the Cardholder shall contact Tangem as soon as possible by sending an email + to support@tangem.com + or by using the chat function on the Official Mobile Application. +

      +


    14. +
    15. + In the event that the + Cardholder wishes + to deactivate their account, they shall contact Tangem by sending an email + to support@tangem.com + or by using the chat function on the Official Mobile Application. +

      +
    16. +
    17. + The Card can be used only with the official Tangem Mobile Application downloaded + from Google Play and + App Store.

    18. +
    19. + The Tangem Mobile Application shall be the exclusive source of information + concerning the Blockchain + Address of the Blockchain Asset and corresponding Public Key stored on the Card.

      +
    20. +
    21. + The Cardholder shall only use Near-Field Devices (“NFC Devices”) that are + capable of running the + Tangem Mobile Application.

    22. +
    23. + The Card shall be used only for physically tapping and holding near the Cardholder’s + NFC Device when the + Tangem Mobile Application requests it. For the avoidance of doubt, the Card cannot + be used to pay using + any method which involves gateways.

      +


    24. +
    +
  8. +
  9. The + Cardholder’s liabilities

    +
      +
    1. + The Cardholder is liable for all liabilities arising from the use and misuse of the + Card. In any case, + the Cardholder is liable for all transactions authorised using a means of + access.

    2. +
    3. + Any loss or damage resulting from the forwarding of the Card and/or means of access + shall be borne by + the Cardholder.

    4. +
    5. + Loss or damage incurred by the Cardholder in connection with the possession or use + of the Card shall be + borne solely by the Cardholder. SaltPay assumes no liability if the Card cannot be + used due to a + technical defect or because it has been cancelled or blocked.

      +


    6. +
    7. + Cardholder is only + permitted to use the Products for his personal, non-commercial use. Cardholder is + not permitted to + resell the Card.

      +


    8. +
    +
  10. +
  11. + Limitation of + liability

    +
      +
    1. + The Card is provided as-is, and except as expressly stated in these Terms, SaltPay + provides no express + or implied warranties or conditions, and SaltPay disclaims and excludes any implied + terms, + representations, warranties, and conditions with respect to the Card, including + warranties of + merchantability, fitness for a particular purpose, title, satisfactory quality and + non-infringement, as + well as any other implied warranties, such as warranties regarding data loss, + availability, accuracy, + functionality and lack of viruses. These disclaimers will apply except to the extent + applicable law does + not permit them. Any warranties, guarantees, or conditions that cannot be disclaimed + as a matter of law, + but which may be limited in duration, last for one year from the date on which you + receive the Card.

      +
    2. +
    3. + SaltPay shall not be liable and does not warrant or make any representations + regarding the services + provided by third parties in relation to the Tangem Service, including but not + limited to the Tangem + Mobile Application and the Utorg Service. The relationship between the Cardholder + and any third party in + relation to the Tangem Service shall be governed by separate agreements provided + below.

    4. +
    5. + In + no event shall SaltPay and/or any of its Affiliates be liable for any damages + whatsoever, including + direct, indirect, extraordinary, incidental or consequential damages of any kind + resulting from or + arising out of the use of Card and/or the Tangem Mobile Application or inability to + use Card and/or the + Tangem Mobile Application, failure of Card and/or the Tangem Mobile Application to + perform as + represented or expected, loss of goodwill or profits.

    6. +
    7. + SaltPay shall not be held liable for any failure to be able to use the Card and/or + the Tangem Mobile + Application for any reason whatsoever, nor will SaltPay be held liable for the loss + of the Blockchain + Asset resulting from a malfunction or inoperability of the blockchain network + hosting Blockchain Asset, + as well as the inaccessibility of its public servers and services.

    8. +
    9. + SaltPay shall not be held liable for the loss of profits, income, value or any + indirect, extraordinary, + consequential, exemplary or punitive damages.

    10. +
    11. + SaltPay shall not be + held liable for loss or breakdown of the Card and/or the Tangem Mobile + Application.

    12. +
    13. + SaltPay shall not be held liable for any loss of Blockchain Asset in the event of + loss or total + breakdown of the Card and/or the Tangem Mobile Application.

    14. +
    15. + The Cardholder hereby acknowledges and agrees that these limitations of liability + are agreed allocations + of risk constituting in part the consideration for using the Card and/or the Tangem + Mobile Application + and such limitations will apply notwithstanding the failure of essential purpose of + any limited remedy, + and even if SaltPay and/or any SaltPay Affiliates have been advised of the + possibility of such + liabilities and/or damages.

      +


    16. +
    +
  12. +
  13. Term + and + Termination

    +
      +
    1. + These Terms become effective upon the date (i) you accept these Terms online (or in + another manner + expressly approved by us) or (ii) you first access or use Tangem and shall terminate + on the date on + which the Card expires.

      +


    2. +
    3. + We may terminate + our arrangement with you under these Terms for any reason or no reason, at any time, + by giving 2 months’ + prior notice to this effect. We may also suspend your access to Tangem if you: (i) + have breached these + Terms or any other agreement you have with SaltPay, including SaltPay’s policies or + instructions; or + (ii) provided any false, incomplete, inaccurate, or misleading information or + otherwise engaged in + fraudulent or illegal conduct c) have misused the Service.

      +


    4. +
    5. + Where your + arrangement with us under these Terms terminates for any reason, you will, where + applicable, remain + liable for any fees, charges and other payment obligations you owe us.

      +


    6. +
    +
  14. +
  15. + General

    +
      +
    1. + Except as expressly provided herein, these Terms are a complete statement of the + agreement between you + and us regarding your use of Tangem. In the event of a conflict between these Terms + and the SaltPay + General Terms or any other SaltPay agreement or policy, these Terms shall + prevail.

      +


    2. +
    3. + These Terms do not + limit any rights that SaltPay may have under trade secret, copyright, patent, or + other laws. No waiver + of any term of these Terms shall be deemed a further or continuing waiver of such + term or any other + term.

    4. +
    5. + In + the event any court shall declare any section or sections of this Agreement invalid + or void, such + declaration shall not invalidate the entire Agreement and all other paragraphs of + the Agreement shall + remain in full force and effect.

    6. +
    7. + For + the avoidance of doubt, + provisions around general terms not covered in these Terms of Service, including + but not limited to + restrictions and unauthorised or illegal use, intellectual property, data, + taxes, indemnification, + warranties, severability, force majeure, and disputes, shall be governed by + the SaltPay General + Terms.

    8. +
    +
  16. +
+

TANGEM + WALLET AND TANGEM MOBILE APPLICATION

+

Terms of Service

+


+

PLEASE READ + THESE TERMS OF SERVICE + CAREFULLY. BY CLICKING TO ACCEPT, OR BY ACCESSING OR USING OUR SERVICES, YOU AGREE THAT YOU HAVE + READ, UNDERSTOOD, + AND ACCEPT ALL OF THE TERMS AND CONDITIONS CONTAINED HEREIN. BY PURCHASE OF THE SALTPAY OLY CARD + WITH THE TANGEM + WALLET OR BY USING THE SALTPAY OLY CARD OR BY USING TANGEM WALLET OR BY USING TANGEM MOBILE + APPLICATION, YOU + DEMONSTRATE YOUR AGREEMENT TO THESE TERMS AND CONDITIONS CONTAINED HEREIN.

+


+
    +
  1. + DEFINITIONS

    +


    +


    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

    + “Cardholder

    + means a person who owns Tangem Wallet and SaltPay Oly Card.

    + “Blockchain Asset

    + means a digital data stored in and protected by the blockchain network, + including but not limited to + ownership right and value of cryptocurrency, tokenized assets, non-fungible + asset.

    + “Blockchain Address

    + means a unique identifier that serves as a virtual location of the Blockchain + Asset in the + blockchain.

    + “Card Transaction

    + means transfer of Blockchain Asset from the Blockchain Address associated with + the Public Key stored + on the SaltPay Oly Card for goods and services purchased through traditional + payments rail.

    + “Official Mobile Application

    + means an application developed and distributed by Tangem, providing + interoperability between SaltPay + Oly Card or Tangem Wallet and blockchain, and working on NFC-capable smartphones + and tablets using + Google Android and Apple iOS operation systems.

    + “Private Key

    + a secret cryptographic key which provides full control over a Blockchain + Asset.

    + “Tangem

    + means Tangem AG, Baarerstrasse 10, 6300 Zug.

    + “Public Key

    + means a cryptographic key, which provides access to information about a + Blockchain Asset, including + but not limited to Blockchain Address

    + “Services

    + means the purchase and/or use of the Tangem Wallet and the services or any other + features, + technologies or functionalities linked to the Card provided or operated by + Tangem AG via the website + or Official Mobile Applications.

    + “Tangem Wallet/Tangem Card / Card

    + means physical card which stores Private Key and Public Key and is used as a + backup card for SaltPay + Oly Card.

    +


  2. +
  3. GENERAL + PROVISIONS

    +


    +
      +
    1. + These Terms of Service (the “Terms”) govern the use of Tangem Wallet and/or + Official Mobile + Application provided by Tangem AG (referred to as "Tangem", + "we" or + "us" in this document) and the related services or any other + features, technologies or + functionalities linked to the Tangem Wallet. Tangem is a company

      +

      + incorporated under the laws of Switzerland, with a registered address at + Baarerstrasse 10, 6300 Zug, + Switzerland.

      +


    2. +
    3. + Tangem Wallet + represents a backup to the SaltPay Oly Card, which may be used only for storage of + Private Key and + Public Key to Cardholder’s Blockchain Assets. Tangem Wallet, unlike SaltPay Oly + Card, can’t be used for + goods and services purchasing through traditional payments rail. The use of SaltPay + Oly Card is governed + by a separate Terms of Service, provided above.

      +


    4. +
    5. + Official Mobile + Application is intended for usage only with SaltPay Oly and Tangem Wallet, providing + interoperability + between the cards and blockchain via NFC interface. Official Mobile Application DOES + NOT:

      +
        +
      1. + Generate, store, transmit, or have access to private (secret) cryptographic + keys to blockchain + wallets holding Blockchain assets.

      2. +
      3. + Generate, + store, transmit, or have access to secret keys, passwords, passphrases, + recovery phrases that + can be used to restore or to copy private (secret) keys to blockchain + wallets holding Blockchain + assets.

      4. +
      5. Provide + exchange, trading, investment services on behalf of Tangem.

        +


      6. +
      +
    6. +
    +
  4. +
  5. + RIGHTS AND + OBLIGATIONS

    +


    +
      +
    1. + Cardholder agrees that + these Terms are binding.

      +


    2. +
    3. + Cardholder shall be the + only person having physical access to the Tangem Wallet.

      +


    4. +
    5. + Cardholder acknowledges and agrees that Tangem does not provide backup or recovery + of Private Key and + Public Key stored by the Tangem Wallet.

      +


    6. +
    7. + Tangem does not + keep records of Cardholder’s personal data, the amount of Blockchain Asset stored on + the Card, the + Private Key, or personalized history of Card usage.

      +


    8. +
    +
  6. +
  7. + COSTS

    +


    +
      +
    1. + Costs, fees and commission (the “Costs”) may be charged in connection with the use + of SaltPay Oly Card. + These Costs are disclosed in the Official Mobile Application to the Cardholder.

      +


    2. +
    3. + Amendments to Costs + due to changing expenses or market conditions may be made at any time via + adjustments to the fee + schedules. Such amendments shall be communicated to the Cardholder in an appropriate + manner. Upon + notification and in the event of the objection, the Cardholder may cancel the + SaltPay Oly Card with + immediate effect.

    4. +
    +
  8. +
  9. + CARDHOLDER’S DUTIES OF CARE

    +


    +

    In particular, the + Cardholder shall exercise the + following duties of care:

    +


    +
      +
    1. + Upon receiving the SaltPay Oly Card and the Tangem Wallet, the Cardholder should + download the Official + Mobile Application from Google Play or Apple app store and create the backup of the + SaltPay Oly Card by + linking SaltPay Oly card with Tangem backup wallet following the instructions in the + Official Mobile + Application.

      +


    2. +
    3. Cardholder + shall keep the SaltPay Oly Card and Tangem Wallet with care and separate from each + other.

      +


    4. +
    5. + Cardholder must always know where the Tangem Wallet is and regularly ensure that it + is still in his/her + possession. He/she shall avoid even temporary possession of the Tangem Wallet by any + other person.

      +


    6. +
    7. + Cardholder shall + treat the Tangem Wallet in the same manner as physical money (cash) and keep it + safe. If the SaltPay Oly + Card and Tangem Wallet are lost, stolen or destroyed at the same time, control over + the corresponding + Blockchain Asset may be permanently lost.

      +


    8. +
    9. + Official Mobile + Application shall be the only source of information about the Blockchain Address of + the Blockchain Asset + and corresponding Public Key stored on the SaltPay Oly Card/Tangem Wallet.

      +


    10. +
    11. + Cardholder shall + only use Near-Field Devices (the “NFC”) devices that are capable of running Official + Mobile + Applications. He/she shall avoid leaving the Tangem Wallet in the proximity of the + NFC devices of other + persons.

      +


    12. +
    13. + Tangem Wallet shall + be used only for physically tapping and holding near Cardholder’s NFC device when + Official Mobile + Application requests it.

      +


    14. +
    15. + Cardholder shall + keep the Tangem Wallet with care and protect the Tangem Wallet from mechanical + damage, high + temperatures, strong electromagnetic fields, and other harmful factors.

      +


    16. +
    +
  10. +
  11. + RIGHTS AND + RESPONSIBILITIES OF CARDHOLDER

    +


    +
      +
    1. + The Cardholder is liable for all liabilities arising from the use of the Tangem + Wallet and/or Tangem + Official Mobile Application. As a matter of principle, the Cardholder is liable for + any risks resulting + from the misuse of the Tangem Wallet and/or Official Mobile Application. In any + case, the Cardholder is + liable for all transactions authorized using a means of access.

      +


    2. +
    3. + Any loss or damage + resulting from the forwarding of the Tangem Wallet and/or means of access shall be + borne by the + Cardholder.

      +


    4. +
    5. + Loss or damage + incurred by the Cardholder in connection with the possession or use of the Tangem + Wallet and/or Official + Mobile Application shall be borne solely by the Cardholder. Tangem assumes no + liability if the Tangem + Wallet and/or Official Mobile Application cannot be used due to a technical defect + or because it has + been canceled, blocked or the spending limit has been adjusted.

      +


    6. +
    7. + Cardholder is only + permitted to use the Tangem Wallet and Official Mobile Application for his personal, + non-commercial use. + Cardholder is not allowed to resell the Tangem Wallet.

    8. +
    9. + Cardholder is solely responsible to determinate what, if any, taxes apply to Card + Transactions. Tangem + or contributors to the Official Mobile Application are NOT responsible for + determining the taxes that + apply to Card Transactions.

      +


    10. +
    11. + Before Cardholder + engages in transactions + using an electronic system, Cardholder should carefully review the rules and + regulations of the + third-party provider Utorg OÜ (https://utorg.pro/terms/), + who provides exchange services for SaltPay Oly Card. Online trading has inherent + risk due to system + response and access times that may vary due to market conditions, system + performance, and other factors. + Cardholder should understand, fully accept and take on these and additional risks + before executing any + Card Transactions.

      +


    12. +
    13. + There is + considerable exposure to risk in the Blockchain Asset exchange transaction. Any + transaction involving + the Blockchain Asset involves risks including, but not limited to, the potential for + changing economic + conditions that may substantially affect the price or liquidity of the Blockchain + Asset. Investments in + the Blockchain Asset exchange speculation may also be susceptible to sharp rises and + falls as the + relevant market values fluctuate. It is for this reason that when speculating in + such markets it is + advisable to use only risk capital.

      +


    14. +
    +
  12. +
  13. + RESPONSIBILITIES AND + LIABILITIES OF TANGEM

    +


    +
      +
    1. + Tangem does not warrant or make any representations regarding the use, the inability + to use or operate, + or the results of the use or operation of the Tangem Wallet and/or Tangem Official + Mobile + Application.

      +


    2. +
    3. Tangem does not + provide any backup or recovery of Private Key and Public Key stored on the Tangem + Wallet.

      +


    4. +
    5. + Tangem does not keep any records of Cardholder information, the amount of Blockchain + Asset stored on the + Tangem Wallet or SaltPay Oly Card, the Private Key, or personalized history of cards + usage.

      +


    6. +
    7. + Tangem shall not be + held liable for any failure to be able to use the Tangem Wallet and/or Official + Mobile Application, for + any reason whatsoever, nor will Tangem be held liable for the loss of the Blockchain + Asset resulting + from a malfunction or inoperability of the blockchain network hosting Blockchain + Asset, as well as the + inaccessibility of its public servers and services.

      +


    8. +
    9. + The Tangem does not + guarantee that the operation of Tangem Wallet and/or Official Mobile Application + will be secure, + accurate, complete, uninterrupted, without error or free of viruses, worms, other + harmful components or + other program limitations. Tangem may, at its sole discretion and without obligation + to do so, correct, + modify, amend, enhance, improve and make any other changes to Tangem Wallet and/or + Official Mobile + Application.

      +


    10. +
    11. + Tangem shall not be + held liable for the loss of profits, income, value or any indirect, extraordinary, + consequential, + exemplary or punitive damages.

      +


    12. +
    13. Tangem shall + not be held liable for loss or breakdown of the Tangem Wallet.

      +


    14. +
    15. + Tangem shall not be held liable for any loss of Blockchain Asset in the event of + loss or total breakdown + of the Tangem Wallet or SaltPay Oly Card.

    16. +
    +
  14. +
  15. + GUARANTEES

    +


    +
      +
    1. + Under the condition that the Cardholder exercises the duties of care as stated in + Clause 5, Tangem + guarantees that the Tangem Wallet will function properly and without restriction for + a period of 2 (two) + years.

      +


    2. +
    3. + Tangem guarantees + that the Tangem Wallet prevents duplication of the Private Key and that the + Cardholder has exclusive + control over the Blockchain Asset unless the contrary is imposed by specific + blockchain network + rules,

      +

      + e.g. two or more + private keys can be used to control the same Blockchain Asset.

      +


    4. +
    +
  16. +
  17. + LIMITATION OF LIABILITY

    +


    +
      +
    1. + The Tangem Wallet, including without limitation any content, data and information + related thereto, is + provided on an “as is” basis and “as available” basis, without any warranties of any + kind, express or + implied warranties of use, merchantability or suitability for a certain purpose or + use, including + without limitation, the quality of products and services provided by users, + third-party services, and/or + exchanges (except for the guarantees set forth in Clause 8).

      +


      +

      + Official Mobile + Application is provided on an “as is” basis and “as available” basis without any + warranties of any + kind regarding Official Mobile Application and/or any content, data, materials + and/or services + provided on Official Mobile Application.

      +


    2. +
    3. + Tangem and its + affiliates, including any of their officers, directors, shareholders, employees, + sub-contractors, + agents, parent companies, subsidiaries and other affiliates (collectively, the + “Tangem Affiliates”), + jointly and severally, disclaim and make no representations or warranties as to the + usability, accuracy, + quality, availability, reliability, suitability, completeness, truthfulness, + usefulness or effectiveness + of any content, data, results or other information obtained or generated by Tangem + and/or any user + related to you or any other user of the Tangem Wallet, and Official Mobile + Applications.

      +


    4. +
    5. + In no event shall + Tangem and/or any of the Tangem Affiliates be liable for any damages whatsoever, + including direct, + indirect, extraordinary, incidental or consequential damages of any kind, but not + limited to, resulting + from or arising out of the use of Tangem Wallet and/or Official Mobile Applications + or inability to use + Tangem Wallet and/or Official Mobile Applications, failure of Tangem Wallet and/or + Official Mobile + Applications to perform as represented or expected, loss of goodwill or profits, or + loss of data arising + out of or in any way connected with the use of Tangem Wallet and/or Official Mobile + Applications. In no + event shall Tangem and/or any of the Tangem Affiliates be liable for the performance + or failure of the + Tangem Wallet and/ or Official Mobile Applications to perform under these Terms of + Use and any other act + or omission by Tangem by any cause whatsoever including without limitation damages + arising from the + conduct of any users, third party services and/or exchanges. In no way Tangem or + contributors to + Official Mobile Application are responsible for the actions, decisions, or other + behavior taken or not + taken by Cardholder in reliance upon Tangem Official Mobile Application.

      +


    6. +
    7. + You hereby + acknowledge and agree that these limitations of liability are agreed allocations of + risk constituting in + part the consideration for using the Tangem Wallet and Official Mobile Applications + and such limitations + will apply notwithstanding the failure of essential purpose of any limited remedy, + and even if Tangem + and/or any Tangem Affiliates has been advised of the possibility of such liabilities + and/or damages.

      +


    8. +
    9. + Tangem will not be + responsible for any losses, damages or claims arising from events falling within the + scope of the + following five categories:

      +
        +
      1. + Mistakes made by Cardholder, e.g., forgotten passwords, payments sent to + wrong addresses, and + accidental deletion of blockchain wallets on Tangem Wallet or SaltPay Oly + cards.

      2. +
      3. + Problems of + Official Mobile Application and/or any blockchain- or cryptocurrency- + related software or + service, e.g., corrupted files, incorrectly constructed transactions, unsafe + cryptographic + libraries, malware.

      4. +
      5. + Technical + failures in the hardware of Cardholder, including cards, of any blockchain- + or + cryptocurrency-related software or service, e.g., data loss due to a faulty + or damaged storage + device.

      6. +
      7. + Security + problems experienced by Cardholder, + e.g., unauthorized + access to Cardholders' wallets and/or accounts.

      8. +
      9. + Actions or + inactions of third parties and/or events experienced by third parties, e.g., + bankruptcy of + service providers, information security attacks on service providers, and + fraud conducted by + third parties.

        +


      10. +
      +
    10. +
    +
  18. +
  19. + SEVERABILITY

    +


    +
      +
    1. + In + the event any court shall declare any section or sections of this Agreement invalid + or void, such + declaration shall not invalidate the entire Agreement and all other paragraphs of + the Agreement shall + remain in full force and effect.

      +


    2. +
    +
  20. +
  21. + ARBITRATION AND + GOVERNING LAW

    +


    +
      +
    1. + These Terms of Service are governed exclusively by the laws of Switzerland, without + regard to its + conflict of law rules. You consent to the exclusive jurisdiction of the court of + Zug, Switzerland for + any dispute arising under this Agreement.

    2. +
    +
  22. +
+

+ Utorg OÜ Terms of Use

+


+

These Terms of + Use constitute an + electronic agreement between you (hereinafter the "User") and Utorg OÜ (hereinafter + the “Utorg.pro”, + “we”, “us”, “our”), a company incorporated in Estonia with Company No. + 14786273, located at + Roosikrantsi tn 2-1068, Kesklinna linnaosa,Tallinn, Harju maakond, 10119, that applies to the + User's use of this + website, any and all services, products and content provided by Utorg.pro.

+


+

As used herein, + "Utorg.pro" refers to the company Utorg OÜ, including but not limited to, its + owners, directors, + investors, officers, employees, agents or other related parties, unless otherwise provided + herein.

+


+

These Terms of + Use contain + important provisions, which the User must consider carefully when choosing whether to use the + services, products and + content of Utorg.pro. Please read these Terms of Use carefully before agreeing to them.

+


+

The User is + solely responsible for + understanding and complying with any and all laws, rules and regulations of his/her specific + jurisdiction that may + be applicable to the User in connection with the use of any and all services, products and + content of Utorg.pro.

+


+

These Terms of Use + incorporate our Privacy Policy + as well as any other policies published on the Site by reference, so by accepting these Terms of Use, the User agrees with and accepts all the policies published on the Site. +

+


+

IF THE USER + DOES NOT ACCEPT THESE + TERMS OF USE, THE USER SHALL NOT USE ANY OF UTORG.PRO’S SERVICES, PRODUCTS AND CONTENT.

+


+
    +
  1. + Terms and + Definitions

    +


    +

    + “Account” + shall mean a record created and maintained by the Software that accumulates and stores + all information about + the Cardholder, including without limitation Transactional history, Cryptocurrency + balance, and the + Cardholder personal information.

    +

    + “SaltPay + Oly Card” shall mean a plastic card linked to the Account. “Cardholder” shall + mean an individual + or entity holding a SaltPay Oly Card.

    +

    + "Confidential + Information" shall mean any data or information, oral or written, treated as + confidential that + relates to either party’s (or, if either party is bound to protect the confidentiality + of any third party’s + information, such third party’s) past, present, or future research, development or + business activities, + including any unannounced products and services, any information relating to Services, + developments, + relevant documentation (in whatever form or media provided), inventions, processes, + plans, financial + information, end-user data, revenue, transaction volume, forecasts, projections, and the + financial terms of + this Agreement. Notwithstanding the foregoing, Confidential Information shall not be + deemed to include + information if: (i) it was already known to the receiving party prior to the Effective + Date of this + Agreement, as established by documentary evidence; (ii) it is in or has entered the + public domain through no + breach of this Agreement or other wrongful act of the receiving party; (iii) it has been + rightfully received + by the receiving party from a third party and without breach of any obligation of + confidentiality of such + third party to the owner of the Confidential Information; (iv) it has been approved for + release by written + authorization of the owner of the Confidential Information; or, (v) it has been + independently developed by a + party without access to or use of the Confidential Information of the other party.”

    +

    + “Cryptocurrency” + shall mean any medium of exchange using cryptography to secure transactions and to + control the creation of + new units, as bitcoins (http://bitcoin.org), or any other that meet those requirements. +

    +

    + “Force-Majeure + Event” shall mean any act or event beyond a Party’s reasonable control, including + without limitation + non-delivery or defective delivery of third party services necessary to provide the + Services (including but + not limited to those of our partners, vendors and suppliers), strikes, lock-outs or + other industrial action + by third parties, civil commotion, riot, invasion, terrorist attack or threat of + terrorist attack, war + (whether declared or not) or threat or

    +

    + preparation for war, fire, explosion, storm, flood, earthquake, subsidence, epidemic or + other natural + disaster, failure of public or private telecommunications networks, worldwide Web + unavailability or + malfunction, governmental prohibition or other limitation of Cryptocurrencies, or + seizing of infrastructure + and operations of Cryptocurrencies.

    +

    + “Over-the-counter + (OTC) deal” shall mean a direct, off-exchange, Transaction between Utorg and + Cardholder. “Price” + shall mean a "price per coin" for which users are willing to purchase or sell + Cryptocurrency using + the Services. “Relevant Exchange Rate” shall mean the weighted spot price average + at the time of the + transaction.

    +

    Services” + shall mean + providing services of exchanging a virtual currency against a fiat currency. + “Site” means Utorg.pro’s website + at https://Utorg.pro.

    +

    Specified + Bank Account” + means the bank account belonging to Salt Pay IIB hf., the details of which shall be + provided to Utorg.pro. +

    +

    Software” + shall mean a + software owned by Utorg and enabling Transactions’ processing.

    +

    + “Transaction” + shall mean any of the following: (i) a purchase of the Cryptocurrency by the Cardholder; + (ii) a sale of the + Cryptocurrency by the Cardholder; (iii) a transfer of Cryptocurrency to a Cardholder. +

    +


  2. +
  3. + Scope of the + Services

    +


    +
      +
    1. + Utorg undertakes to + provide the Cardholder with Services, including but not limited to OTC deal. For the + avoidance of doubt, + Utorg shall use the Relevant Exchange Rate when executing the OTC deal.

      +


    2. +
    3. + The Services will + be provided in such a manner that Utorg will be acting on its own behalf strictly + under the Cardholder’s + instructions regarding any Transaction as well as other matters which the Cardholder + consider to be + necessary hereunder and are accepted by Utorg.

      +


    4. +
    5. + Utorg will treat + the each individual Cardholder as its sole client to which it owes obligations and + will bear no + responsibility to third parties for whom the Cardholder may be acting as an agent, + intermediary or + fiduciary, whether or not the existence or identity of such person(s) has been + disclosed to, or is known + by Utorg.

      +


    6. +
    7. + Utorg does not + provide banking, or payment Services and does not hold or store the Cardholder’s + fiat or crypto + currencies.

      +


    8. +
    +
  4. +
  5. + Warranties

    +


    +

    The + Cardholder hereby warrants + and represents to Utorg as follows:

    +
      +
    1. + It + represents and warrants that all than all outgoing Transactions, cryptocurrency + and/or fiat currency, + are made only to the Specified Bank Account, and Utorg shall not be liable for the + consequences of any + other incoming Transactions.

      +


    2. +
    3. + It also represents + and warrants than all incoming Transactions, cryptocurrency and/or fiat currency, + are made only from its + own wallet and/or bank account, and Utorg shall not be liable for the consequences + of any other incoming + Transactions.

      +


    4. +
    5. + It shall use the + Services in strict conformity with the Terms of Use and for no other purpose and in + no other manner. All + other warranties and representations regarding the use of the Services set forth + below shall be in + addition to and not in limitation of this general overriding warranty and + representation.

      +


    6. +
    7. + The Cardholder represents + and warrants to accept + and comply with all policies posted on Utorg.pro website: https://utorg.pro, Terms of Use (https://utorg.pro/terms/), Privacy Policy + (https://utorg.pro/privacy-policy), KYC/AML Policy + (https://utorg.pro/kyc-aml-policy). +

    8. +
    +
  6. +
  7. Rights + and Obligations

    +


    +
      +
    1. The Cardholder + has right to:

      +


      +
        +
      1. ask + Utorg for any type of assistance and support;

        +


      2. +
      3. + request a confirmation letter stating all information on Cardholder’s + Transactions; and

        +


      4. +
      5. + be promptly notified of any relevant changes, including but not limited to Commissions, bank details, cryptocurrency wallets, etc. +

        +


      6. +
      +
    2. +
    3. + Utorg has the right + to stop providing the Services and terminate the Agreement immediately in case of + any breach of the + Agreement or/and the Terms of Use;

      +


    4. +
    5. Utorg + undertakes to provide the Cardholder with full assistant and support regarding the + Service + provision.

      +


    6. +
    +
  8. +
  9. KYC + Verification

    +


    +
      +
    1. + Identification and + verification procedures (also known as “Know Your Customer” or “KYC”) + are required for all + Transactions, which shall be undertaken by Utorg. If the User refuses to provide + required documents and + information under KYC, including additional information that can be requested after + the business + relationships were established, Utorg.pro reserves the right to immediately + terminate the Services + provision to the User.

      +


    2. +
    3. + The User hereby + authorises Utorg.pro to, directly or indirectly (through third parties), make any + inquiries as + considered necessary to check the relevance and accuracy of the information provided + for verification + purposes. Personal Data transferred will be limited to strictly the necessary and + with security measures + in use to protect the data.

      +


    4. +
    +
  10. +
  11. + Regulatory + Compliance

    +


    +
      +
    1. + For the purposes of + anti-money laundering prevention and combatting terrorist activities, Utorg performs + a thorough due + diligence on each of its customers. Cardholder will have to provide all documents + and information + requested by Utorg for KYC purposesin order to be able to use the Services. Utorg + will keep records of + the Cardholder’s information and documents safe and confidential in accordance with + its Privacy Policy + and the applicable legal and regulatory requirements.

      +


    2. +
    3. + Utorg reserves the + right to request any additional information about the Cardholder and/or the + Cardholder’s Transaction, + and the Cardholder agrees to provide such documents and information in due time. + Utorg has a right to + decline to deliver the Services anytime in case the documents or information + provided by the Cardholder + are unsatisfactory or insufficient, or in case such actions are necessary to comply + with the applicable + laws and regulations, as Utorg may decide in its sole discretion.

      +


    4. +
    5. + The Cardholder will + be able to perform Transactions within certain amounts according to the limits + established by Utorg and + the applicable law. Cardholder will be able to provide some additional information + as instructed by + Utorg to increase or eliminate these limits.

      +


    6. +
    +
  12. +
  13. + Intellectual + Property

    +


    +
      +
    1. + All content at + Utorg's website is the property of Utorg and is protected by copyright, patent, + trademark and any + other applicable laws, unless otherwise specified hereby.

      +


    2. +
    3. + The trademarks, + brands, trade names, service marks and logos of Utorg and others used on the Site + (hereinafter, the + "Trademarks") are the property of Utorg and its respective owners. + The software, + applications, text, images, graphics, data, prices, trades, charts, graphs, video + and audio materials + used on this Site belong to Utorg. The Trademarks and other content on the Platform + and at Utorg's + website should not be copied, reproduced, modified, republished, uploaded, posted, + transmitted, scraped, + collected or distributed in any form or by any means, whether manual or automated. + The use of any + content from the Platform and at Utorg's website on any other site or a + networked computer + environment for any other purpose

      +

      + is + strictly prohibited; any such unauthorized use may violate copyright, patent, + trademark and any + other applicable laws and could result in criminal or civil + penalties/liabilities.

      +


    4. +
    +
  14. +
  15. + Confidentiality

    +


    +
      +
    1. + Any information of + a Party (hereinafter, the “Disclosing Party”) which is not public and which + was obtained by the + other Party (hereinafter, the “Receiving Party”) in the course of performance + of this Agreement, + whether proprietary to such other Party or any third party, whether expressly marked + as confidential or + not and regardless of the form and manner, in which it was obtained, shall be deemed + “Confidential + Information”.

      +


    2. +
    3. With respect to + any and all Confidential Information of the Disclosing Party the Receiving Party + agrees to:

      +


      +
        +
      1. + hold it in + confidence and treat it with the same degree of care and diligence, as it + employs with respect + to its own confidential information, but in no case less than a reasonable + degree of care and + diligence;

        +


      2. +
      3. use + it solely for the purpose of performance of this Agreement;

        +


      4. +
      5. + communicate + it only to the employers and agents of the Receiving Party who have a need + to know such + Confidential Information for the purpose of performance by the Receiving + Party of this Agreement + and who are subject to confidentiality obligations with respect to such + information; and

        +


      6. +
      7. + upon + request of the Disclosing Party or following termination of this Agreement + promptly return to + the Disclosing Party any and all material embodiments of its Confidential + Information and + destroy those material embodiments of Confidential Information return of + which is not + practicable and certify such destruction to the Disclosing Party in + writing.

        +


      8. +
      +
    4. +
    5. + The Parties hereby + agree and acknowledge that restrictions on use of Confidential Information set forth + in Clauses 9.1 and + 9.2 above shall not apply to information, which (subject to the burden of proof + being on the Receiving + Party):

      +


      +
        +
      1. + was known + to the Receiving Party prior to it being obtained from the Disclosing Party + in connection with + this Agreement;

        +


      2. +
      3. + has been + developed or otherwise obtained by the Receiving Party without the use of + the Confidential + Information;

        +


      4. +
      5. has + become public through no fault of the Receiving Party.

        +


      6. +
      +
    6. +
    7. + The Parties hereby + agree that limitations on use and disclosure of Confidential Information as per the + Agreement shall not + apply to any disclosure made in accordance with a valid law, court order, or other + statutory act or + decision of a judicial or governmental authority that has relevant jurisdiction over + the Receiving Party + (the ”Act”), provided that:

      +


      +
        +
      1. The + Receiving Party has provided the Disclosing Party with notice of the Act as + soon as + practicable;

        +


      2. +
      3. + the + Receiving Party has provided all reasonable assistance to the Disclosing + Party (at the + Disclosing Party’s expense) in contesting the Act as the Disclosing Party + had requested;

        +


      4. +
      5. + the + Receiving Party has disclosed only the portion of Confidential Information + which it was obliged + to disclose as per the Act (subject to any alterations due to the Disclosing + Party contesting + the Act).

        +


      6. +
      +
    8. +
    +
  16. +
  17. + Taxation

    +

    + Utorg + makes no representations concerning the tax implications of the Transactions or the + possession or use the + Cryptocurrency. The Cardholder bears the sole responsibility to determine if the + Cryptocurrency or the + potential appreciation or depreciation in the value of the Cryptocurrency over time has + tax implications for + the Cardholder in the

    +

    + Cardholder's + home jurisdiction. By using the Services, and to the extent permitted by law, the + Cardholder agrees not to + hold Utorg liable for any tax liability associated with or arising from the Cardholder's + use of the + Services.

    +


  18. +
  19. + Governing Law

    +

    + This + Agreement shall be governed by the laws of Republic of Estonia. All disputes and + controversies arising out + of or in connection with this Agreement shall be submitted to the relevant Estonian + Court in Tallinn, as the + Court of first instance.

    +


  20. +
  21. + Dispute Resolution

    +


    +
      +
    1. + The Parties + undertake to make every possible effort to resolve disputes that may arise in the + performance of this + Agreement by means of negotiations.

      +


    2. +
    3. + Any dispute, + controversy or claim arising out of or relating to this contract, or the breach + termination or + invalidity thereof, that cannot be solved in the process of negotiations shall be + settled in the + appropriate court according to the Governing Law.

    4. +
    +
  22. +
+
+
+
+
+
+
+ + diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 6a80a26d24..b75cb31efc 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -13,9 +13,12 @@ import com.tangem.TangemSdk import com.tangem.domain.common.FeatureCoroutineExceptionHandler import com.tangem.operations.backup.BackupService import com.tangem.tangem_sdk_new.extensions.init +import com.tangem.tap.common.ActivityResultCallbackHolder import com.tangem.tap.common.DialogManager import com.tangem.tap.common.IntentHandler +import com.tangem.tap.common.OnActivityResultCallback import com.tangem.tap.common.SnackbarHandler +import com.tangem.tap.common.extensions.copyToClipboard import com.tangem.tap.common.redux.NotificationsHandler import com.tangem.tap.common.redux.global.AndroidResources import com.tangem.tap.common.redux.global.GlobalAction @@ -47,15 +50,18 @@ private val mainCoroutineContext: CoroutineContext get() = Job() + Dispatchers.Main + FeatureCoroutineExceptionHandler.create("mainScope") val mainScope = CoroutineScope(mainCoroutineContext) -class MainActivity : AppCompatActivity(), SnackbarHandler { +class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbackHolder { private var snackbar: Snackbar? = null private val dialogManager = DialogManager() private val intentHandler = IntentHandler() private val binding: ActivityMainBinding by viewBinding(ActivityMainBinding::bind) + private val onActivityResultCallbacks = mutableListOf() + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + internalActivity = this setContentView(R.layout.activity_main) systemActions() store.dispatch(NavigationAction.ActivityCreated(WeakReference(this))) @@ -67,18 +73,17 @@ class MainActivity : AppCompatActivity(), SnackbarHandler { store.dispatch(GlobalAction.SetResources(getAndroidResources())) store.dispatch( ShopAction.CheckIfGooglePayAvailable( - GooglePayService(createPaymentsClient(this), this) - ) + GooglePayService(createPaymentsClient(this), this), + ), ) } - private fun getAndroidResources(): AndroidResources { return AndroidResources( AndroidResources.RString( R.string.copy_toast_msg, - R.string.details_notification_erase_wallet_not_possible - ) + R.string.details_notification_erase_wallet_not_possible, + ), ) } @@ -92,7 +97,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler { supportFragmentManager.registerFragmentLifecycleCallbacks( NavBarInsetsFragmentLifecycleCallback(), - true + true, ) requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT @@ -137,7 +142,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler { if (snackbar != null) return snackbar = Snackbar.make( - binding.fragmentContainer, getString(text), Snackbar.LENGTH_INDEFINITE + binding.fragmentContainer, getString(text), Snackbar.LENGTH_INDEFINITE, ) if (buttonTitle != null && action != null) { snackbar?.setAction(getString(buttonTitle), action) @@ -152,12 +157,29 @@ class MainActivity : AppCompatActivity(), SnackbarHandler { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) + + onActivityResultCallbacks.forEach { it(requestCode, resultCode, data) } when (requestCode) { LOAD_PAYMENT_DATA_REQUEST_CODE -> { store.dispatch( - ShopAction.BuyWithGooglePay.HandleGooglePayResponse(resultCode, data) + ShopAction.BuyWithGooglePay.HandleGooglePayResponse(resultCode, data), ) } } } + + override fun addOnActivityResultCallback(callback: OnActivityResultCallback) { + onActivityResultCallbacks.remove(callback) + onActivityResultCallbacks.add(callback) + } + + override fun removeOnActivityResultCallback(callback: OnActivityResultCallback) { + onActivityResultCallbacks.remove(callback) + } +} + +lateinit var internalActivity: AppCompatActivity +//TODO: delete +fun copyToClipboard(text: String) { + internalActivity.copyToClipboard(text) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index 8908d91491..8fa11bf17a 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -32,8 +32,10 @@ import com.tangem.tap.domain.walletconnect.WalletConnectRepository import com.tangem.tap.network.NetworkConnectivity import com.tangem.tap.persistence.PreferencesStorage import com.tangem.wallet.BuildConfig +import com.zendesk.logger.Logger import org.rekotlin.Store import timber.log.Timber +import zendesk.chat.Chat val store = Store( reducer = ::appReducer, @@ -81,7 +83,7 @@ class TapApplication : Application(), ImageLoaderFactory { override fun newImageLoader(): ImageLoader { return createCoilImageLoader( context = this, - logEnabled = LogConfig.imageLoader + logEnabled = LogConfig.imageLoader, ) } @@ -140,12 +142,11 @@ class TapApplication : Application(), ImageLoaderFactory { infoHolder = additionalFeedbackInfo, logCollector = tangemLogCollector, preferencesStorage = preferencesStorage, - logEnabled = LogConfig.zendesk, - ) - feedbackManager.initChat( - context = context, - zendeskConfig = config.zendesk!!, ) + feedbackManager.chatInitializer = { zendeskConfig -> + Chat.INSTANCE.init(context, zendeskConfig.accountKey, zendeskConfig.appId) + Logger.setLoggable(LogConfig.zendesk) + } store.dispatch(GlobalAction.SetFeedbackManager(feedbackManager)) } diff --git a/app/src/main/java/com/tangem/tap/common/ActivityResultCallbackHolder.kt b/app/src/main/java/com/tangem/tap/common/ActivityResultCallbackHolder.kt new file mode 100644 index 0000000000..931922493c --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/ActivityResultCallbackHolder.kt @@ -0,0 +1,13 @@ +package com.tangem.tap.common + +import android.content.Intent + +/** +[REDACTED_AUTHOR] + */ +interface ActivityResultCallbackHolder { + fun addOnActivityResultCallback(callback: OnActivityResultCallback) + fun removeOnActivityResultCallback(callback: OnActivityResultCallback) +} + +typealias OnActivityResultCallback = (Int, Int, Intent?) -> Unit \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/DialogManager.kt b/app/src/main/java/com/tangem/tap/common/DialogManager.kt index ef357f782f..d3fe7eacd2 100644 --- a/app/src/main/java/com/tangem/tap/common/DialogManager.kt +++ b/app/src/main/java/com/tangem/tap/common/DialogManager.kt @@ -17,6 +17,9 @@ import com.tangem.tap.features.onboarding.AddressInfoBottomSheetDialog import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction import com.tangem.tap.features.onboarding.products.twins.ui.dialog.CreateWalletInterruptDialog import com.tangem.tap.features.onboarding.products.wallet.redux.BackupDialog +import com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog.NoFundsForActivationDialog +import com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog.RegistrationErrorDialog +import com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog.SaltPayDialog import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.AddMoreBackupCardsDialog import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.BackupInProgressDialog import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.ConfirmDiscardingBackupDialog @@ -126,6 +129,8 @@ class DialogManager : StoreSubscriber { is BackupDialog.BackupInProgress -> BackupInProgressDialog.create(context) is BackupDialog.UnfinishedBackupFound -> UnfinishedBackupFoundDialog.create(context) is BackupDialog.ConfirmDiscardingBackup -> ConfirmDiscardingBackupDialog.create(context) + is SaltPayDialog.NoFundsForActivation -> NoFundsForActivationDialog.create(context) + is SaltPayDialog.RegistrationError -> RegistrationErrorDialog.create(context, state.dialog) is WalletDialog.CurrencySelectionDialog -> CurrencySelectionDialog.create(state.dialog, context) is WalletDialog.ChooseTradeActionDialog -> diff --git a/app/src/main/java/com/tangem/tap/common/Handler.kt b/app/src/main/java/com/tangem/tap/common/Handler.kt index f36d4d1541..37b39734cb 100644 --- a/app/src/main/java/com/tangem/tap/common/Handler.kt +++ b/app/src/main/java/com/tangem/tap/common/Handler.kt @@ -15,6 +15,10 @@ fun postBackground(ms: Long = 0, func: Runnable) { if (ms == 0L) backgroundHandler.post { func.run() } else backgroundHandler.postDelayed(func, ms) } +fun postUiDelayBg(ms: Long, func: Runnable) { + backgroundHandler.postDelayed({ uiHandler.post(func) }, ms) +} + fun post(ms: Long = 0, func: Runnable) { if (ms == 0L) { func.run() diff --git a/app/src/main/java/com/tangem/tap/common/analytics/TangemSdkError.kt b/app/src/main/java/com/tangem/tap/common/analytics/TangemSdkError.kt index 58b026e655..17e31d88c4 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/TangemSdkError.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/TangemSdkError.kt @@ -65,7 +65,7 @@ object TangemSdk { is TangemSdkError.Busy -> TangemSdkError.Busy() is TangemSdkError.MissingPreflightRead -> TangemSdkError.MissingPreflightRead() is TangemSdkError.WrongCardNumber -> TangemSdkError.WrongCardNumber() - is TangemSdkError.WrongCardType -> TangemSdkError.WrongCardType() + is TangemSdkError.WrongCardType -> TangemSdkError.WrongCardType(null) is TangemSdkError.CardError -> TangemSdkError.CardError() is TangemSdkError.NotSupportedFirmwareVersion -> TangemSdkError.NotSupportedFirmwareVersion() is TangemSdkError.WalletError -> TangemSdkError.WalletError() diff --git a/app/src/main/java/com/tangem/tap/common/compose/ComposeDialogManager.kt b/app/src/main/java/com/tangem/tap/common/compose/ComposeDialogManager.kt index 5031d9d835..fc496f1d9b 100644 --- a/app/src/main/java/com/tangem/tap/common/compose/ComposeDialogManager.kt +++ b/app/src/main/java/com/tangem/tap/common/compose/ComposeDialogManager.kt @@ -1,11 +1,25 @@ package com.tangem.tap.common.compose import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.material.* -import androidx.compose.runtime.* +import androidx.compose.material.AlertDialog +import androidx.compose.material.Button +import androidx.compose.material.LocalTextStyle +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -20,7 +34,7 @@ import com.tangem.domain.DomainDialog import com.tangem.domain.redux.domainStore import com.tangem.domain.redux.global.DomainGlobalAction import com.tangem.domain.redux.global.DomainGlobalState -import com.tangem.tap.common.moduleMessage.ModuleMessageConverter +import com.tangem.tap.domain.moduleMessage.ModuleMessageConverter import com.tangem.tap.features.tokens.addCustomToken.compose.SelectTokenNetworkDialog import com.tangem.wallet.R import org.rekotlin.StoreSubscriber @@ -61,7 +75,7 @@ private fun ShowTheDialog(dialogState: MutableState) { when (val dialog = dialogState.value) { is DomainDialog.DialogError -> ErrorDialog( title = stringResource(id = R.string.common_error), - body = errorConverter.convert(dialog.error), + body = errorConverter.convert(dialog.error).message, onDismissRequest ) is DomainDialog.SelectTokenDialog -> SelectTokenNetworkDialog(dialog, onDismissRequest) diff --git a/app/src/main/java/com/tangem/tap/common/compose/OutlinedTextFieldWidget.kt b/app/src/main/java/com/tangem/tap/common/compose/OutlinedTextFieldWidget.kt index 4eb6f5e2ed..deacbaf05f 100644 --- a/app/src/main/java/com/tangem/tap/common/compose/OutlinedTextFieldWidget.kt +++ b/app/src/main/java/com/tangem/tap/common/compose/OutlinedTextFieldWidget.kt @@ -1,6 +1,11 @@ package com.tangem.tap.common.compose -import androidx.compose.animation.* +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth @@ -26,7 +31,7 @@ import com.tangem.common.module.ModuleError import com.tangem.domain.common.form.Field import com.tangem.tap.common.CompositionLogger import com.tangem.tap.common.compose.extensions.stringResourceDefault -import com.tangem.tap.common.moduleMessage.ModuleMessageConverter +import com.tangem.tap.domain.moduleMessage.ModuleMessageConverter /** [REDACTED_AUTHOR] @@ -67,7 +72,7 @@ fun OutlinedTextFieldWidget( debounce = debounceTextChanges, visualTransformation = visualTransformation, keyboardOptions = keyboardOptions, - onTextChanged = onTextChanged + onTextChanged = onTextChanged, ) errorConverter?.let { AnimatedErrorView(error, it) } } @@ -105,7 +110,8 @@ private fun OutlinedProgressTextField( onValueChanged = { logger.log("DEBOUNCER: onValueChanged: >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> dispatch.toStore([$it])") onTextChanged(it) - }) + }, + ) logger.log("RECOMPOSE ---------------------------------------------------------------START [${logger.count}]") logger.log("RECOMPOSE --data: fieldData.value: [${fieldData}]") @@ -188,7 +194,10 @@ private fun AnimatedErrorView( exit = slideOutVertically() + fadeOut(), ) { error?.let { - ErrorView(errorConverter.convert(it), style = TextStyle(fontSize = 14.sp)) + ErrorView( + text = errorConverter.convert(it).message, + style = TextStyle(fontSize = 14.sp), + ) } } } @@ -203,15 +212,13 @@ fun OutlinedTextFieldWithErrorTest() { override val code: Int = 1, override val message: String = "Error message", override val data: Any? = null, - ) : ModuleError + ) : ModuleError() val modifier = Modifier .fillMaxWidth() .padding(16.dp) - Scaffold( - ) { - Column( - ) { + Scaffold { + Column { OutlinedTextFieldWidget( modifier = modifier, fieldData = Field.Data("", false), diff --git a/app/src/main/java/com/tangem/tap/common/compose/PinCodeWidget.kt b/app/src/main/java/com/tangem/tap/common/compose/PinCodeWidget.kt new file mode 100644 index 0000000000..ad7bbe794b --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/compose/PinCodeWidget.kt @@ -0,0 +1,172 @@ +package com.tangem.tap.common.compose + +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.LocalTextStyle +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.material.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.core.text.isDigitsOnly + +/** +[REDACTED_AUTHOR] + */ +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun PinCodeWidget( + config: PinViewConfig = tangemPinConfig, + onPinChanged: (String, Boolean) -> Unit = { pin, isLastSymbolEntered -> }, +) { + val focusRequester = remember { FocusRequester() } + val keyboardController = LocalSoftwareKeyboardController.current + + val rTextFieldValue = remember { mutableStateOf(TextFieldValue("")) } + val indexedSymbols: List = createPinSymbolsList(config.pinsCount, rTextFieldValue.value.text) + + fun isLastSymbolEntered(): Boolean = rTextFieldValue.value.text.length == config.pinsCount + + fun handleOnTextFieldValueChanged(value: TextFieldValue) { + if (!value.text.isDigitsOnly()) return + + if (value.text.length <= config.pinsCount) { + rTextFieldValue.value = value + onPinChanged(value.text, isLastSymbolEntered()) + } + } + + Box( + modifier = config.modifier + .pointerInput(Unit) { + detectTapGestures { + focusRequester.requestFocus() + keyboardController?.show() + } + }, + ) { + Row { + for (index in 0 until config.pinsCount) { + PinElement( + config = config, + pinSymbol = indexedSymbols[index] ?: "", + ) + } + } + TextField( + modifier = Modifier + .alpha(0f) + .size(1.dp) + .align(Alignment.Center) + .focusRequester(focusRequester), + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Decimal, + imeAction = if (isLastSymbolEntered()) ImeAction.Done else ImeAction.Next, + ), + keyboardActions = KeyboardActions( + onDone = { keyboardController?.hide() }, + ), + value = rTextFieldValue.value, + onValueChange = ::handleOnTextFieldValueChanged, + ) + } + + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } +} + +@Composable +private fun PinElement( + config: PinViewConfig, + pinSymbol: String, +) { + Box(Modifier.padding(config.pinBoxPadding)) { + Box(config.pinBoxModifier) { + Text( + text = pinSymbol, + modifier = config.pinTextModifier.align(Alignment.Center), + style = config.pinsTextStyle ?: LocalTextStyle.current, + ) + } + } +} + +@Composable +private fun HiddenInputField() { +} + +private fun createPinSymbolsList(size: Int, text: String): List = List(size) { + try { + text[it].toString() + } catch (ex: IndexOutOfBoundsException) { + null + } +} + +data class PinViewConfig( + val modifier: Modifier = Modifier, + val pinBoxModifier: Modifier = Modifier, + val pinBoxPadding: Dp = 0.dp, + val pinTextModifier: Modifier = Modifier, + val pinsCount: Int = 4, + val pinsTextStyle: TextStyle? = null, +) + +private val tangemPinConfig = PinViewConfig( + modifier = Modifier + .wrapContentSize(), + pinBoxModifier = Modifier + .width(42.dp) + .height(56.dp) + .clip(RoundedCornerShape(8.dp)) + .background(Color(0xFFF0F0F0)), + pinBoxPadding = 6.dp, + pinTextModifier = Modifier, + pinsCount = 4, + pinsTextStyle = TextStyle( + fontWeight = FontWeight(500), + fontSize = 24.sp, + ), +) + +@Preview +@Composable +fun PinCodeWidgetPreview() { + Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) { + PinCodeWidget(tangemPinConfig) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/compose/Warning.kt b/app/src/main/java/com/tangem/tap/common/compose/Warning.kt index 8ea4eceef2..07c82d1384 100644 --- a/app/src/main/java/com/tangem/tap/common/compose/Warning.kt +++ b/app/src/main/java/com/tangem/tap/common/compose/Warning.kt @@ -13,7 +13,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.tangem.common.module.ModuleMessage -import com.tangem.tap.common.moduleMessage.ModuleMessageConverter +import com.tangem.tap.domain.moduleMessage.ModuleMessageConverter import com.tangem.wallet.R /** @@ -42,7 +42,7 @@ fun AddCustomTokenWarning( ) SpacerH8() Text( - text = converter.convert(warning), + text = converter.convert(warning).message, color = colorResource(id = R.color.white), fontSize = 13.sp, lineHeight = 18.sp diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Context.kt b/app/src/main/java/com/tangem/tap/common/extensions/Context.kt index 60345e1ab7..cba31fe77c 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Context.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Context.kt @@ -1,6 +1,8 @@ package com.tangem.tap.common.extensions import android.content.Context +import android.content.pm.PackageManager +import androidx.core.content.ContextCompat fun Context.readFile(fileName: String): String = this.openFileInput(fileName).bufferedReader().readText() @@ -13,4 +15,8 @@ fun Context.rewriteFile(content: String, fileName: String) { fun Context.readAssetAsString(fileName: String): String { return this.assets.open("$fileName.json").bufferedReader().readText() +} + +fun Context.isPermissionGranted(permission: String): Boolean { + return ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Store.kt b/app/src/main/java/com/tangem/tap/common/extensions/Store.kt index 7a0488d1a5..d88560669e 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Store.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Store.kt @@ -13,16 +13,57 @@ import kotlinx.coroutines.launch import org.rekotlin.Action import org.rekotlin.Store +/** + * Dispatch action with creating the new coroutine with the Main dispatcher + */ fun Store<*>.dispatchOnMain(action: Action) { scope.launch(Dispatchers.Main) { store.dispatch(action) } } +fun Store<*>.dispatchNotification(resId: Int) { + dispatchOnMain(GlobalAction.ShowNotification(resId)) +} + +fun Store<*>.dispatchToastNotification(resId: Int) { + dispatchOnMain(GlobalAction.ShowToastNotification(resId)) +} + +fun Store<*>.dispatchErrorNotification(error: TapError) { + dispatchOnMain(GlobalAction.ShowErrorNotification(error)) +} + +/** + * @param fatal used to indicate errors that should not normally occur + */ +fun Store<*>.dispatchDebugErrorNotification(message: String, fatal: Boolean = false) { + val prefix = if (fatal) "FATAL ERROR: " else "DEBUG ERROR: " + dispatchDebugErrorNotification(TapError.CustomError("$prefix $message")) +} + +fun Store<*>.dispatchDebugErrorNotification(error: TapError) { + dispatchOnMain(GlobalAction.DebugShowErrorNotification(error)) +} + +fun Store<*>.dispatchDialogShow(dialog: StateDialog) { + dispatchOnMain(GlobalAction.ShowDialog(dialog)) +} + +fun Store<*>.dispatchDialogHide() { + dispatchOnMain(GlobalAction.HideDialog) +} + +/** + * Dispatch action inside a coroutine with the Main dispatcher + */ suspend fun dispatchOnMain(vararg actions: Action) { withMainContext { actions.forEach { store.dispatch(it) } } } +/** + * Dispatch action + */ suspend fun Store<*>.onCardScanned(scanResponse: ScanResponse) { store.state.globalState.tapWalletManager.onCardScanned(scanResponse) } @@ -33,49 +74,4 @@ fun Store<*>.dispatchOpenUrl(url: String) { fun Store<*>.dispatchShare(url: String) { store.dispatch(NavigationAction.Share(url)) -} - -fun Store<*>.dispatchNotification(resId: Int) { - scope.launch(Dispatchers.Main) { - store.dispatch(GlobalAction.ShowNotification(resId)) - } -} - -fun Store<*>.dispatchToastNotification(resId: Int) { - scope.launch(Dispatchers.Main) { - store.dispatch(GlobalAction.ShowToastNotification(resId)) - } -} - - -fun Store<*>.dispatchErrorNotification(error: TapError) { - scope.launch(Dispatchers.Main) { - store.dispatch(GlobalAction.ShowErrorNotification(error)) - } -} - -/** - * @param fatal used to indicate errors that should not normally have occurred - */ -fun Store<*>.dispatchDebugErrorNotification(message: String, fatal: Boolean = false) { - val prefix = if (fatal) "FATAL ERROR: " else "DEBUG ERROR: " - dispatchDebugErrorNotification(TapError.CustomError("$prefix $message")) -} - -fun Store<*>.dispatchDebugErrorNotification(error: TapError) { - scope.launch(Dispatchers.Main) { - store.dispatch(GlobalAction.DebugShowErrorNotification(error)) - } -} - -fun Store<*>.dispatchDialogShow(dialog: StateDialog) { - scope.launch(Dispatchers.Main) { - store.dispatch(GlobalAction.ShowDialog(dialog)) - } -} - -fun Store<*>.dispatchDialogHide() { - scope.launch(Dispatchers.Main) { - store.dispatch(GlobalAction.HideDialog) - } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt b/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt index 311932f7c1..76144c6132 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt @@ -1,5 +1,6 @@ package com.tangem.tap.common.extensions +import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.BlockchainSdkError import com.tangem.blockchain.common.Wallet import com.tangem.blockchain.common.WalletManager @@ -69,4 +70,12 @@ fun WalletManager?.getAddressData(): AddressData? { val addressDataList = wallet.createAddressesData() return if (addressDataList.isEmpty()) null else addressDataList[0] +} + +fun WalletManager.Companion.stub(): T { + val wallet = Wallet(Blockchain.Unknown, setOf(), Wallet.PublicKey(byteArrayOf(), null, null), setOf()) + return object : WalletManager(wallet) { + override val currentHost: String = "" + override suspend fun update() {} + } as T } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/WebView.kt b/app/src/main/java/com/tangem/tap/common/extensions/WebView.kt new file mode 100644 index 0000000000..ebe938a1ec --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/extensions/WebView.kt @@ -0,0 +1,19 @@ +package com.tangem.tap.common.extensions + +import android.webkit.WebView + +/** +[REDACTED_AUTHOR] + */ +fun WebView.configureSettings() { + settings.apply { + javaScriptEnabled = true + domStorageEnabled = true + } +} + +fun WebView.stop() { + stopLoading() + pauseTimers() + destroy() +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/feedback/FeedbackManager.kt b/app/src/main/java/com/tangem/tap/common/feedback/FeedbackManager.kt index 1631952ce6..12fe1e42e9 100644 --- a/app/src/main/java/com/tangem/tap/common/feedback/FeedbackManager.kt +++ b/app/src/main/java/com/tangem/tap/common/feedback/FeedbackManager.kt @@ -10,7 +10,6 @@ import com.tangem.tap.foregroundActivityObserver import com.tangem.tap.persistence.PreferencesStorage import com.tangem.tap.withForegroundActivity import com.tangem.wallet.R -import com.zendesk.logger.Logger import timber.log.Timber import zendesk.chat.Chat import zendesk.chat.ChatConfiguration @@ -30,20 +29,17 @@ class FeedbackManager( val infoHolder: AdditionalFeedbackInfo, private val logCollector: TangemLogCollector, private val preferencesStorage: PreferencesStorage, - private val logEnabled: Boolean = false, ) { - fun initChat( - context: Context, - zendeskConfig: ZendeskConfig, - ) { - Chat.INSTANCE.init( - context, - zendeskConfig.accountKey, - zendeskConfig.appId - ) + private var lastUsedConfigForInitialization: ZendeskConfig? = null - // Zendesk logs - Logger.setLoggable(logEnabled) + var chatInitializer: ((ZendeskConfig) -> Unit)? = null + + fun initChat(zendeskConfig: ZendeskConfig) { + // prevent double initialization with the same config + if (lastUsedConfigForInitialization == zendeskConfig) return + + lastUsedConfigForInitialization = zendeskConfig + chatInitializer?.invoke(zendeskConfig) } fun sendEmail(feedbackData: FeedbackData, onFail: ((Exception) -> Unit)? = null) { @@ -55,7 +51,7 @@ class FeedbackManager( subject = activity.getString(feedbackData.subjectResId), message = feedbackData.joinTogether(activity, infoHolder), file = fileLog, - onFail = onFail + onFail = onFail, ) } } diff --git a/app/src/main/java/com/tangem/tap/common/moduleMessage/ModuleMessageConverter.kt b/app/src/main/java/com/tangem/tap/common/moduleMessage/ModuleMessageConverter.kt deleted file mode 100644 index a7247ebe20..0000000000 --- a/app/src/main/java/com/tangem/tap/common/moduleMessage/ModuleMessageConverter.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.tangem.tap.common.moduleMessage - -import android.content.Context -import com.tangem.common.module.ModuleMessage -import com.tangem.common.module.ModuleMessageConverter -import com.tangem.domain.DomainModuleMessage -import com.tangem.tap.common.moduleMessage.domain.DomainMessageConverter - -class ModuleMessageConverter( - private val context: Context -) : ModuleMessageConverter { - - override fun convert(message: ModuleMessage): String { - val convertedMessage = when (message) { - is DomainModuleMessage -> DomainMessageConverter(context).convert(message) - else -> null - } - return convertedMessage ?: convertUnknownMessage(message) - } - - private fun convertUnknownMessage(message: ModuleMessage): String { - return "Unknown message: ${message::class.java.simpleName}" - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/moduleMessage/domain/DomainMessageConverter.kt b/app/src/main/java/com/tangem/tap/common/moduleMessage/domain/DomainMessageConverter.kt deleted file mode 100644 index 3afb79b219..0000000000 --- a/app/src/main/java/com/tangem/tap/common/moduleMessage/domain/DomainMessageConverter.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.tangem.tap.common.moduleMessage.domain - -import android.content.Context -import com.tangem.common.module.ModuleMessageConverter -import com.tangem.domain.DomainError -import com.tangem.domain.DomainModuleMessage - -/** -[REDACTED_AUTHOR] - */ -class DomainMessageConverter( - private val context: Context -) : ModuleMessageConverter { - override fun convert(message: DomainModuleMessage): String? { - return when (message) { - is DomainError -> DomainErrorConverter(context).convert(message) - else -> null - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt index 6f20496105..af79cb81e1 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt @@ -24,6 +24,7 @@ import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState import com.tangem.tap.features.onboarding.products.wallet.redux.BackupMiddleware import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletMiddleware import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletState +import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayMiddleware import com.tangem.tap.features.send.redux.middlewares.SendMiddleware import com.tangem.tap.features.send.redux.states.SendState import com.tangem.tap.features.shop.redux.ShopMiddleware @@ -66,6 +67,7 @@ data class AppState( HomeMiddleware.handler, OnboardingNoteMiddleware.handler, OnboardingWalletMiddleware.handler, + OnboardingSaltPayMiddleware.handler, OnboardingOtherCardsMiddleware.handler, WalletMiddleware().walletMiddleware, TwinCardsMiddleware.handler, diff --git a/app/src/main/java/com/tangem/tap/common/redux/LogMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/LogMiddleware.kt index 1337effab2..30b127fda1 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/LogMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/LogMiddleware.kt @@ -12,6 +12,7 @@ val logMiddleware: Middleware = { dispatch, appState -> { action -> if (LogConfig.storeAction) { Timber.d("Dispatch action: $action") + // printOnboardingWalletState() } nextDispatch(action) } diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt index 4927405ccd..3d596bbce3 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt @@ -14,6 +14,7 @@ import com.tangem.tap.common.redux.ErrorAction import com.tangem.tap.common.redux.NotificationAction import com.tangem.tap.common.redux.StateDialog import com.tangem.tap.common.redux.ToastNotificationAction +import com.tangem.tap.common.zendesk.ZendeskConfig import com.tangem.tap.domain.TapError import com.tangem.tap.domain.configurable.config.ConfigManager import com.tangem.tap.domain.configurable.warningMessage.WarningMessage @@ -36,9 +37,10 @@ sealed class GlobalAction : Action { data class ShowDialog(val stateDialog: StateDialog) : GlobalAction() object HideDialog : GlobalAction() - sealed class Onboarding { - data class Start(val scanResponse: ScanResponse?, val fromHomeScreen: Boolean = true) : GlobalAction() - object Stop : GlobalAction() + sealed class Onboarding : GlobalAction() { + data class Start(val scanResponse: ScanResponse, val canSkipBackup: Boolean = true) : Onboarding() + data class StartForUnfinishedBackup(val isSaltPayBackup: Boolean) : Onboarding() + object Stop : Onboarding() } data class ScanCard( @@ -78,7 +80,7 @@ sealed class GlobalAction : Action { data class SetAnanlyticHandlers(val analyticsHandlers: GlobalAnalyticsHandler) : GlobalAction() data class SendEmail(val feedbackData: FeedbackData) : GlobalAction() - data class OpenChat(val feedbackData: FeedbackData) : GlobalAction() + data class OpenChat(val feedbackData: FeedbackData, val zendeskConfig: ZendeskConfig? = null) : GlobalAction() data class UpdateFeedbackInfo(val walletManagers: List) : GlobalAction() object ExchangeManager : GlobalAction() { diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt index 8173f76aae..88a9c805aa 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt @@ -87,7 +87,31 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di store.state.globalState.feedbackManager?.sendEmail(action.feedbackData) } is GlobalAction.OpenChat -> { - store.state.globalState.feedbackManager?.openChat(action.feedbackData) + val globalState = store.state.globalState + val feedbackManager = globalState.feedbackManager.guard { + store.dispatchDebugErrorNotification("FeedbackManager not initialized") + return + } + val config = globalState.configManager?.config.guard { + store.dispatchDebugErrorNotification("Config not initialized") + return + } + + val scanResponse = globalState.onboardingState.onboardingManager?.scanResponse + ?: globalState.scanResponse + + // if config not set -> try to get it based on a scanResponse.productType + val unsafeZendeskConfig = action.zendeskConfig ?: when { + scanResponse?.isSaltPay() == true -> config.saltPayConfig?.zendesk + else -> config.zendesk + } + + val zendeskConfig = unsafeZendeskConfig.guard { + store.dispatchDebugErrorNotification("ZendeskConfig not initialized") + return + } + feedbackManager.initChat(zendeskConfig) + feedbackManager.openChat(action.feedbackData) } is GlobalAction.UpdateWalletSignedHashes -> { store.dispatch(WalletAction.Warnings.CheckRemainingSignatures(action.remainingSignatures)) diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt index 449d47ae10..d1f9043ca2 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt @@ -18,14 +18,13 @@ fun globalReducer(action: Action, state: AppState): GlobalState { globalState.copy(resources = action.resources) } is GlobalAction.Onboarding.Start -> { - val onboardingManager = if (action.scanResponse != null) { - val usedCardsPrefStorage = preferencesStorage.usedCardsPrefStorage - OnboardingManager(action.scanResponse, usedCardsPrefStorage) - } else { - null - } + val usedCardsPrefStorage = preferencesStorage.usedCardsPrefStorage + val onboardingManager = OnboardingManager(action.scanResponse, usedCardsPrefStorage) globalState.copy(onboardingState = OnboardingState(true, onboardingManager)) } + is GlobalAction.Onboarding.StartForUnfinishedBackup -> { + globalState.copy(onboardingState = OnboardingState(true, null)) + } is GlobalAction.Onboarding.Stop -> { globalState.copy(onboardingState = OnboardingState(false)) } diff --git a/app/src/main/java/com/tangem/tap/common/toggleWidget/IndeterminateProgressButtonWidget.kt b/app/src/main/java/com/tangem/tap/common/toggleWidget/IndeterminateProgressButtonWidget.kt index b4eacfff4e..cc57977202 100644 --- a/app/src/main/java/com/tangem/tap/common/toggleWidget/IndeterminateProgressButtonWidget.kt +++ b/app/src/main/java/com/tangem/tap/common/toggleWidget/IndeterminateProgressButtonWidget.kt @@ -11,17 +11,17 @@ import com.tangem.tap.features.wallet.redux.ProgressState [REDACTED_AUTHOR] */ open class IndeterminateProgressButtonWidget( - private val button: MaterialButton, - private val progress: View, - initialState: ProgressState = ProgressState.Done + private val button: MaterialButton, + private val progress: View, + initialState: ProgressState = ProgressState.Done, ) : ViewStateWidget { - private var initialButtonText: CharSequence = button.text - private var icon: Drawable? = null + private var text: CharSequence = button.text + private var icon: Drawable? = button.icon + private var iconGravity: Int? = button.iconGravity init { - icon = button.icon - changeState(initialState) + if (initialState != ProgressState.Done) changeState(initialState) } override val mainView: View = button @@ -37,15 +37,18 @@ open class IndeterminateProgressButtonWidget( protected open fun switchToNone() { button.isClickable = true + button.text = text button.icon = icon - button.text = initialButtonText + iconGravity?.let { button.iconGravity = it } + progress.hide() } protected open fun switchToProgress() { button.isClickable = false - button.icon = null button.text = "" + button.icon = null + progress.show() } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt index 34e58f0ba7..98694f525b 100644 --- a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt @@ -3,6 +3,7 @@ package com.tangem.tap.domain import CreateProductWalletTask import CreateProductWalletTaskResponse import android.content.Context +import androidx.annotation.StringRes import com.tangem.Message import com.tangem.TangemSdk import com.tangem.blockchain.common.Blockchain @@ -59,32 +60,32 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co } suspend fun createProductWallet( - scanResponse: ScanResponse + scanResponse: ScanResponse, ): CompletionResult { return runTaskAsync( CreateProductWalletTask(scanResponse.productType), scanResponse.card.cardId, - Message(context.getString(R.string.initial_message_create_wallet_body)) + Message(context.getString(R.string.initial_message_create_wallet_body)), ) } private fun sendScanResultsToAnalytics( analyticsHandler: AnalyticsHandler?, - result: CompletionResult + result: CompletionResult, ) { when (result) { is CompletionResult.Success -> { analyticsHandler?.triggerEvent( event = AnalyticsEvent.CARD_IS_SCANNED, card = result.data.card, - blockchain = result.data.walletData?.blockchain + blockchain = result.data.walletData?.blockchain, ) if (DemoHelper.isDemoCard(result.data)) { analyticsHandler?.triggerEvent( event = AnalyticsEvent.DEMO_MODE_ACTIVATED, card = result.data.card, blockchain = result.data.walletData?.blockchain, - params = mapOf(AnalyticsParam.CARD_ID.param to result.data.card.cardId) + params = mapOf(AnalyticsParam.CARD_ID.param to result.data.card.cardId), ) } } @@ -92,20 +93,22 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co (result.error as? TangemSdkError)?.let { error -> analyticsHandler?.logCardSdkError( error = error, - actionToLog = Analytics.ActionToLog.Scan + actionToLog = Analytics.ActionToLog.Scan, ) } } } suspend fun createWallet(cardId: String?): CompletionResult { - return runTaskAsyncReturnOnMain(CreateWalletAndRescanTask(), cardId, - initialMessage = Message(context.getString(R.string.initial_message_create_wallet_body))) + return runTaskAsyncReturnOnMain( + CreateWalletAndRescanTask(), cardId, + initialMessage = Message(context.getString(R.string.initial_message_create_wallet_body)), + ) } suspend fun derivePublicKeys( cardId: String, - derivations: Map> + derivations: Map>, ): CompletionResult { return runTaskAsyncReturnOnMain(DeriveMultipleWalletPublicKeysTask(derivations), cardId) } @@ -114,49 +117,58 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co return runTaskAsyncReturnOnMain( ResetToFactorySettingsTask(), card.cardId, - initialMessage = Message(context.getString(R.string.details_row_title_reset_factory_settings))) + initialMessage = Message(context.getString(R.string.details_row_title_reset_factory_settings)), + ) } suspend fun setPasscode(cardId: String?): CompletionResult { return runTaskAsyncReturnOnMain( SetUserCodeCommand.changePasscode(null), cardId, - initialMessage = Message(context.getString(R.string.initial_message_change_passcode_body))) + initialMessage = Message(context.getString(R.string.initial_message_change_passcode_body)), + ) } suspend fun setAccessCode(cardId: String?): CompletionResult { return runTaskAsyncReturnOnMain( SetUserCodeCommand.changeAccessCode(null), cardId, - initialMessage = Message(context.getString(R.string.initial_message_change_access_code_body))) + initialMessage = Message(context.getString(R.string.initial_message_change_access_code_body)), + ) } suspend fun setLongTap(cardId: String?): CompletionResult { return runTaskAsyncReturnOnMain( SetUserCodeCommand.resetUserCodes(), cardId, - initialMessage = Message(context.getString(R.string.initial_message_tap_header))) + initialMessage = Message(context.getString(R.string.initial_message_tap_header)), + ) } suspend fun checkUserCodes(cardId: String?): CompletionResult { return runTaskAsyncReturnOnMain( CheckUserCodesCommand(), cardId, - initialMessage = Message(context.getString(R.string.initial_message_tap_header))) + initialMessage = Message(context.getString(R.string.initial_message_tap_header)), + ) } suspend fun scanCard(): CompletionResult { return runTaskAsyncReturnOnMain( ScanTask(), - initialMessage = Message(context.getString(R.string.initial_message_tap_header))) + initialMessage = Message(context.getString(R.string.initial_message_tap_header)), + ) } suspend fun runTaskAsync( - runnable: CardSessionRunnable, cardId: String? = null, initialMessage: Message? = null, + runnable: CardSessionRunnable, + cardId: String? = null, + initialMessage: Message? = null, + accessCode: String? = null, ): CompletionResult = withContext(Dispatchers.Main) { suspendCoroutine { continuation -> - tangemSdk.startSessionWithRunnable(runnable, cardId, initialMessage) { result -> + tangemSdk.startSessionWithRunnable(runnable, cardId, initialMessage, accessCode) { result -> if (continuation.context.isActive) continuation.resume(result) } } @@ -170,20 +182,24 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co } fun changeDisplayedCardIdNumbersCount(scanResponse: ScanResponse) { - tangemSdk.config.cardIdDisplayFormat = if (scanResponse.isTangemTwins()) { - CardIdDisplayFormat.LastLuhn(4) - } else { - CardIdDisplayFormat.Full + tangemSdk.config.cardIdDisplayFormat = when { + scanResponse.isTangemTwins() -> CardIdDisplayFormat.LastLuhn(4) + scanResponse.isSaltPay() -> CardIdDisplayFormat.None + else -> CardIdDisplayFormat.Full } } + fun getString(@StringRes stringResId: Int, vararg formatArgs: Any?): String { + return context.getString(stringResId, formatArgs) + } + companion object { val config = Config( linkedTerminal = true, allowUntrustedCards = true, filter = CardFilter( - allowedCardTypes = FirmwareVersion.FirmwareType.values().toList() - ) + allowedCardTypes = FirmwareVersion.FirmwareType.values().toList(), + ), ) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/TapErrors.kt b/app/src/main/java/com/tangem/tap/domain/TapErrors.kt index 5a0cf85668..6e73a11fba 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapErrors.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapErrors.kt @@ -70,6 +70,7 @@ sealed class TapSdkError(override val messageResId: Int?) : Throwable(), TangemE object CardForDifferentApp : TapSdkError(R.string.alert_unsupported_card) object CardNotSupportedByRelease : TapSdkError(R.string.error_update_app) + object ScanPrimaryCard : TapSdkError(R.string.saltpay_backup_warning) } fun TapErrors.assembleErrors(): MutableList?>> { diff --git a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt index 37a48f65a1..b15f2a7431 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt @@ -35,9 +35,11 @@ class TapWalletManager { val walletManagerFactory: WalletManagerFactory by lazy { WalletManagerFactory(blockchainSdkConfig) } val rates: RatesRepository = RatesRepository() + private val blockchainSdkConfig by lazy { store.state.globalState.configManager?.config?.blockchainSdkConfig ?: BlockchainSdkConfig() } + private val walletManagersThrottler = ThrottlerWithValues>(10000) @@ -134,9 +136,7 @@ class TapWalletManager { dispatchOnMain(WalletAction.LoadWallet()) } - private suspend fun loadMultiWalletData( - scanResponse: ScanResponse, - ) { + private suspend fun loadMultiWalletData(scanResponse: ScanResponse) { loadUserCurrencies(scanResponse, walletManagerFactory) } diff --git a/app/src/main/java/com/tangem/tap/domain/configurable/config/ConfigManager.kt b/app/src/main/java/com/tangem/tap/domain/configurable/config/ConfigManager.kt index 62f367cb33..9d0ae5a366 100644 --- a/app/src/main/java/com/tangem/tap/domain/configurable/config/ConfigManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/configurable/config/ConfigManager.kt @@ -4,6 +4,7 @@ import com.tangem.blockchain.common.BlockchainSdkConfig import com.tangem.tap.common.shop.shopify.ShopifyShop import com.tangem.tap.common.zendesk.ZendeskConfig import com.tangem.tap.domain.configurable.Loader +import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayConfig /** [REDACTED_AUTHOR] @@ -22,6 +23,7 @@ data class Config( val isCreatingTwinCardsAllowed: Boolean = false, val shopify: ShopifyShop? = null, val zendesk: ZendeskConfig? = null, + val saltPayConfig: SaltPayConfig? = null, ) class ConfigManager { @@ -90,6 +92,7 @@ class ConfigManager { appsFlyerDevKey = values.appsFlyerDevKey, shopify = values.shopifyShop, zendesk = values.zendesk, + saltPayConfig = values.saltPay, ) defaultConfig = defaultConfig.copy( coinMarketCapKey = values.coinMarketCapKey, @@ -106,6 +109,7 @@ class ConfigManager { appsFlyerDevKey = values.appsFlyerDevKey, shopify = values.shopifyShop, zendesk = values.zendesk, + saltPayConfig = values.saltPay, ) } diff --git a/app/src/main/java/com/tangem/tap/domain/configurable/config/JsonModels.kt b/app/src/main/java/com/tangem/tap/domain/configurable/config/JsonModels.kt index 6e119fc5e6..70c7ca73db 100644 --- a/app/src/main/java/com/tangem/tap/domain/configurable/config/JsonModels.kt +++ b/app/src/main/java/com/tangem/tap/domain/configurable/config/JsonModels.kt @@ -2,32 +2,34 @@ package com.tangem.tap.domain.configurable.config import com.tangem.tap.common.shop.shopify.ShopifyShop import com.tangem.tap.common.zendesk.ZendeskConfig +import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayConfig /** [REDACTED_AUTHOR] */ class FeatureModel( - val isWalletPayIdEnabled: Boolean, - val isTopUpEnabled: Boolean, - val isSendingToPayIdEnabled: Boolean, - val isCreatingTwinCardsAllowed: Boolean, + val isWalletPayIdEnabled: Boolean, + val isTopUpEnabled: Boolean, + val isSendingToPayIdEnabled: Boolean, + val isCreatingTwinCardsAllowed: Boolean, ) class ConfigValueModel( - val coinMarketCapKey: String, - val mercuryoWidgetId: String, - val mercuryoSecret: String, - val moonPayApiKey: String, - val moonPayApiSecretKey: String, - val blockchairApiKey: String?, - val blockchairAuthorizationToken: String?, - val blockcypherTokens: Set?, - val infuraProjectId: String?, - val appsFlyerDevKey: String, - val shopifyShop: ShopifyShop?, - val zendesk: ZendeskConfig?, - val tronGridApiKey: String, + val coinMarketCapKey: String, + val mercuryoWidgetId: String, + val mercuryoSecret: String, + val moonPayApiKey: String, + val moonPayApiSecretKey: String, + val blockchairApiKey: String?, + val blockchairAuthorizationToken: String?, + val blockcypherTokens: Set?, + val infuraProjectId: String?, + val appsFlyerDevKey: String, + val shopifyShop: ShopifyShop?, + val zendesk: ZendeskConfig?, + val saltPay: SaltPayConfig, + val tronGridApiKey: String, ) class ConfigModel(val features: FeatureModel?, val configValues: ConfigValueModel?) { diff --git a/app/src/main/java/com/tangem/tap/domain/extensions/Card.kt b/app/src/main/java/com/tangem/tap/domain/extensions/Card.kt index c627a7de91..5bd5575906 100644 --- a/app/src/main/java/com/tangem/tap/domain/extensions/Card.kt +++ b/app/src/main/java/com/tangem/tap/domain/extensions/Card.kt @@ -6,6 +6,7 @@ import com.tangem.common.card.EllipticCurve import com.tangem.common.card.FirmwareVersion import com.tangem.common.extensions.toHexString import com.tangem.common.services.Result +import com.tangem.domain.common.TapWorkarounds.isSaltPay import com.tangem.domain.common.TapWorkarounds.isStart2Coin import com.tangem.domain.common.TapWorkarounds.isTangemNote import com.tangem.domain.common.TwinCardNumber @@ -23,11 +24,14 @@ val Card.isWalletDataSupported: Boolean val Card.isMultiwalletAllowed: Boolean get() { - return !isTangemTwin() && !isStart2Coin && !isTangemNote() + return !isTangemTwin() && !isStart2Coin && !isTangemNote && !isSaltPay && (firmwareVersion >= FirmwareVersion.MultiWalletAvailable || getSingleWallet()?.curve == EllipticCurve.Secp256k1) } +val Card.isHdWalletAllowedByApp: Boolean + get() = settings.isHDWalletAllowed && !isSaltPay + fun Card.getSingleWallet(): CardWallet? { return wallets.firstOrNull() } diff --git a/app/src/main/java/com/tangem/tap/domain/extensions/WalletManagerFactory.kt b/app/src/main/java/com/tangem/tap/domain/extensions/WalletManagerFactory.kt index 90981e2359..ec95e13dc6 100644 --- a/app/src/main/java/com/tangem/tap/domain/extensions/WalletManagerFactory.kt +++ b/app/src/main/java/com/tangem/tap/domain/extensions/WalletManagerFactory.kt @@ -1,16 +1,20 @@ package com.tangem.tap.domain.extensions +import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.DerivationParams import com.tangem.blockchain.common.DerivationStyle +import com.tangem.blockchain.common.Wallet import com.tangem.blockchain.common.WalletManager import com.tangem.blockchain.common.WalletManagerFactory import com.tangem.common.card.Card import com.tangem.common.card.CardWallet import com.tangem.common.card.EllipticCurve +import com.tangem.common.extensions.guard import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toMapKey import com.tangem.common.hdWallet.DerivationPath +import com.tangem.domain.common.SaltPayWorkaround import com.tangem.domain.common.ScanResponse import com.tangem.domain.common.TapWorkarounds.isTestCard import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation @@ -20,7 +24,7 @@ import com.tangem.tap.features.wallet.models.Currency fun WalletManagerFactory.makeWalletManagerForApp( scanResponse: ScanResponse, blockchain: Blockchain, - derivationParams: DerivationParams? + derivationParams: DerivationParams?, ): WalletManager? { val card = scanResponse.card if (card.isTestCard && blockchain.getTestnetVersion() == null) return null @@ -39,10 +43,10 @@ fun WalletManagerFactory.makeWalletManagerForApp( walletPublicKey = wallet.publicKey, pairPublicKey = scanResponse.secondTwinPublicKey!!.hexToBytes(), blockchain = environmentBlockchain, - curve = wallet.curve + curve = wallet.curve, ) } - seedKey != null && derivationParams != null -> { + scanResponse.card.isHdWalletAllowedByApp && (seedKey != null && derivationParams != null) -> { val derivedKeys = scanResponse.derivedKeys[wallet.publicKey.toMapKey()] val derivationPath = when (derivationParams) { is DerivationParams.Default -> blockchain.derivationPath(derivationParams.style) @@ -55,33 +59,33 @@ fun WalletManagerFactory.makeWalletManagerForApp( blockchain = environmentBlockchain, seedKey = wallet.publicKey, derivedKey = derivedKey, - derivation = derivationParams + derivation = derivationParams, ) } else -> { makeWalletManager( blockchain = environmentBlockchain, walletPublicKey = wallet.publicKey, - curve = wallet.curve + curve = wallet.curve, ) } } } fun WalletManagerFactory.makeWalletManagerForApp( - scanResponse: ScanResponse, blockchainNetwork: BlockchainNetwork + scanResponse: ScanResponse, blockchainNetwork: BlockchainNetwork, ): WalletManager? { return makeWalletManagerForApp( scanResponse, blockchain = blockchainNetwork.blockchain, - derivationParams = getDerivationParams(blockchainNetwork.derivationPath, scanResponse.card) + derivationParams = getDerivationParams(blockchainNetwork.derivationPath, scanResponse.card), ) } private fun getDerivationParams(derivationPath: String?, card: Card): DerivationParams? { return derivationPath?.let { DerivationParams.Custom( - DerivationPath(it) + DerivationPath(it), ) } ?: if (!card.settings.isHDWalletAllowed) { null @@ -93,17 +97,19 @@ private fun getDerivationParams(derivationPath: String?, card: Card): Derivation } fun WalletManagerFactory.makeWalletManagerForApp( - scanResponse: ScanResponse, currency: Currency + scanResponse: ScanResponse, + currency: Currency, ): WalletManager? { return makeWalletManagerForApp( scanResponse, blockchain = currency.blockchain, - derivationParams = getDerivationParams(currency.derivationPath, scanResponse.card) + derivationParams = getDerivationParams(currency.derivationPath, scanResponse.card), ) } fun WalletManagerFactory.makeWalletManagersForApp( - scanResponse: ScanResponse, blockchains: List, + scanResponse: ScanResponse, + blockchains: List, ): List { return blockchains .filter { it.isBlockchain() } @@ -122,7 +128,7 @@ fun WalletManagerFactory.makePrimaryWalletManager( return makeWalletManagerForApp( scanResponse = scanResponse, blockchain = blockchain, - derivationParams = derivationParams + derivationParams = derivationParams, ) } @@ -132,4 +138,23 @@ private fun selectWallet(wallets: List): CardWallet? { 1 -> wallets[0] else -> wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } ?: wallets[0] } +} + +fun WalletManagerFactory.makeSaltPayWalletManager( + scanResponse: ScanResponse, +): EthereumWalletManager { + val blockchain = scanResponse.getBlockchain() + if (blockchain != Blockchain.SaltPay && blockchain != Blockchain.SaltPayTestnet) + throw IllegalArgumentException() + + val token = SaltPayWorkaround.tokenFrom(blockchain) + val cardWallet = scanResponse.card.wallets.firstOrNull().guard { + throw NullPointerException("SaltPay card must have one wallet at least") + } + + return makeWalletManager( + blockchain = blockchain, + publicKey = Wallet.PublicKey(cardWallet.publicKey, null, null), + tokens = listOf(token), + ) as EthereumWalletManager } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/moduleMessage/ConvertedMessage.kt b/app/src/main/java/com/tangem/tap/domain/moduleMessage/ConvertedMessage.kt new file mode 100644 index 0000000000..a8add4f2a5 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/moduleMessage/ConvertedMessage.kt @@ -0,0 +1,32 @@ +package com.tangem.tap.domain.moduleMessage + +/** +[REDACTED_AUTHOR] + * Base interface for conversion outputs from ModuleMessageConverter + */ +interface ConvertedMessage { + val message: String +} + +/** + * Dialog message used to construct a android.Dialog + */ +interface DialogMessage : ConvertedMessage { + val title: String + override val message: String + val onPositive: String? + val onNegative: String? + val onNeutral: String? +} + +data class ConvertedStringMessage( + override val message: String, +) : ConvertedMessage + +data class ConvertedDialogMessage( + override val title: String, + override val message: String, + override val onPositive: String? = null, + override val onNegative: String? = null, + override val onNeutral: String? = null, +) : DialogMessage \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/moduleMessage/ModuleMessageConverter.kt b/app/src/main/java/com/tangem/tap/domain/moduleMessage/ModuleMessageConverter.kt new file mode 100644 index 0000000000..7c665e5cdd --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/moduleMessage/ModuleMessageConverter.kt @@ -0,0 +1,27 @@ +package com.tangem.tap.domain.moduleMessage + +import android.content.Context +import com.tangem.common.module.ModuleMessage +import com.tangem.common.module.ModuleMessageConverter +import com.tangem.domain.DomainModuleMessage +import com.tangem.tap.domain.moduleMessage.domain.DomainMessageConverter +import com.tangem.tap.domain.moduleMessage.saltPay.SaltPayMessageConverter +import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayModuleMessage + +class ModuleMessageConverter( + private val context: Context, +) : ModuleMessageConverter { + + override fun convert(message: ModuleMessage): ConvertedMessage { + val convertedMessage = when (message) { + is DomainModuleMessage -> DomainMessageConverter(context).convert(message) + is SaltPayModuleMessage -> SaltPayMessageConverter(context).convert(message) + else -> null + } + return convertedMessage ?: convertUnknownMessage(message) + } + + private fun convertUnknownMessage(message: ModuleMessage): ConvertedMessage { + return ConvertedStringMessage("Unknown message: ${message::class.java.simpleName}") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/moduleMessage/domain/DomainMessageConverter.kt b/app/src/main/java/com/tangem/tap/domain/moduleMessage/domain/DomainMessageConverter.kt new file mode 100644 index 0000000000..67c58be34a --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/moduleMessage/domain/DomainMessageConverter.kt @@ -0,0 +1,34 @@ +package com.tangem.tap.domain.moduleMessage.domain + +import android.content.Context +import com.tangem.common.module.ModuleMessageConverter +import com.tangem.domain.AddCustomTokenError +import com.tangem.domain.DomainModuleError +import com.tangem.domain.DomainModuleMessage +import com.tangem.tap.domain.moduleMessage.ConvertedMessage +import com.tangem.tap.domain.moduleMessage.domain.converter.AddCustomTokenErrorConverter + +/** +[REDACTED_AUTHOR] + */ +class DomainMessageConverter( + private val context: Context, +) : ModuleMessageConverter { + + override fun convert(message: DomainModuleMessage): ConvertedMessage? { + return when (message) { + is DomainModuleError -> DomainErrorConverter(context).convert(message) + else -> null + } + } +} + +class DomainErrorConverter( + private val context: Context, +) : ModuleMessageConverter { + + override fun convert(message: DomainModuleError): ConvertedMessage? = when (message) { + is AddCustomTokenError -> AddCustomTokenErrorConverter(context).convert(message) + else -> null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/moduleMessage/domain/DomainErrorConverters.kt b/app/src/main/java/com/tangem/tap/domain/moduleMessage/domain/converter/AddCustomTokenErrorConverter.kt similarity index 68% rename from app/src/main/java/com/tangem/tap/common/moduleMessage/domain/DomainErrorConverters.kt rename to app/src/main/java/com/tangem/tap/domain/moduleMessage/domain/converter/AddCustomTokenErrorConverter.kt index 7f5221041e..66e41dc48e 100644 --- a/app/src/main/java/com/tangem/tap/common/moduleMessage/domain/DomainErrorConverters.kt +++ b/app/src/main/java/com/tangem/tap/domain/moduleMessage/domain/converter/AddCustomTokenErrorConverter.kt @@ -1,28 +1,21 @@ -package com.tangem.tap.common.moduleMessage.domain +package com.tangem.tap.domain.moduleMessage.domain.converter import android.content.Context import com.tangem.common.module.ModuleMessageConverter import com.tangem.domain.AddCustomTokenError -import com.tangem.domain.DomainError +import com.tangem.domain.DomainModuleError +import com.tangem.tap.domain.moduleMessage.ConvertedMessage +import com.tangem.tap.domain.moduleMessage.ConvertedStringMessage import com.tangem.wallet.R /** [REDACTED_AUTHOR] */ -class DomainErrorConverter( - private val context: Context -) : ModuleMessageConverter { - override fun convert(message: DomainError): String? = when (message) { - is AddCustomTokenError -> AddCustomTokenConverter(context).convert(message) - else -> null - } -} +internal class AddCustomTokenErrorConverter( + private val context: Context, +) : ModuleMessageConverter { -private class AddCustomTokenConverter( - private val context: Context -) : ModuleMessageConverter { - - override fun convert(message: DomainError): String? { + override fun convert(message: DomainModuleError): ConvertedMessage? { val customTokenError = (message as? AddCustomTokenError) ?: throw UnsupportedOperationException() val rawMessage = when (customTokenError) { @@ -40,8 +33,8 @@ private class AddCustomTokenConverter( } return when (rawMessage) { - is Int -> context.getString(rawMessage) - is String -> rawMessage + is Int -> ConvertedStringMessage(context.getString(rawMessage)) + is String -> ConvertedStringMessage(rawMessage) else -> null } } diff --git a/app/src/main/java/com/tangem/tap/domain/moduleMessage/saltPay/SaltPayMessageConverter.kt b/app/src/main/java/com/tangem/tap/domain/moduleMessage/saltPay/SaltPayMessageConverter.kt new file mode 100644 index 0000000000..6ad14c42b0 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/moduleMessage/saltPay/SaltPayMessageConverter.kt @@ -0,0 +1,34 @@ +package com.tangem.tap.domain.moduleMessage.saltPay + +import android.content.Context +import com.tangem.common.module.ModuleMessageConverter +import com.tangem.tap.domain.moduleMessage.ConvertedMessage +import com.tangem.tap.domain.moduleMessage.saltPay.converter.SaltPayRegistrationErrorConverter +import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayModuleError +import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayModuleMessage +import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayRegistrationError + +/** +[REDACTED_AUTHOR] + */ +class SaltPayMessageConverter( + private val context: Context, +) : ModuleMessageConverter { + + override fun convert(message: SaltPayModuleMessage): ConvertedMessage? { + return when (message) { + is SaltPayModuleError -> SaltPayErrorConverter(context).convert(message) + else -> null + } + } +} + +class SaltPayErrorConverter( + private val context: Context, +) : ModuleMessageConverter { + + override fun convert(message: SaltPayModuleError): ConvertedMessage? = when (message) { + is SaltPayRegistrationError -> SaltPayRegistrationErrorConverter(context).convert(message) + else -> null + } +} diff --git a/app/src/main/java/com/tangem/tap/domain/moduleMessage/saltPay/converter/SaltPayRegistrationErrorConverter.kt b/app/src/main/java/com/tangem/tap/domain/moduleMessage/saltPay/converter/SaltPayRegistrationErrorConverter.kt new file mode 100644 index 0000000000..554a0cab44 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/moduleMessage/saltPay/converter/SaltPayRegistrationErrorConverter.kt @@ -0,0 +1,53 @@ +package com.tangem.tap.domain.moduleMessage.saltPay.converter + +import android.content.Context +import com.tangem.common.module.ModuleMessageConverter +import com.tangem.tap.domain.moduleMessage.ConvertedDialogMessage +import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayRegistrationError +import com.tangem.wallet.R + +/** +[REDACTED_AUTHOR] + */ +internal class SaltPayRegistrationErrorConverter( + private val context: Context, +) : ModuleMessageConverter { + + override fun convert(message: SaltPayRegistrationError): ConvertedDialogMessage? { + val saltPayError = (message as? SaltPayRegistrationError) ?: throw UnsupportedOperationException() + + val dialogMessage = when (saltPayError) { + SaltPayRegistrationError.NoGas -> { + ConvertedDialogMessage( + title = context.getString(R.string.saltpay_error_no_gas_title), + message = context.getString(R.string.saltpay_error_no_gas_message), + ) + } + SaltPayRegistrationError.EmptyBackupCardScanned -> { + ConvertedDialogMessage( + title = context.getString(R.string.saltpay_error_empty_backup_title), + message = context.getString(R.string.saltpay_error_empty_backup_message), + ) + } + SaltPayRegistrationError.WeakPin -> { + ConvertedDialogMessage( + title = context.getString(R.string.saltpay_error_pin_weak_title), + message = context.getString(R.string.saltpay_error_pin_weak_message), + ) + } + else -> { + val errorMessage = if (saltPayError.message == SaltPayRegistrationError.EMPTY_MESSAGE) { + saltPayError::class.java.simpleName + } else { + saltPayError.message + } + ConvertedDialogMessage( + title = context.getString(R.string.common_error), + message = errorMessage, + ) + } + } + + return dialogMessage + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/statePrinter/OnboardingWalletStateConverter.kt b/app/src/main/java/com/tangem/tap/domain/statePrinter/OnboardingWalletStateConverter.kt new file mode 100644 index 0000000000..858dc8b5d1 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/statePrinter/OnboardingWalletStateConverter.kt @@ -0,0 +1,88 @@ +package com.tangem.tap.domain.statePrinter + +import com.squareup.moshi.FromJson +import com.squareup.moshi.ToJson +import com.tangem.common.json.MoshiJsonConverter +import com.tangem.domain.redux.state.StringStateConverter +import com.tangem.tap.common.redux.AppState +import com.tangem.tap.features.onboarding.products.wallet.redux.BackupStep +import com.tangem.tap.store +import timber.log.Timber +import java.math.BigInteger + +/** +[REDACTED_AUTHOR] + */ +class OnboardingWalletStateConverter : StringStateConverter { + private val converter = MoshiJsonConverter( + MoshiJsonConverter.getTangemSdkAdapters() + internalAdapters(), + MoshiJsonConverter.getTangemSdkTypedAdapters(), + ) + + private fun internalAdapters(): List { + return listOf( + BackupStepAdapter(), + BigIntegerAdapter(), + + ) + } + + override fun convert(stateHolder: AppState): String { + val model = stateHolder.onboardingWalletState + val json = converter.prettyPrint(model) + + return json + } +} + +fun printOnboardingWalletState() { + val stringState = OnboardingWalletStateConverter().convert(store.state) + Timber.d(stringState) +} + +class BackupStepAdapter { + @ToJson + fun toJson(src: BackupStep): String { + return when (src) { + BackupStep.AddBackupCards -> "AddBackupCards" + BackupStep.EnterAccessCode -> "EnterAccessCode" + BackupStep.Finished -> "Finished" + BackupStep.InitBackup -> "InitBackup" + BackupStep.ReenterAccessCode -> "ReenterAccessCode" + BackupStep.ScanOriginCard -> "ScanOriginCard" + BackupStep.SetAccessCode -> "SetAccessCode" + is BackupStep.WriteBackupCard -> "WriteBackupCard[${src.cardNumber}]" + BackupStep.WritePrimaryCard -> "WritePrimaryCard" + } + } + + @FromJson + fun fromJson(json: String): BackupStep { + return when (json) { + "AddBackupCards" -> BackupStep.AddBackupCards + "EnterAccessCode" -> BackupStep.EnterAccessCode + "Finished" -> BackupStep.Finished + "InitBackup" -> BackupStep.InitBackup + "ReenterAccessCode" -> BackupStep.ReenterAccessCode + "ScanOriginCard" -> BackupStep.ScanOriginCard + "SetAccessCode" -> BackupStep.SetAccessCode + "WriteBackupCard[1]" -> BackupStep.WriteBackupCard(1) + "WriteBackupCard[2]" -> BackupStep.WriteBackupCard(2) + "WriteBackupCard[3]" -> BackupStep.WriteBackupCard(3) + "WritePrimaryCard" -> BackupStep.WritePrimaryCard + else -> throw UnsupportedOperationException() + } + } +} + +class BigIntegerAdapter { + @ToJson + fun toJson(src: BigInteger): String { + return src.toString() + } + + @FromJson + fun fromJson(json: String): BigInteger { + return BigInteger(json) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt index 485ee0eaea..3bfef52562 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt @@ -4,7 +4,6 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.common.CompletionResult import com.tangem.common.card.Card import com.tangem.common.card.EllipticCurve -import com.tangem.common.card.FirmwareVersion import com.tangem.common.core.CardSession import com.tangem.common.core.CardSessionRunnable import com.tangem.common.core.TangemError @@ -18,10 +17,10 @@ import com.tangem.domain.common.ProductType import com.tangem.domain.common.ScanResponse import com.tangem.domain.common.TapWorkarounds.isExcluded import com.tangem.domain.common.TapWorkarounds.isNotSupportedInThatRelease -import com.tangem.domain.common.TapWorkarounds.isTangemNote +import com.tangem.domain.common.TapWorkarounds.isSaltPay +import com.tangem.domain.common.TapWorkarounds.isTangemTwins import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation import com.tangem.domain.common.TwinsHelper -import com.tangem.domain.common.isTangemTwins import com.tangem.operations.PreflightReadMode import com.tangem.operations.PreflightReadTask import com.tangem.operations.ScanTask @@ -32,6 +31,7 @@ import com.tangem.operations.issuerAndUserData.ReadIssuerDataCommand import com.tangem.tap.domain.TapSdkError import com.tangem.tap.domain.extensions.getPrimaryCurve import com.tangem.tap.domain.extensions.getSingleWallet +import com.tangem.tap.domain.extensions.isMultiwalletAllowed import com.tangem.tap.domain.tokens.UserTokensRepository import com.tangem.tap.domain.tokens.models.BlockchainNetwork import com.tangem.tap.preferencesStorage @@ -58,8 +58,7 @@ class ScanProductTask( } val commandProcessor = when { - card.isTangemNote() -> ScanNoteProcessor() - card.isTangemTwins() -> ScanTwinProcessor() + card.isTangemTwins -> ScanTwinProcessor() else -> ScanWalletProcessor(userTokensRepository, additionalBlockchainsToDerive) } commandProcessor.proceed(card, session) { processorResult -> @@ -83,31 +82,12 @@ class ScanProductTask( } private fun getErrorIfExcludedCard(card: Card): TangemError? { - if (card.isExcluded()) return TapSdkError.CardForDifferentApp - if (card.isNotSupportedInThatRelease()) return TapSdkError.CardNotSupportedByRelease - + if (card.isExcluded) return TapSdkError.CardForDifferentApp + if (card.isNotSupportedInThatRelease) return TapSdkError.CardNotSupportedByRelease return null } } -private class ScanNoteProcessor : ProductCommandProcessor { - override fun proceed( - card: Card, - session: CardSession, - callback: (result: CompletionResult) -> Unit - ) { - callback( - CompletionResult.Success( - ScanResponse( - card = card, - productType = ProductType.Note, - walletData = session.environment.walletData, - ), - ), - ) - } -} - private class ScanWalletProcessor( private val userTokensRepository: UserTokensRepository?, private val additionalBlockchainsToDerive: Collection? = null, @@ -121,12 +101,14 @@ private class ScanWalletProcessor( ) { createMissingWalletsIfNeeded(card, session, callback) } + private fun createMissingWalletsIfNeeded( card: Card, session: CardSession, callback: (result: CompletionResult) -> Unit, ) { - if (card.wallets.isEmpty() || card.firmwareVersion < FirmwareVersion.MultiWalletAvailable) { + if (card.wallets.isEmpty() || !card.isMultiwalletAllowed) { + // match for the saltPay card to startLinkingForBackupIfNeeded(card, session, callback) return } @@ -182,13 +164,17 @@ private class ScanWalletProcessor( session: CardSession, callback: (result: CompletionResult) -> Unit, ) { + val productType = when(card.isSaltPay){ + true -> ProductType.SaltPay + else -> ProductType.Wallet + } val derivations = collectDerivations(card) if (derivations.isEmpty() || !card.settings.isHDWalletAllowed) { callback( CompletionResult.Success( ScanResponse( card = card, - productType = ProductType.Wallet, + productType = productType, walletData = session.environment.walletData, primaryCard = primaryCard, ), @@ -202,7 +188,7 @@ private class ScanWalletProcessor( is CompletionResult.Success -> { val response = ScanResponse( card = card, - productType = ProductType.Wallet, + productType = productType, walletData = session.environment.walletData, derivedKeys = result.data.entries, primaryCard = primaryCard, @@ -256,6 +242,7 @@ private class ScanWalletProcessor( val curve = blockchain.blockchain.getPrimaryCurve() val wallet = card.wallets.firstOrNull { it.curve == curve } ?: return@forEach if (wallet.chainCode == null) return@forEach + val key = wallet.publicKey.toMapKey() val path = blockchain.derivationPath?.let { DerivationPath(it) } if (path != null) { diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensRepository.kt b/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensRepository.kt index fd1583d9ef..67eda0ee93 100644 --- a/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensRepository.kt @@ -106,17 +106,10 @@ class UserTokensRepository( private fun Card.getUserId(): String { val walletPublicKey = this.wallets.firstOrNull()?.publicKey ?: return "" - return calculateUserId(walletPublicKey) - } - - private fun calculateUserId(walletPublicKey: ByteArray): String { - val message = MESSAGE.toByteArray() - val keyHash = walletPublicKey.calculateSha256() - return message.calculateHmacSha256(keyHash).toHexString() + return UserWalletId(walletPublicKey).stringValue } companion object { - const val MESSAGE = "UserWalletID" const val SORT_DEFAULT_VALUE = "manual" const val GROUP_DEFAULT_VALUE = "none" fun init(context: Context, tangemTechService: TangemTechService): UserTokensRepository { @@ -129,4 +122,16 @@ class UserTokensRepository( return UserTokensRepository(storageService, networkService) } } +} + +data class UserWalletId( + val walletPublicKey: ByteArray, +) { + val stringValue: String = calculateUserId(walletPublicKey) + + private fun calculateUserId(walletPublicKey: ByteArray): String { + val message = "UserWalletID".toByteArray() + val keyHash = walletPublicKey.calculateSha256() + return message.calculateHmacSha256(keyHash).toHexString() + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt index fa96a81962..cc9ccef1b6 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt @@ -2,7 +2,7 @@ package com.tangem.tap.features.details.redux import com.tangem.common.CompletionResult import com.tangem.common.core.TangemSdkError -import com.tangem.domain.common.isTangemTwins +import com.tangem.domain.common.TapWorkarounds.isTangemTwins import com.tangem.tap.common.analytics.Analytics import com.tangem.tap.common.analytics.AnalyticsParam import com.tangem.tap.common.extensions.dispatchNotification @@ -69,13 +69,8 @@ class DetailsMiddleware { } is DetailsAction.CreateBackup -> { store.state.detailsState.scanResponse?.let { + store.dispatch(GlobalAction.Onboarding.Start(it, canSkipBackup = false)) store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingWallet)) - store.dispatch( - GlobalAction.Onboarding.Start( - it, - fromHomeScreen = false, - ), - ) } } DetailsAction.ScanCard -> { @@ -98,7 +93,7 @@ class DetailsMiddleware { when (action) { is DetailsAction.ResetToFactory.Start -> { val card = store.state.detailsState.cardSettingsState?.card ?: return - if (card.isTangemTwins()) { + if (card.isTangemTwins) { store.dispatch(DetailsAction.ReCreateTwinsWallet) return } else { diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt index b327e24452..ef4a0aa975 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt @@ -79,7 +79,7 @@ private fun prepareSecurityOptions(card: Card): ManageSecurityState { } } val allowedSecurityOptions = when { - card.isStart2Coin || card.isTangemNote() -> { + card.isStart2Coin || card.isTangemNote || card.isSaltPay -> { EnumSet.of(SecurityOption.LongTap) } card.settings.isBackupAllowed -> { @@ -97,7 +97,7 @@ private fun prepareSecurityOptions(card: Card): ManageSecurityState { private fun isResetToFactoryAllowedByCard(card: Card): Boolean { val notAllowedByAnyWallet = card.wallets.any { it.settings.isPermanent } val notAllowedByCard = notAllowedByAnyWallet || - (card.isWalletDataSupported && (!card.isTangemNote() && !card.settings.isBackupAllowed)) || + (card.isWalletDataSupported && (!card.isTangemNote && !card.settings.isBackupAllowed)) || card.isSaltPay return !notAllowedByCard } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt index 60affefd8b..b238ffe8f2 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt @@ -1,7 +1,7 @@ package com.tangem.tap.features.details.ui.cardsettings +import com.tangem.domain.common.TapWorkarounds.isTangemTwins import com.tangem.domain.common.getTwinCardIdForUser -import com.tangem.domain.common.isTangemTwins import com.tangem.tap.common.redux.AppState import com.tangem.tap.features.details.redux.CardSettingsState import com.tangem.tap.features.details.redux.DetailsAction @@ -20,7 +20,7 @@ class CardSettingsViewModel(private val store: Store) { }, ) } else { - val cardId = if (state.card.isTangemTwins()) { + val cardId = if (state.card.isTangemTwins) { state.card.getTwinCardIdForUser() } else { state.cardInfo.cardId @@ -30,7 +30,7 @@ class CardSettingsViewModel(private val store: Store) { CardInfo.Issuer(state.cardInfo.issuer), ) - if (!state.card.isTangemTwins()) { + if (!state.card.isTangemTwins) { cardDetails.add(CardInfo.SignedHashes(state.cardInfo.signedHashes.toString())) } cardDetails.add( diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt index 97ce019a84..502437104c 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt @@ -1,5 +1,6 @@ package com.tangem.tap.features.details.ui.details +import com.tangem.domain.common.TapWorkarounds.isSaltPay import com.tangem.domain.common.TapWorkarounds.isStart2Coin import com.tangem.tap.common.feedback.FeedbackEmail import com.tangem.tap.common.feedback.SupportInfo @@ -25,7 +26,9 @@ class DetailsViewModel(private val store: Store) { if (state.scanResponse?.card?.isMultiwalletAllowed == true) it else null } SettingsElement.LinkMoreCards -> { - if (state.createBackupAllowed) it else null + // if (state.createBackupAllowed) it else null + // TODO: SaltPay: temporary excluding backup process for Visa cards + if (state.createBackupAllowed && state.scanResponse?.card?.isSaltPay != true) it else null } SettingsElement.PrivacyPolicy -> { if (state.privacyPolicyUrl != null) it else null diff --git a/app/src/main/java/com/tangem/tap/features/disclaimer/ui/DisclaimerFragment.kt b/app/src/main/java/com/tangem/tap/features/disclaimer/ui/DisclaimerFragment.kt index 4538be7224..abbd7bf8de 100644 --- a/app/src/main/java/com/tangem/tap/features/disclaimer/ui/DisclaimerFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/disclaimer/ui/DisclaimerFragment.kt @@ -6,6 +6,7 @@ import androidx.activity.OnBackPressedCallback import androidx.fragment.app.Fragment import androidx.transition.TransitionInflater import by.kirich1409.viewbindingdelegate.viewBinding +import com.tangem.tap.common.extensions.configureSettings import com.tangem.tap.common.extensions.hide import com.tangem.tap.common.redux.navigation.NavigationAction import com.tangem.tap.features.disclaimer.redux.DisclaimerAction @@ -16,17 +17,20 @@ import com.tangem.wallet.databinding.FragmentDisclaimerBinding import org.rekotlin.StoreSubscriber class DisclaimerFragment : Fragment(R.layout.fragment_disclaimer), - StoreSubscriber { + StoreSubscriber { private val binding: FragmentDisclaimerBinding by viewBinding(FragmentDisclaimerBinding::bind) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - store.dispatch(NavigationAction.PopBackTo()) - } - }) + activity?.onBackPressedDispatcher?.addCallback( + this, + object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + store.dispatch(NavigationAction.PopBackTo()) + } + }, + ) val inflater = TransitionInflater.from(requireContext()) enterTransition = inflater.inflateTransition(android.R.transition.slide_bottom) exitTransition = inflater.inflateTransition(android.R.transition.slide_top) @@ -51,9 +55,15 @@ class DisclaimerFragment : Fragment(R.layout.fragment_disclaimer), binding.toolbar.setNavigationOnClickListener { store.dispatch(NavigationAction.PopBackTo()) } + initAndLoadTOS() setOnClickListeners() } + private fun initAndLoadTOS() { + binding.webView.configureSettings() + binding.webView.loadUrl("file:///android_asset/tos.html") + } + private fun setOnClickListeners() { binding.btnAccept.setOnClickListener { store.dispatch(DisclaimerAction.AcceptDisclaimer) } } diff --git a/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt b/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt index 27953640ea..487362e5c1 100644 --- a/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt @@ -81,16 +81,12 @@ class HomeFragment : Fragment(), StoreSubscriber { StoriesScreen( homeState, onScanButtonClick = { store.dispatch(HomeAction.ReadCard) }, - onShopButtonClick = { - store.dispatch( - HomeAction.GoToShop(store.state.globalState.userCountryCode) - ) - }, + onShopButtonClick = { store.dispatch(HomeAction.GoToShop(store.state.globalState.userCountryCode)) }, onSearchTokensClick = { store.dispatch(NavigationAction.NavigateTo(AppScreen.AddTokens)) store.dispatch(TokensAction.AllowToAddTokens(false)) store.dispatch(TokensAction.LoadCurrencies()) - } + }, ) } diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/StoriesScreen.kt b/app/src/main/java/com/tangem/tap/features/home/compose/StoriesScreen.kt index 21adc5e516..a459a4d701 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/StoriesScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/home/compose/StoriesScreen.kt @@ -49,7 +49,6 @@ import com.tangem.tap.features.home.compose.content.StoriesWeb3 import com.tangem.tap.features.home.compose.views.HomeButtons import com.tangem.tap.features.home.compose.views.StoriesProgressBar import com.tangem.tap.features.home.redux.HomeState -import com.tangem.tap.features.wallet.redux.ProgressState import com.tangem.wallet.R import kotlin.math.max @@ -74,7 +73,7 @@ fun StoriesScreen( } val isPressed = remember { mutableStateOf(false) } - val isPaused = isPressed.value || homeState.value.btnScanState.progressState == ProgressState.Loading + val isPaused = isPressed.value || homeState.value.scanInProgress val hideContent = remember { mutableStateOf(true) } @@ -88,10 +87,10 @@ fun StoriesScreen( Box( modifier = Modifier .fillMaxSize() - .background(Color(0xFF090E13)) + .background(Color(0xFF090E13)), ) { Row( - modifier = Modifier.fillMaxSize() + modifier = Modifier.fillMaxSize(), ) { Box( Modifier @@ -109,7 +108,7 @@ fun StoriesScreen( isPressed.value = false }, ) - } + }, ) Box( Modifier @@ -127,7 +126,7 @@ fun StoriesScreen( isPressed.value = false }, ) - } + }, ) } if (!isDarkBackground) { @@ -135,7 +134,7 @@ fun StoriesScreen( modifier = Modifier.fillMaxSize(), painter = painterResource(id = R.drawable.ic_overlay), contentDescription = null, - contentScale = ContentScale.FillBounds + contentScale = ContentScale.FillBounds, ) } @@ -165,7 +164,7 @@ fun StoriesScreen( .height(17.dp) .alpha(if (hideContent.value) 0f else 1f) .align(Alignment.Start), - colorFilter = if (isDarkBackground) null else ColorFilter.tint(Color.Black) + colorFilter = if (isDarkBackground) null else ColorFilter.tint(Color.Black), ) when (currentStep.value) { 1 -> FirstStoriesContent(isPaused, currentStep.duration()) { hideContent.value = it } @@ -190,27 +189,28 @@ fun StoriesScreen( .height(48.dp), colors = ButtonDefaults.textButtonColors( backgroundColor = Color.White, - contentColor = Color(0xFF080C10) - ) + contentColor = Color(0xFF080C10), + ), ) { Image( painter = painterResource(id = R.drawable.ic_search), - contentDescription = null + contentDescription = null, ) Text( text = stringResource(id = R.string.search_tokens_title), fontWeight = FontWeight.Medium, fontSize = 16.sp, - textAlign = TextAlign.Center + textAlign = TextAlign.Center, ) } HomeButtons( - isDarkBackground = isDarkBackground, modifier = Modifier .padding(start = 16.dp, top = 0.dp, end = 16.dp, bottom = 37.dp) .fillMaxWidth(), + isDarkBackground = isDarkBackground, + btnScanStateInProgress = homeState.value.btnScanStateInProgress, onScanButtonClick = onScanButtonClick, - onShopButtonClick = onShopButtonClick + onShopButtonClick = onShopButtonClick, ) } } diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/views/HomeButtons.kt b/app/src/main/java/com/tangem/tap/features/home/compose/views/HomeButtons.kt index d49013db87..cb40186a87 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/views/HomeButtons.kt +++ b/app/src/main/java/com/tangem/tap/features/home/compose/views/HomeButtons.kt @@ -3,8 +3,10 @@ package com.tangem.tap.features.home.compose.views import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size import androidx.compose.material.Button import androidx.compose.material.ButtonDefaults +import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -19,53 +21,89 @@ import com.tangem.wallet.R @Composable fun HomeButtons( - isDarkBackground: Boolean, modifier: Modifier = Modifier, + modifier: Modifier = Modifier, + isDarkBackground: Boolean, + btnScanStateInProgress: Boolean, onScanButtonClick: () -> Unit, - onShopButtonClick: () -> Unit + onShopButtonClick: () -> Unit, ) { val darkColorBackground = Color(0xFF26292E) val darkColorButton = Color(0xFF080C10) Row( horizontalArrangement = Arrangement.SpaceEvenly, - modifier = modifier + modifier = modifier, ) { - Button( + ProgressButton( modifier = Modifier .weight(1f) .height(48.dp), + inProgress = btnScanStateInProgress, + backgroundColor = if (isDarkBackground) darkColorBackground else Color.White, + contentColor = if (isDarkBackground) Color.White else darkColorButton, onClick = onScanButtonClick, - colors = ButtonDefaults.textButtonColors( - backgroundColor = if (isDarkBackground) darkColorBackground else Color.White, - contentColor = if (isDarkBackground) Color.White else darkColorButton - ) - ) { - Text( - text = stringResource(id = R.string.home_button_scan), - fontWeight = FontWeight.Medium, - fontSize = 16.sp, - textAlign = TextAlign.Center, - maxLines = 1 - ) - } + progressContent = { + CircularProgressIndicator( + modifier = Modifier.size(30.dp), + color = if (isDarkBackground) Color.White else darkColorBackground, + strokeWidth = 3.dp, + ) + }, + content = { + Text( + text = stringResource(id = R.string.home_button_scan), + fontWeight = FontWeight.Medium, + fontSize = 16.sp, + textAlign = TextAlign.Center, + maxLines = 1, + ) + }, + ) SpacerS8() Button( modifier = Modifier .weight(1f) .height(48.dp), onClick = onShopButtonClick, + enabled = !btnScanStateInProgress, colors = ButtonDefaults.textButtonColors( backgroundColor = if (isDarkBackground) Color.White else darkColorBackground, - contentColor = if (isDarkBackground) darkColorButton else Color.White - ) + contentColor = if (isDarkBackground) darkColorButton else Color.White, + ), ) { Text( text = stringResource(id = R.string.home_button_order), fontWeight = FontWeight.Medium, fontSize = 16.sp, textAlign = TextAlign.Center, - maxLines = 1 + maxLines = 1, ) } } +} + +@Composable +fun ProgressButton( + modifier: Modifier, + backgroundColor: Color, + contentColor: Color, + onClick: () -> Unit, + inProgress: Boolean, + progressContent: @Composable (() -> Unit)? = null, + content: @Composable () -> Unit, +) { + Button( + modifier = modifier, + onClick = onClick, + colors = ButtonDefaults.textButtonColors( + backgroundColor = backgroundColor, + contentColor = contentColor, + ), + ) { + if (progressContent != null && inProgress) { + progressContent() + } else { + content() + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt index 1328156afa..c22bd6c043 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt +++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt @@ -6,6 +6,7 @@ import org.rekotlin.Action sealed class HomeAction : Action { // from ui object ReadCard : HomeAction() + data class ScanInProgress(val scanInProgress: Boolean) : HomeAction() data class GoToShop(val userCountryCode: String?) : HomeAction() // internal @@ -14,5 +15,4 @@ sealed class HomeAction : Action { data class SetTermsOfUseState(val isDisclaimerAccepted: Boolean) : HomeAction() data class ChangeScanCardButtonState(val state: IndeterminateProgressButton) : HomeAction() - } diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt index 6b0bf8e8e0..7ecc17b56e 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt @@ -1,15 +1,21 @@ package com.tangem.tap.features.home.redux +import com.tangem.common.core.TangemError +import com.tangem.common.services.Result +import com.tangem.domain.common.ScanResponse +import com.tangem.domain.common.extensions.withIOContext import com.tangem.domain.common.extensions.withMainContext import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE import com.tangem.tap.common.analytics.AnalyticsEvent import com.tangem.tap.common.analytics.AnalyticsParam import com.tangem.tap.common.analytics.GetCardSourceParams import com.tangem.tap.common.entities.IndeterminateProgressButton +import com.tangem.tap.common.extensions.dispatchDebugErrorNotification +import com.tangem.tap.common.extensions.dispatchDialogShow import com.tangem.tap.common.extensions.dispatchOpenUrl import com.tangem.tap.common.extensions.onCardScanned -import com.tangem.tap.common.post -import com.tangem.tap.common.postUi +import com.tangem.tap.common.postUiDelayBg +import com.tangem.tap.common.redux.AppDialog import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.common.redux.navigation.AppScreen @@ -19,13 +25,22 @@ import com.tangem.tap.features.home.BELARUS_COUNTRY_CODE import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE import com.tangem.tap.features.home.redux.HomeMiddleware.Companion.BUY_WALLET_URL import com.tangem.tap.features.onboarding.OnboardingHelper +import com.tangem.tap.features.onboarding.OnboardingSaltPayHelper import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsStep +import com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog.SaltPayDialog +import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayRegistrationError +import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayAction +import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayState import com.tangem.tap.features.send.redux.states.ButtonState import com.tangem.tap.preferencesStorage import com.tangem.tap.scope import com.tangem.tap.store +import com.tangem.wallet.R +import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import org.rekotlin.Action +import org.rekotlin.DispatchFunction import org.rekotlin.Middleware class HomeMiddleware { @@ -36,87 +51,146 @@ class HomeMiddleware { } } -private val homeMiddleware: Middleware = { _, _ -> +private val homeMiddleware: Middleware = { dispatch, state -> { next -> { action -> - when (action) { - is HomeAction.Init -> { - store.dispatch(GlobalAction.RestoreAppCurrency) - store.dispatch(GlobalAction.ExchangeManager.Init) - store.dispatch(HomeAction.SetTermsOfUseState(preferencesStorage.wasDisclaimerAccepted())) - store.dispatch(GlobalAction.FetchUserCountry) - } - is HomeAction.ShouldScanCardOnResume -> { - if (action.shouldScanCard) { - store.dispatch(HomeAction.ShouldScanCardOnResume(false)) - postUi(700) { store.dispatch(HomeAction.ReadCard) } - } - } - is HomeAction.ReadCard -> { - handleReadCard() -// store.dispatch(NavigationAction.NavigateTo(AppScreen.AddCustomTokens)) - } - is HomeAction.GoToShop -> { - when (action.userCountryCode) { - RUSSIA_COUNTRY_CODE, BELARUS_COUNTRY_CODE -> - store.dispatchOpenUrl(BUY_WALLET_URL) - else -> store.dispatch(NavigationAction.NavigateTo(AppScreen.Shop)) - } - store.state.globalState.analyticsHandlers?.triggerEvent( - event = AnalyticsEvent.GET_CARD, - params = mapOf(AnalyticsParam.SOURCE.param to GetCardSourceParams.WELCOME.param) - ) - } - } + handleHomeAction(state, action, dispatch) next(action) } } } -private fun handleReadCard() { - if (!preferencesStorage.wasDisclaimerAccepted()) { - store.dispatch(NavigationAction.NavigateTo(AppScreen.Disclaimer)) - } else { - changeButtonState(ButtonState.PROGRESS) - store.dispatch( - GlobalAction.ScanCard( - onSuccess = { scanResponse -> - store.state.globalState.tapWalletManager.updateConfigManager(scanResponse) - store.dispatch(TwinCardsAction.IfTwinsPrepareState(scanResponse)) - - if (OnboardingHelper.isOnboardingCase(scanResponse)) { - val navigateTo = OnboardingHelper.whereToNavigate(scanResponse) - store.dispatch(GlobalAction.Onboarding.Start(scanResponse)) - navigateTo(navigateTo) - } else { - scope.launch { - withMainContext { - if (scanResponse.twinsIsTwinned() && !preferencesStorage.wasTwinsOnboardingShown()) { - store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly)) - navigateTo(AppScreen.OnboardingTwins) - } else { - navigateTo(AppScreen.Wallet, null) - } - } - store.onCardScanned(scanResponse) - } - } - }, - onFailure = { - changeButtonState(ButtonState.ENABLED) - }, - ), - ) +private fun handleHomeAction(appState: () -> AppState?, action: Action, dispatch: DispatchFunction) { + when (action) { + is HomeAction.Init -> { + store.dispatch(GlobalAction.RestoreAppCurrency) + store.dispatch(GlobalAction.ExchangeManager.Init) + store.dispatch(HomeAction.SetTermsOfUseState(preferencesStorage.wasDisclaimerAccepted())) + store.dispatch(GlobalAction.FetchUserCountry) + } + is HomeAction.ShouldScanCardOnResume -> { + if (action.shouldScanCard) { + store.dispatch(HomeAction.ShouldScanCardOnResume(false)) + postUiDelayBg(700) { store.dispatch(HomeAction.ReadCard) } + } + } + is HomeAction.ReadCard -> { + if (!preferencesStorage.wasDisclaimerAccepted()) { + store.dispatch(NavigationAction.NavigateTo(AppScreen.Disclaimer)) + } else { + changeButtonState(ButtonState.PROGRESS) + val scanCardAction = GlobalAction.ScanCard( + onSuccess = ::onScanSuccess, + onFailure = ::onScanFailure, + ) + store.dispatch(HomeAction.ScanInProgress(true)) + postUiDelayBg(300) { store.dispatch(scanCardAction) } + } + } + is HomeAction.GoToShop -> { + when (action.userCountryCode) { + RUSSIA_COUNTRY_CODE, BELARUS_COUNTRY_CODE -> store.dispatchOpenUrl(BUY_WALLET_URL) + else -> store.dispatch(NavigationAction.NavigateTo(AppScreen.Shop)) + } + store.state.globalState.analyticsHandlers?.triggerEvent( + event = AnalyticsEvent.GET_CARD, + params = mapOf(AnalyticsParam.SOURCE.param to GetCardSourceParams.WELCOME.param), + ) + } } } +private fun onScanSuccess(scanResponse: ScanResponse) { + val globalState = store.state.globalState + val tapWalletManager = globalState.tapWalletManager + tapWalletManager.updateConfigManager(scanResponse) + + store.dispatch(HomeAction.ScanInProgress(false)) + store.dispatch(TwinCardsAction.IfTwinsPrepareState(scanResponse)) + + if (scanResponse.isSaltPay()) { + if (scanResponse.isSaltPayVisa()) { + scope.launch { + val result = OnboardingSaltPayHelper.isOnboardingCase(scanResponse) + delay(500) + withMainContext { + when (result) { + is Result.Success -> { + val isOnboardingCase = result.data + if (isOnboardingCase) { + store.dispatch(GlobalAction.Onboarding.Start(scanResponse, canSkipBackup = false)) + val (manager, config) = OnboardingSaltPayState.initDependency(scanResponse) + store.dispatch(OnboardingSaltPayAction.Init.SetDependencies(manager, config)) + store.dispatch(OnboardingSaltPayAction.Update) + // store.dispatch(OnboardingSaltPayAction.Init.DiscardBackupSteps) + navigateTo(AppScreen.OnboardingWallet) + } else { + navigateTo(AppScreen.Wallet) + withIOContext { store.onCardScanned(scanResponse) } + } + } + is Result.Failure -> { + changeButtonState(ButtonState.ENABLED) + when (val error = result.error) { + is SaltPayRegistrationError -> { + val dialog = when (error) { + is SaltPayRegistrationError.NoGas -> SaltPayDialog.NoFundsForActivation + else -> SaltPayDialog.RegistrationError(error) + } + store.dispatchDialogShow(dialog) + } + else -> { + store.dispatchDebugErrorNotification( + error.localizedMessage ?: "SaltPay: Unknown error", + ) + } + } + } + } + } + } + } else { + if (scanResponse.card.backupStatus?.isActive == false) { + changeButtonState(ButtonState.ENABLED) + store.dispatchDialogShow( + AppDialog.SimpleOkDialogRes( + headerId = R.string.saltpay_error_empty_backup_title, + messageId = R.string.saltpay_error_empty_backup_message, + ), + ) + } else { + navigateTo(AppScreen.Wallet, null) + scope.launch { store.onCardScanned(scanResponse) } + } + } + } else { + if (OnboardingHelper.isOnboardingCase(scanResponse)) { + store.dispatch(GlobalAction.Onboarding.Start(scanResponse, canSkipBackup = true)) + val appScreen = OnboardingHelper.whereToNavigate(scanResponse) + navigateTo(appScreen) + } else { + if (scanResponse.twinsIsTwinned() && !preferencesStorage.wasTwinsOnboardingShown()) { + store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly)) + navigateTo(AppScreen.OnboardingTwins) + } else { + navigateTo(AppScreen.Wallet, null) + } + scope.launch { store.onCardScanned(scanResponse) } + } + } +} + +private fun onScanFailure(error: TangemError) { + store.dispatch(HomeAction.ScanInProgress(false)) + changeButtonState(ButtonState.ENABLED) +} + private fun changeButtonState(state: ButtonState) { - val btnState = IndeterminateProgressButton(state) - store.dispatch(HomeAction.ChangeScanCardButtonState(btnState)) + store.dispatch(HomeAction.ChangeScanCardButtonState(IndeterminateProgressButton(state))) } private fun navigateTo(screen: AppScreen, transition: FragmentShareTransition? = null) { - post(DELAY_SDK_DIALOG_CLOSE) { + postUiDelayBg(DELAY_SDK_DIALOG_CLOSE) { changeButtonState(ButtonState.ENABLED) store.dispatch(NavigationAction.NavigateTo(screen, transition)) } diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeReducer.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeReducer.kt index f4a7a9d407..511aea3ddc 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeReducer.kt @@ -15,6 +15,9 @@ private fun internalReduce(action: Action, state: AppState): HomeState { var state = state.homeState when (action) { + is HomeAction.ScanInProgress -> { + state = state.copy(scanInProgress = action.scanInProgress) + } is HomeAction.ShouldScanCardOnResume -> { state = state.copy(shouldScanCardOnResume = action.shouldScanCard) } diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeState.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeState.kt index 38b19013d8..9ef6ea2b57 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeState.kt +++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeState.kt @@ -2,9 +2,15 @@ package com.tangem.tap.features.home.redux import com.tangem.tap.common.entities.IndeterminateProgressButton import com.tangem.tap.features.send.redux.states.ButtonState +import com.tangem.tap.features.wallet.redux.ProgressState import org.rekotlin.StateType data class HomeState( val shouldScanCardOnResume: Boolean = false, + val scanInProgress: Boolean = false, val btnScanState: IndeterminateProgressButton = IndeterminateProgressButton(ButtonState.ENABLED), -) : StateType \ No newline at end of file +) : StateType { + + val btnScanStateInProgress: Boolean + get() = btnScanState.progressState == ProgressState.Loading +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt index 7efb84deb6..f2227fcc32 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt @@ -15,16 +15,14 @@ class OnboardingHelper { fun isOnboardingCase(response: ScanResponse): Boolean { val cardInfoStorage = preferencesStorage.usedCardsPrefStorage return when { - response.productType == ProductType.Twins -> { + response.isTangemTwins() -> { if (!response.twinsIsTwinned()) { true } else { cardInfoStorage.activationIsStarted(response.card.cardId) } } - response.card.hasWallets() -> { - cardInfoStorage.activationIsStarted(response.card.cardId) - } + response.card.hasWallets() -> cardInfoStorage.activationIsStarted(response.card.cardId) else -> true } } @@ -38,6 +36,7 @@ class OnboardingHelper { AppScreen.OnboardingOther } ProductType.Twins -> AppScreen.OnboardingTwins + ProductType.SaltPay -> AppScreen.OnboardingWallet } } } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingSaltPayHelper.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingSaltPayHelper.kt new file mode 100644 index 0000000000..8b56bbc65a --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingSaltPayHelper.kt @@ -0,0 +1,34 @@ +package com.tangem.tap.features.onboarding + +import com.tangem.common.services.Result +import com.tangem.domain.common.ScanResponse +import com.tangem.domain.common.extensions.successOr +import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayRegistrationError +import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayState +import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.SaltPayRegistrationStep +import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.toSaltPayStep + +/** +[REDACTED_AUTHOR] + */ +class OnboardingSaltPayHelper { + companion object { + + suspend fun isOnboardingCase(scanResponse: ScanResponse): Result { + return try { + val (manager, config) = OnboardingSaltPayState.initDependency(scanResponse) + val registrationResponseItem = manager.checkRegistration().successOr { return it } + val registrationStep = registrationResponseItem.toSaltPayStep() + manager.checkHasGas().successOr { return it } + + val isRegistrationOnboardingCase = registrationStep != SaltPayRegistrationStep.Finished + val isBackupOnboardingCase = scanResponse.card.backupStatus?.isActive == false + Result.Success(isRegistrationOnboardingCase || isBackupOnboardingCase) + } catch (ex: Exception) { + Result.Failure(ex) + } catch (error: SaltPayRegistrationError){ + Result.Failure(error) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/OnboardingNoteFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/OnboardingNoteFragment.kt index b31dd02832..a81bc28e87 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/OnboardingNoteFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/OnboardingNoteFragment.kt @@ -49,7 +49,7 @@ class OnboardingNoteFragment : BaseOnboardingFragment() { startPostponedEnterTransition() binding.toolbar.setTitle(R.string.onboarding_title) - btnRefreshBalanceWidget = RefreshBalanceWidget(binding.onboardingTopContainer.onboardingMainContainer) + btnRefreshBalanceWidget = RefreshBalanceWidget(binding.onboardingTopContainer.onboardingWalletContainer) store.dispatch(OnboardingNoteAction.LoadCardArtwork) store.dispatch(OnboardingNoteAction.DetermineStepOfScreen) @@ -207,10 +207,10 @@ class OnboardingNoteFragment : BaseOnboardingFragment() { with(binding.onboardingTopContainer) { val constraintSet = ConstraintSet() constraintSet.clone(requireContext(), layoutId) - constraintSet.applyTo(onboardingMainContainer) + constraintSet.applyTo(onboardingWalletContainer) val transition = InternalNoteLayoutTransition() transition.interpolator = OvershootInterpolator() - TransitionManager.beginDelayedTransition(onboardingMainContainer, transition) + TransitionManager.beginDelayedTransition(onboardingWalletContainer, transition) } } } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/OnboardingOtherCardsFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/OnboardingOtherCardsFragment.kt index e2b1e015c4..bb09b3d434 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/OnboardingOtherCardsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/OnboardingOtherCardsFragment.kt @@ -116,10 +116,10 @@ class OnboardingOtherCardsFragment : BaseOnboardingFragment() { 285f * deviceScaleFactorForWelcomeState } btnRefreshBalanceWidget = - RefreshBalanceWidget(binding.onboardingTopContainer.onboardingMainContainer) + RefreshBalanceWidget(binding.onboardingTopContainer.onboardingWalletContainer) binding.toolbar.title = getText(R.string.twins_recreate_toolbar) @@ -212,7 +212,7 @@ class TwinsCardsFragment : BaseOnboardingFragment() { chbUnderstand.hide() pbBinding.pbState.show() - binding.onboardingTopContainer.onboardingMainContainer.beginDelayedTransition() + binding.onboardingTopContainer.onboardingWalletContainer.beginDelayedTransition() when (previousStep) { TwinCardsStep.None -> { @@ -383,10 +383,10 @@ class TwinsCardsFragment : BaseOnboardingFragment() { with(binding.onboardingTopContainer) { val constraintSet = ConstraintSet() constraintSet.clone(requireContext(), layoutId) - constraintSet.applyTo(onboardingMainContainer) + constraintSet.applyTo(onboardingWalletContainer) val transition = InternalNoteLayoutTransition() transition.interpolator = OvershootInterpolator() - TransitionManager.beginDelayedTransition(onboardingMainContainer, transition) + TransitionManager.beginDelayedTransition(onboardingWalletContainer, transition) } } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletAction.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletAction.kt index 78631a152f..ce9a6b6c41 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletAction.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletAction.kt @@ -5,12 +5,14 @@ import org.rekotlin.Action sealed class OnboardingWalletAction : Action { object Init : OnboardingWalletAction() + object StartSaltPayCardActivation: OnboardingWalletAction() object GetToCreateWalletStep : OnboardingWalletAction() + object GetToSaltPayStep : OnboardingWalletAction() object CreateWallet : OnboardingWalletAction() object Done : OnboardingWalletAction() object FinishOnboarding : OnboardingWalletAction() - object ProceedBackup : OnboardingWalletAction() + object ResumeBackup : OnboardingWalletAction() object LoadArtwork : OnboardingWalletAction() class SetArtworkUrl(val artworkUrl: String?) : OnboardingWalletAction() @@ -53,15 +55,13 @@ sealed class BackupAction : Action { data class SaveAccessCodeConfirmation(val accessCodeConfirmation: String) : BackupAction() object OnAccessCodeDialogClosed : BackupAction() - data class PrepareToWriteBackupCard(val cardNumber: Int) : BackupAction() - data class WriteBackupCard(val cardNumber: Int) : BackupAction() - object PrepareToWritePrimaryCard : BackupAction() object WritePrimaryCard : BackupAction() + data class PrepareToWriteBackupCard(val cardNumber: Int) : BackupAction() + data class WriteBackupCard(val cardNumber: Int) : BackupAction() object FinishBackup : BackupAction() object DiscardBackup : BackupAction() object DiscardSavedBackup : BackupAction() - object ResumeBackup : BackupAction() - + object ResumeFoundUnfinishedBackup : BackupAction() } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt index 25fc8e2f38..eb23b6b4e0 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt @@ -1,12 +1,15 @@ package com.tangem.tap.features.onboarding.products.wallet.redux import com.tangem.blockchain.common.Blockchain +import com.tangem.common.CardFilter import com.tangem.common.CompletionResult import com.tangem.common.card.Card +import com.tangem.common.extensions.ifNotNull +import com.tangem.domain.common.SaltPayWorkaround import com.tangem.domain.common.ScanResponse import com.tangem.domain.common.extensions.withMainContext import com.tangem.operations.backup.BackupService -import com.tangem.tap.* +import com.tangem.tap.backupService import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction @@ -16,10 +19,18 @@ import com.tangem.tap.domain.extensions.hasWallets import com.tangem.tap.domain.tokens.models.BlockchainNetwork import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.features.home.redux.HomeAction +import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayAction +import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayState import com.tangem.tap.features.wallet.redux.Artwork import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.preferencesStorage +import com.tangem.tap.scope +import com.tangem.tap.store +import com.tangem.tap.tangemSdk +import com.tangem.tap.tangemSdkManager import kotlinx.coroutines.launch import org.rekotlin.Action +import org.rekotlin.DispatchFunction import org.rekotlin.Middleware class OnboardingWalletMiddleware { @@ -31,35 +42,57 @@ class OnboardingWalletMiddleware { private val onboardingWalletMiddleware: Middleware = { dispatch, state -> { next -> { action -> - handleWalletAction(action) + handleWalletAction(action, state, dispatch) next(action) } } } -private fun handleWalletAction(action: Action) { +private fun handleWalletAction(action: Action, state: () -> AppState?, dispatch: DispatchFunction) { if (action !is OnboardingWalletAction) return + val globalState = store.state.globalState val onboardingManager = globalState.onboardingState.onboardingManager val scanResponse = onboardingManager?.scanResponse val card = scanResponse?.card - val walletState = store.state.onboardingWalletState + val onboardingWalletState = store.state.onboardingWalletState when (action) { OnboardingWalletAction.Init -> { - val action = when { + // if (walletState.onboardingSaltPayState?.isTest == true) { + // store.dispatch(BackupAction.FinishBackup) + // return + // } + + when { card == null -> { - OnboardingWalletAction.ProceedBackup + // StartForUnfinishedBackup -> navigateTo(OnboardingWallet) -> OnboardingWalletAction.Init + store.dispatch(OnboardingWalletAction.ResumeBackup) + } + card.hasWallets() && card.backupStatus == Card.BackupStatus.NoBackup -> { + store.dispatch(OnboardingWalletAction.ResumeBackup) + } + card.hasWallets() && card.backupStatus?.isActive == true -> { + if (scanResponse.isSaltPay()) { + store.dispatch(OnboardingWalletAction.GetToSaltPayStep) + } + store.dispatch(BackupAction.FinishBackup) + } + else -> { + store.dispatch(OnboardingWalletAction.GetToCreateWalletStep) + } + } + } + is OnboardingWalletAction.StartSaltPayCardActivation -> { + // we need to do this because the standard activation happens in the OnboardingWalletAction.CreateWallet + // step which is omitted for SaltPay cards + if (onboardingWalletState.isSaltPay) { + ifNotNull(onboardingManager, scanResponse?.card?.cardId) { manager, cardId -> + manager.activationStarted(cardId) } - card.hasWallets() && card.backupStatus == Card.BackupStatus.NoBackup -> - OnboardingWalletAction.ProceedBackup - card.hasWallets() && card.backupStatus?.isActive == true -> - BackupAction.FinishBackup - else -> OnboardingWalletAction.GetToCreateWalletStep } - store.dispatch(action) } is OnboardingWalletAction.LoadArtwork -> { scope.launch { @@ -80,25 +113,24 @@ private fun handleWalletAction(action: Action) { when (result) { is CompletionResult.Success -> { //here we must use updated scanResponse after createWallet & derivation - val updatedResponse = - globalState.onboardingState.onboardingManager.scanResponse.copy( - card = result.data.card, - derivedKeys = result.data.derivedKeys, - primaryCard = result.data.primaryCard - ) + val updatedResponse = globalState.onboardingState.onboardingManager.scanResponse.copy( + card = result.data.card, + derivedKeys = result.data.derivedKeys, + primaryCard = result.data.primaryCard, + ) onboardingManager.scanResponse = updatedResponse val blockchainNetworks = listOf( BlockchainNetwork(Blockchain.Bitcoin, result.data.card), - BlockchainNetwork(Blockchain.Ethereum, result.data.card) + BlockchainNetwork(Blockchain.Ethereum, result.data.card), ) - store.dispatchOnMain( + store.dispatch( WalletAction.MultiWallet.SaveCurrencies( blockchainNetworks = blockchainNetworks, card = result.data.card, ), ) onboardingManager.activationStarted(updatedResponse.card.cardId) - store.dispatch(OnboardingWalletAction.ProceedBackup) + store.dispatch(OnboardingWalletAction.ResumeBackup) } is CompletionResult.Failure -> { // do nothing @@ -110,6 +142,7 @@ private fun handleWalletAction(action: Action) { OnboardingWalletAction.FinishOnboarding -> { store.dispatch(GlobalAction.Onboarding.Stop) + //TODO: SaltPay: scanResponse if (scanResponse == null) { store.dispatch(NavigationAction.PopBackTo()) store.dispatch(HomeAction.ReadCard) @@ -120,15 +153,19 @@ private fun handleWalletAction(action: Action) { store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Wallet)) } } - OnboardingWalletAction.ProceedBackup -> { + is OnboardingWalletAction.ResumeBackup -> { val newAction = when (val backupState = backupService.currentState) { BackupService.State.FinalizingPrimaryCard -> BackupAction.PrepareToWritePrimaryCard is BackupService.State.FinalizingBackupCard -> BackupAction.PrepareToWriteBackupCard(backupState.index) else -> { - if (walletState.backupState.backupStep == BackupStep.InitBackup || - walletState.backupState.backupStep == BackupStep.Finished + if (onboardingWalletState.backupState.backupStep == BackupStep.InitBackup || + onboardingWalletState.backupState.backupStep == BackupStep.Finished ) { - BackupAction.IntroduceBackup + if (onboardingWalletState.isSaltPay) { + BackupAction.StartBackup + } else { + BackupAction.IntroduceBackup + } } else { null } @@ -137,7 +174,7 @@ private fun handleWalletAction(action: Action) { newAction?.let { store.dispatch(it) } } OnboardingWalletAction.OnBackPressed -> { - when (walletState.backupState.backupStep) { + when (onboardingWalletState.backupState.backupStep) { BackupStep.InitBackup, BackupStep.Finished -> store.dispatch(NavigationAction.PopBackTo()) BackupStep.ScanOriginCard, BackupStep.AddBackupCards, BackupStep.EnterAccessCode, BackupStep.ReenterAccessCode, BackupStep.SetAccessCode, BackupStep.WritePrimaryCard, @@ -159,7 +196,7 @@ private fun updateScanResponseAfterBackup( val cardsCount = backupState.backupCardsNumber scanResponse.card.copy( backupStatus = Card.BackupStatus.Active(cardCount = cardsCount), - isAccessCodeSet = true + isAccessCodeSet = true, ) } else { scanResponse.card @@ -180,13 +217,11 @@ class BackupMiddleware { private fun handleBackupAction(appState: () -> AppState?, action: BackupAction) { if (DemoHelper.tryHandle(appState, action)) return + val globalState = appState()?.globalState ?: return + val onboardingWalletState = appState()?.onboardingWalletState ?: return - val backupState = store.state.onboardingWalletState.backupState - - val globalState = store.state.globalState - val onboardingManager = globalState.onboardingState.onboardingManager - - val scanResponse = onboardingManager?.scanResponse + val backupState = onboardingWalletState.backupState + val scanResponse = globalState.onboardingState.onboardingManager?.scanResponse val card = scanResponse?.card when (action) { @@ -212,32 +247,34 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction) } } is BackupAction.AddBackupCard -> { + backupService.skipCompatibilityChecks = true + tangemSdk.config.filter.cardIdFilter = CardFilter.Companion.CardIdFilter.Allow( + items = SaltPayWorkaround.walletCardIds.toSet(), + ranges = SaltPayWorkaround.walletCardIdRanges, + ) + backupService.addBackupCard { result -> + backupService.skipCompatibilityChecks = false + tangemSdk.config.filter.cardIdFilter = null when (result) { is CompletionResult.Success -> { store.dispatchOnMain(BackupAction.AddBackupCard.Success) } is CompletionResult.Failure -> { - } } } } is BackupAction.FinishAddingBackupCards -> { - if (backupService.addedBackupCardsCount == 1) { - store.dispatchOnMain(GlobalAction.ShowDialog(BackupDialog.AddMoreBackupCards)) - } - if (backupService.addedBackupCardsCount == 2) { + if (backupService.addedBackupCardsCount == backupState.maxBackupCards) { store.dispatchOnMain(BackupAction.ShowAccessCodeInfoScreen) + } else { + store.dispatchOnMain(GlobalAction.ShowDialog(BackupDialog.AddMoreBackupCards)) } } is BackupAction.CheckAccessCode -> { if (action.accessCode.length < 4) { - store.dispatch( - BackupAction.SetAccessCodeError( - AccessCodeError.CodeTooShort - ) - ) + store.dispatch(BackupAction.SetAccessCodeError(AccessCodeError.CodeTooShort)) } else { store.dispatch(BackupAction.SetAccessCodeError(null)) store.dispatch(BackupAction.SaveFirstAccessCode(action.accessCode)) @@ -247,22 +284,17 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction) if (action.accessCodeConfirmation == backupState.accessCode) { store.dispatch(BackupAction.SetAccessCodeError(null)) backupService.setAccessCode(action.accessCodeConfirmation) + store.dispatch(OnboardingSaltPayAction.SetAccessCode(action.accessCodeConfirmation)) store.dispatch(BackupAction.PrepareToWritePrimaryCard) } else { - store.dispatch( - BackupAction.SetAccessCodeError( - AccessCodeError.CodesDoNotMatch - ) - ) + store.dispatch(BackupAction.SetAccessCodeError(AccessCodeError.CodesDoNotMatch)) } } is BackupAction.WritePrimaryCard -> { backupService.proceedBackup { result -> when (result) { is CompletionResult.Success -> { - store.dispatchOnMain( - BackupAction.PrepareToWriteBackupCard(1) - ) + store.dispatchOnMain(BackupAction.PrepareToWriteBackupCard(1)) } is CompletionResult.Failure -> { } @@ -274,15 +306,13 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction) when (result) { is CompletionResult.Success -> { if (backupService.currentState == BackupService.State.Finished) { + initSaltPayOnBackupFinishedIfNeeded(scanResponse, onboardingWalletState) store.dispatchOnMain(BackupAction.FinishBackup) } else { - store.dispatchOnMain( - BackupAction.PrepareToWriteBackupCard(action.cardNumber + 1) - ) + store.dispatchOnMain(BackupAction.PrepareToWriteBackupCard(action.cardNumber + 1)) } } is CompletionResult.Failure -> { - } } } @@ -308,13 +338,31 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction) store.dispatch(GlobalAction.ShowDialog(BackupDialog.UnfinishedBackupFound)) } } - is BackupAction.ResumeBackup -> { - store.dispatch(GlobalAction.Onboarding.Start(null, true)) - store.dispatch(OnboardingWalletAction.ProceedBackup) - store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingWallet)) + is BackupAction.ResumeFoundUnfinishedBackup -> { + // val isSaltPay = backupService.primaryBatchId?.let { SaltPayWorkaround.isVisaBatchId(it) } ?: false + // store.dispatch(GlobalAction.Onboarding.StartForUnfinishedBackup(isSaltPay)) + // store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingWallet)) } is BackupAction.DismissBackup -> { + if (onboardingWalletState.isSaltPay) throw UnsupportedOperationException() store.dispatch(BackupAction.FinishBackup) } } +} + +/** + * SaltPay state maybe not initialized if backup resumed from ResumeFoundUnfinishedBackup action + */ +private fun initSaltPayOnBackupFinishedIfNeeded( + scanResponse: ScanResponse?, + onboardingWalletState: OnboardingWalletState, +) { + if (onboardingWalletState.isSaltPay && onboardingWalletState.onboardingSaltPayState == null) { + //TODO: SaltPay: scanResponse + if (scanResponse == null) throw IllegalArgumentException() + + val (manager, config) = OnboardingSaltPayState.initDependency(scanResponse) + store.dispatchOnMain(OnboardingSaltPayAction.Init.SetDependencies(manager, config)) + store.dispatchOnMain(OnboardingSaltPayAction.Update) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletReducer.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletReducer.kt index ab36973e17..0f79ba3da0 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletReducer.kt @@ -3,12 +3,13 @@ package com.tangem.tap.features.onboarding.products.wallet.redux import com.tangem.tap.backupService import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction +import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayAction +import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayReducer import org.rekotlin.Action class OnboardingWalletReducer { companion object { - fun reduce(action: Action, state: AppState): OnboardingWalletState = - internalReduce(action, state) + fun reduce(action: Action, state: AppState): OnboardingWalletState = internalReduce(action, state) } } @@ -16,13 +17,17 @@ private fun internalReduce(action: Action, appState: AppState): OnboardingWallet val state = appState.onboardingWalletState return when (action) { - is BackupAction -> state.copy(backupState = BackupReducer.reduce(action, state.backupState)) - is GlobalAction.Onboarding.Start -> OnboardingWalletState( - backupState = state.backupState.copy(canSkipBackup = action.fromHomeScreen) + is GlobalAction.Onboarding -> ReducerForGlobalAction.reduce(action, state) + is OnboardingSaltPayAction -> OnboardingSaltPayReducer.reduce(action, state) + is BackupAction -> state.copy(backupState = BackupReducer.reduce(action, state.backupState, state.isSaltPay)) + + is OnboardingWalletAction.GetToCreateWalletStep -> OnboardingWalletState( + step = OnboardingWalletStep.CreateWallet, ) - is OnboardingWalletAction.GetToCreateWalletStep -> - OnboardingWalletState(step = OnboardingWalletStep.CreateWallet) - is OnboardingWalletAction.ProceedBackup -> state.copy(step = OnboardingWalletStep.Backup) + is OnboardingWalletAction.GetToSaltPayStep -> state.copy( + step = OnboardingWalletStep.SaltPay, + ) + is OnboardingWalletAction.ResumeBackup -> state.copy(step = OnboardingWalletStep.Backup) is OnboardingWalletAction.SetArtworkUrl -> { state.copy(cardArtworkUrl = action.artworkUrl) } @@ -31,17 +36,45 @@ private fun internalReduce(action: Action, appState: AppState): OnboardingWallet } } +private class ReducerForGlobalAction { + companion object { + fun reduce(action: GlobalAction.Onboarding, state: OnboardingWalletState): OnboardingWalletState = when (action) { + is GlobalAction.Onboarding.Start -> { + OnboardingWalletState( + backupState = state.backupState.copy( + maxBackupCards = if (action.scanResponse.isSaltPay()) 1 else 2, + canSkipBackup = if (action.scanResponse.isSaltPay()) false else action.canSkipBackup, + ), + isSaltPay = action.scanResponse.isSaltPay() + ) + } + is GlobalAction.Onboarding.StartForUnfinishedBackup -> { + OnboardingWalletState( + backupState = state.backupState.copy( + maxBackupCards = if (action.isSaltPayBackup) 1 else 2, + canSkipBackup = false, + ), + isSaltPay = action.isSaltPayBackup, + ) + } + else -> state + } + } +} -class BackupReducer { +private class BackupReducer { companion object { fun reduce( - action: BackupAction, state: BackupState, + action: BackupAction, + state: BackupState, + isSaltPay: Boolean, ): BackupState { return when (action) { is BackupAction.IntroduceBackup -> BackupState( backupStep = BackupStep.InitBackup, - canSkipBackup = state.canSkipBackup, + maxBackupCards = if (isSaltPay) 1 else 2, + canSkipBackup = !isSaltPay, ) BackupAction.StartAddingPrimaryCard -> state.copy(backupStep = BackupStep.ScanOriginCard) BackupAction.StartAddingBackupCards -> { @@ -50,9 +83,6 @@ class BackupReducer { BackupAction.AddBackupCard.Success -> { state.copy(backupCardsNumber = state.backupCardsNumber + 1) } - BackupAction.FinishAddingBackupCards -> { - state - } BackupAction.ShowAccessCodeInfoScreen -> { state.copy(backupStep = BackupStep.SetAccessCode) } @@ -62,13 +92,11 @@ class BackupReducer { is BackupAction.SaveFirstAccessCode -> { state.copy( backupStep = BackupStep.ReenterAccessCode, - accessCode = action.accessCode + accessCode = action.accessCode, ) } is BackupAction.SetAccessCodeError -> { - state.copy( - accessCodeError = action.error - ) + state.copy(accessCodeError = action.error) } is BackupAction.PrepareToWritePrimaryCard -> { if (state.primaryCardId == null) { @@ -76,12 +104,10 @@ class BackupReducer { primaryCardId = backupService.primaryCardId, backupCardIds = backupService.backupCardIds, backupCardsNumber = backupService.backupCardIds.size, - backupStep = BackupStep.WritePrimaryCard + backupStep = BackupStep.WritePrimaryCard, ) } else { - state.copy( - backupStep = BackupStep.WritePrimaryCard - ) + state.copy(backupStep = BackupStep.WritePrimaryCard) } } is BackupAction.PrepareToWriteBackupCard -> { @@ -90,43 +116,21 @@ class BackupReducer { primaryCardId = backupService.primaryCardId, backupCardIds = backupService.backupCardIds, backupCardsNumber = backupService.backupCardIds.size, - backupStep = BackupStep.WriteBackupCard(action.cardNumber) + backupStep = BackupStep.WriteBackupCard(action.cardNumber), ) } else { - state.copy( - backupStep = BackupStep.WriteBackupCard(action.cardNumber) - ) + state.copy(backupStep = BackupStep.WriteBackupCard(action.cardNumber)) } } - is BackupAction.FinishBackup -> { - state.copy( - backupStep = BackupStep.Finished - ) + state.copy(backupStep = BackupStep.Finished) } - BackupAction.OnAccessCodeDialogClosed -> { state.copy(backupStep = BackupStep.AddBackupCards) } BackupAction.DiscardBackup -> BackupState() - - BackupAction.WritePrimaryCard -> state - is BackupAction.WriteBackupCard -> state - is BackupAction.SaveAccessCodeConfirmation -> state - BackupAction.ScanPrimaryCard -> state - BackupAction.ShowEnterAccessCodeScreen -> state - BackupAction.ShowReenterAccessCodeScreen -> state - BackupAction.AddBackupCard -> state - BackupAction.DismissBackup -> state - is BackupAction.LoadBackupCardArtwork -> state - is BackupAction.CheckAccessCode -> state - BackupAction.DetermineBackupStep -> state - BackupAction.CheckForUnfinishedBackup -> state - BackupAction.ResumeBackup -> state - BackupAction.DiscardSavedBackup -> state - BackupAction.StartBackup -> state + else -> state } - } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletState.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletState.kt index 591573b35b..174766a762 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletState.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletState.kt @@ -2,6 +2,8 @@ package com.tangem.tap.features.onboarding.products.wallet.redux import android.graphics.Bitmap import com.tangem.tap.common.redux.StateDialog +import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayState +import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.SaltPayRegistrationStep import org.rekotlin.StateType /** @@ -10,15 +12,20 @@ import org.rekotlin.StateType data class OnboardingWalletState( val step: OnboardingWalletStep = OnboardingWalletStep.None, val backupState: BackupState = BackupState(), + val onboardingSaltPayState: OnboardingSaltPayState? = null, + val isSaltPay: Boolean = false, val cardArtworkUrl: String? = null, val showConfetti: Boolean = false, ) : StateType { + fun getMaxProgress(): Int = when { + isSaltPay -> 12 + else -> 6 + } + fun getProgressStep(): Int { return when { - step == OnboardingWalletStep.CreateWallet -> { - 1 - } + step == OnboardingWalletStep.CreateWallet -> 1 step == OnboardingWalletStep.Backup -> { when (backupState.backupStep) { BackupStep.InitBackup -> 2 @@ -29,23 +36,35 @@ data class OnboardingWalletState( BackupStep.ReenterAccessCode -> 4 BackupStep.SetAccessCode -> 4 BackupStep.WritePrimaryCard, is BackupStep.WriteBackupCard -> 5 - BackupStep.Finished -> 6 + BackupStep.Finished -> getMaxProgress() } } - step == OnboardingWalletStep.Done -> 6 + step == OnboardingWalletStep.SaltPay && isSaltPay -> { + when (onboardingSaltPayState!!.step) { + SaltPayRegistrationStep.NoGas -> 1 + SaltPayRegistrationStep.NeedPin -> 7 + SaltPayRegistrationStep.CardRegistration -> 8 + SaltPayRegistrationStep.KycIntro -> 9 + SaltPayRegistrationStep.KycStart -> 10 + SaltPayRegistrationStep.KycWaiting -> 11 + SaltPayRegistrationStep.Finished -> getMaxProgress() + } + } + step == OnboardingWalletStep.Done -> getMaxProgress() else -> 1 } } } enum class OnboardingWalletStep { - None, CreateWallet, Backup, Done + None, CreateWallet, Backup, SaltPay, Done } data class BackupState( val primaryCardId: String? = null, val backupCardsNumber: Int = 0, val backupCardIds: List = emptyList(), + @Transient val backupCardsArtworks: Map = emptyMap(), val accessCode: String = "", val accessCodeError: AccessCodeError? = null, @@ -72,7 +91,7 @@ sealed class BackupStep { object Finished : BackupStep() } -sealed class BackupDialog: StateDialog { +sealed class BackupDialog : StateDialog { object AddMoreBackupCards : StateDialog object BackupInProgress : StateDialog object UnfinishedBackupFound : StateDialog diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/GnosisRegistrator.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/GnosisRegistrator.kt new file mode 100644 index 0000000000..5937e34621 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/GnosisRegistrator.kt @@ -0,0 +1,164 @@ +package com.tangem.tap.features.onboarding.products.wallet.saltPay + +import com.tangem.blockchain.blockchains.ethereum.CompiledEthereumTransaction +import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager +import com.tangem.blockchain.blockchains.ethereum.SignedEthereumTransaction +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.AmountType +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.BlockchainSdkError +import com.tangem.blockchain.common.WalletManager +import com.tangem.blockchain.extensions.Result +import com.tangem.blockchain.extensions.SimpleResult +import com.tangem.blockchain.extensions.successOr +import com.tangem.common.extensions.guard +import com.tangem.domain.common.extensions.successOr +import com.tangem.tap.common.extensions.isPositive +import com.tangem.tap.common.extensions.safeUpdate +import com.tangem.tap.common.extensions.stub +import com.tangem.tap.domain.getFirstToken +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import java.math.BigDecimal +import java.math.BigInteger +import java.util.concurrent.atomic.AtomicLong + +/** +[REDACTED_AUTHOR] + */ +class GnosisRegistrator( + val walletManager: EthereumWalletManager, +) { + + private val walletAddress = walletManager.wallet.address + private val token = walletManager.wallet.getFirstToken().guard { + throw NullPointerException("WalletManager for GnosisRegistrator must contain token") + } + + private val otpProcessorContractAddress: String = when (walletManager.wallet.blockchain) { + Blockchain.SaltPay -> "0x3B4397C817A26521Df8bD01a949AFDE2251d91C2" + Blockchain.SaltPayTestnet -> "0x710BF23486b549836509a08c184cE0188830f197" + else -> throw IllegalArgumentException("GnosisRegistrator supports only the SaltPay blockchain") + } + + private val atomicNonce = AtomicLong() + + suspend fun checkHasGas(): Result { + val wallet = walletManager.safeUpdate().successOr { + return Result.Failure(BlockchainSdkError.WrappedThrowable(it.error)) + } + + val hasGas = wallet.amounts[AmountType.Coin]?.value?.isPositive() ?: false + return Result.Success(hasGas) + } + + suspend fun prepareBeforeMakingTxs(): Result { + walletManager.safeUpdate().successOr { + return Result.Failure(BlockchainSdkError.WrappedThrowable(it.error)) + } + atomicNonce.set(walletManager.txCount) + + return Result.Success(Unit) + } + + suspend fun sendTransactions(transactions: List): Result> { + val results = transactions.map { + coroutineScope { async { walletManager.sendRaw(it.compiledTransaction, it.signature) } } + }.awaitAll() + + val errors = results.filterIsInstance() + return if (errors.isEmpty()) { + Result.Success(listOf()) + } else { + Result.Failure(errors[0].error) + } + } + + suspend fun makeSetSpendLimitTx(value: BigDecimal): Result { + val limitAmount = Amount(walletManager.cardTokens.first(), value) + val feeAmount = walletManager.getFeeToSetSpendLimit(otpProcessorContractAddress, limitAmount) + .extractFeeAmount().successOr { return it } + + val compiledEthereumTransaction = walletManager.transactionBuilder.buildSetSpendLimitToSign( + processorContractAddress = otpProcessorContractAddress, + cardAddress = walletAddress, + amount = limitAmount, + transactionFee = feeAmount, + gasLimit = walletManager.gasLimitToSetSpendLimit, + nonce = atomicNonce.getAndIncrement().toBigInteger(), + ) ?: return Result.Failure(BlockchainSdkError.CustomError("Can't create the 'setSpendLimit' transaction")) + + return Result.Success(compiledEthereumTransaction) + } + + suspend fun makeInitOtpTx(rootOTP: ByteArray, rootOTPCounter: Int): Result { + val feeAmount = walletManager.getFeeToInitOTP(otpProcessorContractAddress, rootOTP, rootOTPCounter) + .extractFeeAmount().successOr { return it } + + val compiledEthereumTransaction = walletManager.transactionBuilder.buildInitOTPToSign( + processorContractAddress = otpProcessorContractAddress, + cardAddress = walletAddress, + otp = rootOTP, + otpCounter = rootOTPCounter, + transactionFee = feeAmount, + gasLimit = walletManager.gasLimitToInitOTP, + nonce = atomicNonce.getAndIncrement().toBigInteger(), + ) ?: return Result.Failure(BlockchainSdkError.CustomError("Can't create the 'initOtp' transaction")) + + return Result.Success(compiledEthereumTransaction) + } + + suspend fun makeSetWalletTx(): Result { + val feeAmount = walletManager.getFeeToSetWallet(otpProcessorContractAddress) + .extractFeeAmount().successOr { return it } + + val compiledEthereumTransaction = walletManager.transactionBuilder.buildSetWalletToSign( + processorContractAddress = otpProcessorContractAddress, + cardAddress = walletAddress, + transactionFee = feeAmount, + gasLimit = walletManager.gasLimitToSetWallet, + nonce = atomicNonce.getAndIncrement().toBigInteger(), + ) ?: return Result.Failure(BlockchainSdkError.CustomError("Can't create the 'setWallet' transaction")) + + return Result.Success(compiledEthereumTransaction) + } + + suspend fun makeApprovalTx(value: BigDecimal): Result { + val approveAmount = Amount(token, value) + val feeAmount = walletManager.getFeeToApprove(approveAmount, otpProcessorContractAddress) + .extractFeeAmount().successOr { return it } + + val compiledEthereumTransaction = walletManager.transactionBuilder.buildApproveToSign( + transactionData = walletManager.createTransaction( + amount = approveAmount, + fee = feeAmount, + destination = otpProcessorContractAddress, + ), + nonce = atomicNonce.getAndIncrement().toBigInteger(), + gasLimit = walletManager.gasLimitToApprove, + ) ?: return Result.Failure(BlockchainSdkError.CustomError("Can't create the 'approval' transaction")) + + return Result.Success(compiledEthereumTransaction) + } + + private fun Result>.extractFeeAmount(): Result { + return when (this) { + is Result.Success -> { + if (this.data.size != 3) Result.Failure(BlockchainSdkError.FailedToLoadFee) + else Result.Success(this.data[1]) + } + is Result.Failure -> this + } + } + + private fun Long?.toBigInteger(): BigInteger? = this?.let { BigInteger.valueOf(it) } + + private fun Long.toBigInteger(): BigInteger = BigInteger.valueOf(this) + + companion object { + fun stub(): GnosisRegistrator { + return GnosisRegistrator(WalletManager.stub()) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/PinFilters.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/PinFilters.kt new file mode 100644 index 0000000000..15a4137ec0 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/PinFilters.kt @@ -0,0 +1,18 @@ +package com.tangem.tap.features.onboarding.products.wallet.saltPay + +import com.tangem.common.Filter + +/** +[REDACTED_AUTHOR] + */ +abstract class PinFilter : Filter + +class AllSymbolsTheSameFilter : PinFilter() { + override fun filter(value: String): Boolean { + if (value.isEmpty()) return false + + val array = value.toCharArray() + val firstSymbol = array[0] + return array.all { it == firstSymbol } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/SaltPayConfig.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/SaltPayConfig.kt new file mode 100644 index 0000000000..d853819a57 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/SaltPayConfig.kt @@ -0,0 +1,27 @@ +package com.tangem.tap.features.onboarding.products.wallet.saltPay + +import com.tangem.tap.common.zendesk.ZendeskConfig + +/** +[REDACTED_AUTHOR] + */ +data class SaltPayConfig( + val zendesk: ZendeskConfig, + val kycProvider: KYCProvider, +) { + companion object { + fun stub(): SaltPayConfig { + return SaltPayConfig( + zendesk = ZendeskConfig("", "", "", "", ""), + kycProvider = KYCProvider("", "", "", ""), + ) + } + } +} + +data class KYCProvider( + val baseUrl: String, + val externalIdParameterKey: String, + val sidParameterKey: String, + val sidValue: String, +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/SaltPayRegistrationManager.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/SaltPayRegistrationManager.kt new file mode 100644 index 0000000000..db09316125 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/SaltPayRegistrationManager.kt @@ -0,0 +1,156 @@ +package com.tangem.tap.features.onboarding.products.wallet.saltPay + +import android.net.Uri +import com.tangem.blockchain.blockchains.ethereum.SignedEthereumTransaction +import com.tangem.common.services.Result +import com.tangem.domain.common.extensions.successOr +import com.tangem.network.api.paymentology.AttestationResponse +import com.tangem.network.api.paymentology.PaymentologyApiService +import com.tangem.network.api.paymentology.RegisterKYCRequest +import com.tangem.network.api.paymentology.RegisterWalletRequest +import com.tangem.network.api.paymentology.RegisterWalletResponse +import com.tangem.network.api.paymentology.RegistrationResponse +import com.tangem.operations.attestation.AttestWalletKeyResponse +import com.tangem.tap.domain.tokens.UserWalletId +import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayRegistrationError +import com.tangem.tap.persistence.SaltPayRegistrationStorage +import java.math.BigDecimal + +/** +[REDACTED_AUTHOR] + */ +class SaltPayRegistrationManager( + val cardId: String, + val cardPublicKey: ByteArray, + val walletPublicKey: ByteArray, + private val kycProvider: KYCProvider, + private val paymentologyService: PaymentologyApiService, + private val gnosisRegistrator: GnosisRegistrator, + private val registrationStorage: SaltPayRegistrationStorage, +) { + val kycUrlProvider = KYCUrlProvider(walletPublicKey, kycProvider) + + private val approvalValue: BigDecimal = BigDecimal.valueOf(Math.pow(2.toDouble(), 255.toDouble())) + private val spendLimitValue: BigDecimal = BigDecimal("100") + + fun transactionIsSent(): Boolean { + return registrationStorage.data.transactionsSent + } + + suspend fun checkHasGas(): Result { + return when (val hasGasResult = gnosisRegistrator.checkHasGas()) { + is com.tangem.blockchain.extensions.Result.Success -> if (hasGasResult.data) { + Result.Success(Unit) + } else { + Result.Failure(SaltPayRegistrationError.NoGas) + } + is com.tangem.blockchain.extensions.Result.Failure -> Result.Failure(SaltPayRegistrationError.NoGas) + } + } + + suspend fun registerKYC(): Result { + return when (val result = paymentologyService.registerKYC(makeRegisterKYCRequest())) { + is Result.Success -> Result.Success(Unit) + is Result.Failure -> result + } + } + + suspend fun checkRegistration(): Result { + val registrationResponse = paymentologyService.checkRegistration(cardId, cardPublicKey).successOr { return it } + + return try { + if (!registrationResponse.success || registrationResponse.results.isEmpty()) { + throw SaltPayRegistrationError.EmptyResponse(registrationResponse.error) + } + Result.Success(registrationResponse.results[0]) + } catch (ex: SaltPayRegistrationError) { + Result.Failure(ex) + } + } + + suspend fun requestAttestationChallenge(): Result { + return paymentologyService.requestAttestationChallenge(cardId, cardPublicKey) + } + + suspend fun sendTransactions( + signedTransactions: List, + ): com.tangem.blockchain.extensions.Result> { + return gnosisRegistrator.sendTransactions(signedTransactions) + } + + suspend fun registerWallet( + attestResponse: AttestWalletKeyResponse, + pinCode: String, + ): Result { + val request = RegisterWalletRequest( + cardId = cardId, + publicKey = cardPublicKey, + walletPublicKey = walletPublicKey, + walletSalt = attestResponse.salt, + walletSignature = attestResponse.walletSignature, + cardSalt = attestResponse.publicKeySalt ?: byteArrayOf(), + cardSignature = attestResponse.cardSignature ?: byteArrayOf(), + pin = pinCode, + ) + + val result = paymentologyService.registerWallet(request) + + registrationStorage.data = registrationStorage.data.copy( + transactionsSent = result is Result.Success, + ) + + return result + } + + fun makeRegistrationTask(challenge: ByteArray): SaltPayRegistrationTask { + return SaltPayRegistrationTask( + gnosisRegistrator = gnosisRegistrator, + challenge = challenge, + walletPublicKey = walletPublicKey, + approvalValue = approvalValue, + spendLimitValue = spendLimitValue, + ) + } + + private fun makeRegisterKYCRequest(): RegisterKYCRequest = RegisterKYCRequest( + cardId = cardId, + publicKey = cardPublicKey, + kycProvider = "UTORG", + kycRefId = kycUrlProvider.kycRefId, + ) + + companion object { + fun stub(): SaltPayRegistrationManager = SaltPayRegistrationManager( + kycProvider = SaltPayConfig.stub().kycProvider, + paymentologyService = PaymentologyApiService.stub(), + gnosisRegistrator = GnosisRegistrator.stub(), + registrationStorage = SaltPayRegistrationStorage.stub(), + cardId = "", + cardPublicKey = byteArrayOf(), + walletPublicKey = byteArrayOf(), + ) + } +} + +class KYCUrlProvider( + walletPublicKey: ByteArray, + kycProvider: KYCProvider, +) { + + val kycRefId = UserWalletId(walletPublicKey).stringValue + + val requestUrl: String = makeWebRequestUrl(kycProvider) + + val doneUrl = "https://success.tangem.com/" + + private fun makeWebRequestUrl(kycProvider: KYCProvider): String { + val baseUri = Uri.parse(kycProvider.baseUrl) + return Uri.Builder() + .scheme(baseUri.scheme) + .authority(baseUri.authority) + .encodedPath(baseUri.path) + .appendQueryParameter(kycProvider.sidParameterKey, kycProvider.sidValue) + .appendQueryParameter(kycProvider.externalIdParameterKey, kycRefId) + .build().toString() + } +} diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/SaltPayRegistrationTask.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/SaltPayRegistrationTask.kt new file mode 100644 index 0000000000..11a0a30404 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/SaltPayRegistrationTask.kt @@ -0,0 +1,170 @@ +package com.tangem.tap.features.onboarding.products.wallet.saltPay + +import com.tangem.blockchain.blockchains.ethereum.CompiledEthereumTransaction +import com.tangem.blockchain.blockchains.ethereum.SignedEthereumTransaction +import com.tangem.blockchain.extensions.successOr +import com.tangem.common.CompletionResult +import com.tangem.common.core.CardSession +import com.tangem.common.core.CardSessionRunnable +import com.tangem.common.core.CompletionCallback +import com.tangem.common.core.TangemSdkError +import com.tangem.common.extensions.guard +import com.tangem.operations.CommandResponse +import com.tangem.operations.GenerateOTPCommand +import com.tangem.operations.GenerateOTPResponse +import com.tangem.operations.attestation.AttestWalletKeyCommand +import com.tangem.operations.attestation.AttestWalletKeyResponse +import com.tangem.operations.sign.SignHashesCommand +import com.tangem.tap.scope +import kotlinx.coroutines.launch +import java.math.BigDecimal + +/** +[REDACTED_AUTHOR] + */ +data class RegistrationTaskResponse( + val signedTransactions: List, + val attestResponse: AttestWalletKeyResponse, +) : CommandResponse + +class SaltPayRegistrationTask( + private var gnosisRegistrator: GnosisRegistrator, + private var challenge: ByteArray, + private val walletPublicKey: ByteArray, + private val approvalValue: BigDecimal, + private val spendLimitValue: BigDecimal, +) : CardSessionRunnable { + + private var generateOTPResponse: GenerateOTPResponse? = null + private var attestWalletResponse: AttestWalletKeyResponse? = null + private var signedTransactions: List = listOf() + + override fun run(session: CardSession, callback: CompletionCallback) { + generateOTP(session, callback) + } + + private fun generateOTP(session: CardSession, callback: CompletionCallback) { + GenerateOTPCommand().run(session) { result -> + when (result) { + is CompletionResult.Success -> { + generateOTPResponse = result.data + attestWallet(session, callback) + } + is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error)) + } + } + } + + private fun attestWallet( + session: CardSession, + callback: CompletionCallback, + ) { + val card = session.environment.card.guard { + callback(CompletionResult.Failure(TangemSdkError.MissingPreflightRead())) + return + } + + val walletPublicKey = card.wallets.firstOrNull()?.publicKey + if (walletPublicKey == null || !walletPublicKey.contentEquals(this.walletPublicKey)) { + callback(CompletionResult.Failure(TangemSdkError.WalletNotFound())) + return + } + + val attestWalletCommand = AttestWalletKeyCommand( + publicKey = walletPublicKey, + challenge = challenge, + confirmationMode = AttestWalletKeyCommand.ConfirmationMode.Dynamic, + ) + attestWalletCommand.run(session) { result -> + when (result) { + is CompletionResult.Success -> { + attestWalletResponse = result.data + scope.launch { prepareTransactions(session, callback) } + } + is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error)) + } + } + } + + private suspend fun prepareTransactions( + session: CardSession, + callback: CompletionCallback, + ) { + val generateOTPResponse = this.generateOTPResponse.guard { + callback(CompletionResult.Failure(TangemSdkError.UnknownError())) + return + } + + gnosisRegistrator.prepareBeforeMakingTxs().successOr { + callback(CompletionResult.Failure(it.error)) + return + } + + val approvalTx = gnosisRegistrator.makeApprovalTx(approvalValue).successOr { + callback(CompletionResult.Failure(it.error)) + return + } + val setWalletTx = gnosisRegistrator.makeSetWalletTx().successOr { + callback(CompletionResult.Failure(it.error)) + return + } + val initOtpTx = gnosisRegistrator.makeInitOtpTx( + generateOTPResponse.rootOTP, + generateOTPResponse.rootOTPCounter, + ).successOr { + callback(CompletionResult.Failure(it.error)) + return + } + + val setSpendLimitTx = gnosisRegistrator.makeSetSpendLimitTx(spendLimitValue).successOr { + callback(CompletionResult.Failure(it.error)) + return + } + + signTransactions( + transactions = listOf( + approvalTx, + setWalletTx, + initOtpTx, + setSpendLimitTx, + ), + session = session, + callback = callback, + ) + } + + private fun signTransactions( + transactions: List, + session: CardSession, + callback: CompletionCallback, + ) { + val walletPublicKey = session.environment.card?.wallets?.firstOrNull()?.publicKey.guard { + callback(CompletionResult.Failure(TangemSdkError.WalletNotFound())) + return + } + + val hashes = transactions.map { it.hash }.toTypedArray() + val signHashesCommand = SignHashesCommand(hashes, walletPublicKey) + + signHashesCommand.run(session) { result -> + when (result) { + is CompletionResult.Success -> { + signedTransactions = transactions.zip(result.data.signatures).map { + SignedEthereumTransaction(it.first, it.second) + } + complete(callback) + } + is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error)) + } + } + } + + private fun complete(callback: CompletionCallback) { + if (signedTransactions.isEmpty() || attestWalletResponse == null) { + callback(CompletionResult.Failure(TangemSdkError.UnknownError())) + } else { + val result = RegistrationTaskResponse(signedTransactions, attestWalletResponse!!) + callback(CompletionResult.Success(result)) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/UtorgWebView.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/UtorgWebView.kt new file mode 100644 index 0000000000..b773adcb70 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/UtorgWebView.kt @@ -0,0 +1,166 @@ +package com.tangem.tap.features.onboarding.products.wallet.saltPay + +import android.app.Activity +import android.content.Intent +import android.net.Uri +import android.provider.MediaStore +import android.text.TextUtils +import android.webkit.PermissionRequest +import android.webkit.ValueCallback +import android.webkit.WebChromeClient +import android.webkit.WebResourceRequest +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.activity.ComponentActivity +import androidx.core.app.ActivityCompat +import com.tangem.tap.common.ActivityResultCallbackHolder +import com.tangem.tap.common.OnActivityResultCallback +import com.tangem.tap.common.extensions.dispatchDialogShow +import com.tangem.tap.common.extensions.isPermissionGranted +import com.tangem.tap.common.redux.AppDialog +import com.tangem.tap.store +import com.tangem.wallet.R +import java.io.File + +/** +[REDACTED_AUTHOR] + */ +class UtorgWebViewClient( + private val onSuccess: () -> Unit, + private val successUrl: String, +) : WebViewClient() { + + override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean = + if (successUrl == request.url.toString()) { + onSuccess() + true + } else { + false + } +} + +class UtorgWebChromeClient( + private val activity: ComponentActivity, + private val activityResultCallbackHolder: ActivityResultCallbackHolder = activity as ActivityResultCallbackHolder, +) : WebChromeClient() { + + private val fileChooseRequestCode = 1002 + private val permissionRequestCode = 1003 + private val onActivityResultCallback: OnActivityResultCallback = ::onActivityResult + + private val permissionList = listOf( + "android.permission.CAMERA", + ) + + private var filePathCallback: ValueCallback>? = null + private var mediaUri: Uri? = null + + override fun onShowFileChooser( + webView: WebView?, + filePathCallback: ValueCallback>, + fileChooserParams: FileChooserParams?, + ): Boolean { + activityResultCallbackHolder.addOnActivityResultCallback(onActivityResultCallback) + if (!readyToShowFileChooser()) return false + + val type: String + if (fileChooserParams != null && fileChooserParams.acceptTypes != null && fileChooserParams.acceptTypes.isNotEmpty()) { + type = if (!TextUtils.isEmpty(fileChooserParams.acceptTypes[0])) fileChooserParams.acceptTypes[0] else "*/*" + this.filePathCallback = filePathCallback + } else { + return false + } + proceedOnType(type, fileChooserParams.isCaptureEnabled) + + return true + } + + private fun readyToShowFileChooser(): Boolean { + val permissionsToGrant = permissionList.filterNot { activity.isPermissionGranted(it) }.toTypedArray() + return if (permissionsToGrant.isNotEmpty()) { + ActivityCompat.requestPermissions(activity, permissionsToGrant, permissionRequestCode) + false + } else { + true + } + } + + private fun proceedOnType(type: String, isCaptureEnabled: Boolean) { + if (isCaptureEnabled) { + mediaUri = Uri.fromFile(File(getImageCaptureCachePath())) + val imageCaptureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE) + imageCaptureIntent.putExtra("output", mediaUri) + activity.startActivityForResult( + Intent.createChooser(imageCaptureIntent, "Image Chooser"), + fileChooseRequestCode, + ) + } else { + mediaUri = Uri.fromFile(File(getImageCaptureCachePath())) + + val getContentIntent = Intent(Intent.ACTION_GET_CONTENT) + getContentIntent.addCategory(Intent.CATEGORY_OPENABLE) + getContentIntent.type = "image/*" + + // val imageCaptureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE) + // imageCaptureIntent.putExtra("output", mediaUri) + + val chooserIntent = Intent.createChooser(getContentIntent, "Pick a photo") + // chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, arrayOf(imageCaptureIntent)) + + activity.startActivityForResult( + Intent.createChooser(chooserIntent, "Image Chooser"), + fileChooseRequestCode, + ) + } + } + + private fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + when (requestCode) { + fileChooseRequestCode -> { + if (resultCode != Activity.RESULT_OK) { + filePathCallback?.onReceiveValue(null) + clean() + return + } + if (data == null) { + filePathCallback?.onReceiveValue(arrayOf(mediaUri!!)) + } else { + val uri = FileChooserParams.parseResult(resultCode, data) + filePathCallback?.onReceiveValue(uri) + } + clean() + } + permissionRequestCode -> { + val notGranted = permissionList.filterNot { activity.isPermissionGranted(it) } + if (notGranted.isNotEmpty()) { + val permissionsName = notGranted.joinToString("\n") { it.permissionName() } + store.dispatchDialogShow( + AppDialog.SimpleOkDialog( + header = activity.getString(R.string.common_warning), + message = "Permission access is required to pass authorization." + + "To grant permission \n" + + "$permissionsName \n" + + "you must enable them in the app's settings", + ), + ) + } + } + } + } + + override fun onPermissionRequest(request: PermissionRequest) { + request.grant(request.resources) + } + + private fun getImageCaptureCachePath(): String = "cached_photo_" + System.currentTimeMillis() + ".jpg" + + private fun clean() { + activityResultCallbackHolder.removeOnActivityResultCallback(onActivityResultCallback) + filePathCallback = null + mediaUri = null + } + + private fun String.permissionName(): String { + return this.slice(this.lastIndexOf(".") until this.length) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/dialog/NoFundsForActivationDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/dialog/NoFundsForActivationDialog.kt new file mode 100644 index 0000000000..579f34d614 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/dialog/NoFundsForActivationDialog.kt @@ -0,0 +1,39 @@ +package com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog + +import android.app.Dialog +import android.content.Context +import androidx.appcompat.app.AlertDialog +import com.tangem.common.extensions.guard +import com.tangem.tap.common.extensions.dispatchDebugErrorNotification +import com.tangem.tap.common.feedback.SupportInfo +import com.tangem.tap.common.redux.global.GlobalAction +import com.tangem.tap.store +import com.tangem.wallet.R + +/** +[REDACTED_AUTHOR] + */ +class NoFundsForActivationDialog { + companion object { + fun create(context: Context): Dialog { + return AlertDialog.Builder(context).apply { + setTitle(R.string.saltpay_error_no_gas_title) + setMessage(R.string.saltpay_error_no_gas_message) + //TODO: SaltPay: change onboarding_supplement_button_kyc_waiting -> to appropriate string + setPositiveButton(R.string.onboarding_supplement_button_kyc_waiting) { _, _ -> + val config = store.state.globalState.configManager?.config?.saltPayConfig?.zendesk.guard { + store.dispatchDebugErrorNotification("SaltPayConfig not initialized") + return@setPositiveButton + } + store.dispatch(GlobalAction.OpenChat(SupportInfo(), config)) + } + setNegativeButton(R.string.common_cancel) { _, _ -> + } + setOnDismissListener { + store.dispatch(GlobalAction.HideDialog) + } + setCancelable(false) + }.create() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/dialog/RegistrationErrorDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/dialog/RegistrationErrorDialog.kt new file mode 100644 index 0000000000..f7ddfbea66 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/dialog/RegistrationErrorDialog.kt @@ -0,0 +1,33 @@ +package com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog + +import android.app.Dialog +import android.content.Context +import androidx.appcompat.app.AlertDialog +import com.tangem.tap.common.redux.global.GlobalAction +import com.tangem.tap.domain.moduleMessage.ConvertedDialogMessage +import com.tangem.tap.domain.moduleMessage.saltPay.SaltPayErrorConverter +import com.tangem.tap.store +import com.tangem.wallet.R + +/** +[REDACTED_AUTHOR] + */ +class RegistrationErrorDialog { + companion object { + fun create(context: Context, dialog: SaltPayDialog.RegistrationError): Dialog { + val convertedMessage = SaltPayErrorConverter(context).convert(dialog.error) as ConvertedDialogMessage + + return AlertDialog.Builder(context).apply { + setTitle(convertedMessage.title) + setMessage(convertedMessage.message) + setPositiveButton(R.string.common_ok) { _, _ -> + store.dispatch(GlobalAction.HideDialog) + } + setOnDismissListener { + store.dispatch(GlobalAction.HideDialog) + } + setCancelable(false) + }.create() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/dialog/SaltPayDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/dialog/SaltPayDialog.kt new file mode 100644 index 0000000000..5bd2e1aac0 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/dialog/SaltPayDialog.kt @@ -0,0 +1,12 @@ +package com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog + +import com.tangem.tap.common.redux.StateDialog +import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayRegistrationError + +/** +[REDACTED_AUTHOR] + */ +sealed class SaltPayDialog : StateDialog { + object NoFundsForActivation : SaltPayDialog() + data class RegistrationError(val error: SaltPayRegistrationError) : SaltPayDialog() +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/message/SaltPayModuleMessage.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/message/SaltPayModuleMessage.kt new file mode 100644 index 0000000000..144647b876 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/message/SaltPayModuleMessage.kt @@ -0,0 +1,51 @@ +package com.tangem.tap.features.onboarding.products.wallet.saltPay.message + +import com.tangem.common.module.ModuleError +import com.tangem.common.module.ModuleErrorCode +import com.tangem.common.module.ModuleMessage + +sealed interface SaltPayModuleMessage : ModuleMessage + +sealed class SaltPayModuleError( + subCode: Int, + override val message: String, + override val data: Any? = null, +) : SaltPayModuleMessage, ModuleError() { + override val code: Int = ModuleErrorCode.SALT_PAY + subCode + + companion object { + // base code used for all errors in the module + internal const val ERROR_CODE_REGISTRATION = 100 +// const val CODE_ANY_OTHER = 200..299, 300..399 etc + } +} + +sealed class SaltPayRegistrationError( + subCode: Int, + message: String? = null, + override val data: Any? = null, +) : SaltPayModuleError( + subCode = ERROR_CODE_REGISTRATION + subCode, + message = message ?: EMPTY_MESSAGE, +) { + object Unknown : SaltPayRegistrationError(4) + object Empty : SaltPayRegistrationError(4) + + // gnosis + object FailedToMakeTxData : SaltPayRegistrationError(1) + object FailedToSendTx : SaltPayRegistrationError(2) + + object NeedPin : SaltPayRegistrationError(3) + object NoGas : SaltPayRegistrationError(5) // + class EmptyResponse(message: String?) : SaltPayRegistrationError(6, message) + class CardNotFound(message: String?) : SaltPayRegistrationError(8, message) + class CardDisabled(message: String?) : SaltPayRegistrationError(9, message) + class CardNotPassed(message: String?) : SaltPayRegistrationError(10, message) + object EmptyDynamicAttestResponse : SaltPayRegistrationError(11) + object EmptyBackupCardScanned : SaltPayRegistrationError(12) // + object WeakPin : SaltPayRegistrationError(13) // + + companion object { + const val EMPTY_MESSAGE = "" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/redux/OnboardingSaltPayAction.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/redux/OnboardingSaltPayAction.kt new file mode 100644 index 0000000000..54876f0a64 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/redux/OnboardingSaltPayAction.kt @@ -0,0 +1,33 @@ +package com.tangem.tap.features.onboarding.products.wallet.saltPay.redux + +import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayConfig +import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayRegistrationManager +import org.rekotlin.Action + +/** +[REDACTED_AUTHOR] + */ +sealed class OnboardingSaltPayAction : Action { + sealed class Init : OnboardingSaltPayAction() { + data class SetDependencies( + val registrationManager: SaltPayRegistrationManager, + val saltPayConfig: SaltPayConfig, + ) : Init() + + // only for tests + object DiscardBackupSteps : Init() + } + + data class SetIsBusy(val isBusy: Boolean) : OnboardingSaltPayAction() + data class SetAccessCode(val accessCode: String?) : OnboardingSaltPayAction() + + object Update : OnboardingSaltPayAction() + object RegisterCard : OnboardingSaltPayAction() + object KYCStart : OnboardingSaltPayAction() + object OnFinishKYC : OnboardingSaltPayAction() + + data class TrySetPin(val pin: String) : OnboardingSaltPayAction() + data class SetPin(val pin: String) : OnboardingSaltPayAction() + + data class SetStep(val newStep: SaltPayRegistrationStep? = null) : OnboardingSaltPayAction() +} diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/redux/OnboardingSaltPayMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/redux/OnboardingSaltPayMiddleware.kt new file mode 100644 index 0000000000..16c7594774 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/redux/OnboardingSaltPayMiddleware.kt @@ -0,0 +1,289 @@ +package com.tangem.tap.features.onboarding.products.wallet.saltPay.redux + +import com.tangem.Message +import com.tangem.blockchain.extensions.successOr +import com.tangem.common.Filter +import com.tangem.common.core.TangemSdkError +import com.tangem.common.extensions.guard +import com.tangem.common.services.Result +import com.tangem.domain.common.extensions.successOr +import com.tangem.domain.common.extensions.withMainContext +import com.tangem.network.api.paymentology.RegistrationResponse +import com.tangem.tap.common.extensions.dispatchDialogShow +import com.tangem.tap.common.extensions.dispatchErrorNotification +import com.tangem.tap.common.extensions.dispatchOnMain +import com.tangem.tap.common.redux.AppState +import com.tangem.tap.domain.TapError +import com.tangem.tap.features.demo.DemoHelper +import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletState +import com.tangem.tap.features.onboarding.products.wallet.saltPay.AllSymbolsTheSameFilter +import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayRegistrationManager +import com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog.SaltPayDialog +import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayRegistrationError +import com.tangem.tap.scope +import com.tangem.tap.store +import com.tangem.tap.tangemSdkManager +import com.tangem.wallet.R +import kotlinx.coroutines.launch +import org.rekotlin.Action +import org.rekotlin.Middleware +import timber.log.Timber + +/** +[REDACTED_AUTHOR] + */ +class OnboardingSaltPayMiddleware { + companion object { + val handler = onboardingSaltPayMiddleware + } +} + +private val onboardingSaltPayMiddleware: Middleware = { dispatch, state -> + { next -> + { action -> + handleOnboardingSaltPayAction(action, state) + next(action) + } + } +} + +private fun handleOnboardingSaltPayAction(anyAction: Action, appState: () -> AppState?) { + if (DemoHelper.tryHandle(appState, anyAction)) return + val action = anyAction as? OnboardingSaltPayAction ?: return + appState() ?: return + + fun getAppState(): AppState = appState()!! + fun getOnboardingWalletState(): OnboardingWalletState = getAppState().onboardingWalletState + fun getState(): OnboardingSaltPayState = getOnboardingWalletState().onboardingSaltPayState!! + + when (action) { + is OnboardingSaltPayAction.RegisterCard -> { + handleIsBusy = true + val state = getState() + + scope.launch { + val saltPayManager = state.saltPayManager + checkGasIfNeeded(state.saltPayManager, state.step).successOr { + onException(it.error) + return@launch + } + + val pinCode = state.pinCode.guard { + onError(SaltPayRegistrationError.NeedPin) + return@launch + } + + val attestationResponse = saltPayManager.requestAttestationChallenge().successOr { + onException(it.error) + return@launch + } + + if (!attestationResponse.success) { + onError(SaltPayRegistrationError.CardNotFound(attestationResponse.error)) + return@launch + } + + val registrationResponse = tangemSdkManager.runTaskAsync( + runnable = saltPayManager.makeRegistrationTask(attestationResponse.challenge!!), + cardId = saltPayManager.cardId, + initialMessage = Message( + header = null, + body = tangemSdkManager.getString(R.string.registration_task_alert_message), + ), + accessCode = state.accessCode, + ).successOr { + onException(it.error as Exception) + return@launch + } + + saltPayManager.sendTransactions(registrationResponse.signedTransactions).successOr { + onError(SaltPayRegistrationError.FailedToSendTx) + return@launch + } + + saltPayManager.registerWallet(registrationResponse.attestResponse, pinCode).successOr { + onException(it.error) + return@launch + } + + withMainContext { + handleIsBusy = false + store.dispatch(OnboardingSaltPayAction.SetStep(SaltPayRegistrationStep.KycIntro)) + } + } + } + is OnboardingSaltPayAction.KYCStart -> { + store.dispatch(OnboardingSaltPayAction.SetStep(SaltPayRegistrationStep.KycStart)) + } + is OnboardingSaltPayAction.OnFinishKYC -> { + scope.launch { + registerKYCIfNeeded(getState()).successOr { + onException(it.error) + } + } + } + is OnboardingSaltPayAction.TrySetPin -> { + try { + assertPinValid(action.pin, getState().pinLength) + store.dispatch(OnboardingSaltPayAction.SetPin(action.pin)) + store.dispatch(OnboardingSaltPayAction.SetStep(SaltPayRegistrationStep.CardRegistration)) + } catch (error: SaltPayRegistrationError) { + handleError = error + } + } + is OnboardingSaltPayAction.Update -> { + handleIsBusy = true + + val state = getState() + scope.launch { + checkGasIfNeeded(state.saltPayManager, state.step).successOr { + onException(it.error) + return@launch + } + + registerKYCIfNeeded(state).successOr { + onException(it.error) + return@launch + } + + val newStep = checkRegistration(state).successOr { + onException(it.error) + return@launch + } + + withMainContext { + handleIsBusy = false + store.dispatch(OnboardingSaltPayAction.SetStep(newStep)) + } + } + } + else -> { + /* do nothing, only reduce */ + } + } +} + +private var handleIsBusy: Boolean = false + set(value) { + field = value + store.dispatchOnMain(OnboardingSaltPayAction.SetIsBusy(value)) + } + +private suspend fun onError(error: SaltPayRegistrationError) { + withMainContext { + handleIsBusy = false + handleError = error + } +} + +private suspend fun onException(error: Throwable) { + withMainContext { + handleIsBusy = false + handleException = error + } +} + +private var handleException: Throwable = Throwable("Init") + set(value) { + field = value + when (value) { + is SaltPayRegistrationError -> handleError = value + else -> { + Timber.e(value) + when (value) { + is TangemSdkError -> { + // store.dispatchErrorNotification(TapError.CustomError(value.customMessage)) + } + else -> { + store.dispatchErrorNotification( + TapError.CustomError(value.message ?: "Unknown SaltPay exception"), + ) + } + } + } + } + } + +private var handleError: SaltPayRegistrationError = SaltPayRegistrationError.Unknown + set(value) { + field = value + when (value) { + SaltPayRegistrationError.NoGas -> { + store.dispatch(OnboardingSaltPayAction.SetStep(SaltPayRegistrationStep.NoGas)) + } + else -> { + store.dispatchDialogShow(SaltPayDialog.RegistrationError(value)) + } + } + } + +private suspend fun checkGasIfNeeded( + saltPayManager: SaltPayRegistrationManager, + step: SaltPayRegistrationStep, +): Result { + if (step == SaltPayRegistrationStep.KycStart || + step == SaltPayRegistrationStep.KycWaiting || + step == SaltPayRegistrationStep.Finished + ) { + return Result.Success(Unit) + } + + return saltPayManager.checkHasGas() +} + +private suspend fun registerKYCIfNeeded(state: OnboardingSaltPayState): Result { + if (state.step != SaltPayRegistrationStep.KycStart) { + return Result.Success(Unit) + } + + withMainContext { + store.dispatch(OnboardingSaltPayAction.SetStep(SaltPayRegistrationStep.KycWaiting)) + } + return state.saltPayManager.registerKYC() +} + +private suspend fun checkRegistration( + state: OnboardingSaltPayState, +): Result { + return try { + val registrationResponseItem = state.saltPayManager.checkRegistration().successOr { return it } + val step = registrationResponseItem.toSaltPayStep() + Result.Success(step) + } catch (ex: SaltPayRegistrationError) { + Result.Failure(ex) + } +} + +@Throws(SaltPayRegistrationError::class) +fun RegistrationResponse.Item.toSaltPayStep(): SaltPayRegistrationStep { + return when { + passed != true -> throw SaltPayRegistrationError.CardNotPassed(this.error) + disabledByAdmin == true -> throw SaltPayRegistrationError.CardDisabled(this.error) + + // go to success screen + active == true -> SaltPayRegistrationStep.Finished + + // pinSet is false, go toPin screen + pinSet == false -> SaltPayRegistrationStep.NeedPin + + // kycDate is set, go to kyc waiting screen + kycDate != null -> SaltPayRegistrationStep.KycWaiting + + else -> SaltPayRegistrationStep.KycIntro + } +} + +@Throws(SaltPayRegistrationError::class) +private fun assertPinValid(pin: String, pinLength: Int) { + val array = pin.toCharArray() + + if (array.size < pinLength) throw SaltPayRegistrationError.WeakPin + + val filters: List> = listOf( + AllSymbolsTheSameFilter(), + ) + + if (filters.any { it.filter(pin) }) { + throw SaltPayRegistrationError.WeakPin + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/redux/OnboardingSaltPayReducer.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/redux/OnboardingSaltPayReducer.kt new file mode 100644 index 0000000000..ac9ed3c67b --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/redux/OnboardingSaltPayReducer.kt @@ -0,0 +1,86 @@ +package com.tangem.tap.features.onboarding.products.wallet.saltPay.redux + +import com.tangem.tap.features.onboarding.products.wallet.redux.BackupStep +import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletState +import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletStep +import org.rekotlin.Action + +/** +[REDACTED_AUTHOR] + */ +class OnboardingSaltPayReducer { + companion object { + fun reduce(action: Action, state: OnboardingWalletState): OnboardingWalletState { + return internalReduce(action, state) + } + } +} + +private fun internalReduce(anyAction: Action, onboardingWalletState: OnboardingWalletState): OnboardingWalletState { + val action = anyAction as? OnboardingSaltPayAction ?: return onboardingWalletState + + if (onboardingWalletState.onboardingSaltPayState == null && action is OnboardingSaltPayAction.Init.SetDependencies) { + return onboardingWalletState.copy( + onboardingSaltPayState = OnboardingSaltPayState( + saltPayManager = action.registrationManager, + saltPayConfig = action.saltPayConfig, + ), + ) + } + + val oldState = onboardingWalletState.onboardingSaltPayState ?: return onboardingWalletState + + val newState = when (action) { + OnboardingSaltPayAction.Init.DiscardBackupSteps -> { + return onboardingWalletState.copy( + step = OnboardingWalletStep.SaltPay, + backupState = onboardingWalletState.backupState.copy( + backupStep = BackupStep.Finished, + maxBackupCards = 1, + canSkipBackup = false, + ), + onboardingSaltPayState = oldState.copy( + isTest = true, + step = SaltPayRegistrationStep.NeedPin, + ), + ) + } + is OnboardingSaltPayAction.SetAccessCode -> { + oldState.copy( + accessCode = action.accessCode, + ) + } + is OnboardingSaltPayAction.SetPin -> { + oldState.copy(pinCode = action.pin) + } + is OnboardingSaltPayAction.SetStep -> { + if (action.newStep == null) { + oldState + } else { + oldState.copy( + step = action.newStep, + ) + } + } + is OnboardingSaltPayAction.SetIsBusy -> { + oldState.copy( + isBusy = action.isBusy, + ) + } + else -> oldState + // is OnboardingSaltPayAction.Init.PrepareAndSetDependencies -> oldState + // OnboardingSaltPayAction.OnCreate -> oldState + // is OnboardingSaltPayAction.OnFinishKYC -> oldState + // is OnboardingSaltPayAction.Register -> oldState + // is OnboardingSaltPayAction.TrySetPin -> oldState + // is OnboardingSaltPayAction.Update -> oldState + // is OnboardingSaltPayAction.TryUpdateStep -> oldState + } + + return when (newState) { + oldState -> onboardingWalletState + else -> { + onboardingWalletState.copy(onboardingSaltPayState = newState) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/redux/OnboardingSaltPayState.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/redux/OnboardingSaltPayState.kt new file mode 100644 index 0000000000..2ecaa4b382 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/redux/OnboardingSaltPayState.kt @@ -0,0 +1,118 @@ +package com.tangem.tap.features.onboarding.products.wallet.saltPay.redux + +import com.tangem.blockchain.common.WalletManagerFactory +import com.tangem.common.extensions.guard +import com.tangem.common.extensions.toHexString +import com.tangem.common.json.MoshiJsonConverter +import com.tangem.domain.common.ScanResponse +import com.tangem.network.api.paymentology.PaymentologyApiService +import com.tangem.tap.common.toggleWidget.WidgetState +import com.tangem.tap.copyToClipboard +import com.tangem.tap.domain.extensions.makeSaltPayWalletManager +import com.tangem.tap.features.onboarding.products.wallet.saltPay.GnosisRegistrator +import com.tangem.tap.features.onboarding.products.wallet.saltPay.KYCProvider +import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayConfig +import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayRegistrationManager +import com.tangem.tap.features.wallet.redux.ProgressState +import com.tangem.tap.persistence.SaltPayRegistrationStorage +import com.tangem.tap.preferencesStorage +import com.tangem.tap.store + +/** +[REDACTED_AUTHOR] + */ +data class OnboardingSaltPayState( + @Transient + val saltPayManager: SaltPayRegistrationManager = SaltPayRegistrationManager.stub(), + val saltPayConfig: SaltPayConfig = SaltPayConfig.stub(), + val pinCode: String? = null, + val accessCode: String? = null, + val step: SaltPayRegistrationStep = SaltPayRegistrationStep.NeedPin, + val saltPayCardArtworkUrl: String? = null, + val isBusy: Boolean = false, + val isTest: Boolean = false, +) { + + val mainButtonState: WidgetState + get() = if (isBusy) ProgressState.Loading else ProgressState.Done + + val pinLength: Int = 4 + + companion object { + fun initDependency(scanResponse: ScanResponse): Pair { + val globalState = store.state.globalState + val saltPayConfig = globalState.configManager?.config?.saltPayConfig.guard { + throw NullPointerException("SaltPayConfig is not initialized") + } + val gnosisRegistrator = makeGnosisRegistrator( + scanResponse = scanResponse, + wmFactory = globalState.tapWalletManager.walletManagerFactory, + ) + val registrationManager = makeSaltPayRegistrationManager( + scanResponse = scanResponse, + gnosisRegistrator = gnosisRegistrator, + paymentologyService = store.state.domainNetworks.paymentologyService, + registrationStorage = preferencesStorage.saltPayRegistrationStorage, + kycProvider = saltPayConfig.kycProvider, + ) + test(scanResponse, gnosisRegistrator) + return registrationManager to saltPayConfig + } + + //TODO: delete + private fun test(scanResponse: ScanResponse, gnosisRegistrator: GnosisRegistrator) { + val map = mapOf( + "CID" to scanResponse.card.cardId, + "cardPublicKey" to scanResponse.card.cardPublicKey.toHexString(), + "walletPublicKey" to gnosisRegistrator.walletManager.wallet.publicKey.blockchainKey.toHexString(), + "walletAddress" to gnosisRegistrator.walletManager.wallet.address, + ) + val json = MoshiJsonConverter.INSTANCE.prettyPrint(map) + copyToClipboard(json) + } + + fun makeSaltPayRegistrationManager( + scanResponse: ScanResponse, + gnosisRegistrator: GnosisRegistrator, + paymentologyService: PaymentologyApiService, + registrationStorage: SaltPayRegistrationStorage, + kycProvider: KYCProvider, + ): SaltPayRegistrationManager { + if (!scanResponse.isSaltPay()) { + throw IllegalArgumentException("Can't initialize the OnboardingSaltPayMiddleware if card is not SalPay") + } + val saltPaySingleWallet = scanResponse.card.wallets.firstOrNull().guard { + throw NullPointerException("SaltPay card must have one wallet at least") + } + + return SaltPayRegistrationManager( + cardId = scanResponse.card.cardId, + cardPublicKey = scanResponse.card.cardPublicKey, + walletPublicKey = saltPaySingleWallet.publicKey, + kycProvider = kycProvider, + paymentologyService = paymentologyService, + gnosisRegistrator = gnosisRegistrator, + registrationStorage = registrationStorage, + ) + } + + fun makeGnosisRegistrator( + scanResponse: ScanResponse, + wmFactory: WalletManagerFactory, + ): GnosisRegistrator { + return GnosisRegistrator( + walletManager = wmFactory.makeSaltPayWalletManager(scanResponse), + ) + } + } +} + +enum class SaltPayRegistrationStep { + NoGas, + NeedPin, + CardRegistration, + KycIntro, + KycStart, + KycWaiting, + Finished; +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt index 056d5a28bb..8a444373b5 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt @@ -6,20 +6,37 @@ import android.view.View import android.view.ViewGroup import android.view.WindowManager import android.widget.ImageView +import android.widget.ProgressBar import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.children import androidx.fragment.app.Fragment import androidx.transition.TransitionInflater import androidx.transition.TransitionManager import by.kirich1409.viewbindingdelegate.viewBinding import coil.load +import coil.size.Scale +import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.button.MaterialButton import com.google.android.material.tabs.TabLayoutMediator import com.tangem.common.CardIdFormatter import com.tangem.common.core.CardIdDisplayFormat +import com.tangem.common.extensions.guard import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapfrogWidget import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.PropertyCalculator +import com.tangem.tap.common.compose.PinCodeWidget +import com.tangem.tap.common.extensions.beginDelayedTransition +import com.tangem.tap.common.extensions.configureSettings +import com.tangem.tap.common.extensions.dispatchDebugErrorNotification +import com.tangem.tap.common.extensions.dispatchDialogShow import com.tangem.tap.common.extensions.hide import com.tangem.tap.common.extensions.show +import com.tangem.tap.common.extensions.stop +import com.tangem.tap.common.feedback.SupportInfo +import com.tangem.tap.common.redux.global.GlobalAction +import com.tangem.tap.common.redux.navigation.AppScreen +import com.tangem.tap.common.redux.navigation.NavigationAction +import com.tangem.tap.common.toggleWidget.IndeterminateProgressButtonWidget import com.tangem.tap.features.FragmentOnBackPressedHandler import com.tangem.tap.features.addBackPressHandler import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction @@ -28,24 +45,31 @@ import com.tangem.tap.features.onboarding.products.wallet.redux.BackupStep import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletAction import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletState import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletStep +import com.tangem.tap.features.onboarding.products.wallet.saltPay.UtorgWebChromeClient +import com.tangem.tap.features.onboarding.products.wallet.saltPay.UtorgWebViewClient +import com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog.SaltPayDialog +import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayAction +import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayState +import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.SaltPayRegistrationStep import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.AccessCodeDialog import com.tangem.tap.store import com.tangem.wallet.R import com.tangem.wallet.databinding.FragmentOnboardingWalletBinding +import com.tangem.wallet.databinding.LayoutOnboardingSaltpayBinding import com.tangem.wallet.databinding.ViewOnboardingProgressBinding import org.rekotlin.StoreSubscriber class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet), StoreSubscriber, FragmentOnBackPressedHandler { - private var accessCodeDialog: AccessCodeDialog? = null - private lateinit var cardsWidget: WalletCardsWidget - private val binding: FragmentOnboardingWalletBinding by viewBinding( - FragmentOnboardingWalletBinding::bind - ) - private val pbBinding: ViewOnboardingProgressBinding by viewBinding( - ViewOnboardingProgressBinding::bind) + private val pbBinding: ViewOnboardingProgressBinding by viewBinding(ViewOnboardingProgressBinding::bind) + private val binding: FragmentOnboardingWalletBinding by viewBinding(FragmentOnboardingWalletBinding::bind) + private val bindingSaltPay: LayoutOnboardingSaltpayBinding by lazy { binding.onboardingSaltpayContainer } + private val saltPayView: SaltPayView = SaltPayView(this) + + private lateinit var cardsWidget: WalletCardsWidget + private var accessCodeDialog: AccessCodeDialog? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -74,19 +98,19 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet), binding.viewPagerBackupInfo.adapter = BackupInfoAdapter() TabLayoutMediator( binding.tabLayoutBackupInfo, - binding.viewPagerBackupInfo + binding.viewPagerBackupInfo, ) { tab, position -> //Some implementation }.attach() (activity as? AppCompatActivity)?.setSupportActionBar(binding.toolbar) - store.dispatch(OnboardingWalletAction.Init) binding.toolbar.setNavigationOnClickListener { activity?.onBackPressed() } - - store.dispatch(OnboardingWalletAction.LoadArtwork) - addBackPressHandler(this) + + store.dispatch(OnboardingWalletAction.Init) + store.dispatch(OnboardingWalletAction.StartSaltPayCardActivation) + store.dispatch(OnboardingWalletAction.LoadArtwork) } override fun onStart() { @@ -108,23 +132,16 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet), requireActivity().invalidateOptionsMenu() - with(binding) { - loadImageIntoImageView(state.cardArtworkUrl, imvFrontCard) - loadImageIntoImageView(state.cardArtworkUrl, imvFirstBackupCard) - loadImageIntoImageView(state.cardArtworkUrl, imvSecondBackupCard) - } - - pbBinding.pbState.max = 6 + pbBinding.pbState.max = state.getMaxProgress() pbBinding.pbState.progress = state.getProgressStep() - when (state.step) { - OnboardingWalletStep.None -> { - } - OnboardingWalletStep.CreateWallet -> setupCreateWalletState() - OnboardingWalletStep.Backup -> setBackupState(state.backupState) - OnboardingWalletStep.Done -> { - - } + if (state.isSaltPay) { + saltPayView.newState(state) + } else { + loadImageIntoImageView(state.cardArtworkUrl, binding.imvFrontCard) + loadImageIntoImageView(state.cardArtworkUrl, binding.imvFirstBackupCard) + loadImageIntoImageView(state.cardArtworkUrl, binding.imvSecondBackupCard) + handleOnboardingStep(state) } } @@ -136,6 +153,14 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet), } } + private fun handleOnboardingStep(state: OnboardingWalletState) { + when (state.step) { + OnboardingWalletStep.CreateWallet -> setupCreateWalletState() + OnboardingWalletStep.Backup -> setBackupState(state.backupState, state.isSaltPay) + else -> {} + } + } + private fun setupCreateWalletState() = with(binding) { layoutButtonsCommon.btnMainAction.setText(R.string.onboarding_create_wallet_button_create_wallet) layoutButtonsCommon.btnMainAction.setOnClickListener { store.dispatch(OnboardingWalletAction.CreateWallet) } @@ -149,22 +174,21 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet), cardsWidget.toFolded(false) { startPostponedEnterTransition() } } - - private fun setBackupState(state: BackupState) { + private fun setBackupState(state: BackupState, isSaltPay: Boolean) { when (state.backupStep) { - BackupStep.InitBackup -> showBackupIntro(state) + BackupStep.InitBackup -> showBackupIntro(state, isSaltPay) BackupStep.ScanOriginCard -> showScanOriginCard() - BackupStep.AddBackupCards -> showAddBackupCards(state) + BackupStep.AddBackupCards -> showAddBackupCards(state, isSaltPay) BackupStep.SetAccessCode -> showSetAccessCode() BackupStep.EnterAccessCode -> showEnterAccessCode(state) BackupStep.ReenterAccessCode -> showReenterAccessCode(state) - is BackupStep.WritePrimaryCard -> showWritePrimaryCard(state) - is BackupStep.WriteBackupCard -> showWriteBackupCard(state) + is BackupStep.WritePrimaryCard -> showWritePrimaryCard(state, isSaltPay) + is BackupStep.WriteBackupCard -> showWriteBackupCard(state, isSaltPay) BackupStep.Finished -> showSuccess() } } - private fun showBackupIntro(state: BackupState) = with(binding) { + private fun showBackupIntro(state: BackupState, isSaltPay: Boolean) = with(binding) { imvFirstBackupCard.show() imvSecondBackupCard.show() @@ -177,10 +201,11 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet), with(layoutButtonsCommon) { btnMainAction.text = getText(R.string.onboarding_button_backup_now) - btnAlternativeAction.text = getText(R.string.onboarding_button_skip_backup) - btnAlternativeAction.show(state.canSkipBackup) btnMainAction.setOnClickListener { store.dispatch(BackupAction.StartBackup) } + + btnAlternativeAction.text = getText(R.string.onboarding_button_skip_backup) btnAlternativeAction.setOnClickListener { store.dispatch(BackupAction.DismissBackup) } + btnAlternativeAction.show(state.canSkipBackup && !isSaltPay) } startPostponedEnterTransition() @@ -215,7 +240,7 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet), imvSecondBackupCard.show() } - private fun showAddBackupCards(state: BackupState) = with(binding) { + private fun showAddBackupCards(state: BackupState, isSaltPay: Boolean) = with(binding) { prepareBackupView() accessCodeDialog?.dismiss() @@ -230,11 +255,20 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet), layoutButtonsAddCards.btnAddCard.isEnabled = false } + layoutButtonsAddCards.btnContinue.text = getText(R.string.onboarding_button_finalize_backup) + layoutButtonsAddCards.btnContinue.setOnClickListener { store.dispatch(BackupAction.FinishAddingBackupCards) } + layoutButtonsAddCards.btnContinue.isEnabled = state.backupCardsNumber != 0 + + if (isSaltPay) { + saltPayView.showAddBackupCards(state) + return@with + } + when (state.backupCardsNumber) { 0 -> { - cardsWidget.toFan() { - cardsWidget.getFirstBackupCardView().animate().alpha(0.6f).setDuration(200) - cardsWidget.getSecondBackupCardView().animate().alpha(0.2f).setDuration(200) + cardsWidget.toFan { + cardsWidget.getFirstBackupCardView().animate().alpha(0.6f).duration = 200 + cardsWidget.getSecondBackupCardView().animate().alpha(0.2f).duration = 200 } tvHeader.text = getText(R.string.onboarding_title_no_backup_cards) tvBody.text = getText(R.string.onboarding_subtitle_no_backup_cards) @@ -244,7 +278,7 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet), tvBody.text = getText(R.string.onboarding_subtitle_one_backup_card) cardsWidget.toFan(false) - cardsWidget.getFirstBackupCardView().animate().alpha(1f).setDuration(400) + cardsWidget.getFirstBackupCardView().animate().alpha(1f).duration = 400 cardsWidget.getSecondBackupCardView().alpha = 0.2f } 2 -> { @@ -253,13 +287,9 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet), cardsWidget.toFan(false) cardsWidget.getFirstBackupCardView().alpha = 1f - cardsWidget.getSecondBackupCardView().animate().alpha(1f).setDuration(400) + cardsWidget.getSecondBackupCardView().animate().alpha(1f).duration = 400 } } - - layoutButtonsAddCards.btnContinue.text = getText(R.string.onboarding_button_finalize_backup) - layoutButtonsAddCards.btnContinue.setOnClickListener { store.dispatch(BackupAction.FinishAddingBackupCards) } - layoutButtonsAddCards.btnContinue.isEnabled = state.backupCardsNumber != 0 } private fun showSetAccessCode() { @@ -290,24 +320,31 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet), accessCodeDialog?.showError(state.accessCodeError) } - private fun showWritePrimaryCard(state: BackupState) = with(binding) { + private fun showWritePrimaryCard(state: BackupState, isSaltPay: Boolean) = with(binding) { accessCodeDialog?.dismiss() prepareViewForFinalizeStep() cardsWidget.getSecondBackupCardView().show(state.backupCardsNumber == 2) - cardsWidget.toLeapfrog() { + cardsWidget.toLeapfrog { cardsWidget.getFirstBackupCardView().alpha = 0.6f cardsWidget.getSecondBackupCardView().alpha = 0.2f } val cardIdFormatter = CardIdFormatter(CardIdDisplayFormat.LastMasked(4)) - tvHeader.text = getText(R.string.onboarding_title_prepare_origin) - tvBody.text = getString( - R.string.onboarding_subtitle_scan_primary_card_format, - state.primaryCardId?.let { cardIdFormatter.getFormattedCardId(it) } - ) - layoutButtonsCommon.btnMainAction.text = getText(R.string.onboarding_button_backup_origin) + if (isSaltPay) { + tvHeader.text = getText(R.string.onboarding_saltpay_title_prepare_origin) + tvBody.text = getString(R.string.onboarding_twins_interrupt_warning) + layoutButtonsCommon.btnMainAction.text = getText(R.string.onboarding_saltpay_button_backup_origin) + } else { + tvHeader.text = getText(R.string.onboarding_title_prepare_origin) + tvBody.text = getString( + R.string.onboarding_subtitle_scan_primary_card_format, + state.primaryCardId?.let { cardIdFormatter.getFormattedCardId(it) }, + ) + layoutButtonsCommon.btnMainAction.text = getText(R.string.onboarding_button_backup_origin) + } + layoutButtonsCommon.btnMainAction.setOnClickListener { store.dispatch(BackupAction.WritePrimaryCard) } } @@ -328,19 +365,19 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet), layoutButtonsCommon.btnAlternativeAction.hide() } - private fun showWriteBackupCard(state: BackupState) = with(binding) { + private fun showWriteBackupCard(state: BackupState, isSaltPay: Boolean) = with(binding) { prepareViewForFinalizeStep() cardsWidget.getSecondBackupCardView().show(state.backupCardsNumber == 2) val cardNumber = (state.backupStep as? BackupStep.WriteBackupCard)?.cardNumber ?: 1 when (cardNumber) { - 1 -> cardsWidget.leapfrogWidget.leap() { + 1 -> cardsWidget.leapfrogWidget.leap { cardsWidget.getOriginCardView().alpha = 0.4f cardsWidget.getFirstBackupCardView().alpha = 0.2f cardsWidget.getSecondBackupCardView().alpha = 1f } - 2 -> cardsWidget.leapfrogWidget.leap() { + 2 -> cardsWidget.leapfrogWidget.leap { cardsWidget.getOriginCardView().alpha = 0.4f cardsWidget.getFirstBackupCardView().alpha = 0.2f cardsWidget.getSecondBackupCardView().alpha = 1f @@ -348,22 +385,28 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet), } val cardIdFormatter = CardIdFormatter(CardIdDisplayFormat.LastMasked(4)) - tvHeader.text = - getString(R.string.onboarding_title_backup_card_format, cardNumber) - tvBody.text = getString( - R.string.onboarding_subtitle_scan_backup_card_format, - cardIdFormatter.getFormattedCardId(state.backupCardIds[cardNumber - 1]) - ) - layoutButtonsCommon.btnMainAction.text = getString( - R.string.onboarding_button_backup_card_format, - cardNumber - ) + if (isSaltPay) { + tvHeader.text = getString(R.string.onboarding_saltpay_title_backup_card) + tvBody.text = getString(R.string.onboarding_twins_interrupt_warning) + layoutButtonsCommon.btnMainAction.text = getString(R.string.onboarding_saltpay_title_backup_card) + } else { + tvHeader.text = getString(R.string.onboarding_title_backup_card_format, cardNumber) + tvBody.text = getString( + R.string.onboarding_subtitle_scan_backup_card_format, + cardIdFormatter.getFormattedCardId(state.backupCardIds[cardNumber - 1]), + ) + layoutButtonsCommon.btnMainAction.text = getString( + R.string.onboarding_button_backup_card_format, + cardNumber, + ) + } layoutButtonsCommon.btnMainAction.setOnClickListener { store.dispatch(BackupAction.WriteBackupCard(cardNumber)) } } private fun showSuccess() = with(binding) { + toolbar.title = getString(R.string.onboarding_done_header) tvHeader.text = getText(R.string.onboarding_done_header) tvHeader.show() @@ -389,12 +432,211 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet), imvSuccess.show() imvSuccess.animate() - ?.alpha(1f) - ?.setDuration(400) + ?.alpha(1f)?.duration = 400 } } override fun handleOnBackPressed() { store.dispatch(OnboardingWalletAction.OnBackPressed) } + + private class SaltPayView( + private val walletFragment: OnboardingWalletFragment, + ) { + + private val toolbar: MaterialToolbar by lazy { walletFragment.binding.toolbar } + + private var progressButton: SaltPayProgressButton? = null + + fun newState(state: OnboardingWalletState) { + handleCardArtworks(state) + when (state.step) { + OnboardingWalletStep.SaltPay -> setSaltPayStep(state.onboardingSaltPayState) + OnboardingWalletStep.Backup -> { + if (state.backupState.backupStep == BackupStep.Finished) { + // if backup finished -> switch OnboardingWalletStep to the SaltPay + store.dispatch(OnboardingWalletAction.GetToSaltPayStep) + } else { + // if not -> back to the standard backup process + walletFragment.handleOnboardingStep(state) + } + } + else -> walletFragment.handleOnboardingStep(state) + } + } + + private fun handleCardArtworks(state: OnboardingWalletState) = with(walletFragment.binding) { + //TODO: SaltPay: remove hardCode + if (state.onboardingSaltPayState?.saltPayCardArtworkUrl == null) { + imvFrontCard.load(R.drawable.img_salt_pay_visa) { + scale(Scale.FILL) + crossfade(enable = true) + } + } else { + walletFragment.loadImageIntoImageView(state.onboardingSaltPayState.saltPayCardArtworkUrl, imvFrontCard) + } + imvFirstBackupCard.load(R.drawable.card_placeholder_wallet) + + // if (state.onboardingSaltPayState?.saltPayCardArtworkUrl == null) { + // //if saltPay url not loaded -> load from resource + // val bitmap = BitmapFactory.decodeResource(walletFragment.resources, R.drawable.img_salt_pay_visa) + // imvFrontCard.setImageBitmap(bitmap) + // } else { + // walletFragment.loadImageIntoImageView(state.onboardingSaltPayState.saltPayCardArtworkUrl, imvFrontCard) + // } + // walletFragment.loadImageIntoImageView(state.cardArtworkUrl, imvFirstBackupCard) + + //TODO: at now we can hide the image only by changing alpha channel to 0, because + // the OnboardingWalletFragment and WalletCardsWidget manipulate it visibility through changing + // View.VISIBILITY states. + imvSecondBackupCard.alpha = 0f + } + + fun showAddBackupCards(state: BackupState) = with(walletFragment.binding) { + walletFragment.cardsWidget.getSecondBackupCardView().alpha = 0f + when (state.backupCardsNumber) { + 0 -> { + tvHeader.text = walletFragment.getText(R.string.onboarding_saltpay_title_no_backup_card) + tvBody.text = walletFragment.getText(R.string.onboarding_saltpay_subtitle_no_backup_cards) + walletFragment.cardsWidget.getFirstBackupCardView().alpha = 0.6f + walletFragment.cardsWidget.toFan() + } + 1 -> { + tvHeader.text = walletFragment.getText(R.string.onboarding_saltpay_title_one_backup_card) + tvBody.text = walletFragment.getText(R.string.onboarding_saltpay_subtitle_one_backup_card) + walletFragment.cardsWidget.toFan(false) + walletFragment.cardsWidget.getFirstBackupCardView().animate().alpha(1f).duration = 400 + } + } + } + + private fun setSaltPayStep(onboardingSaltPayState: OnboardingSaltPayState?) { + val state = onboardingSaltPayState.guard { + store.dispatchDebugErrorNotification("SaltPayView: setSaltPayStep: onboardingSaltPayState = null") + return + } + + walletFragment.binding.onboardingWalletContainer.hide() + walletFragment.bindingSaltPay.onboardingSaltpayContainer.show() + + when (state.step) { + SaltPayRegistrationStep.NoGas -> handleNoGas() + SaltPayRegistrationStep.NeedPin -> handleNeedPin() + SaltPayRegistrationStep.CardRegistration -> handleCardRegistration() + SaltPayRegistrationStep.KycIntro -> handleKycIntro() + SaltPayRegistrationStep.KycStart -> handleKycStart(state) + SaltPayRegistrationStep.KycWaiting -> handleKycWaiting() + SaltPayRegistrationStep.Finished -> handleFinished() + } + progressButton?.changeState(state.mainButtonState) + } + + private fun handleNoGas() { + // normally this shouldn't happen + store.dispatch(NavigationAction.PopBackTo(AppScreen.Home)) + store.dispatchDialogShow(SaltPayDialog.NoFundsForActivation) + } + + private fun handleNeedPin() = with(walletFragment.bindingSaltPay) { + toolbar.title = walletFragment.getString(R.string.onboarding_navbar_pin) + showOnlyView(pinCode.root) { + progressButton = SaltPayProgressButton(pinCode.root) + progressButton?.mainView?.isEnabled = false + + var enteredPinCode = "" + pinCode.flPinCodeContainer.setContent { + PinCodeWidget { code, isLastSymbolEntered -> + enteredPinCode = code + progressButton?.mainView?.isEnabled = isLastSymbolEntered + } + } + pinCode.btnSetPin.setOnClickListener { + store.dispatch(OnboardingSaltPayAction.TrySetPin(enteredPinCode)) + } + } + } + + private fun handleCardRegistration() = with(walletFragment.bindingSaltPay) { + toolbar.title = walletFragment.getString(R.string.onboarding_navbar_register_wallet) + showOnlyView(connectCard.root) { + progressButton = SaltPayProgressButton(connectCard.root) + connectCard.btnConnect.setOnClickListener { + store.dispatch(OnboardingSaltPayAction.RegisterCard) + } + } + } + + private fun handleKycIntro() = with(walletFragment.bindingSaltPay) { + toolbar.title = walletFragment.getString(R.string.onboarding_navbar_kyc_start) + showOnlyView(verifyIdentity.root) { + progressButton = SaltPayProgressButton(verifyIdentity.root) + verifyIdentity.btnVerify.setOnClickListener { + store.dispatch(OnboardingSaltPayAction.KYCStart) + } + } + } + + private fun handleKycStart(state: OnboardingSaltPayState) = with(walletFragment.bindingSaltPay) { + toolbar.title = walletFragment.getString(R.string.onboarding_navbar_kyc_start) + showOnlyView(webvVerifyIdentity) { + progressButton = null + webvVerifyIdentity.configureSettings() + webvVerifyIdentity.webViewClient = UtorgWebViewClient( + successUrl = state.saltPayManager.kycUrlProvider.doneUrl, + onSuccess = { + webvVerifyIdentity.stop() + store.dispatch(OnboardingSaltPayAction.OnFinishKYC) + }, + ) + webvVerifyIdentity.webChromeClient = UtorgWebChromeClient(walletFragment.requireActivity()) + webvVerifyIdentity.loadUrl(state.saltPayManager.kycUrlProvider.requestUrl) + } + } + + private fun handleKycWaiting() = with(walletFragment.bindingSaltPay) { + toolbar.title = walletFragment.getString(R.string.onboarding_navbar_kyc_waiting) + showOnlyView(verifyIdentityInProgress.root) { + progressButton = SaltPayProgressButton(verifyIdentityInProgress.root) + verifyIdentityInProgress.btnOpenSupportChat.setOnClickListener { + store.dispatch(GlobalAction.OpenChat(SupportInfo())) + } + verifyIdentityInProgress.btnRefresh.setOnClickListener { + store.dispatch(OnboardingSaltPayAction.Update) + } + } + } + + private fun handleFinished() { + walletFragment.binding.onboardingRoot.beginDelayedTransition() + walletFragment.bindingSaltPay.onboardingSaltpayContainer.hide() + walletFragment.binding.onboardingWalletContainer.show() + walletFragment.showSuccess() + } + + private fun showOnlyView(view: View, onShowListener: (() -> Unit)? = null) { + // onboardingSaltpayContainer.beginDelayedTransition() + walletFragment.bindingSaltPay.onboardingSaltpayContainer.children + .filter { it.id != view.id } + .forEach { it.hide() } + view.show { onShowListener?.invoke() } + } + + private class SaltPayProgressButton( + root: ViewGroup, + ) : IndeterminateProgressButtonWidget(findButton(root), findProgress(root)) { + companion object { + fun findButton(view: ViewGroup): MaterialButton { + return findContainer(view).children.firstOrNull { it is MaterialButton } as MaterialButton + } + + fun findProgress(view: ViewGroup): View { + return findContainer(view).children.firstOrNull { it is ProgressBar }!! + } + + fun findContainer(view: ViewGroup): ViewGroup { + return view.children.first { it.id == R.id.btn_container } as ViewGroup + } + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/ConfirmDiscardingBackupDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/ConfirmDiscardingBackupDialog.kt index 8b88890be8..4d35050937 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/ConfirmDiscardingBackupDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/ConfirmDiscardingBackupDialog.kt @@ -14,7 +14,7 @@ class ConfirmDiscardingBackupDialog { setTitle(R.string.welcome_interrupted_backup_discard_title) setMessage(R.string.welcome_interrupted_backup_discard_message) setPositiveButton(R.string.welcome_interrupted_backup_discard_resume) { _, _ -> - store.dispatch(BackupAction.ResumeBackup) + store.dispatch(BackupAction.ResumeFoundUnfinishedBackup) } setNegativeButton(R.string.welcome_interrupted_backup_discard_discard) { _, _ -> store.dispatch(BackupAction.DiscardSavedBackup) diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/UnfinishedBackupFoundDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/UnfinishedBackupFoundDialog.kt index b51a53e29f..697dc71f67 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/UnfinishedBackupFoundDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/UnfinishedBackupFoundDialog.kt @@ -16,7 +16,7 @@ class UnfinishedBackupFoundDialog { setMessage(R.string.welcome_interrupted_backup_alert_message) setPositiveButton(R.string.welcome_interrupted_backup_alert_resume) { _, _ -> store.dispatch(GlobalAction.HideDialog) - store.dispatch(BackupAction.ResumeBackup) + store.dispatch(BackupAction.ResumeFoundUnfinishedBackup) } setNegativeButton(R.string.welcome_interrupted_backup_alert_discard) { _, _ -> store.dispatch(GlobalAction.HideDialog) diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/service/OnboardingWalletService.kt b/app/src/main/java/com/tangem/tap/features/onboarding/service/OnboardingWalletService.kt deleted file mode 100644 index db7f77b99c..0000000000 --- a/app/src/main/java/com/tangem/tap/features/onboarding/service/OnboardingWalletService.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.tangem.tap.features.onboarding.service - -//class OnboardingWalletService( -// override val fromScreen: AppScreen, -// override var scanResponse: ScanResponse, -// cardInfoStorage: UsedCardsPrefStorage, -// walletManagerFactory: WalletManagerFactory, -//) : ProductOnboardingService(cardInfoStorage, walletManagerFactory) { -// -// override fun navigateToScreen(): AppScreen { -// return AppScreen.OnboardingWallet -// } -//} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/AddCustomTokenScreen.kt b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/AddCustomTokenScreen.kt index 14f9fa3397..93066fc926 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/AddCustomTokenScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/AddCustomTokenScreen.kt @@ -1,11 +1,37 @@ package com.tangem.tap.features.tokens.addCustomToken.compose -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +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.lazy.LazyColumn -import androidx.compose.material.* +import androidx.compose.material.BottomSheetScaffold +import androidx.compose.material.BottomSheetScaffoldState +import androidx.compose.material.BottomSheetState +import androidx.compose.material.BottomSheetValue +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.ExtendedFloatingActionButton +import androidx.compose.material.FabPosition +import androidx.compose.material.FloatingActionButtonDefaults +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Scaffold +import androidx.compose.material.Surface +import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add -import androidx.compose.runtime.* +import androidx.compose.material.rememberBottomSheetScaffoldState +import androidx.compose.material.rememberScaffoldState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext @@ -15,14 +41,23 @@ import androidx.compose.ui.unit.dp import com.tangem.domain.AddCustomTokenError import com.tangem.domain.common.form.DataField import com.tangem.domain.common.form.FieldId -import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.* +import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.ContractAddress +import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.Decimals +import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.DerivationPath +import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.Name +import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.Network +import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.Symbol import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState import com.tangem.domain.features.addCustomToken.redux.ScreenState import com.tangem.domain.features.addCustomToken.redux.ViewStates import com.tangem.domain.redux.domainStore -import com.tangem.tap.common.compose.* -import com.tangem.tap.common.moduleMessage.ModuleMessageConverter +import com.tangem.tap.common.compose.AddCustomTokenWarning +import com.tangem.tap.common.compose.ClosePopupTrigger +import com.tangem.tap.common.compose.ComposeDialogManager +import com.tangem.tap.common.compose.ToggledRippleTheme +import com.tangem.tap.common.compose.keyboardAsState +import com.tangem.tap.domain.moduleMessage.ModuleMessageConverter import com.tangem.tap.features.tokens.addCustomToken.compose.test.TestCase import com.tangem.tap.features.tokens.addCustomToken.compose.test.TestCasesList import com.tangem.wallet.R diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt index d7e6ceba13..ba68023190 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt @@ -143,8 +143,8 @@ class MultiWalletMiddleware { is WalletAction.MultiWallet.ShowWalletBackupWarning -> Unit is WalletAction.MultiWallet.BackupWallet -> { store.state.globalState.scanResponse?.let { + store.dispatch(GlobalAction.Onboarding.Start(it, canSkipBackup = false)) store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingWallet)) - store.dispatch(GlobalAction.Onboarding.Start(it, fromHomeScreen = false)) } } is WalletAction.MultiWallet.ScanToGetDerivations -> { diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt index 256e72fd37..19d4a51cf0 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt @@ -135,7 +135,19 @@ class WalletMiddleware { .plus(Currency.Blockchain(wallet.blockchain, wallet.publicKey.derivationPath?.rawPath)) } action.coinsList != null -> action.coinsList - else -> walletState.walletsData.map { it.currency } + else -> { + if (walletState.isMultiwalletAllowed) { + walletState.walletsData.map { it.currency } + } else { + val derivationPath = walletState.primaryWallet?.currency?.derivationPath + val primaryBlockchain = walletState.primaryBlockchain + val primaryToken = walletState.primaryToken + listOfNotNull( + primaryBlockchain?.let { Currency.Blockchain(it, derivationPath) }, + primaryToken?.let { Currency.Token(it, primaryBlockchain!!, derivationPath) }, + ) + } + } } val ratesResult = globalState.tapWalletManager.rates.loadFiatRate( currencyId = appCurrencyId, diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/OnWalletLoadedReducer.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/OnWalletLoadedReducer.kt index 63d378718f..7d124f2a41 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/OnWalletLoadedReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/OnWalletLoadedReducer.kt @@ -21,6 +21,7 @@ import com.tangem.tap.features.wallet.ui.BalanceStatus import com.tangem.tap.features.wallet.ui.BalanceWidgetData import com.tangem.tap.features.wallet.ui.TokenData import com.tangem.tap.store +import java.math.BigDecimal class OnWalletLoadedReducer { @@ -124,10 +125,12 @@ class OnWalletLoadedReducer { val tokenFiatAmount = tokenFiatRate?.let { tokenAmount.value?.toFiatString(it, fiatCurrencyName) } TokenData( - tokenAmount.value?.toFormattedCurrencyString( - token.decimals, token.symbol + amount = tokenAmount.value ?: BigDecimal.ZERO, + tokenSymbol = tokenAmount.currencySymbol, fiatAmountFormatted = tokenFiatAmount, + fiatAmount = tokenFiatRate?.let { tokenAmount.value?.toFiatValue(tokenFiatRate) }, + amountFormatted = tokenAmount.value?.toFormattedCurrencyString( + token.decimals, token.symbol, ) ?: "", - tokenAmount.currencySymbol, tokenFiatAmount ) } else { null diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt index fbac7c7daa..3a180040a3 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt @@ -31,6 +31,7 @@ import com.tangem.tap.features.wallet.redux.WalletStore import com.tangem.tap.features.wallet.ui.BalanceStatus import com.tangem.tap.features.wallet.ui.BalanceWidgetData import org.rekotlin.Action +import timber.log.Timber import java.math.BigDecimal class WalletReducer { @@ -389,16 +390,11 @@ private fun setNewFiatRate( state = state ) } else { - val fiatRate = fiatRates.entries.firstOrNull() - val currency = fiatRate?.key ?: return state - val rate = fiatRate.value ?: return state - - setSingleWalletFiatRate( - rate = rate, - rateFormatted = rateFormatter(rate), - currency = currency, + setSingleWalletFiatRates( + fiatRates = fiatRates.mapNotNullValues { it.value }, + rateFormatter = rateFormatter, appCurrency = appCurrency, - state = state + state = state, ) } } @@ -429,10 +425,10 @@ private fun setMultiWalletFiatRate( walletData.copy( currencyData = currencyData.copy( fiatAmountFormatted = fiatAmountFormatted, - fiatAmount = fiatAmount + fiatAmount = fiatAmount, ), fiatRate = rate, - fiatRateString = rateFormatter(rate) + fiatRateString = rateFormatter(rate), ) } @@ -440,15 +436,51 @@ private fun setMultiWalletFiatRate( .updateWalletsData(newWalletsData) } +private fun setSingleWalletFiatRates( + fiatRates: Map, + appCurrency: FiatCurrency, + rateFormatter: (BigDecimal) -> String, + state: WalletState, +): WalletState { + val blockchainFiatRate = fiatRates.entries.firstOrNull { it.key.isBlockchain() } + val tokenFiatRate = fiatRates.entries.firstOrNull { it.key.isToken() } + Timber.e("Token Fiat Rate is ${tokenFiatRate ?: "NULL"}") + val updatedState = updateStateWithFiatRate(blockchainFiatRate?.toPair(), appCurrency, rateFormatter, state) + return updateStateWithFiatRate(tokenFiatRate?.toPair(), appCurrency, rateFormatter, updatedState) +} + +private fun updateStateWithFiatRate( + fiatRate: Pair?, + appCurrency: FiatCurrency, + rateFormatter: (BigDecimal) -> String, + state: WalletState, +): WalletState { + return if (fiatRate != null) { + val currency = fiatRate.first + val rate = fiatRate.second + + setSingleWalletFiatRate( + rate = rate, + rateFormatted = rateFormatter(rate), + currency = currency, + appCurrency = appCurrency, + state = state, + ) + } else { + state + } +} + private fun setSingleWalletFiatRate( rate: BigDecimal, rateFormatted: String, currency: Currency, appCurrency: FiatCurrency, - state: WalletState + state: WalletState, ): WalletState { val wallet = state.primaryWalletManager?.wallet ?: return state val token = wallet.getFirstToken() + Timber.e("Working with currency: ${currency.currencyName}") if (currency == state.primaryWallet?.currency) { val fiatAmount = wallet.amounts[AmountType.Coin]?.value @@ -459,23 +491,38 @@ private fun setSingleWalletFiatRate( fiatRateString = rateFormatted ) return state.updateWalletData(walletData) + } else if (currency is Currency.Token && currency.token == token) { + Timber.e("Working with token fiat rate") + val tokenFiatAmount = wallet.getTokenAmount(token) ?.value - ?.toFiatString(rate, appCurrency.code) + val tokenAmountFormatted = tokenFiatAmount?.toFiatString(rate, appCurrency.code) + val tokenData = state.primaryWallet?.currencyData?.token?.copy( + fiatAmountFormatted = tokenAmountFormatted, fiatAmount = tokenFiatAmount, fiatRate = rate, - fiatRateString = rateFormatted + fiatRateString = rateFormatted, ) + // ?: TokenData( + // fiatAmountFormatted = tokenAmountFormatted, + // fiatAmount = tokenFiatAmount, + // fiatRate = rate, + // fiatRateString = rateFormatted, + // amount = "", + // tokenSymbol = currency.currencySymbol + // ) + + Timber.e("Token Data is ${tokenData ?: "NULL"}") val walletData = state.primaryWallet?.copy( currencyData = state.primaryWallet.currencyData.copy( - token = tokenData - ) + token = tokenData, + ), ) - val wallets = walletData?.let { listOf(walletData) } ?: emptyList() - return state.updateWalletsData(wallets) + Timber.e("Wallet Data is ${walletData ?: "NULL"}") + return state.updateWalletData(walletData) } return state } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/BalanceWidget.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/BalanceWidget.kt index c627aa2e32..34ca1f10c1 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/BalanceWidget.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/BalanceWidget.kt @@ -34,9 +34,11 @@ data class BalanceWidgetData( ) data class TokenData( - val amount: String, + val amountFormatted: String, + val amount: BigDecimal? = null, val tokenSymbol: String, - val fiatAmount: String? = null, + val fiatAmountFormatted: String? = null, + val fiatAmount: BigDecimal? = null, val fiatRateString: String? = null, val fiatRate: BigDecimal? = null, ) @@ -151,11 +153,11 @@ class BalanceWidget( groupBaseCurrency.show() tvCurrency.text = data.token?.tokenSymbol tvBaseCurrency.text = data.currency - tvAmount.text = if (showAmount) data.token?.amount else "" + tvAmount.text = if (showAmount) data.token?.amountFormatted else "" tvBaseAmount.text = if (showAmount) data.amountFormatted else "" if (showAmount) { tvFiatAmount.show() - tvFiatAmount.text = data.token?.fiatAmount + tvFiatAmount.text = data.token?.fiatAmountFormatted } } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt index 1389600f94..1f7d881721 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt @@ -26,7 +26,6 @@ import com.tangem.tap.domain.statePrinter.printScanResponseState import com.tangem.tap.domain.statePrinter.printWalletState import com.tangem.tap.domain.termsOfUse.CardTou import com.tangem.tap.features.details.redux.DetailsAction -import com.tangem.tap.features.wallet.redux.Artwork import com.tangem.tap.features.wallet.redux.ErrorType import com.tangem.tap.features.wallet.redux.ProgressState import com.tangem.tap.features.wallet.redux.WalletAction @@ -42,7 +41,6 @@ import com.tangem.wallet.R import com.tangem.wallet.databinding.FragmentWalletBinding import org.rekotlin.StoreSubscriber - class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber { private lateinit var warningsAdapter: WarningMessagesAdapter @@ -54,11 +52,14 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber { + !state.isMultiwalletAllowed && !isSaltPay && walletView !is SingleWalletView -> { walletView = SingleWalletView() walletView.changeWalletView(this, binding) } @@ -143,7 +143,7 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber { store.dispatch(GlobalAction.UpdateFeedbackInfo(store.state.walletState.walletManagers)) store.state.globalState.scanResponse?.let { scanNoteResponse -> - store.dispatch(DetailsAction.PrepareScreen( - scanNoteResponse, - store.state.walletState.walletManagers.map { it.wallet }, - CardTou(), - )) + store.dispatch( + DetailsAction.PrepareScreen( + scanNoteResponse, + store.state.walletState.walletManagers.map { it.wallet }, + CardTou(), + ), + ) store.dispatch(NavigationAction.NavigateTo(AppScreen.Details)) true } @@ -208,5 +221,4 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber(500, mainScope) { store.dispatch(it) } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SaltPaySingleWalletView.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SaltPaySingleWalletView.kt index 0139a7493c..e765e738e8 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SaltPaySingleWalletView.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SaltPaySingleWalletView.kt @@ -2,11 +2,12 @@ package com.tangem.tap.features.wallet.ui.wallet import com.tangem.tap.common.extensions.hide import com.tangem.tap.common.extensions.show -import com.tangem.tap.features.wallet.redux.WalletData import com.tangem.tap.features.wallet.redux.WalletState +import com.tangem.tap.features.wallet.ui.TokenData import com.tangem.tap.features.wallet.ui.WalletFragment import com.tangem.tap.store import com.tangem.wallet.databinding.FragmentWalletBinding +import timber.log.Timber class SaltPaySingleWalletView : WalletView() { override fun changeWalletView(fragment: WalletFragment, binding: FragmentWalletBinding) { @@ -18,6 +19,7 @@ class SaltPaySingleWalletView : WalletView() { private fun showSingleWalletView(binding: FragmentWalletBinding) = with(binding) { rvMultiwallet.hide() btnAddToken.hide() + rowButtons.hide() rvPendingTransaction.hide() tvTwinCardNumber.hide() pbLoadingUserTokens.hide() @@ -31,20 +33,21 @@ class SaltPaySingleWalletView : WalletView() { override fun onNewState(state: WalletState) { val binding = binding ?: return - state.primaryWallet ?: return + val tokenData = state.primaryWallet?.currencyData?.token ?: return - setupBalance(state, state.primaryWallet, binding) + setupBalance(state, tokenData, binding) } - private fun setupBalance(state: WalletState, primaryWallet: WalletData, binding: FragmentWalletBinding) { + private fun setupBalance(state: WalletState, tokenData: TokenData, binding: FragmentWalletBinding) { binding.lSingleWalletBalance.root.show() + Timber.e("Current address is ${state.primaryWalletManager?.wallet?.address}") SaltPayBalanceWidget( binding = binding.lSingleWalletBalance, data = SaltPayBalanceWidgetData( state = state.state, - currencySymbol = primaryWallet.currencyData.currencySymbol, - currency = primaryWallet.currencyData.amountFormatted, - fiatAmount = primaryWallet.currencyData.fiatAmount, + currencySymbol = tokenData.tokenSymbol, + currency = tokenData.amountFormatted, + fiatAmount = tokenData.fiatAmount, // TODO: show fiatAmount fiatCurrency = store.state.globalState.appCurrency, ), ).setup() diff --git a/app/src/main/java/com/tangem/tap/persistence/PreferencesStorage.kt b/app/src/main/java/com/tangem/tap/persistence/PreferencesStorage.kt index aeab14497b..723af0df99 100644 --- a/app/src/main/java/com/tangem/tap/persistence/PreferencesStorage.kt +++ b/app/src/main/java/com/tangem/tap/persistence/PreferencesStorage.kt @@ -15,6 +15,7 @@ class PreferencesStorage(applicationContext: Application) { val appRatingLaunchObserver: AppRatingLaunchObserver val usedCardsPrefStorage: UsedCardsPrefStorage val fiatCurrenciesPrefStorage: FiatCurrenciesPrefStorage + val saltPayRegistrationStorage: SaltPayRegistrationStorage init { incrementLaunchCounter() @@ -23,6 +24,7 @@ class PreferencesStorage(applicationContext: Application) { usedCardsPrefStorage.migrate() fiatCurrenciesPrefStorage = FiatCurrenciesPrefStorage(preferences, MoshiJsonConverter.INSTANCE) fiatCurrenciesPrefStorage.migrate() + saltPayRegistrationStorage = SaltPayRegistrationPrefStorage(applicationContext, MoshiJsonConverter.INSTANCE) } var chatFirstLaunchTime: Long? diff --git a/app/src/main/java/com/tangem/tap/persistence/SaltPayRegistrationPrefStorage.kt b/app/src/main/java/com/tangem/tap/persistence/SaltPayRegistrationPrefStorage.kt new file mode 100644 index 0000000000..0fd6a6cac6 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/persistence/SaltPayRegistrationPrefStorage.kt @@ -0,0 +1,82 @@ +package com.tangem.tap.persistence + +import android.content.Context +import androidx.core.content.edit +import at.favre.lib.armadillo.ArmadilloSharedPreferences +import com.tangem.common.json.MoshiJsonConverter +import com.tangem.common.services.secure.SecureStorage +import com.tangem.tangem_sdk_new.storage.createEncryptedSharedPreferences + +/** +[REDACTED_AUTHOR] + */ +interface SaltPayRegistrationStorage { + var data: SaltPayRegistratorData + + fun reset() + + companion object { + fun stub(): SaltPayRegistrationStorage = object : SaltPayRegistrationStorage { + override var data: SaltPayRegistratorData = SaltPayRegistratorData() + override fun reset() {} + } + } +} + +data class SaltPayRegistratorData( + val transactionsSent: Boolean = false, +) + +class SaltPayRegistrationPrefStorage( + context: Context, + private val converter: MoshiJsonConverter, +) : SaltPayRegistrationStorage { + + private val storage: ArmadilloSharedPreferences = SecureStorage.createEncryptedSharedPreferences( + context = context, + storageName = "saltpay", + ) + + private var isFetching: Boolean = false + + init { + fetch() + } + + override var data: SaltPayRegistratorData = SaltPayRegistratorData() + set(value) { + field = value + if (!isFetching) save() + } + + override fun reset() { + storage.edit(true) { + this.remove(REGISTRATION_DATA_KEY) + data = SaltPayRegistratorData() + } + } + + private fun save() { + storage.edit(true) { + putString(REGISTRATION_DATA_KEY, converter.toJson(data)) + } + } + + private fun fetch(): SaltPayRegistratorData { + isFetching = true + + val jsonData = storage.getString(REGISTRATION_DATA_KEY, null) + data = if (jsonData == null) { + SaltPayRegistratorData() + } else { + converter.fromJson(jsonData) ?: SaltPayRegistratorData() + } + isFetching = false + + return data + } + + companion object { + private const val REGISTRATION_DATA_KEY = "saltpay_registration_data" + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_connecting_socket.xml b/app/src/main/res/drawable/ic_connecting_socket.xml new file mode 100644 index 0000000000..a07fbe5515 --- /dev/null +++ b/app/src/main/res/drawable/ic_connecting_socket.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_in_progress.xml b/app/src/main/res/drawable/ic_in_progress.xml new file mode 100644 index 0000000000..17f814a06d --- /dev/null +++ b/app/src/main/res/drawable/ic_in_progress.xml @@ -0,0 +1,22 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_passport.xml b/app/src/main/res/drawable/ic_passport.xml new file mode 100644 index 0000000000..e793f05866 --- /dev/null +++ b/app/src/main/res/drawable/ic_passport.xml @@ -0,0 +1,26 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/ic_scan_card.xml b/app/src/main/res/drawable/ic_scan_card.xml index e30ad93bea..d190690243 100644 --- a/app/src/main/res/drawable/ic_scan_card.xml +++ b/app/src/main/res/drawable/ic_scan_card.xml @@ -1,21 +1,31 @@ + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + + - - - - - + android:pathData="M12,7m-1.25,0a1.25,1.25 0,1 1,2.5 0a1.25,1.25 0,1 1,-2.5 0" + android:fillColor="#000000"/> + + + + + diff --git a/app/src/main/res/drawable/ic_tangem.xml b/app/src/main/res/drawable/ic_tangem.xml index 5140d0444c..93f8741777 100644 --- a/app/src/main/res/drawable/ic_tangem.xml +++ b/app/src/main/res/drawable/ic_tangem.xml @@ -1,7 +1,15 @@ - - - - + + + + diff --git a/app/src/main/res/drawable/img_salt_pay_visa.png b/app/src/main/res/drawable/img_salt_pay_visa.png new file mode 100644 index 0000000000..19f69d5eb6 Binary files /dev/null and b/app/src/main/res/drawable/img_salt_pay_visa.png differ diff --git a/app/src/main/res/layout/fragment_disclaimer.xml b/app/src/main/res/layout/fragment_disclaimer.xml index bec0ceace0..556960be96 100644 --- a/app/src/main/res/layout/fragment_disclaimer.xml +++ b/app/src/main/res/layout/fragment_disclaimer.xml @@ -27,30 +27,21 @@ - - - - - + + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" /> + app:layout_constraintStart_toEndOf="@+id/guideline" /> diff --git a/app/src/main/res/layout/fragment_onboarding_wallet.xml b/app/src/main/res/layout/fragment_onboarding_wallet.xml index 903a3c218b..aa2c8c7494 100644 --- a/app/src/main/res/layout/fragment_onboarding_wallet.xml +++ b/app/src/main/res/layout/fragment_onboarding_wallet.xml @@ -28,16 +28,17 @@ - - - - - - - - - - - - - - - - - - - - - - - - - + android:layout_height="match_parent" + android:clipChildren="false"> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + android:layout_height="match_parent" + android:visibility="gone" /> - - - - - - + @@ -60,7 +60,6 @@ android:adjustViewBounds="true" android:contentDescription="@null" android:elevation="3dp" - android:maxHeight="215dp" android:scaleType="fitCenter" app:layout_constraintTop_toTopOf="parent" tools:src="@drawable/card_placeholder_black" /> diff --git a/app/src/main/res/layout/layout_onboarding_buttons_add_cards.xml b/app/src/main/res/layout/layout_onboarding_buttons_add_cards.xml index 1de5055e97..2d9ddedad7 100644 --- a/app/src/main/res/layout/layout_onboarding_buttons_add_cards.xml +++ b/app/src/main/res/layout/layout_onboarding_buttons_add_cards.xml @@ -1,32 +1,29 @@ + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + app:layout_constraintStart_toStartOf="@id/btn_continue" + tools:text="+ Add backup card" /> + app:layout_constraintStart_toStartOf="parent" + tools:text="Main Action" /> \ No newline at end of file diff --git a/app/src/main/res/layout/layout_onboarding_buttons_common.xml b/app/src/main/res/layout/layout_onboarding_buttons_common.xml index e99f7dfe4a..9bd99de16a 100644 --- a/app/src/main/res/layout/layout_onboarding_buttons_common.xml +++ b/app/src/main/res/layout/layout_onboarding_buttons_common.xml @@ -18,13 +18,10 @@ diff --git a/app/src/main/res/layout/layout_onboarding_saltpay.xml b/app/src/main/res/layout/layout_onboarding_saltpay.xml new file mode 100644 index 0000000000..193cf0753c --- /dev/null +++ b/app/src/main/res/layout/layout_onboarding_saltpay.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_onboarding_saltpay_connect_card.xml b/app/src/main/res/layout/layout_onboarding_saltpay_connect_card.xml new file mode 100644 index 0000000000..0c86022df3 --- /dev/null +++ b/app/src/main/res/layout/layout_onboarding_saltpay_connect_card.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_onboarding_saltpay_pin_code.xml b/app/src/main/res/layout/layout_onboarding_saltpay_pin_code.xml new file mode 100644 index 0000000000..08e2bfe29b --- /dev/null +++ b/app/src/main/res/layout/layout_onboarding_saltpay_pin_code.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_onboarding_saltpay_verify_identity.xml b/app/src/main/res/layout/layout_onboarding_saltpay_verify_identity.xml new file mode 100644 index 0000000000..f1d03e8347 --- /dev/null +++ b/app/src/main/res/layout/layout_onboarding_saltpay_verify_identity.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_onboarding_saltpay_verify_identity_in_progress.xml b/app/src/main/res/layout/layout_onboarding_saltpay_verify_identity_in_progress.xml new file mode 100644 index 0000000000..c9fb7d611b --- /dev/null +++ b/app/src/main/res/layout/layout_onboarding_saltpay_verify_identity_in_progress.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_single_wallet_balance.xml b/app/src/main/res/layout/layout_single_wallet_balance.xml index 119baed8cf..984d73bf24 100644 --- a/app/src/main/res/layout/layout_single_wallet_balance.xml +++ b/app/src/main/res/layout/layout_single_wallet_balance.xml @@ -4,15 +4,16 @@ xmlns:tools="http://schemas.android.com/tools" android:id="@+id/card_balance" android:layout_width="match_parent" - android:layout_height="wrap_content"> + android:layout_height="wrap_content" + app:cardCornerRadius="12dp"> @@ -50,8 +52,8 @@ android:layout_height="wrap_content" android:maxLines="1" android:minWidth="152dp" - android:textColor="@color/darkGray6" - android:textSize="26sp" + android:textColor="@color/text_primary_1" + android:textSize="24sp" android:textStyle="bold" tools:text="22 325.40 $" /> @@ -104,10 +106,12 @@ android:id="@+id/tv_currency_name" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:textColor="@color/darkGray1" - android:textSize="16sp" + android:gravity="center_vertical" + android:textColor="@color/text_tertiary" + android:textSize="14sp" + android:textStyle="bold" app:drawableEndCompat="@drawable/ic_arrow_angle_down" - app:drawableTint="@color/darkGray1" + app:drawableTint="@color/text_tertiary" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" tools:text="USD" /> diff --git a/app/src/main/res/layout/lp_onboarding_create_wallet.xml b/app/src/main/res/layout/lp_onboarding_create_wallet.xml index 3c5cd91655..68e87f3e61 100644 --- a/app/src/main/res/layout/lp_onboarding_create_wallet.xml +++ b/app/src/main/res/layout/lp_onboarding_create_wallet.xml @@ -1,7 +1,7 @@ diff --git a/app/src/main/res/layout/lp_onboarding_done.xml b/app/src/main/res/layout/lp_onboarding_done.xml index e6f1cb76fa..8f7be0c671 100644 --- a/app/src/main/res/layout/lp_onboarding_done.xml +++ b/app/src/main/res/layout/lp_onboarding_done.xml @@ -1,7 +1,7 @@ diff --git a/app/src/main/res/layout/lp_onboarding_done_activation.xml b/app/src/main/res/layout/lp_onboarding_done_activation.xml index 70be6417e0..02512421c5 100644 --- a/app/src/main/res/layout/lp_onboarding_done_activation.xml +++ b/app/src/main/res/layout/lp_onboarding_done_activation.xml @@ -1,7 +1,7 @@ diff --git a/app/src/main/res/layout/lp_onboarding_done_activation_twins.xml b/app/src/main/res/layout/lp_onboarding_done_activation_twins.xml index eeea439aad..c465795c75 100644 --- a/app/src/main/res/layout/lp_onboarding_done_activation_twins.xml +++ b/app/src/main/res/layout/lp_onboarding_done_activation_twins.xml @@ -1,7 +1,7 @@ diff --git a/app/src/main/res/layout/lp_onboarding_topup_wallet.xml b/app/src/main/res/layout/lp_onboarding_topup_wallet.xml index dc6efbcc96..e4e5587025 100644 --- a/app/src/main/res/layout/lp_onboarding_topup_wallet.xml +++ b/app/src/main/res/layout/lp_onboarding_topup_wallet.xml @@ -1,7 +1,7 @@ diff --git a/app/src/main/res/layout/lp_onboarding_topup_wallet_twins.xml b/app/src/main/res/layout/lp_onboarding_topup_wallet_twins.xml index a7ab2ca9fd..66351f4062 100644 --- a/app/src/main/res/layout/lp_onboarding_topup_wallet_twins.xml +++ b/app/src/main/res/layout/lp_onboarding_topup_wallet_twins.xml @@ -1,7 +1,7 @@ diff --git a/app/src/main/res/values-de/strings_final.xml b/app/src/main/res/values-de/strings_final.xml index 086542b47f..6c801e756c 100644 --- a/app/src/main/res/values-de/strings_final.xml +++ b/app/src/main/res/values-de/strings_final.xml @@ -48,6 +48,10 @@ Notice Attention The server is not available, please try again later + Total balance + The amount does not include some of your funds + Manage tokens + No rate Hide token Hide %s Hide @@ -57,6 +61,15 @@ %s network not found. Please, add it first and try again. This network is not supported. Please select another network. %s network + Your wallet has not been backed up + To protect your assets, we advise you to carry out this procedure + Remove token + Russian bank cards are not accepted at the moment + Do you have a bank card of another country or a UnionPay card? + Yes + No + Chat + Tangem Bot Card Settings Security Mode Selected application protection method @@ -94,4 +107,38 @@ Invalid Memo. It won\'t be added to the transaction. Invalid Tag. It won\'t be added to the transaction. Connection with this Dapp cannot be established due to its technical implementation. + To start working with the card scan the first card and make a backup + Attention + Tap the card with the visa logo + No funds for activation + Please contact support + Four identical digits isn\'t safe + Such a PIN can be brute-forced easily + Pin code + Connect + KYC + Verify your identity + In progress + Set PIN code + Register + Verify + Refresh + Connect your card + Verify your identity + KYC is in progress + Connect your card to the decentralized payment system + To start using your card you have to pass the KYC process + Please wait until the verification is completed. Usually it takes up to 1 hour. You can close the app and come back later. + PIN Code + Set PIN code for your SaltPay card + Chat with support + This card is not designed to SaltPay backup + Please hold the card until the operation complete + To start the backup process you have to add Tangem card as your backup + No backup card + Backup card ready + Finalize the backup process by creating an access code + Prepare the SaltPay card + Tap the SaltPay card + Tap the Tangem card \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings_final.xml b/app/src/main/res/values-fr/strings_final.xml index 81184eacbd..6c801e756c 100644 --- a/app/src/main/res/values-fr/strings_final.xml +++ b/app/src/main/res/values-fr/strings_final.xml @@ -45,9 +45,13 @@ Contract address copied! If you chose the wrong network during your crypto transfer from the exchange, this guide will help you recover your funds Custom - The server is not available, please try again later Notice Attention + The server is not available, please try again later + Total balance + The amount does not include some of your funds + Manage tokens + No rate Hide token Hide %s Hide @@ -57,6 +61,15 @@ %s network not found. Please, add it first and try again. This network is not supported. Please select another network. %s network + Your wallet has not been backed up + To protect your assets, we advise you to carry out this procedure + Remove token + Russian bank cards are not accepted at the moment + Do you have a bank card of another country or a UnionPay card? + Yes + No + Chat + Tangem Bot Card Settings Security Mode Selected application protection method @@ -94,4 +107,38 @@ Invalid Memo. It won\'t be added to the transaction. Invalid Tag. It won\'t be added to the transaction. Connection with this Dapp cannot be established due to its technical implementation. + To start working with the card scan the first card and make a backup + Attention + Tap the card with the visa logo + No funds for activation + Please contact support + Four identical digits isn\'t safe + Such a PIN can be brute-forced easily + Pin code + Connect + KYC + Verify your identity + In progress + Set PIN code + Register + Verify + Refresh + Connect your card + Verify your identity + KYC is in progress + Connect your card to the decentralized payment system + To start using your card you have to pass the KYC process + Please wait until the verification is completed. Usually it takes up to 1 hour. You can close the app and come back later. + PIN Code + Set PIN code for your SaltPay card + Chat with support + This card is not designed to SaltPay backup + Please hold the card until the operation complete + To start the backup process you have to add Tangem card as your backup + No backup card + Backup card ready + Finalize the backup process by creating an access code + Prepare the SaltPay card + Tap the SaltPay card + Tap the Tangem card \ No newline at end of file diff --git a/app/src/main/res/values-it/strings_final.xml b/app/src/main/res/values-it/strings_final.xml index 81184eacbd..6c801e756c 100644 --- a/app/src/main/res/values-it/strings_final.xml +++ b/app/src/main/res/values-it/strings_final.xml @@ -45,9 +45,13 @@ Contract address copied! If you chose the wrong network during your crypto transfer from the exchange, this guide will help you recover your funds Custom - The server is not available, please try again later Notice Attention + The server is not available, please try again later + Total balance + The amount does not include some of your funds + Manage tokens + No rate Hide token Hide %s Hide @@ -57,6 +61,15 @@ %s network not found. Please, add it first and try again. This network is not supported. Please select another network. %s network + Your wallet has not been backed up + To protect your assets, we advise you to carry out this procedure + Remove token + Russian bank cards are not accepted at the moment + Do you have a bank card of another country or a UnionPay card? + Yes + No + Chat + Tangem Bot Card Settings Security Mode Selected application protection method @@ -94,4 +107,38 @@ Invalid Memo. It won\'t be added to the transaction. Invalid Tag. It won\'t be added to the transaction. Connection with this Dapp cannot be established due to its technical implementation. + To start working with the card scan the first card and make a backup + Attention + Tap the card with the visa logo + No funds for activation + Please contact support + Four identical digits isn\'t safe + Such a PIN can be brute-forced easily + Pin code + Connect + KYC + Verify your identity + In progress + Set PIN code + Register + Verify + Refresh + Connect your card + Verify your identity + KYC is in progress + Connect your card to the decentralized payment system + To start using your card you have to pass the KYC process + Please wait until the verification is completed. Usually it takes up to 1 hour. You can close the app and come back later. + PIN Code + Set PIN code for your SaltPay card + Chat with support + This card is not designed to SaltPay backup + Please hold the card until the operation complete + To start the backup process you have to add Tangem card as your backup + No backup card + Backup card ready + Finalize the backup process by creating an access code + Prepare the SaltPay card + Tap the SaltPay card + Tap the Tangem card \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 38ee22a3ca..41ae2c46a7 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -242,7 +242,7 @@ Баланс Отправляйте только %s (%s) на этот адрес. Иначе это может привести к утрате средств. Отправляйте только %s (%s) из сети %s на этот адрес. Иначе это может привести к утрате средств. - Если процесс повторного создания кошелька каким-либо образом прервется, вам придется начать все сначала. + Если процесс создания кошелька каким-либо образом прервется, вам придется начать все сначала. Процесс связывания карт частично завершен. Вы не можете выйти из него сейчас. Внутренняя ошибка: не удается создать менеджер кошельков Блокчейн недоступен. Попробуй позже. @@ -295,7 +295,7 @@ Все резервные карты могут использоваться как полнофункциональные с одинаковыми ключами Вы сможете установить код доступа для защиты своих кошельков Код доступа можно восстановить с помощью одной из резервных карт - Ваша карта Tangem Wallet настроена и готова к использованию + Ваша карта настроена и готова к использованию. Вы создали 1 резервную карту, и теперь эта карта готова к использованию Основная карта Я понимаю diff --git a/app/src/main/res/values-ru/strings_final.xml b/app/src/main/res/values-ru/strings_final.xml index 81edc55d19..add278e752 100644 --- a/app/src/main/res/values-ru/strings_final.xml +++ b/app/src/main/res/values-ru/strings_final.xml @@ -105,4 +105,38 @@ Недопустимый Tag. Он не будет добавлен в транзакцию. Не удалось установить сессию WalletConnect Cоединение с этим Dapp сервисом не может быть установлено из-за его технической реализации. + Для начала работы с картой сначала отсканируйте первую карту и сделайте бэкап + Внимание + Приложите карту с логотипом Visa + Недостаточно средств для активации + Пожалуйста обратитесь в службу поддержки + Ввод одинаковых цифр является не безопасным + Данный Код доступа может быть легко взломан + Код доступа + Подключиться + Верификация клиента + Подтвердите свою личность + В обработке + Установить Код доступа + Зарегистрироваться + Подтвердить + Обновить + Подключите свою карту + Подтвердите свою личность + Подтверждение личности в процессе + Подключите вашу карту к децентрализованной платежной системе + Для начала работы с картой вам необходимо завершить процесс подтверждения личности + Пожалуйста дождитесь завершения процесса подтверждения вашей личности. Как правило, это занимает меньше часа. Вы можете закрыть приложение и вернуться позже. + Код доступа + Установите Код доступа для вашей SaltPay карты + Чат поддержки + Данная карта не подходит для бэкапа SaltPay + Пожалуйста, удерживайте карту до завершения операции + Для начала процесса бэкапа вам необходимо добавить Tangem карту + Бэкап карта не добавлена + Бэкап карта создана + Завершите процесс бэкапа создав код доступа + Приготовьте SaltPay карту + Приложите SaltPay карту + Приложите Tangem карту diff --git a/app/src/main/res/values/strings_final.xml b/app/src/main/res/values/strings_final.xml index d7853e1ac9..5a47c7ed20 100644 --- a/app/src/main/res/values/strings_final.xml +++ b/app/src/main/res/values/strings_final.xml @@ -107,4 +107,38 @@ Invalid Memo. It won\'t be added to the transaction. Invalid Tag. It won\'t be added to the transaction. Connection with this Dapp cannot be established due to its technical implementation. + To start working with the card scan the first card and make a backup + Attention + Tap the card with the visa logo + No funds for activation + Please contact support + Four identical digits isn\'t safe + Such a PIN can be brute-forced easily + Pin code + Connect + KYC + Verify your identity + In progress + Set PIN code + Register + Verify + Refresh + Connect your card + Verify your identity + KYC is in progress + Connect your card to the decentralized payment system + To start using your card you have to pass the KYC process + Please wait until the verification is completed. Usually it takes up to 1 hour. You can close the app and come back later. + PIN Code + Set PIN code for your SaltPay card + Chat with support + This card is not designed to SaltPay backup + Please hold the card until the operation complete + To start the backup process you have to add Tangem card as your backup + No backup card + Backup card ready + Finalize the backup process by creating an access code + Prepare the SaltPay card + Tap the SaltPay card + Tap the Tangem card diff --git a/app/src/main/res/values/strings_untranslated.xml b/app/src/main/res/values/strings_untranslated.xml index 8d6c5017af..153f80c666 100644 --- a/app/src/main/res/values/strings_untranslated.xml +++ b/app/src/main/res/values/strings_untranslated.xml @@ -199,7 +199,7 @@ Send only %s (%s) to this address. Using other tokens and networks may result in loss of funds. Send only %s (%s) from %s network to this address. Using other tokens and networks may result in loss of funds. - If the process of re-creating the wallet gets interrupted in any way, you\'ll have to start over. + If the process of creating the wallet gets interrupted in any way, you\'ll have to start over. The twinning process is partly complete. You can\'t exit it now. Internal error: can\'t create wallet manager @@ -265,7 +265,7 @@ You will be able to set an access code to protect your wallets. Access code can be restored with one of backup cards. - Your Tangem Wallet card is configured and ready for use. + Your wallet card is configured and ready for use. You have created 1 backup card, and now this card are ready for use. Primary Card I understand diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 09cdff0b35..dad17e5962 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -70,6 +70,20 @@ 2dp + + + + + +