diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index ba5af7a4ce..c914631447 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -84,9 +84,9 @@ - - - + + + \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index f1fa3b08ef..70b6edcebd 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -122,12 +122,12 @@ dependencies { // Firebase implementation platform('com.google.firebase:firebase-bom:26.0.0') - implementation 'com.google.firebase:firebase-crashlytics' - implementation 'com.google.firebase:firebase-config-ktx' implementation 'com.google.firebase:firebase-analytics-ktx' + implementation 'com.google.firebase:firebase-crashlytics-ktx' - // AppsFlyer + // Analytics implementation 'com.appsflyer:af-android-sdk:6.5.1' + implementation 'com.amplitude:android-sdk:2.36.1' // WalletConnect implementation 'com.github.salomonbrys.kotson:kotson:2.5.0' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4422e037f9..874783a15d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -15,6 +15,7 @@ + diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index c8f577f2ab..cf6ea50867 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit c8f577f2ab48e0d6c49f79b6cff21eabe67cd63e +Subproject commit cf6ea50867477f97655da9da47ff94987e8f3354 diff --git a/app/src/main/assets/tos.html b/app/src/main/assets/tos.html deleted file mode 100644 index 67c6eba9b4..0000000000 --- a/app/src/main/assets/tos.html +++ /dev/null @@ -1,1832 +0,0 @@ - - - - - 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 b75cb31efc..a94cf067f5 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -18,7 +18,6 @@ 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 @@ -61,7 +60,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - internalActivity = this setContentView(R.layout.activity_main) systemActions() store.dispatch(NavigationAction.ActivityCreated(WeakReference(this))) @@ -176,10 +174,4 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac 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 8fa11bf17a..c0d6e566b4 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -5,16 +5,16 @@ import android.content.Context import android.content.pm.PackageManager import coil.ImageLoader import coil.ImageLoaderFactory -import com.appsflyer.AppsFlyerLib import com.tangem.Log import com.tangem.LogFormat import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder +import com.tangem.common.json.MoshiJsonConverter import com.tangem.domain.DomainLayer import com.tangem.domain.common.LogConfig import com.tangem.network.common.MoshiConverter import com.tangem.tap.common.AndroidAssetReader import com.tangem.tap.common.AssetReader -import com.tangem.tap.common.analytics.GlobalAnalyticsHandler +import com.tangem.tap.common.analytics.GlobalAnalyticsEventHandlerBuilder import com.tangem.tap.common.feedback.AdditionalFeedbackInfo import com.tangem.tap.common.feedback.FeedbackManager import com.tangem.tap.common.images.createCoilImageLoader @@ -97,15 +97,19 @@ class TapApplication : Application(), ImageLoaderFactory { private fun initWithConfigDependency(config: Config) { shopService = TangemShopService(this, config.shopify!!) - initAppsFlyer(this, config) + initAnalytics(this, config) initFeedbackManager(this, preferencesStorage, config) } - private fun initAppsFlyer(context: Context, config: Config) { - AppsFlyerLib.getInstance().init(config.appsFlyerDevKey, null, context) - AppsFlyerLib.getInstance().start(context) - val analyticsHandler = GlobalAnalyticsHandler.createDefaultAnalyticHandlers(context) - store.dispatch(GlobalAction.SetAnanlyticHandlers(analyticsHandler)) + private fun initAnalytics(application: Application, config: Config) { + val globalAnalyticsHandler = GlobalAnalyticsEventHandlerBuilder( + application = application, + config = config, + isDebug = BuildConfig.DEBUG, + logConfig = LogConfig.analyticsHandlers, + jsonConverter = MoshiJsonConverter.INSTANCE, + ).default() + store.dispatch(GlobalAction.SetGlobalAnalyticsHandler(globalAnalyticsHandler)) } private fun initFeedbackManager(context: Context, preferencesStorage: PreferencesStorage, config: Config) { 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 d3fe7eacd2..5ae8d9ce04 100644 --- a/app/src/main/java/com/tangem/tap/common/DialogManager.kt +++ b/app/src/main/java/com/tangem/tap/common/DialogManager.kt @@ -20,6 +20,7 @@ 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.saltPay.dialog.TryToInterruptRegistrationDialog 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 @@ -54,7 +55,6 @@ class DialogManager : StoreSubscriber { store.unsubscribe(this) } - override fun newState(state: GlobalState) { if (state.dialog == null) { dialog?.dismiss() @@ -67,6 +67,8 @@ class DialogManager : StoreSubscriber { dialog = when (state.dialog) { is AppDialog.SimpleOkDialog -> SimpleOkDialog.create(state.dialog, context) is AppDialog.SimpleOkDialogRes -> SimpleOkDialog.create(state.dialog, context) + is AppDialog.SimpleOkErrorDialog -> SimpleOkDialog.create(state.dialog, context) + is AppDialog.SimpleOkWarningDialog -> SimpleOkDialog.create(state.dialog, context) is AppDialog.ScanFailsDialog -> ScanFailsDialog.create(context) is AppDialog.AddressInfoDialog -> AddressInfoBottomSheetDialog(state.dialog, context) is AppDialog.TestActionsDialog -> TestActionsBottomSheetDialog(state.dialog, context) @@ -76,16 +78,16 @@ class DialogManager : StoreSubscriber { SimpleAlertDialog.create( titleRes = R.string.wallet_connect, messageRes = R.string.wallet_connect_scanner_error_no_ethereum_wallet, - context = context + context = context, ) is WalletConnectDialog.AddNetwork -> SimpleAlertDialog.create( titleRes = R.string.wallet_connect, message = context.getString( R.string.wallet_connect_network_not_found_format, - state.dialog.network + state.dialog.network, ), - context = context + context = context, ) is WalletConnectDialog.OpeningSessionRejected -> { SimpleAlertDialog.create( @@ -123,26 +125,23 @@ class DialogManager : StoreSubscriber { SimpleAlertDialog.create( titleRes = R.string.wallet_connect, messageRes = R.string.wallet_connect_scanner_error_unsupported_network, - context = context + context = context, ) is BackupDialog.AddMoreBackupCards -> AddMoreBackupCardsDialog.create(context) 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 -> - ChooseTradeActionBottomSheetDialog(context) - is WalletDialog.SelectAmountToSendDialog -> - AmountToSendBottomSheetDialog(context, state.dialog) - is WalletDialog.SignedHashesMultiWalletDialog -> - SignedHashesWarningDialog.create(context) + is SaltPayDialog.Activation.NoGas -> NoFundsForActivationDialog.create(context) + is SaltPayDialog.Activation.TryToInterrupt -> TryToInterruptRegistrationDialog.create(context, state.dialog) + is SaltPayDialog.Activation.OnError -> RegistrationErrorDialog.create(context, state.dialog) + is WalletDialog.CurrencySelectionDialog -> CurrencySelectionDialog.create(state.dialog, context) + is WalletDialog.ChooseTradeActionDialog -> ChooseTradeActionBottomSheetDialog(context) + is WalletDialog.SelectAmountToSendDialog -> AmountToSendBottomSheetDialog(context, state.dialog) + is WalletDialog.SignedHashesMultiWalletDialog -> SignedHashesWarningDialog.create(context) is WalletDialog.TokensAreLinkedDialog -> SimpleAlertDialog.create( title = context.getString(state.dialog.titleRes, state.dialog.currencySymbol), message = context.getString( - state.dialog.messageRes, state.dialog.currencySymbol, state.dialog.currencyTitle + state.dialog.messageRes, state.dialog.currencySymbol, state.dialog.currencyTitle, ), context = context, ) @@ -151,7 +150,7 @@ class DialogManager : StoreSubscriber { messageRes = state.dialog.messageRes, context = context, primaryButtonRes = state.dialog.primaryButtonRes, - primaryButtonAction = state.dialog.onOk + primaryButtonAction = state.dialog.onOk, ) is WalletDialog.RussianCardholdersWarningDialog -> RussianCardholdersWarningBottomSheetDialog(context) diff --git a/app/src/main/java/com/tangem/tap/common/TestActions.kt b/app/src/main/java/com/tangem/tap/common/TestActions.kt index d5939dae64..5eb1021886 100644 --- a/app/src/main/java/com/tangem/tap/common/TestActions.kt +++ b/app/src/main/java/com/tangem/tap/common/TestActions.kt @@ -27,8 +27,8 @@ object TestActions { * @param isTestView - true must be used if you want to show or hide your view depends on BuildConfig */ fun initFor(view: View, actions: List, isTestView: Boolean = false) { - if (isTestView) view.show(BuildConfig.TEST_ACTION_ENABLED) if (!BuildConfig.TEST_ACTION_ENABLED) return + if (isTestView) view.show(BuildConfig.TEST_ACTION_ENABLED) view.setOnClickListener { store.dispatchDialogShow(AppDialog.TestActionsDialog(actions)) diff --git a/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsEvent.kt b/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsEventAnOld.kt similarity index 92% rename from app/src/main/java/com/tangem/tap/common/analytics/AnalyticsEvent.kt rename to app/src/main/java/com/tangem/tap/common/analytics/AnalyticsEventAnOld.kt index 90ff7d7f3b..41cc0d9b96 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsEvent.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsEventAnOld.kt @@ -1,6 +1,6 @@ package com.tangem.tap.common.analytics -enum class AnalyticsEvent(val event: String) { +enum class AnalyticsEventAnOld(val event: String) { CARD_IS_SCANNED("card_is_scanned"), TRANSACTION_IS_SENT("transaction_is_sent"), READY_TO_SCAN("ready_to_scan"), @@ -19,7 +19,7 @@ enum class AnalyticsEvent(val event: String) { GET_CARD("get_card"), } -enum class AnalyticsParam(val param: String) { +enum class AnalyticsParamAnOld(val param: String) { BLOCKCHAIN("blockchain"), CARD_ID("cardId"), BATCH_ID("batch_id"), @@ -37,13 +37,13 @@ enum class AnalyticsParam(val param: String) { SOURCE("source"), } -enum class GetCardSourceParams(val param: String) { +enum class GetCardSourceParamsAnOld(val param: String) { WELCOME("welcome"), ONBOARDING_BUY_MORE("wallet_onboaring_buy_more_cards"), ONBOARDING("wallet_onboarding") } -object Analytics { +object AnalyticsAnOld { enum class ActionToLog(val key: String) { Scan("tap_scan_task"), SendTransaction("send_transaction"), diff --git a/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsEventsLogger.kt b/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsEventsLogger.kt new file mode 100644 index 0000000000..0865d2aa81 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsEventsLogger.kt @@ -0,0 +1,26 @@ +package com.tangem.tap.common.analytics + +import com.tangem.common.json.MoshiJsonConverter +import com.tangem.tap.common.analytics.api.ErrorEventLogger +import com.tangem.tap.common.analytics.api.EventLogger +import timber.log.Timber + +class AnalyticsEventsLogger( + private val name: String, + private val jsonConverter: MoshiJsonConverter, +) : EventLogger, ErrorEventLogger { + + override fun logEvent(event: String, params: Map) { + Timber.d(jsonConverter.prettyPrint(PrintEventModel(name, event, params))) + } + + override fun logErrorEvent(error: Throwable, params: Map) { + Timber.e(error, jsonConverter.prettyPrint(PrintEventModel(name, "error", params))) + } +} + +private data class PrintEventModel( + val client: String, + val event: String, + val params: Map, +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsHandler.kt deleted file mode 100644 index 424f06d9ce..0000000000 --- a/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsHandler.kt +++ /dev/null @@ -1,89 +0,0 @@ -package com.tangem.tap.common.analytics - -import com.shopify.buy3.Storefront -import com.tangem.common.card.Card -import com.tangem.common.core.TangemSdkError -import com.tangem.tap.common.extensions.filterNotNull - - -abstract class AnalyticsHandler { - abstract fun triggerEvent( - event: AnalyticsEvent, - card: Card? = null, - blockchain: String? = null, - params: Map = emptyMap() - ) - - abstract fun triggerEvent( - event: String, - params: Map - ) - - abstract fun logCardSdkError( - error: TangemSdkError, - actionToLog: Analytics.ActionToLog, - parameters: Map? = null, - card: Card? = null - ) - - abstract fun logError( - error: Throwable, - params: Map = emptyMap() - ) - - protected fun prepareParams(card: Card?, blockchain: String? = null): Map { - return mapOf( - AnalyticsParam.FIRMWARE.param to card?.firmwareVersion?.stringValue, - AnalyticsParam.BATCH_ID.param to card?.batchId, - AnalyticsParam.BLOCKCHAIN.param to blockchain, - ).filterNotNull() - } - - fun logWcEvent(event: Analytics.WcAnalyticsEvent) { - when (event) { - is Analytics.WcAnalyticsEvent.Action -> { - triggerEvent( - event = AnalyticsEvent.WC_SUCCESS_RESPONSE, - params = mapOf( - AnalyticsParam.WALLET_CONNECT_ACTION.param to event.action.name - ) - ) - } - is Analytics.WcAnalyticsEvent.Error -> { - val params = mapOf( - AnalyticsParam.WALLET_CONNECT_ACTION.param to event.action?.name, - AnalyticsParam.ERROR_DESCRIPTION.param to event.error.message - ) - .filterNotNull() - logError(event.error, params) - } - is Analytics.WcAnalyticsEvent.InvalidRequest -> - triggerEvent( - event = AnalyticsEvent.WC_INVALID_REQUEST, - params = mapOf( - AnalyticsParam.WALLET_CONNECT_REQUEST.param to event.json - ).filterNotNull() - ) - is Analytics.WcAnalyticsEvent.Session -> { - val analyticsEvent = when (event.event) { - Analytics.WcSessionEvent.Disconnect -> AnalyticsEvent.WC_SESSION_DISCONNECTED - Analytics.WcSessionEvent.Connect -> AnalyticsEvent.WC_NEW_SESSION - } - triggerEvent( - event = analyticsEvent, - params = mapOf( - AnalyticsParam.WALLET_CONNECT_DAPP_URL.param to event.url - ).filterNotNull() - ) - } - } - } - - fun logShopifyOrder(order: Storefront.Order) { - triggerEvent(getOrderEvent(), getOrderParams(order)) - } - - protected abstract fun getOrderEvent(): String - - protected abstract fun getOrderParams(order: Storefront.Order): Map -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsInterfaces.kt b/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsInterfaces.kt new file mode 100644 index 0000000000..6fd6fc7dc8 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsInterfaces.kt @@ -0,0 +1,3 @@ +package com.tangem.tap.common.analytics + + diff --git a/app/src/main/java/com/tangem/tap/common/analytics/AppsFlyerAnalyticsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/AppsFlyerAnalyticsHandler.kt deleted file mode 100644 index 16fa1644ab..0000000000 --- a/app/src/main/java/com/tangem/tap/common/analytics/AppsFlyerAnalyticsHandler.kt +++ /dev/null @@ -1,61 +0,0 @@ -package com.tangem.tap.common.analytics - -import android.content.Context -import com.appsflyer.AFInAppEventParameterName -import com.appsflyer.AFInAppEventType -import com.appsflyer.AppsFlyerLib -import com.shopify.buy3.Storefront -import com.tangem.common.card.Card -import com.tangem.common.core.TangemSdkError - -class AppsFlyerAnalyticsHandler(val context: Context): AnalyticsHandler() { - - override fun triggerEvent( - event: AnalyticsEvent, - card: Card?, - blockchain: String?, - params: Map - ) { - AppsFlyerLib.getInstance().logEvent(context, event.event, prepareParams(card, blockchain)) - } - - override fun logCardSdkError( - error: TangemSdkError, - actionToLog: Analytics.ActionToLog, - parameters: Map?, - card: Card? - ) { - //TODO - } - - override fun logError(error: Throwable, params: Map) { - //TODO - } - - override fun getOrderEvent(): String = AFInAppEventType.PURCHASE - - override fun getOrderParams(order: Storefront.Order): Map { - - val sku = order.lineItems.edges.firstOrNull()?.node?.variant?.sku ?: "unknown" - - val discountCode = - (order.discountApplications.edges.firstOrNull()?.node as? Storefront.DiscountCodeApplication)?.code - - val discountParams = if (discountCode != null ){ - mapOf(AFInAppEventParameterName.COUPON_CODE to discountCode) - } else { - mapOf() - } - - return mapOf( - AFInAppEventParameterName.CONTENT_ID to sku, - AFInAppEventParameterName.REVENUE to order.totalPriceV2.amount, - AFInAppEventParameterName.CURRENCY to order.currencyCode.name - ) + discountParams - } - - override fun triggerEvent(event: String, params: Map) { - AppsFlyerLib.getInstance().logEvent(context, event, params) - } - -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/FirebaseAnalyticsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/FirebaseAnalyticsHandler.kt deleted file mode 100644 index 4e0371206a..0000000000 --- a/app/src/main/java/com/tangem/tap/common/analytics/FirebaseAnalyticsHandler.kt +++ /dev/null @@ -1,88 +0,0 @@ -package com.tangem.tap.common.analytics - -import android.os.Bundle -import androidx.core.os.bundleOf -import com.google.firebase.analytics.FirebaseAnalytics -import com.google.firebase.analytics.ktx.analytics -import com.google.firebase.crashlytics.FirebaseCrashlytics -import com.google.firebase.ktx.Firebase -import com.shopify.buy3.Storefront -import com.tangem.common.card.Card -import com.tangem.common.core.TangemSdkError - -object FirebaseAnalyticsHandler : AnalyticsHandler() { - override fun triggerEvent( - event: AnalyticsEvent, - card: Card?, - blockchain: String?, - params: Map - ) { - val fullParams = prepareParams(card, blockchain) + params - Firebase.analytics.logEvent(event.event, fullParams.toBundle()) - } - - fun logException(name: String, throwable: Throwable) { - Firebase.analytics.logEvent(name, bundleOf( - "message" to (throwable.message ?: "none"), - "cause_message" to (throwable.cause?.message ?: "none"), - "stack_trace" to throwable.stackTraceToString() - )) - } - - private fun Map.toBundle(): Bundle { - return bundleOf(*this.toList().toTypedArray()) - } - - override fun logCardSdkError( - error: TangemSdkError, - actionToLog: Analytics.ActionToLog, - parameters: Map?, - card: Card?, - ) { - if (error is TangemSdkError.UserCancelled) return - - val params = parameters?.toMutableMap() ?: mutableMapOf() - if (card != null) params + prepareParams(card) - params[AnalyticsParam.ACTION] = actionToLog.key - params[AnalyticsParam.ERROR_CODE] = error.code.toString() - params[AnalyticsParam.ERROR_DESCRIPTION] = error.javaClass.simpleName - params[AnalyticsParam.ERROR_KEY] = "TangemSdkError" - - params.forEach { - FirebaseCrashlytics.getInstance().setCustomKey(it.key.param, it.value) - } - val cardError = TangemSdk.map(error) - FirebaseCrashlytics.getInstance().recordException(cardError) - } - - override fun logError(error: Throwable, params: Map) { - params.forEach { (key, value) -> FirebaseCrashlytics.getInstance().setCustomKey(key, value) } - FirebaseCrashlytics.getInstance().recordException(error) - } - - override fun getOrderEvent(): String = FirebaseAnalytics.Event.PURCHASE - - override fun getOrderParams(order: Storefront.Order): Map { - - val sku = order.lineItems.edges.firstOrNull()?.node?.variant?.sku ?: "unknown" - - val discountCode = - (order.discountApplications.edges.firstOrNull()?.node as? Storefront.DiscountCodeApplication)?.code - - val discountParams = if (discountCode != null ){ - mapOf(FirebaseAnalytics.Param.DISCOUNT to discountCode) - } else { - mapOf() - } - - return mapOf( - FirebaseAnalytics.Param.ITEM_ID to sku, - FirebaseAnalytics.Param.VALUE to order.totalPriceV2.amount, - FirebaseAnalytics.Param.CURRENCY to order.currencyCode.name - ) + discountParams - } - - override fun triggerEvent(event: String, params: Map) { - Firebase.analytics.logEvent(event, params.toBundle()) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/GlobalAnalyticsEventHandlerBuilder.kt b/app/src/main/java/com/tangem/tap/common/analytics/GlobalAnalyticsEventHandlerBuilder.kt new file mode 100644 index 0000000000..8e2b439e08 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/GlobalAnalyticsEventHandlerBuilder.kt @@ -0,0 +1,79 @@ +package com.tangem.tap.common.analytics + +import android.app.Application +import com.tangem.common.json.MoshiJsonConverter +import com.tangem.domain.common.AnalyticsHandlersLogConfig +import com.tangem.tap.common.analytics.api.AnalyticsEventHandler +import com.tangem.tap.common.analytics.handlers.amplitude.AmplitudeAnalyticsHandler +import com.tangem.tap.common.analytics.handlers.amplitude.AmplitudeClient +import com.tangem.tap.common.analytics.handlers.amplitude.AmplitudeLogClient +import com.tangem.tap.common.analytics.handlers.appsFlyer.AppsFlyerAnalyticsHandler +import com.tangem.tap.common.analytics.handlers.appsFlyer.AppsFlyerClient +import com.tangem.tap.common.analytics.handlers.appsFlyer.AppsFlyerLogClient +import com.tangem.tap.common.analytics.handlers.firebase.FirebaseAnalyticsHandler +import com.tangem.tap.common.analytics.handlers.firebase.FirebaseClient +import com.tangem.tap.common.analytics.handlers.firebase.FirebaseLogClient +import com.tangem.tap.domain.configurable.config.Config + +/** +[REDACTED_AUTHOR] + */ +class GlobalAnalyticsEventHandlerBuilder( + val application: Application, + val config: Config, + val isDebug: Boolean, + val logConfig: AnalyticsHandlersLogConfig, + val jsonConverter: MoshiJsonConverter, +) { + + private val analyticHandlers = mutableListOf() + + fun build(): GlobalAnalyticsEventHandler { + return GlobalAnalyticsHandler(analyticHandlers.toList()) + } + + fun default(): GlobalAnalyticsEventHandler { + addFirebaseAnalytics() + addAppsFlyer() + addAmplitude() + + return GlobalAnalyticsHandler(analyticHandlers.toList()) + } + + fun addFirebaseAnalytics(): GlobalAnalyticsEventHandlerBuilder { + if (isDebug) { + if (logConfig.firebase) FirebaseLogClient(createLogger("FirebaseAnalyticsHandler")) else null + } else { + FirebaseClient() + }?.let { + analyticHandlers.add(FirebaseAnalyticsHandler(it)) + } + return this + } + + fun addAppsFlyer(): GlobalAnalyticsEventHandlerBuilder { + if (isDebug) { + if (logConfig.appsFlyer) AppsFlyerLogClient(createLogger("AppsFlyerAnalyticsHandler")) else null + } else { + AppsFlyerClient(application, config.appsFlyerDevKey) + }?.let { + analyticHandlers.add(AppsFlyerAnalyticsHandler(it)) + } + return this + } + + fun addAmplitude(): GlobalAnalyticsEventHandlerBuilder { + if (isDebug) { + if (logConfig.firebase) AmplitudeLogClient(createLogger("AmplitudeAnalyticsHandler")) else null + } else { + AmplitudeClient(application, config.amplitudeApiKey) + }?.let { + analyticHandlers.add(AmplitudeAnalyticsHandler(it)) + } + return this + } + + private fun createLogger(name: String): AnalyticsEventsLogger { + return AnalyticsEventsLogger(name, jsonConverter) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/GlobalAnalyticsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/GlobalAnalyticsHandler.kt index 197f396621..d1bef7d01b 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/GlobalAnalyticsHandler.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/GlobalAnalyticsHandler.kt @@ -1,53 +1,160 @@ package com.tangem.tap.common.analytics -import android.content.Context import com.shopify.buy3.Storefront +import com.tangem.blockchain.common.BlockchainError import com.tangem.common.card.Card import com.tangem.common.core.TangemSdkError +import com.tangem.tap.common.analytics.api.AnalyticsEventHandler +import com.tangem.tap.common.analytics.api.BlockchainSdkErrorEventHandler +import com.tangem.tap.common.analytics.api.CardSdkErrorEventHandler +import com.tangem.tap.common.analytics.api.ErrorEventHandler +import com.tangem.tap.common.analytics.api.ErrorEventLogger +import com.tangem.tap.common.analytics.api.SdkErrorEventHandler +import com.tangem.tap.common.analytics.api.ShopifyOrderEventHandler +import com.tangem.tap.common.analytics.events.AnalyticsEvent +import com.tangem.tap.common.extensions.filterNotNull -class GlobalAnalyticsHandler(val analyticsHandlers: List) : - AnalyticsHandler() { - override fun triggerEvent( +interface GlobalAnalyticsEventHandler : AnalyticsEventHandler, + ErrorEventHandler, + SdkErrorEventHandler, + ShopifyOrderEventHandler { + + fun attachToAllEvents(key: String, value: String) + + companion object { + fun stub(): GlobalAnalyticsEventHandler { + return object : GlobalAnalyticsEventHandler { + override fun attachToAllEvents(key: String, value: String) {} + override fun handleEvent(event: String, params: Map) {} + override fun handleErrorEvent(error: Throwable, params: Map) {} + override fun handleCardSdkErrorEvent( + error: TangemSdkError, + action: AnalyticsAnOld.ActionToLog, + params: Map, + card: Card?, + ) { + } + + override fun handleBlockchainSdkErrorEvent( + error: BlockchainError, + action: AnalyticsAnOld.ActionToLog, + params: Map, + card: Card?, + ) { + } + + override fun handleShopifyOrderEvent(order: Storefront.Order) {} + } + } + } +} + +class GlobalAnalyticsHandler( + private val analyticsHandlers: List, +) : GlobalAnalyticsEventHandler { + + private val attachToAllEventsParams: MutableMap = mutableMapOf() + + override fun attachToAllEvents(key: String, value: String) { + attachToAllEventsParams[key] = value + } + + override fun handleEvent(event: String, params: Map) { + analyticsHandlers.forEach { it.handleEvent(event, params) } + } + + override fun handleAnalyticsEvent( event: AnalyticsEvent, card: Card?, blockchain: String?, - params: Map ) { - analyticsHandlers.forEach { it.triggerEvent(event, card, blockchain, params) } + analyticsHandlers.forEach { it.handleAnalyticsEvent(event, card, blockchain) } } - override fun triggerEvent(event: String, params: Map) { - analyticsHandlers.forEach { it.triggerEvent(event, params) } + override fun handleAnalyticsEvent( + event: AnalyticsEventAnOld, + params: Map, + card: Card?, + blockchain: String?, + ) { + analyticsHandlers.forEach { it.handleAnalyticsEvent(event, params, card, blockchain) } } - override fun logCardSdkError( + override fun handleErrorEvent(error: Throwable, params: Map) { + analyticsHandlers.filterIsInstance().forEach { + it.logErrorEvent(error, params) + } + } + + override fun handleCardSdkErrorEvent( error: TangemSdkError, - actionToLog: Analytics.ActionToLog, - parameters: Map?, - card: Card? + action: AnalyticsAnOld.ActionToLog, + params: Map, + card: Card?, ) { - analyticsHandlers.forEach { it.logCardSdkError(error, actionToLog, parameters, card) } + analyticsHandlers.filterIsInstance().forEach { + it.handleCardSdkErrorEvent(error, action, params, card) + } } - override fun logError(error: Throwable, params: Map) { - analyticsHandlers.forEach { it.logError(error, params) } + override fun handleBlockchainSdkErrorEvent( + error: BlockchainError, + action: AnalyticsAnOld.ActionToLog, + params: Map, + card: Card?, + ) { + analyticsHandlers.filterIsInstance().forEach { + it.handleBlockchainSdkErrorEvent(error, action, params, card) + } } - override fun getOrderEvent(): String { - return "" + override fun handleShopifyOrderEvent(order: Storefront.Order) { + analyticsHandlers.filterIsInstance().forEach { + it.handleShopifyOrderEvent(order) + } } - override fun getOrderParams(order: Storefront.Order): Map { - return emptyMap() + override fun prepareParams(card: Card?, blockchain: String?, params: Map): Map { + return super.prepareParams(card, blockchain, params).toMutableMap().apply { + putAll(attachToAllEventsParams) + } } +} - companion object { - fun createDefaultAnalyticHandlers(context: Context): GlobalAnalyticsHandler { - return GlobalAnalyticsHandler( - listOf( - FirebaseAnalyticsHandler, - AppsFlyerAnalyticsHandler(context) - ) +fun GlobalAnalyticsEventHandler.logWcEvent(event: AnalyticsAnOld.WcAnalyticsEvent) { + when (event) { + is AnalyticsAnOld.WcAnalyticsEvent.Action -> { + handleAnalyticsEvent( + event = AnalyticsEventAnOld.WC_SUCCESS_RESPONSE, + params = mapOf( + AnalyticsParamAnOld.WALLET_CONNECT_ACTION.param to event.action.name, + ), + ) + } + is AnalyticsAnOld.WcAnalyticsEvent.Error -> { + val params = mapOf( + AnalyticsParamAnOld.WALLET_CONNECT_ACTION.param to event.action?.name, + AnalyticsParamAnOld.ERROR_DESCRIPTION.param to event.error.message, + ).filterNotNull() + handleErrorEvent(event.error, params) + } + is AnalyticsAnOld.WcAnalyticsEvent.InvalidRequest -> + handleAnalyticsEvent( + event = AnalyticsEventAnOld.WC_INVALID_REQUEST, + params = mapOf( + AnalyticsParamAnOld.WALLET_CONNECT_REQUEST.param to event.json, + ).filterNotNull(), + ) + is AnalyticsAnOld.WcAnalyticsEvent.Session -> { + val analyticsEvent = when (event.event) { + AnalyticsAnOld.WcSessionEvent.Disconnect -> AnalyticsEventAnOld.WC_SESSION_DISCONNECTED + AnalyticsAnOld.WcSessionEvent.Connect -> AnalyticsEventAnOld.WC_NEW_SESSION + } + handleAnalyticsEvent( + event = analyticsEvent, + params = mapOf( + AnalyticsParamAnOld.WALLET_CONNECT_DAPP_URL.param to event.url, + ).filterNotNull(), ) } } diff --git a/app/src/main/java/com/tangem/tap/common/analytics/api/ClientApi.kt b/app/src/main/java/com/tangem/tap/common/analytics/api/ClientApi.kt new file mode 100644 index 0000000000..3b5b0720f2 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/api/ClientApi.kt @@ -0,0 +1,18 @@ +package com.tangem.tap.common.analytics.api + +/** +[REDACTED_AUTHOR] + */ +interface EventLogger { + fun logEvent( + event: String, + params: Map = emptyMap(), + ) +} + +interface ErrorEventLogger { + fun logErrorEvent( + error: Throwable, + params: Map = emptyMap(), + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/api/EventHandlerApi.kt b/app/src/main/java/com/tangem/tap/common/analytics/api/EventHandlerApi.kt new file mode 100644 index 0000000000..08e1609e3c --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/api/EventHandlerApi.kt @@ -0,0 +1,88 @@ +package com.tangem.tap.common.analytics.api + +import com.shopify.buy3.Storefront +import com.tangem.blockchain.common.BlockchainError +import com.tangem.common.card.Card +import com.tangem.common.core.TangemSdkError +import com.tangem.tap.common.analytics.AnalyticsAnOld +import com.tangem.tap.common.analytics.AnalyticsEventAnOld +import com.tangem.tap.common.analytics.AnalyticsParamAnOld +import com.tangem.tap.common.analytics.events.AnalyticsEvent +import com.tangem.tap.common.analytics.events.AnalyticsParam +import com.tangem.tap.common.extensions.filterNotNull + +/** +[REDACTED_AUTHOR] + */ +interface AnalyticsEventHandler { + fun handleEvent( + event: String, + params: Map = emptyMap(), + ) + + fun handleAnalyticsEvent( + event: AnalyticsEvent, + card: Card? = null, + blockchain: String? = null, + ) { + handleEvent( + event = prepareEventString(event.category, event.event), + params = prepareParams(card, blockchain, event.params), + ) + } + + @Deprecated("Migrate to AnalyticsEvent") + fun handleAnalyticsEvent( + event: AnalyticsEventAnOld, + params: Map = emptyMap(), + card: Card? = null, + blockchain: String? = null, + ) { + handleEvent(event.event, prepareParams(card, blockchain, params)) + } + + fun prepareParams( + card: Card? = null, + blockchain: String? = null, + params: Map = emptyMap(), + ): Map = mapOf( + AnalyticsParam.Firmware to card?.firmwareVersion?.stringValue, + AnalyticsParam.BatchId to card?.batchId, + AnalyticsParam.Blockchain to blockchain, + ).filterNotNull() + params + + fun prepareEventString(category: String, event: String): String { + return "[$category] $event" + } +} + +interface ErrorEventHandler { + fun handleErrorEvent( + error: Throwable, + params: Map = emptyMap(), + ) +} + +interface SdkErrorEventHandler : CardSdkErrorEventHandler, BlockchainSdkErrorEventHandler + +interface CardSdkErrorEventHandler { + fun handleCardSdkErrorEvent( + error: TangemSdkError, + action: AnalyticsAnOld.ActionToLog, + params: Map = emptyMap(), + card: Card? = null, + ) +} + +interface BlockchainSdkErrorEventHandler { + fun handleBlockchainSdkErrorEvent( + error: BlockchainError, + action: AnalyticsAnOld.ActionToLog, + params: Map = mapOf(), + card: Card? = null, + ) +} + +interface ShopifyOrderEventHandler { + fun handleShopifyOrderEvent(order: Storefront.Order) +} diff --git a/app/src/main/java/com/tangem/tap/common/analytics/converters/SdkErrorConverters.kt b/app/src/main/java/com/tangem/tap/common/analytics/converters/SdkErrorConverters.kt new file mode 100644 index 0000000000..5229df8174 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/converters/SdkErrorConverters.kt @@ -0,0 +1,88 @@ +package com.tangem.tap.common.analytics.converters + +import com.tangem.blockchain.common.BlockchainError +import com.tangem.blockchain.common.BlockchainSdkError +import com.tangem.common.Converter +import com.tangem.common.core.TangemSdkError +import com.tangem.tap.common.analytics.AnalyticsAnOld +import com.tangem.tap.common.analytics.AnalyticsParamAnOld +import com.tangem.tap.common.analytics.TangemSdk +import com.tangem.tap.features.demo.DemoTransactionSender + +/** +[REDACTED_AUTHOR] + */ +internal data class ErrorEvent( + val throwable: Throwable, + val params: Map, +) + +internal class CardSdkErrorConverter : Converter { + + override fun convert(value: Model): ErrorEvent? { + if (value.error is TangemSdkError.UserCancelled) return null + + val mutableParams = value.params.convertToParamValue().toMutableMap().apply { + putAll(value.cardParams) + } + mutableParams[AnalyticsParamAnOld.ACTION.param] = value.action.key + mutableParams[AnalyticsParamAnOld.ERROR_CODE.param] = value.error.code.toString() + mutableParams[AnalyticsParamAnOld.ERROR_DESCRIPTION.param] = value.error.javaClass.simpleName + mutableParams[AnalyticsParamAnOld.ERROR_KEY.param] = "TangemSdkError" + + return ErrorEvent(TangemSdk.map(value.error), mutableParams) + } + + internal data class Model( + val error: TangemSdkError, + val action: AnalyticsAnOld.ActionToLog, + val params: Map, + val cardParams: Map, + ) +} + +internal class BlockchainSdkErrorConverter( + private val cardSdkErrorConverter: CardSdkErrorConverter, +) : Converter { + + override fun convert(value: Model): ErrorEvent? { + val error = (value.error as? BlockchainSdkError) ?: return null + + if (error is BlockchainSdkError.WrappedTangemError) { + val tangemSdkError = (error.tangemError as? TangemSdkError) ?: return null + + val cardSdkModel = CardSdkErrorConverter.Model( + error = tangemSdkError, + action = value.action, + params = value.params, + cardParams = value.cardParams, + ) + return cardSdkErrorConverter.convert(cardSdkModel) + } + + if (error.customMessage.contains(DemoTransactionSender.ID)) return null + + val mutableParams = value.params.convertToParamValue().toMutableMap() + mutableParams[AnalyticsParamAnOld.ACTION.param] = value.action.key + mutableParams[AnalyticsParamAnOld.ERROR_CODE.param] = error.code.toString() + mutableParams[AnalyticsParamAnOld.ERROR_DESCRIPTION.param] = "${error.javaClass.simpleName}: ${error.customMessage}" + mutableParams[AnalyticsParamAnOld.ERROR_KEY.param] = "BlockchainSdkError" + + return ErrorEvent(value.error, mutableParams) + } + + data class Model( + val error: BlockchainError, + val action: AnalyticsAnOld.ActionToLog, + val params: Map, + val cardParams: Map, + ) +} + +internal fun Map.convertToParamValue(): Map { + return mutableMapOf().also { newMap -> + this.forEach { + newMap[it.key.param] = it.value + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsEvent.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsEvent.kt new file mode 100644 index 0000000000..0451fdd68c --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsEvent.kt @@ -0,0 +1,10 @@ +package com.tangem.tap.common.analytics.events + +/** +[REDACTED_AUTHOR] + */ +sealed class AnalyticsEvent( + val category: String, + val event: String, + val params: Map = mapOf(), +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt new file mode 100644 index 0000000000..8c69239429 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt @@ -0,0 +1,58 @@ +package com.tangem.tap.common.analytics.events + +sealed class AnalyticsParam { + + sealed class CurrencyType(val value: String) { + class Currency(currency: com.tangem.tap.features.wallet.models.Currency) : CurrencyType(currency.currencySymbol) + class Blockchain(blockchain: com.tangem.blockchain.common.Blockchain) : CurrencyType(blockchain.currency) + class Token(token: com.tangem.blockchain.common.Token) : CurrencyType(token.symbol) + } + + // Multicurrency or CurrencyType + sealed class CardCurrency(val value: String) { + object MultiCurrency : CardCurrency("Multicurrency") + class SingleCurrency(type: CurrencyType) : CardCurrency(type.value) + } + + sealed class CardState(val value: String) { + object Empty : CardState("Empty") + object Full : CardState("Full") + } + + sealed class RateApp(val value: String) { + object Liked : RateApp("Liked") + object Disliked : RateApp("Disliked") + object Closed : RateApp("Close") + } + + sealed class OnOffState(val value: String) { + object On : OnOffState("On") + object Off : OnOffState("Off") + } + + sealed class UserCode(val value: String) { + object AccessCode : UserCode("Access Code") + object Passcode : UserCode("Passcode") + } + + sealed class SecurityMode(val value: String) { + object AccessCode : SecurityMode("Access Code") + object Passcode : SecurityMode("Passcode") + object LongTap : SecurityMode("Long Tap") + } + + sealed class SocialNetwork(val value: String) { + object Facebook : SocialNetwork("Facebook") + object Instagram : SocialNetwork("Instagram") + object Youtube : SocialNetwork("Youtube") + object Twitter : SocialNetwork("Twitter") + object LinkedIn : SocialNetwork("LinkedIn") + object GitHub : SocialNetwork("GitHub") + } + + companion object Key { + const val Blockchain = "blockchain" + const val BatchId = "batch_id" + const val Firmware = "firmware" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/Basic.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Basic.kt new file mode 100644 index 0000000000..bb37145672 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/Basic.kt @@ -0,0 +1,28 @@ +package com.tangem.tap.common.analytics.events + +/** +[REDACTED_AUTHOR] + */ +sealed class Basic( + event: String, + params: Map = mapOf(), +) : AnalyticsEvent("Basic", event, params) { + + sealed class SignedIn( + state: AnalyticsParam.CardState, + currency: AnalyticsParam.CardCurrency, + batch: String, + ) : Basic( + event = "Signed in", + params = mapOf( + "State" to state.value, + "Currency" to currency.value, + "Batch" to batch, + ), + ) + + class ToppedUp(currency: AnalyticsParam.CardCurrency) : Basic( + event = "Topped up", + params = mapOf("Currency" to currency.value), + ) +} diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/Chat.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Chat.kt new file mode 100644 index 0000000000..a26109ae2a --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/Chat.kt @@ -0,0 +1,12 @@ +package com.tangem.tap.common.analytics.events + +/** +[REDACTED_AUTHOR] + */ +sealed class Chat( + event: String, + params: Map = mapOf(), +) : AnalyticsEvent("Chat", event, params) { + + class ScreenOpened : Chat("Chat Screen Opened") +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/DetailsScreen.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/DetailsScreen.kt new file mode 100644 index 0000000000..1ec8d81533 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/DetailsScreen.kt @@ -0,0 +1,12 @@ +package com.tangem.tap.common.analytics.events + +/** +[REDACTED_AUTHOR] + */ +sealed class DetailsScreen( + event: String, + params: Map = mapOf(), +) : AnalyticsEvent("Details Screen", event, params) { + + class ScreenOpened : DetailsScreen("Details Screen Opened") +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/IntroductionProcess.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/IntroductionProcess.kt new file mode 100644 index 0000000000..4b3eb7f431 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/IntroductionProcess.kt @@ -0,0 +1,17 @@ +package com.tangem.tap.common.analytics.events + +/** +[REDACTED_AUTHOR] + */ +sealed class IntroductionProcess( + event: String, + params: Map = mapOf(), +) : AnalyticsEvent("Introduction Process", event, params) { + + class ScreenOpened : IntroductionProcess("Introduction Process Screen Opened") + class ButtonTokensList : IntroductionProcess("Button - Tokens List") + class ButtonBuyCards : IntroductionProcess("Button - Buy Cards") + class ButtonScanCard : IntroductionProcess("Button - Scan Card") + class CardWasScanned : IntroductionProcess("Card Was Scanned") + class ButtonRequestSupport : IntroductionProcess("Button - Request Support") +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/MainScreen.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/MainScreen.kt new file mode 100644 index 0000000000..c6825718b7 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/MainScreen.kt @@ -0,0 +1,29 @@ +package com.tangem.tap.common.analytics.events + +/** +[REDACTED_AUTHOR] + */ +sealed class MainScreen( + event: String, + params: Map = mapOf(), +) : AnalyticsEvent("Main Screen", event, params) { + + class ScreenOpened : MainScreen("Screen opened") + + class ButtonScanCard : MainScreen("Button - Scan Card") + class CardWasScanned : MainScreen("Card Was Scanned") + class ButtonMyWallets : MainScreen("Button - My Wallets") + + class MainCurrencyChanged(currencyType: AnalyticsParam.CurrencyType) : MainScreen( + event = "Main Currency Changed", + params = mapOf("Currency Type" to currencyType.value), + ) + + sealed class NoticeRateAppButton(result: AnalyticsParam.RateApp) : MainScreen( + event = "Notice - Rate The App Button Tapped", + params = mapOf("Result" to result.value), + ) + + class NoticeBackupYourWalletTapped : MainScreen("Notice - Backup Your Wallet Tapped") + class NoticeScanYourCardTapped : MainScreen("Notice - Scan Your Card Tapped") +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/ManageTokens.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/ManageTokens.kt new file mode 100644 index 0000000000..e5e3fdb96a --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/ManageTokens.kt @@ -0,0 +1,33 @@ +package com.tangem.tap.common.analytics.events + +/** +[REDACTED_AUTHOR] + */ +sealed class ManageTokens( + event: String, + params: Map = mapOf(), +) : AnalyticsEvent("Manage Tokens", event, params) { + + class ScreenOpened : ManageTokens("Manage Tokens Screen Opened") + class TokenSearched : ManageTokens("Token Searched") + class TokenSwitcherChanged : ManageTokens("Token Switcher Changed") + class ButtonSaveChanges : ManageTokens("Button - Save Changes") + class ButtonCustomToken : ManageTokens("Button - Custom Token") + + sealed class CustomToken( + event: String, + params: Map = mapOf(), + ) : ManageTokens(event, params) { + + class ScreenOpened : ManageTokens("Custom Token Screen Opened") + + class TokenWasAdded(symbol: String, network: String, address: String) : ManageTokens( + event = "Custom Token Was Added", + params = mapOf( + "Symbol" to symbol, + "Network" to network, + "Address" to address, + ), + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/Onboarding.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Onboarding.kt new file mode 100644 index 0000000000..b65408b7d5 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/Onboarding.kt @@ -0,0 +1,76 @@ +package com.tangem.tap.common.analytics.events + +/** +[REDACTED_AUTHOR] + */ +sealed class Onboarding( + category: String, + event: String, + params: Map = mapOf(), +) : AnalyticsEvent(category, event, params) { + + class Started : Onboarding("Onboarding", "Onboarding Started") + class Finished : Onboarding("Onboarding", "Onboarding Finished") + + sealed class CreateWallet( + event: String, + params: Map = mapOf(), + ) : Onboarding("Onboarding / Create Wallet", event, params) { + + class ScreenOpened : CreateWallet("Create Wallet Screen Opened") + class ButtonCreateWallet : CreateWallet("Button - Create Wallet") + class WalletCreatedSuccessfully : CreateWallet("Wallet Created Successfully") + } + + sealed class Backup( + event: String, + params: Map = mapOf(), + ) : Onboarding("Onboarding / Backup", event, params) { + + class ScreenOpened : Backup("Backup Screen Opened") + class Started : Backup("Backup Started") + class Skipped : Backup("Backup Skipped") + class SettingAccessCodeStarted : Backup("Setting Access Code Started") + class AccessCodeEntered : Backup("Access Code Entered") + class AccessCodeReEntered : Backup("Access Code Re-entered") + + class Finished(cardsCount: String) : Backup( + event = "Backup Finished", + params = mapOf("Cards count" to cardsCount), + ) + } + + sealed class Topup( + event: String, + params: Map = mapOf(), + ) : Onboarding("Onboarding / Top Up", event, params) { + + class ScreenOpened : Topup("Activation Screen Opened") + + class ButtonBuyCrypto(currency: AnalyticsParam.CurrencyType) : Topup( + event = "Button - Buy Crypto", + params = mapOf("Currency" to currency.value), + ) + + class ButtonShowWalletAddress : Topup("Button - Show the Wallet Address") + } + + sealed class Twins( + event: String, + params: Map = mapOf(), + ) : Onboarding("Onboarding / Twins", event, params) { + + class ScreenOpened : Twins("Twinning Screen Opened") + class SetupStarted : Twins("Twin Setup Started") + class SetupFinished : Twins("Twin Setup Finished") + } + + class PinCodeSet: Onboarding("Onboarding", "PIN code set") + class ButtonConnect: Onboarding("Onboarding", "Button - Connect") + class KYCStarted: Onboarding("Onboarding", "KYC started") + class KYCInProgress: Onboarding("Onboarding", "KYC in progress") + class KYCRejected: Onboarding("Onboarding", "KYC rejected") + class ClaimScreenOpened: Onboarding("Onboarding", "Claim screen opened") + class ButtonClaim: Onboarding("Onboarding", "Button - Claim") + class ClaimWasSuccessfully: Onboarding("Onboarding", "Claim was successfully") +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/Portfolio.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Portfolio.kt new file mode 100644 index 0000000000..6747c2172d --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/Portfolio.kt @@ -0,0 +1,15 @@ +package com.tangem.tap.common.analytics.events + +/** +[REDACTED_AUTHOR] + */ + +sealed class Portfolio( + event: String, + params: Map = mapOf(), +) : AnalyticsEvent("Portfolio", event, params) { + + class Refreshed : Portfolio("Refreshed") + class ButtonManageTokens : Portfolio("Button - Manage Tokens") + class TokenTapped : Portfolio("Token is Tapped") +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/Settings.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Settings.kt new file mode 100644 index 0000000000..56893d2f31 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/Settings.kt @@ -0,0 +1,64 @@ +package com.tangem.tap.common.analytics.events + +/** +[REDACTED_AUTHOR] + */ +sealed class Settings( + category: String = "Settings", + event: String, + params: Map = mapOf(), +) : AnalyticsEvent(category, event, params) { + + class ScreenOpened : Settings(event = "Settings Screen Opened") + class ButtonChat : Settings(event = "Button - Chat") + class ButtonSendFeedback : Settings(event = "Button - Send Feedback") + class ButtonStartWalletConnectSession : Settings(event = "Button - Start Wallet Connect Session") + class ButtonStopWalletConnectSession : Settings(event = "Button - Stop Wallet Connect Session") + class ButtonCardSettings : Settings(event = "Button - Card Settings") + class ButtonAppSettings : Settings(event = "Button - App Settings") + class ButtonCreateBackup : Settings(event = "Button - Create Backup") + + sealed class ButtonSocialNetwork(network: AnalyticsParam.SocialNetwork) : Settings( + event = "Button - Social Network", + params = mapOf("Network" to network.value), + ) + + sealed class CardSettings( + event: String, + params: Map = mapOf(), + ) : Settings("Settings / Card Settings", event, params) { + + class ButtonFactoryReset : CardSettings("Button - Factory Reset") + class FactoryResetFinished : CardSettings("Factory Reset Finished") + class UserCodeChanged : CardSettings("User Code Changed") + class ButtonChangeSecurityMode : CardSettings("Button - Change Security Mode") + + sealed class ButtonChangeUserCode(type: AnalyticsParam.UserCode) : CardSettings( + event = "Button - Change User Code", + params = mapOf("Type" to type.value), + ) + + sealed class SecurityModeChanged(mode: AnalyticsParam.SecurityMode) : CardSettings( + event = "Security Mode Changed", + params = mapOf("Mode" to mode.value), + ) + } + + sealed class AppSettings( + event: String, + params: Map = mapOf(), + ) : Settings("Settings / App Settings", event, params) { + + sealed class FaceIDSwitcherChanged(state: AnalyticsParam.OnOffState) : CardSettings( + event = "Face ID Switcher Changed", + params = mapOf("State" to state.value), + ) + + sealed class SaveAccessCodeSwitcherChanged(state: AnalyticsParam.OnOffState) : CardSettings( + event = "Save Access Code Switcher Changed", + params = mapOf("State" to state.value), + ) + + class ButtonEnableBiometricAuthentication : AppSettings("Button - Enable Biometric Authentication") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/Shop.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Shop.kt new file mode 100644 index 0000000000..c09bf89ab3 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/Shop.kt @@ -0,0 +1,22 @@ +package com.tangem.tap.common.analytics.events + +/** +[REDACTED_AUTHOR] + */ +sealed class Shop( + event: String, + params: Map = mapOf(), +) : AnalyticsEvent("Shop", event, params) { + + class ScreenOpened : IntroductionProcess("Shop Screen Opened") + + class Purchased(productSku: String) : Shop( + event = "Purchased", + params = mapOf("SKU" to productSku), + ) + + class Redirected(partnerName: String) : Shop( + event = "Redirected", + params = mapOf("Partner" to partnerName), + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/Token.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Token.kt new file mode 100644 index 0000000000..afe5da529a --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/Token.kt @@ -0,0 +1,62 @@ +package com.tangem.tap.common.analytics.events + +/** +[REDACTED_AUTHOR] + */ +sealed class Token( + category: String, + event: String, + params: Map = mapOf(), +) : AnalyticsEvent(category, event, params) { + + class Refreshed : Token("Token", "Refreshed") + class ButtonRemoveToken : Token("Token", "Button - Remove Token") + class ButtonExplore : Token("Token", "Button - Explore") + class ButtonBuy(token: String) : Token("Token", "Button - Buy", mapOf("Token" to token)) + class ButtonSell(token: String) : Token("Token", "Button - Sell", mapOf("Token" to token)) + class ButtonExchange(token: String) : Token("Token", "Button - Exchange", mapOf("Token" to token)) + class ButtonSend(token: String) : Token("Token", "Button - Send", mapOf("Token" to token)) + + sealed class Recieve( + event: String, + params: Map = mapOf(), + ) : Token("Token / Recieve", event, params) { + + class ScreenOpened : Recieve("Recieve Screen Opened") + class ButtonCopyAddress : Recieve("Button - Copy Address") + class ButtonShareAddress : Recieve("Button - Share Address") + } + + sealed class Send( + event: String, + params: Map = mapOf(), + ) : Token("Token / Send", event, params) { + + class ScreenOpened : Send("Send Screen Opened") + class ButtonPaste : Send("Button - Paste") + class ButtonQRCode : Send("Button - QR Code") + class ButtonSwapCurrency : Send("Button - Swap Currency") + + class TransactionSent(token: AnalyticsParam.CurrencyType) : Send( + event = "Transaction Sent", + params = mapOf("Token" to token.value), + ) + } + + sealed class Topup( + event: String, + params: Map = mapOf(), + ) : Token("Token / Topup", event, params) { + + class ScreenOpened : Topup("Top Up Screen Opened") + class P2PScreenOpened : Topup("P2P Screen Opened") + } + + sealed class Withdraw( + event: String, + params: Map = mapOf(), + ) : Token("Token / Withdraw", event, params) { + + class ScreenOpened : Withdraw("Withdraw Screen Opened") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/WalletConnect.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/WalletConnect.kt new file mode 100644 index 0000000000..624145c63d --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/WalletConnect.kt @@ -0,0 +1,14 @@ +package com.tangem.tap.common.analytics.events + +/** +[REDACTED_AUTHOR] + */ +sealed class WalletConnect( + event: String, + params: Map = mapOf(), +) : AnalyticsEvent("Wallet Connect", event, params) { + + class NewSessionEstablished : WalletConnect("New Session Established") + class SessionDisconnected : WalletConnect("Session Disconnected") + class RequestSigned : WalletConnect("Request Signed") +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeAnalyticsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeAnalyticsHandler.kt new file mode 100644 index 0000000000..27132a1f61 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeAnalyticsHandler.kt @@ -0,0 +1,12 @@ +package com.tangem.tap.common.analytics.handlers.amplitude + +import com.tangem.tap.common.analytics.api.AnalyticsEventHandler + +class AmplitudeAnalyticsHandler( + private val client: AmplitudeAnalyticsClient, +) : AnalyticsEventHandler { + + override fun handleEvent(event: String, params: Map) { + client.logEvent(event, params) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeClient.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeClient.kt new file mode 100644 index 0000000000..d90ed084b5 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeClient.kt @@ -0,0 +1,36 @@ +package com.tangem.tap.common.analytics.handlers.amplitude + +import android.app.Application +import com.amplitude.api.Amplitude +import com.amplitude.api.AmplitudeClient +import com.tangem.common.Converter +import com.tangem.tap.common.analytics.api.EventLogger +import org.json.JSONObject + +/** +[REDACTED_AUTHOR] + */ +interface AmplitudeAnalyticsClient : EventLogger + +internal class AmplitudeClient( + application: Application, + key: String, +) : AmplitudeAnalyticsClient { + + private val client: AmplitudeClient = Amplitude.getInstance() + + init { + client.initialize(application, key) + client.enableForegroundTracking(application) + } + + override fun logEvent(event: String, params: Map) { + client.logEvent(event, ParamsToJSONObjectConverter().convert(params)) + } +} + +private class ParamsToJSONObjectConverter : Converter, JSONObject> { + override fun convert(value: Map): JSONObject = JSONObject().apply { + value.forEach { this.put(it.key, it.value) } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeLogClient.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeLogClient.kt new file mode 100644 index 0000000000..b800259b5f --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeLogClient.kt @@ -0,0 +1,15 @@ +package com.tangem.tap.common.analytics.handlers.amplitude + +import com.tangem.tap.common.analytics.AnalyticsEventsLogger + +/** +[REDACTED_AUTHOR] + */ +internal class AmplitudeLogClient( + private val logger: AnalyticsEventsLogger, +) : AmplitudeAnalyticsClient { + + override fun logEvent(event: String, params: Map) { + logger.logEvent(event, params) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsFlyer/AppsFlyerAnalyticsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsFlyer/AppsFlyerAnalyticsHandler.kt new file mode 100644 index 0000000000..0acfb1dc2f --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsFlyer/AppsFlyerAnalyticsHandler.kt @@ -0,0 +1,47 @@ +package com.tangem.tap.common.analytics.handlers.appsFlyer + +import com.appsflyer.AFInAppEventParameterName +import com.appsflyer.AFInAppEventType +import com.shopify.buy3.Storefront +import com.tangem.common.Converter +import com.tangem.tap.common.analytics.api.AnalyticsEventHandler +import com.tangem.tap.common.analytics.api.ShopifyOrderEventHandler + +class AppsFlyerAnalyticsHandler( + private val client: AppsFlyerAnalyticsClient, +) : AnalyticsEventHandler, ShopifyOrderEventHandler { + + override fun handleEvent(event: String, params: Map) { + client.logEvent(event, params) + } + + override fun handleShopifyOrderEvent(order: Storefront.Order) { + handleEvent(ORDER_EVENT, OrderToParamsConverter().convert(order)) + } + + companion object { + const val ORDER_EVENT = AFInAppEventType.PURCHASE + } +} + +private class OrderToParamsConverter : Converter> { + + override fun convert(value: Storefront.Order): Map { + val sku = value.lineItems.edges.firstOrNull()?.node?.variant?.sku ?: "unknown" + + val discountCode = + (value.discountApplications.edges.firstOrNull()?.node as? Storefront.DiscountCodeApplication)?.code + + val discountParams = if (discountCode != null) { + mapOf(AFInAppEventParameterName.COUPON_CODE to discountCode) + } else { + mapOf() + } + + return mapOf( + AFInAppEventParameterName.CONTENT_ID to sku, + AFInAppEventParameterName.REVENUE to value.totalPriceV2.amount, + AFInAppEventParameterName.CURRENCY to value.currencyCode.name, + ) + discountParams + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsFlyer/AppsFlyerClient.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsFlyer/AppsFlyerClient.kt new file mode 100644 index 0000000000..6a7aa7cfed --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsFlyer/AppsFlyerClient.kt @@ -0,0 +1,27 @@ +package com.tangem.tap.common.analytics.handlers.appsFlyer + +import android.content.Context +import com.appsflyer.AppsFlyerLib +import com.tangem.tap.common.analytics.api.EventLogger + +/** +[REDACTED_AUTHOR] + */ +interface AppsFlyerAnalyticsClient : EventLogger + +internal class AppsFlyerClient( + private val context: Context, + key: String, +) : AppsFlyerAnalyticsClient { + + private val appsFlyerLib: AppsFlyerLib = AppsFlyerLib.getInstance() + + init { + appsFlyerLib.init(key, null, context) + appsFlyerLib.start(context) + } + + override fun logEvent(event: String, params: Map) { + appsFlyerLib.logEvent(context, event, params) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsFlyer/AppsFlyerLogClient.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsFlyer/AppsFlyerLogClient.kt new file mode 100644 index 0000000000..0d3737a161 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsFlyer/AppsFlyerLogClient.kt @@ -0,0 +1,15 @@ +package com.tangem.tap.common.analytics.handlers.appsFlyer + +import com.tangem.tap.common.analytics.AnalyticsEventsLogger + +/** +[REDACTED_AUTHOR] + */ +internal class AppsFlyerLogClient( + private val logger: AnalyticsEventsLogger, +) : AppsFlyerAnalyticsClient { + + override fun logEvent(event: String, params: Map) { + logger.logEvent(event, params) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAnalyticsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAnalyticsHandler.kt new file mode 100644 index 0000000000..3ab6326670 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAnalyticsHandler.kt @@ -0,0 +1,81 @@ +package com.tangem.tap.common.analytics.handlers.firebase + +import com.google.firebase.analytics.FirebaseAnalytics +import com.shopify.buy3.Storefront +import com.tangem.blockchain.common.BlockchainError +import com.tangem.common.Converter +import com.tangem.common.card.Card +import com.tangem.common.core.TangemSdkError +import com.tangem.tap.common.analytics.AnalyticsAnOld +import com.tangem.tap.common.analytics.AnalyticsParamAnOld +import com.tangem.tap.common.analytics.api.AnalyticsEventHandler +import com.tangem.tap.common.analytics.api.ErrorEventHandler +import com.tangem.tap.common.analytics.api.SdkErrorEventHandler +import com.tangem.tap.common.analytics.api.ShopifyOrderEventHandler +import com.tangem.tap.common.analytics.converters.BlockchainSdkErrorConverter +import com.tangem.tap.common.analytics.converters.CardSdkErrorConverter + +class FirebaseAnalyticsHandler( + private val client: FirebaseAnalyticsClient, +) : AnalyticsEventHandler, ErrorEventHandler, SdkErrorEventHandler, ShopifyOrderEventHandler { + + override fun handleEvent(event: String, params: Map) { + client.logEvent(event, params) + } + + override fun handleErrorEvent(error: Throwable, params: Map) { + client.logErrorEvent(error, params) + } + + override fun handleCardSdkErrorEvent( + error: TangemSdkError, + action: AnalyticsAnOld.ActionToLog, + params: Map, + card: Card?, + ) { + val model = CardSdkErrorConverter.Model(error, action, params, prepareParams(card)) + val converter = CardSdkErrorConverter() + converter.convert(model)?.let { + handleErrorEvent(it.throwable, it.params) + } + } + + override fun handleBlockchainSdkErrorEvent( + error: BlockchainError, + action: AnalyticsAnOld.ActionToLog, + params: Map, + card: Card?, + ) { + + val model = BlockchainSdkErrorConverter.Model(error, action, params, prepareParams(card)) + val converter = BlockchainSdkErrorConverter(CardSdkErrorConverter()) + converter.convert(model)?.let { + handleErrorEvent(it.throwable, it.params) + } + } + + override fun handleShopifyOrderEvent(order: Storefront.Order) { + handleEvent(FirebaseClient.ORDER_EVENT, OrderToParamsConverter().convert(order)) + } +} + +private class OrderToParamsConverter : Converter> { + override fun convert(value: Storefront.Order): Map { + val sku = value.lineItems.edges.firstOrNull()?.node?.variant?.sku ?: "unknown" + + val discountCode = + (value.discountApplications.edges.firstOrNull()?.node as? Storefront.DiscountCodeApplication)?.code + + val discountParams = if (discountCode != null) { + mapOf(FirebaseAnalytics.Param.DISCOUNT to discountCode) + } else { + mapOf() + } + + return mapOf( + FirebaseAnalytics.Param.ITEM_ID to sku, + FirebaseAnalytics.Param.VALUE to value.totalPriceV2.amount, + FirebaseAnalytics.Param.CURRENCY to value.currencyCode.name, + ) + discountParams + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseClient.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseClient.kt new file mode 100644 index 0000000000..07ed4c4551 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseClient.kt @@ -0,0 +1,36 @@ +package com.tangem.tap.common.analytics.handlers.firebase + +import android.os.Bundle +import androidx.core.os.bundleOf +import com.google.firebase.analytics.FirebaseAnalytics +import com.google.firebase.analytics.ktx.analytics +import com.google.firebase.crashlytics.ktx.crashlytics +import com.google.firebase.ktx.Firebase +import com.tangem.tap.common.analytics.api.ErrorEventLogger +import com.tangem.tap.common.analytics.api.EventLogger + +/** +[REDACTED_AUTHOR] + */ +interface FirebaseAnalyticsClient : EventLogger, ErrorEventLogger + +internal class FirebaseClient : FirebaseAnalyticsClient { + + private val fbAnalytics = Firebase.analytics + private val fbCrashlytics = Firebase.crashlytics + + override fun logEvent(event: String, params: Map) { + fbAnalytics.logEvent(event, params.toBundle()) + } + + override fun logErrorEvent(error: Throwable, params: Map) { + params.forEach { fbCrashlytics.setCustomKey(it.key, it.value) } + fbCrashlytics.recordException(error) + } + + private fun Map.toBundle(): Bundle = bundleOf(*this.toList().toTypedArray()) + + companion object { + const val ORDER_EVENT = FirebaseAnalytics.Event.PURCHASE + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseLogClient.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseLogClient.kt new file mode 100644 index 0000000000..ccacc09f9e --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseLogClient.kt @@ -0,0 +1,19 @@ +package com.tangem.tap.common.analytics.handlers.firebase + +import com.tangem.tap.common.analytics.AnalyticsEventsLogger + +/** +[REDACTED_AUTHOR] + */ +internal class FirebaseLogClient( + private val logger: AnalyticsEventsLogger, +) : FirebaseAnalyticsClient { + + override fun logEvent(event: String, params: Map) { + logger.logEvent(event, params) + } + + override fun logErrorEvent(error: Throwable, params: Map) { + logger.logErrorEvent(error, params) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/AnalyticsHandler.kt b/app/src/main/java/com/tangem/tap/common/extensions/AnalyticsHandler.kt deleted file mode 100644 index f2897e2b63..0000000000 --- a/app/src/main/java/com/tangem/tap/common/extensions/AnalyticsHandler.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.tangem.tap.common.extensions - -import com.google.firebase.crashlytics.FirebaseCrashlytics -import com.tangem.blockchain.common.BlockchainError -import com.tangem.blockchain.common.BlockchainSdkError -import com.tangem.common.card.Card -import com.tangem.common.core.TangemSdkError -import com.tangem.tap.common.analytics.Analytics -import com.tangem.tap.common.analytics.AnalyticsHandler -import com.tangem.tap.common.analytics.AnalyticsParam -import com.tangem.tap.features.demo.DemoTransactionSender - -/** -[REDACTED_AUTHOR] - */ -fun AnalyticsHandler.logSendTransactionError( - error: BlockchainError, - action: Analytics.ActionToLog, - parameters: Map? = mapOf(), - card: Card? = null, -) { - when (val blockchainSdkError = (error as BlockchainSdkError)) { - is BlockchainSdkError.WrappedTangemError -> { - val tangemSdkError = (blockchainSdkError.tangemError as? TangemSdkError) ?: return - - logCardSdkError( - error = tangemSdkError, - actionToLog = action, - parameters = parameters, - card = card, - ) - } - else -> { - when { - blockchainSdkError.customMessage.contains(DemoTransactionSender.ID) -> return - else -> { - val params = parameters?.toMutableMap() ?: mutableMapOf() - params[AnalyticsParam.ACTION] = action.key - params[AnalyticsParam.ERROR_CODE] = error.code.toString() - params[AnalyticsParam.ERROR_DESCRIPTION] = "${error.javaClass.simpleName}: ${error.customMessage}" - params[AnalyticsParam.ERROR_KEY] = "BlockchainSdkError" - - FirebaseCrashlytics.getInstance().apply { - params.forEach { setCustomKey(it.key.param, it.value) } - recordException(error) - } - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/BackupService.kt b/app/src/main/java/com/tangem/tap/common/extensions/BackupService.kt new file mode 100644 index 0000000000..abe1863c6d --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/extensions/BackupService.kt @@ -0,0 +1,13 @@ +package com.tangem.tap.common.extensions + +import com.tangem.domain.common.SaltPayWorkaround +import com.tangem.operations.backup.BackupService + +/** +[REDACTED_AUTHOR] + */ +fun BackupService.primaryCardIsSaltPayVisa(): Boolean { + return primaryCardId?.slice(0..3)?.let { + SaltPayWorkaround.isVisaBatchId(it) + } ?: false +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/ImageView.kt b/app/src/main/java/com/tangem/tap/common/extensions/ImageView.kt new file mode 100644 index 0000000000..0c3a471d54 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/extensions/ImageView.kt @@ -0,0 +1,11 @@ +package com.tangem.tap.common.extensions + +import android.widget.ImageView +import androidx.annotation.DrawableRes + +/** +[REDACTED_AUTHOR] + */ +fun ImageView.setDrawable(@DrawableRes resId: Int) { + setImageDrawable(context.getDrawableCompat(resId)) +} \ 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 76144c6132..15d02907bf 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 @@ -44,8 +44,13 @@ suspend fun WalletManager.safeUpdate(): Result = try { if (exception is BlockchainSdkError.AccountNotFound && amountToCreateAccount != null) { Result.Failure(TapError.WalletManager.NoAccountError(amountToCreateAccount.toString())) } else { - val message = exception.localizedMessage ?: "An error has occurred. Try later" - Result.Failure(TapError.WalletManager.InternalError(message)) + when (exception) { + is BlockchainSdkError -> Result.Failure(exception) + else -> { + val message = exception.cause?.localizedMessage ?: "Unknown error" + Result.Failure(TapError.WalletManager.InternalError(message)) + } + } } } } @@ -72,7 +77,7 @@ fun WalletManager?.getAddressData(): AddressData? { else addressDataList[0] } -fun WalletManager.Companion.stub(): T { +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 = "" 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 index ebe938a1ec..7aa93b2bb1 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/WebView.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/WebView.kt @@ -6,6 +6,7 @@ import android.webkit.WebView [REDACTED_AUTHOR] */ fun WebView.configureSettings() { + resumeTimers() settings.apply { javaScriptEnabled = true domStorageEnabled = true @@ -15,5 +16,4 @@ fun WebView.configureSettings() { fun WebView.stop() { stopLoading() pauseTimers() - destroy() } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/StateDialog.kt b/app/src/main/java/com/tangem/tap/common/redux/StateDialog.kt index 89d609b32c..e216493dd6 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/StateDialog.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/StateDialog.kt @@ -12,19 +12,22 @@ interface StateDialog sealed class AppDialog : StateDialog { data class SimpleOkDialog(val header: String, val message: String, val onOk: VoidCallback? = null) : AppDialog() + data class SimpleOkErrorDialog(val message: String, val onOk: VoidCallback? = null) : AppDialog() + data class SimpleOkWarningDialog(val message: String, val onOk: VoidCallback? = null) : AppDialog() data class SimpleOkDialogRes( val headerId: Int, val messageId: Int, - val onOk: VoidCallback? = null + val onOk: VoidCallback? = null, ) : AppDialog() object ScanFailsDialog : AppDialog() + data class AddressInfoDialog( val currency: Currency, val addressData: AddressData, ) : AppDialog() data class TestActionsDialog( - val actionsList: List + val actionsList: List, ) : AppDialog() } \ No newline at end of file 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 3d596bbce3..b226be3401 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 @@ -5,7 +5,7 @@ import com.tangem.blockchain.common.WalletManager import com.tangem.common.CompletionResult import com.tangem.common.core.TangemError import com.tangem.domain.common.ScanResponse -import com.tangem.tap.common.analytics.GlobalAnalyticsHandler +import com.tangem.tap.common.analytics.GlobalAnalyticsEventHandler import com.tangem.tap.common.entities.FiatCurrency import com.tangem.tap.common.feedback.FeedbackData import com.tangem.tap.common.feedback.FeedbackManager @@ -38,8 +38,20 @@ sealed class GlobalAction : Action { object HideDialog : GlobalAction() sealed class Onboarding : GlobalAction() { + /** + * Initiate an onboarding process. + * For SaltPay cards it's additionally checks for unfinished backup. + * For resuming unfinished backup for standard Wallet cards see CheckForUnfinishedBackup and + * StartForUnfinishedBackup + */ data class Start(val scanResponse: ScanResponse, val canSkipBackup: Boolean = true) : Onboarding() - data class StartForUnfinishedBackup(val isSaltPayBackup: Boolean) : Onboarding() + + /** + * Initiate resuming of unfinished backup only for standard Wallet cards. + * For SaltPay cards unfinished backup resumed after scanning the card on HomeScreen through Onboarding.Start. + * See more Onboarding.Start, CheckForUnfinishedBackup + */ + object StartForUnfinishedBackup : Onboarding() object Stop : Onboarding() } @@ -77,7 +89,7 @@ sealed class GlobalAction : Action { data class SetConfigManager(val configManager: ConfigManager) : GlobalAction() data class SetWarningManager(val warningManager: WarningMessagesManager) : GlobalAction() data class SetFeedbackManager(val feedbackManager: FeedbackManager) : GlobalAction() - data class SetAnanlyticHandlers(val analyticsHandlers: GlobalAnalyticsHandler) : GlobalAction() + data class SetGlobalAnalyticsHandler(val analyticsHandler: GlobalAnalyticsEventHandler) : GlobalAction() data class SendEmail(val feedbackData: FeedbackData) : GlobalAction() data class OpenChat(val feedbackData: FeedbackData, val zendeskConfig: ZendeskConfig? = null) : 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 67e326564d..f5c5840ffd 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 @@ -140,7 +140,10 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di secretKey = moonPaySecretKey, logEnabled = LogConfig.network.moonPayService, ) - val cardProvider = { store.state.globalState.scanResponse?.card } + val cardProvider = { + store.state.globalState.scanResponse?.card + ?: store.state.globalState.onboardingState.onboardingManager?.scanResponse?.card + } val exchangeManager = CurrencyExchangeManager( buyService = buyService, @@ -163,7 +166,7 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di is GlobalAction.ScanCard -> { scope.launch { val result = tangemSdkManager.scanProduct( - store.state.globalState.analyticsHandlers, + store.state.globalState.analyticsHandler, userTokensRepository, action.additionalBlockchainsToDerive, action.messageResId, 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 d1f9043ca2..b0cefc6d9e 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 @@ -48,8 +48,7 @@ fun globalReducer(action: Action, state: AppState): GlobalState { globalState.copy(configManager = action.configManager) } is GlobalAction.SetWarningManager -> globalState.copy(warningManager = action.warningManager) - is GlobalAction.SetAnanlyticHandlers -> - globalState.copy(analyticsHandlers = action.analyticsHandlers) + is GlobalAction.SetGlobalAnalyticsHandler -> globalState.copy(analyticsHandler = action.analyticsHandler) is GlobalAction.UpdateWalletSignedHashes -> { val card = globalState.scanResponse?.card ?: return globalState val wallet = card.wallet(action.walletPublicKey) ?: return globalState diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt index 1fed436b47..a02172db23 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt @@ -1,7 +1,7 @@ package com.tangem.tap.common.redux.global import com.tangem.domain.common.ScanResponse -import com.tangem.tap.common.analytics.AnalyticsHandler +import com.tangem.tap.common.analytics.GlobalAnalyticsEventHandler import com.tangem.tap.common.entities.FiatCurrency import com.tangem.tap.common.feedback.FeedbackManager import com.tangem.tap.common.redux.StateDialog @@ -27,7 +27,7 @@ data class GlobalState( val dialog: StateDialog? = null, val exchangeManager: CurrencyExchangeManager = CurrencyExchangeManager.dummy(), val resources: AndroidResources = AndroidResources(), - val analyticsHandlers: AnalyticsHandler? = null, + val analyticsHandler: GlobalAnalyticsEventHandler = GlobalAnalyticsEventHandler.stub(), val userCountryCode: String? = null, ) : StateType diff --git a/app/src/main/java/com/tangem/tap/common/shop/TangemShopService.kt b/app/src/main/java/com/tangem/tap/common/shop/TangemShopService.kt index f5281cc796..c8bd0a5405 100644 --- a/app/src/main/java/com/tangem/tap/common/shop/TangemShopService.kt +++ b/app/src/main/java/com/tangem/tap/common/shop/TangemShopService.kt @@ -4,7 +4,8 @@ import android.app.Application import android.content.Intent import com.google.android.gms.wallet.PaymentData import com.shopify.buy3.Storefront -import com.tangem.tap.common.analytics.AnalyticsHandler +import com.tangem.common.extensions.ifNotNull +import com.tangem.tap.common.analytics.GlobalAnalyticsEventHandler import com.tangem.tap.common.extensions.filterNotNull import com.tangem.tap.common.shop.data.ProductType import com.tangem.tap.common.shop.data.TangemProduct @@ -195,15 +196,10 @@ class TangemShopService(application: Application, shopifyShop: ShopifyShop) { return checkouts[productType]!!.webUrl } - suspend fun waitForCheckout( - productType: ProductType, - analyticsHandler: AnalyticsHandler? - ) { + suspend fun waitForCheckout(productType: ProductType, analyticsHandler: GlobalAnalyticsEventHandler?) { val result = shopifyService.checkout(true, checkouts[productType]!!.id) result.onSuccess { - if (it.order != null) { - analyticsHandler?.logShopifyOrder(order = it.order) - } + ifNotNull(analyticsHandler, it.order) { handler, order -> handler.handleShopifyOrderEvent(order) } } } 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 cc57977202..4b15ee3cb5 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 @@ -24,6 +24,12 @@ open class IndeterminateProgressButtonWidget( if (initialState != ProgressState.Done) changeState(initialState) } + var isEnabled: Boolean + get() = button.isEnabled + set(value) { + button.isEnabled = value + } + override val mainView: View = button override fun changeState(state: WidgetState) { diff --git a/app/src/main/java/com/tangem/tap/common/toggleWidget/RefreshBalanceWidget.kt b/app/src/main/java/com/tangem/tap/common/toggleWidget/RefreshBalanceWidget.kt index 277f839c05..7bccdffc08 100644 --- a/app/src/main/java/com/tangem/tap/common/toggleWidget/RefreshBalanceWidget.kt +++ b/app/src/main/java/com/tangem/tap/common/toggleWidget/RefreshBalanceWidget.kt @@ -2,7 +2,14 @@ package com.tangem.tap.common.toggleWidget import android.view.View import android.view.ViewGroup -import android.view.animation.* +import android.view.animation.AccelerateInterpolator +import android.view.animation.AlphaAnimation +import android.view.animation.Animation +import android.view.animation.AnimationSet +import android.view.animation.AnticipateOvershootInterpolator +import android.view.animation.LinearInterpolator +import android.view.animation.RotateAnimation +import android.view.animation.ScaleAnimation import android.widget.ViewSwitcher import com.tangem.tap.features.wallet.redux.ProgressState import com.tangem.wallet.R @@ -11,7 +18,7 @@ import com.tangem.wallet.R [REDACTED_AUTHOR] */ class RefreshBalanceWidget( - private val root: ViewGroup + private val root: ViewGroup, ) : ViewStateWidget { var isShowing: Boolean? = null @@ -19,7 +26,6 @@ class RefreshBalanceWidget( override val mainView: View = root.findViewById(R.id.btn_refresh_balance) - private val viewSwitcher: ViewSwitcher by lazy { mainView.findViewById(R.id.switcher_refresh_arrow_to_progress) } private val arrowView = mainView.findViewById(R.id.imv_arrow_refresh) @@ -46,6 +52,7 @@ class RefreshBalanceWidget( currentStateByView == ProgressState.Done -> if (progressState == ProgressState.Error) return } + mainView.isClickable = currentStateByView == ProgressState.Done || currentStateByView == ProgressState.Error animateState(progressState) viewSwitcher.showNext() } @@ -60,7 +67,7 @@ class RefreshBalanceWidget( progressViewAnimation = RotateAnimation( 0f, 360f, Animation.RELATIVE_TO_SELF, 0.5f, - Animation.RELATIVE_TO_SELF, 0.5f + Animation.RELATIVE_TO_SELF, 0.5f, ) progressViewAnimation?.duration = 700 progressViewAnimation?.interpolator = AccelerateInterpolator() @@ -88,11 +95,13 @@ class RefreshBalanceWidget( class ShowAnimation : AnimationSet(true) { init { - addAnimation(ScaleAnimation( - 0f, 1f, - 0f, 1f, - Animation.RELATIVE_TO_SELF, 0.5f, - Animation.RELATIVE_TO_SELF, 0.5f) + addAnimation( + ScaleAnimation( + 0f, 1f, + 0f, 1f, + Animation.RELATIVE_TO_SELF, 0.5f, + Animation.RELATIVE_TO_SELF, 0.5f, + ), ) addAnimation(AlphaAnimation(0f, 1f)) duration = 300 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 98694f525b..296b47185b 100644 --- a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt @@ -26,10 +26,10 @@ import com.tangem.operations.derivation.DeriveMultipleWalletPublicKeysTask import com.tangem.operations.pins.CheckUserCodesCommand import com.tangem.operations.pins.CheckUserCodesResponse import com.tangem.operations.pins.SetUserCodeCommand -import com.tangem.tap.common.analytics.Analytics -import com.tangem.tap.common.analytics.AnalyticsEvent -import com.tangem.tap.common.analytics.AnalyticsHandler -import com.tangem.tap.common.analytics.AnalyticsParam +import com.tangem.tap.common.analytics.AnalyticsAnOld +import com.tangem.tap.common.analytics.AnalyticsEventAnOld +import com.tangem.tap.common.analytics.AnalyticsParamAnOld +import com.tangem.tap.common.analytics.GlobalAnalyticsEventHandler import com.tangem.tap.domain.tasks.CreateWalletAndRescanTask import com.tangem.tap.domain.tasks.product.ResetToFactorySettingsTask import com.tangem.tap.domain.tasks.product.ScanProductTask @@ -45,12 +45,12 @@ import kotlin.coroutines.suspendCoroutine class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Context) { suspend fun scanProduct( - analyticsHandler: AnalyticsHandler?, + analyticsHandler: GlobalAnalyticsEventHandler?, userTokensRepository: UserTokensRepository, additionalBlockchainsToDerive: Collection? = null, messageRes: Int? = null, ): CompletionResult { - analyticsHandler?.triggerEvent(AnalyticsEvent.READY_TO_SCAN, null) + analyticsHandler?.handleAnalyticsEvent(AnalyticsEventAnOld.READY_TO_SCAN, card = null) val message = Message(context.getString(messageRes ?: R.string.initial_message_scan_header)) return runTaskAsyncReturnOnMain( @@ -70,30 +70,30 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co } private fun sendScanResultsToAnalytics( - analyticsHandler: AnalyticsHandler?, + analyticsHandler: GlobalAnalyticsEventHandler?, result: CompletionResult, ) { when (result) { is CompletionResult.Success -> { - analyticsHandler?.triggerEvent( - event = AnalyticsEvent.CARD_IS_SCANNED, + analyticsHandler?.handleAnalyticsEvent( + event = AnalyticsEventAnOld.CARD_IS_SCANNED, card = result.data.card, blockchain = result.data.walletData?.blockchain, ) if (DemoHelper.isDemoCard(result.data)) { - analyticsHandler?.triggerEvent( - event = AnalyticsEvent.DEMO_MODE_ACTIVATED, + analyticsHandler?.handleAnalyticsEvent( + event = AnalyticsEventAnOld.DEMO_MODE_ACTIVATED, + params = mapOf(AnalyticsParamAnOld.CARD_ID.param to result.data.card.cardId), card = result.data.card, blockchain = result.data.walletData?.blockchain, - params = mapOf(AnalyticsParam.CARD_ID.param to result.data.card.cardId), ) } } is CompletionResult.Failure -> (result.error as? TangemSdkError)?.let { error -> - analyticsHandler?.logCardSdkError( + analyticsHandler?.handleCardSdkErrorEvent( error = error, - actionToLog = Analytics.ActionToLog.Scan, + action = AnalyticsAnOld.ActionToLog.Scan, ) } } @@ -101,7 +101,8 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co suspend fun createWallet(cardId: String?): CompletionResult { return runTaskAsyncReturnOnMain( - CreateWalletAndRescanTask(), cardId, + CreateWalletAndRescanTask(), + cardId, initialMessage = Message(context.getString(R.string.initial_message_create_wallet_body)), ) } diff --git a/app/src/main/java/com/tangem/tap/domain/TangemSigner.kt b/app/src/main/java/com/tangem/tap/domain/TangemSigner.kt index 9e22833682..af5cdb6a5c 100644 --- a/app/src/main/java/com/tangem/tap/domain/TangemSigner.kt +++ b/app/src/main/java/com/tangem/tap/domain/TangemSigner.kt @@ -14,6 +14,7 @@ class TangemSigner( private val card: Card, private val tangemSdk: TangemSdk, private val initialMessage: Message, + private val accessCode: String? = null, private val signerCallback: (TangemSignerResponse) -> Unit, ) : TransactionSigner { @@ -29,6 +30,7 @@ class TangemSigner( runnable = task, cardId = cardId, initialMessage = initialMessage, + accessCode = accessCode, ) { result -> when (result) { is CompletionResult.Success -> { 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 9d0ae5a366..bd530b0796 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 @@ -16,6 +16,7 @@ data class Config( val mercuryoWidgetId: String = "", val mercuryoSecret: String = "", val appsFlyerDevKey: String = "", + val amplitudeApiKey: String = "", val blockchainSdkConfig: BlockchainSdkConfig = BlockchainSdkConfig(), val isSendingToPayIdEnabled: Boolean = true, val isTopUpEnabled: Boolean = false, @@ -90,6 +91,7 @@ class ConfigManager { tronGridApiKey = values.tronGridApiKey ), appsFlyerDevKey = values.appsFlyerDevKey, + amplitudeApiKey = values.amplitudeApiKey, shopify = values.shopifyShop, zendesk = values.zendesk, saltPayConfig = values.saltPay, @@ -107,6 +109,7 @@ class ConfigManager { infuraProjectId = values.infuraProjectId ), appsFlyerDevKey = values.appsFlyerDevKey, + amplitudeApiKey = values.amplitudeApiKey, 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 70c7ca73db..1c537e7909 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 @@ -30,9 +30,13 @@ class ConfigValueModel( val zendesk: ZendeskConfig?, val saltPay: SaltPayConfig, val tronGridApiKey: String, + val amplitudeApiKey: String, ) -class ConfigModel(val features: FeatureModel?, val configValues: ConfigValueModel?) { +class ConfigModel( + val features: FeatureModel?, + val configValues: ConfigValueModel?, +) { companion object { fun empty(): ConfigModel = ConfigModel(null, 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 index 6ad14c42b0..ac8c8cd359 100644 --- 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 @@ -3,10 +3,10 @@ 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.domain.moduleMessage.saltPay.converter.SaltPayActivationErrorConverter +import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayActivationError 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] @@ -28,7 +28,7 @@ class SaltPayErrorConverter( ) : ModuleMessageConverter { override fun convert(message: SaltPayModuleError): ConvertedMessage? = when (message) { - is SaltPayRegistrationError -> SaltPayRegistrationErrorConverter(context).convert(message) + is SaltPayActivationError -> SaltPayActivationErrorConverter(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/SaltPayActivationErrorConverter.kt similarity index 74% rename from app/src/main/java/com/tangem/tap/domain/moduleMessage/saltPay/converter/SaltPayRegistrationErrorConverter.kt rename to app/src/main/java/com/tangem/tap/domain/moduleMessage/saltPay/converter/SaltPayActivationErrorConverter.kt index 554a0cab44..65de384d44 100644 --- 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/SaltPayActivationErrorConverter.kt @@ -3,40 +3,40 @@ 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.tap.features.onboarding.products.wallet.saltPay.message.SaltPayActivationError import com.tangem.wallet.R /** [REDACTED_AUTHOR] */ -internal class SaltPayRegistrationErrorConverter( +internal class SaltPayActivationErrorConverter( private val context: Context, -) : ModuleMessageConverter { +) : ModuleMessageConverter { - override fun convert(message: SaltPayRegistrationError): ConvertedDialogMessage? { - val saltPayError = (message as? SaltPayRegistrationError) ?: throw UnsupportedOperationException() + override fun convert(message: SaltPayActivationError): ConvertedDialogMessage? { + val saltPayError = (message as? SaltPayActivationError) ?: throw UnsupportedOperationException() val dialogMessage = when (saltPayError) { - SaltPayRegistrationError.NoGas -> { + SaltPayActivationError.NoGas -> { ConvertedDialogMessage( title = context.getString(R.string.saltpay_error_no_gas_title), message = context.getString(R.string.saltpay_error_no_gas_message), ) } - SaltPayRegistrationError.EmptyBackupCardScanned -> { + SaltPayActivationError.EmptyBackupCardScanned -> { ConvertedDialogMessage( title = context.getString(R.string.saltpay_error_empty_backup_title), message = context.getString(R.string.saltpay_error_empty_backup_message), ) } - SaltPayRegistrationError.WeakPin -> { + SaltPayActivationError.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) { + val errorMessage = if (saltPayError.message == SaltPayActivationError.EMPTY_MESSAGE) { saltPayError::class.java.simpleName } else { saltPayError.message diff --git a/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt b/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt index 1d02623442..feb58ca6f0 100644 --- a/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt @@ -14,14 +14,14 @@ import com.tangem.domain.common.ScanResponse import com.tangem.network.common.MoshiConverter import com.tangem.operations.wallet.CreateWalletResponse import com.tangem.tap.common.AssetReader -import com.tangem.tap.common.analytics.Analytics -import com.tangem.tap.common.analytics.AnalyticsHandler +import com.tangem.tap.common.analytics.AnalyticsAnOld +import com.tangem.tap.common.analytics.GlobalAnalyticsEventHandler import com.tangem.tap.tangemSdkManager class TwinCardsManager( private val card: Card, assetReader: AssetReader, - val analyticsHandler: AnalyticsHandler? + val analyticsHandler: GlobalAnalyticsEventHandler? ) { private val currentCardId: String = card.cardId @@ -37,9 +37,9 @@ class TwinCardsManager( is CompletionResult.Success -> currentCardPublicKey = response.data.wallet.publicKey.toHexString() is CompletionResult.Failure -> { (response.error as? TangemSdkError)?.let { error -> - analyticsHandler?.logCardSdkError( + analyticsHandler?.handleCardSdkErrorEvent( error, - Analytics.ActionToLog.CreateWallet, + AnalyticsAnOld.ActionToLog.CreateWallet, card = card ) } @@ -67,9 +67,9 @@ class TwinCardsManager( } is CompletionResult.Failure -> { (response.error as? TangemSdkError)?.let { error -> - analyticsHandler?.logCardSdkError( + analyticsHandler?.handleCardSdkErrorEvent( error, - Analytics.ActionToLog.CreateWallet, + AnalyticsAnOld.ActionToLog.CreateWallet, card = card ) } @@ -87,9 +87,9 @@ class TwinCardsManager( is CompletionResult.Success -> Result.Success(response.data) is CompletionResult.Failure -> { (response.error as? TangemSdkError)?.let { error -> - analyticsHandler?.logCardSdkError( + analyticsHandler?.handleCardSdkErrorEvent( error, - Analytics.ActionToLog.WriteIssuerData, + AnalyticsAnOld.ActionToLog.WriteIssuerData, card = card ) } diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectManager.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectManager.kt index de71029d2c..6bd6cddada 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectManager.kt @@ -4,7 +4,8 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.common.card.EllipticCurve import com.tangem.common.extensions.guard import com.tangem.domain.common.ScanResponse -import com.tangem.tap.common.analytics.Analytics +import com.tangem.tap.common.analytics.AnalyticsAnOld +import com.tangem.tap.common.analytics.logWcEvent import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.TapError @@ -222,9 +223,9 @@ class WalletConnectManager { } private fun onSessionClosed(session: WCSession) { - store.state.globalState.analyticsHandlers?.logWcEvent( - Analytics.WcAnalyticsEvent.Session( - Analytics.WcSessionEvent.Disconnect, sessions[session]?.peerMeta?.url + store.state.globalState.analyticsHandler?.logWcEvent( + AnalyticsAnOld.WcAnalyticsEvent.Session( + AnalyticsAnOld.WcSessionEvent.Disconnect, sessions[session]?.peerMeta?.url ) ) sessions.remove(session) @@ -371,9 +372,9 @@ class WalletConnectManager { ), ) } - store.state.globalState.analyticsHandlers?.logWcEvent( - Analytics.WcAnalyticsEvent.Session( - Analytics.WcSessionEvent.Connect, peer.url, + store.state.globalState.analyticsHandler?.logWcEvent( + AnalyticsAnOld.WcAnalyticsEvent.Session( + AnalyticsAnOld.WcSessionEvent.Connect, peer.url, ), ) } @@ -386,9 +387,9 @@ class WalletConnectManager { } client.onEthSendTransaction = { id: Long, transaction: WCEthereumTransaction -> Timber.d("onEthSendTransaction: $transaction") - store.state.globalState.analyticsHandlers?.logWcEvent( - Analytics.WcAnalyticsEvent.Action( - Analytics.WcAction.SendTransaction + store.state.globalState.analyticsHandler?.logWcEvent( + AnalyticsAnOld.WcAnalyticsEvent.Action( + AnalyticsAnOld.WcAction.SendTransaction ) ) sessions[client.session]?.toWalletConnectSession()?.let { sessionData -> @@ -404,9 +405,9 @@ class WalletConnectManager { } client.onEthSignTransaction = { id: Long, transaction: WCEthereumTransaction -> Timber.d("onEthSignTransaction: $transaction") - store.state.globalState.analyticsHandlers?.logWcEvent( - Analytics.WcAnalyticsEvent.Action( - Analytics.WcAction.SignTransaction + store.state.globalState.analyticsHandler?.logWcEvent( + AnalyticsAnOld.WcAnalyticsEvent.Action( + AnalyticsAnOld.WcAction.SignTransaction ) ) sessions[client.session]?.toWalletConnectSession()?.let { sessionData -> @@ -422,9 +423,9 @@ class WalletConnectManager { } client.onEthSign = { id: Long, message: WCEthereumSignMessage -> Timber.d("onEthSign: $message") - store.state.globalState.analyticsHandlers?.logWcEvent( - Analytics.WcAnalyticsEvent.Action( - Analytics.WcAction.PersonalSign + store.state.globalState.analyticsHandler?.logWcEvent( + AnalyticsAnOld.WcAnalyticsEvent.Action( + AnalyticsAnOld.WcAction.PersonalSign ) ) sessions[client.session]?.toWalletConnectSession()?.let { sessionData -> @@ -498,9 +499,9 @@ class WalletConnectManager { val message = EthSignHelper.tryToParseEthTypedMessage(request) if (message != null) { Timber.d("onEthSign_v4: $message") - store.state.globalState.analyticsHandlers?.logWcEvent( - Analytics.WcAnalyticsEvent.Action( - Analytics.WcAction.PersonalSign + store.state.globalState.analyticsHandler?.logWcEvent( + AnalyticsAnOld.WcAnalyticsEvent.Action( + AnalyticsAnOld.WcAction.PersonalSign ) ) sessions[client.session]?.toWalletConnectSession()?.let { sessionData -> diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt index b23a5e1456..f90eb7cbb9 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt @@ -24,8 +24,7 @@ import com.tangem.common.extensions.toDecompressedPublicKey import com.tangem.common.extensions.toHexString import com.tangem.crypto.CryptoUtils import com.tangem.operations.sign.SignHashCommand -import com.tangem.tap.common.analytics.Analytics -import com.tangem.tap.common.extensions.logSendTransactionError +import com.tangem.tap.common.analytics.AnalyticsAnOld import com.tangem.tap.common.extensions.safeUpdate import com.tangem.tap.common.extensions.toFormattedString import com.tangem.tap.features.details.redux.walletconnect.WalletConnectSession @@ -144,9 +143,9 @@ class WalletConnectSdkHelper { HEX_PREFIX + data.walletManager.wallet.recentTransactions.last().hash } is SimpleResult.Failure -> { - store.state.globalState.analyticsHandlers?.logSendTransactionError( + store.state.globalState.analyticsHandler?.handleBlockchainSdkErrorEvent( result.error, - Analytics.ActionToLog.WalletConnectTransaction, + AnalyticsAnOld.ActionToLog.WalletConnectTransaction, ) Timber.e(result.error as BlockchainSdkError) null @@ -174,9 +173,9 @@ class WalletConnectSdkHelper { } is CompletionResult.Failure -> { (result.error as? TangemSdkError)?.let { error -> - store.state.globalState.analyticsHandlers?.logCardSdkError( + store.state.globalState.analyticsHandler?.handleCardSdkErrorEvent( error, - Analytics.ActionToLog.WalletConnectSign, + AnalyticsAnOld.ActionToLog.WalletConnectSign, ) } Timber.e(result.error.customMessage) @@ -203,9 +202,9 @@ class WalletConnectSdkHelper { } is CompletionResult.Failure -> { (result.error as? TangemSdkError)?.let { error -> - store.state.globalState.analyticsHandlers?.logCardSdkError( + store.state.globalState.analyticsHandler?.handleCardSdkErrorEvent( error, - Analytics.ActionToLog.WalletConnectTransaction, + AnalyticsAnOld.ActionToLog.WalletConnectTransaction, ) } Timber.e(result.error.customMessage) @@ -279,9 +278,9 @@ class WalletConnectSdkHelper { } is CompletionResult.Failure -> { (result.error as? TangemSdkError)?.let { error -> - store.state.globalState.analyticsHandlers?.logCardSdkError( + store.state.globalState.analyticsHandler?.handleCardSdkErrorEvent( error, - Analytics.ActionToLog.WalletConnectSign, + AnalyticsAnOld.ActionToLog.WalletConnectSign, ) } Timber.e(result.error.customMessage) diff --git a/app/src/main/java/com/tangem/tap/features/BaseFragment.kt b/app/src/main/java/com/tangem/tap/features/BaseFragment.kt index 8b4d0d3f70..f0e6016021 100644 --- a/app/src/main/java/com/tangem/tap/features/BaseFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/BaseFragment.kt @@ -7,7 +7,10 @@ import android.view.View import android.view.ViewGroup import androidx.activity.OnBackPressedCallback import androidx.appcompat.widget.Toolbar +import androidx.core.view.MenuHost +import androidx.core.view.MenuProvider import androidx.fragment.app.Fragment +import androidx.lifecycle.Lifecycle import androidx.transition.TransitionInflater import com.google.android.material.snackbar.Snackbar import com.tangem.common.extensions.VoidCallback @@ -42,6 +45,17 @@ abstract class BaseFragment(layoutId: Int) : Fragment(layoutId), FragmentOnBackP store.dispatch(NavigationAction.PopBackTo()) } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + loadToolbarMenu()?.let { + val menuHost = requireActivity() as? MenuHost ?: return + menuHost.addMenuProvider(it, viewLifecycleOwner, Lifecycle.State.RESUMED) + } + } + + protected open fun loadToolbarMenu():MenuProvider? = null + fun showRetrySnackbar(message: String, action: VoidCallback) { val snackbar = Snackbar.make(mainView, message, Snackbar.LENGTH_INDEFINITE) snackbar.setAction(getString(R.string.common_retry)) { 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 cc9ccef1b6..309c2900d3 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 @@ -3,8 +3,8 @@ package com.tangem.tap.features.details.redux import com.tangem.common.CompletionResult import com.tangem.common.core.TangemSdkError 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.analytics.AnalyticsAnOld +import com.tangem.tap.common.analytics.AnalyticsParamAnOld import com.tangem.tap.common.extensions.dispatchNotification import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.redux.AppState @@ -110,9 +110,9 @@ class DetailsMiddleware { } is CompletionResult.Failure -> { (result.error as? TangemSdkError)?.let { error -> - store.state.globalState.analyticsHandlers?.logCardSdkError( + store.state.globalState.analyticsHandler?.handleCardSdkErrorEvent( error, - Analytics.ActionToLog.PurgeWallet, + AnalyticsAnOld.ActionToLog.PurgeWallet, card = store.state.detailsState.scanResponse?.card, ) } @@ -154,11 +154,11 @@ class DetailsMiddleware { } is CompletionResult.Failure -> { (result.error as? TangemSdkError)?.let { error -> - store.state.globalState.analyticsHandlers?.logCardSdkError( + store.state.globalState.analyticsHandler?.handleCardSdkErrorEvent( error = error, - actionToLog = Analytics.ActionToLog.ChangeSecOptions, - parameters = mapOf( - AnalyticsParam.NEW_SECURITY_OPTION to + action = AnalyticsAnOld.ActionToLog.ChangeSecOptions, + params = mapOf( + AnalyticsParamAnOld.NEW_SECURITY_OPTION to (selectedOption?.name ?: ""), ), card = store.state.detailsState.scanResponse?.card, 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 502437104c..1de39fa063 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 @@ -25,6 +25,7 @@ class DetailsViewModel(private val store: Store) { SettingsElement.WalletConnect -> { if (state.scanResponse?.card?.isMultiwalletAllowed == true) it else null } + SettingsElement.SendFeedback -> if (state.scanResponse?.card?.isSaltPay != true) it else null SettingsElement.LinkMoreCards -> { // if (state.createBackupAllowed) it else null // TODO: SaltPay: temporary excluding backup process for Visa cards @@ -78,8 +79,7 @@ class DetailsViewModel(private val store: Store) { store.dispatch(DetailsAction.CreateBackup) } SettingsElement.TermsOfService -> { - store.dispatch(DisclaimerAction.ShowAcceptedDisclaimer) - store.dispatch(NavigationAction.NavigateTo(AppScreen.Disclaimer)) + store.dispatch(DisclaimerAction.Show()) } SettingsElement.TermsOfUse -> { store.dispatch(DetailsAction.ShowDisclaimer) diff --git a/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerAction.kt b/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerAction.kt index 0a444895d7..ca82ff1fee 100644 --- a/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerAction.kt +++ b/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerAction.kt @@ -1,8 +1,17 @@ package com.tangem.tap.features.disclaimer.redux +import com.tangem.common.extensions.VoidCallback import org.rekotlin.Action -sealed class DisclaimerAction: Action { - object AcceptDisclaimer : DisclaimerAction() - object ShowAcceptedDisclaimer : DisclaimerAction() +sealed class DisclaimerAction : Action { + + data class SetDisclaimerType( + val type: DisclaimerType, + ) : DisclaimerAction() + + data class Show(val onAcceptCallback: VoidCallback? = null) : DisclaimerAction() + data class AcceptDisclaimer(val type: DisclaimerType) : DisclaimerAction() + + internal data class UpdateState(val type: DisclaimerType, val accepted: Boolean) : DisclaimerAction() + internal data class SetOnAcceptCallback(val onAcceptCallback: VoidCallback? = null) : DisclaimerAction() } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerMiddleware.kt b/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerMiddleware.kt index 12a1cfaccd..1b42d7b4fc 100644 --- a/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerMiddleware.kt @@ -1,28 +1,65 @@ package com.tangem.tap.features.disclaimer.redux -import com.tangem.tap.common.post import com.tangem.tap.common.redux.AppState +import com.tangem.tap.common.redux.navigation.AppScreen import com.tangem.tap.common.redux.navigation.NavigationAction -import com.tangem.tap.features.home.redux.HomeAction import com.tangem.tap.preferencesStorage import com.tangem.tap.store +import org.rekotlin.Action import org.rekotlin.Middleware class DisclaimerMiddleware { val disclaimerMiddleware: Middleware = { dispatch, state -> { next -> { action -> - when (action) { - is DisclaimerAction.AcceptDisclaimer -> { - preferencesStorage.saveDisclaimerAccepted() - store.dispatch(NavigationAction.PopBackTo()) - - //delayed sending used for better animation - post(550) { store.dispatch(HomeAction.ReadCard) } - } - } + state()?.let { handleDisclaimerMiddleware(action, it) } next(action) } } } -} \ No newline at end of file +} + +private fun handleDisclaimerMiddleware(action: Action, appState: AppState) { + val state = appState.disclaimerState + + when (action) { + is DisclaimerAction.SetDisclaimerType -> { + handleUpdateState = action.type.createUpdateState() + } + is DisclaimerAction.Show -> { + handleUpdateState = state.type.createUpdateState() + store.dispatch(DisclaimerAction.SetOnAcceptCallback(action.onAcceptCallback)) + store.dispatch(NavigationAction.NavigateTo(AppScreen.Disclaimer)) + } + is DisclaimerAction.AcceptDisclaimer -> { + action.type.accept() + handleUpdateState = action.type.createUpdateState() + store.dispatch(NavigationAction.PopBackTo()) + state.onAcceptCallback?.invoke() + store.dispatch(DisclaimerAction.SetOnAcceptCallback(null)) + } + } +} + +private fun DisclaimerType.accept() = when (this) { + DisclaimerType.SaltPay -> preferencesStorage.disclaimerPrefStorage.hasSaltPayTosAccepted = true + DisclaimerType.Tangem -> preferencesStorage.disclaimerPrefStorage.hasTangemTosAccepted = true +} + +fun DisclaimerType.isAccepted(): Boolean = when (this) { + DisclaimerType.SaltPay -> preferencesStorage.disclaimerPrefStorage.hasSaltPayTosAccepted + DisclaimerType.Tangem -> preferencesStorage.disclaimerPrefStorage.hasTangemTosAccepted +} + +private fun DisclaimerType.createUpdateState(): DisclaimerAction.UpdateState { + return DisclaimerAction.UpdateState(this, this.isAccepted()) +} + +private var handleUpdateState: DisclaimerAction.UpdateState = DisclaimerAction.UpdateState( + type = DisclaimerType.Tangem, + accepted = false, +) + set(value) { + field = value + store.dispatch(value) + } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerReducer.kt b/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerReducer.kt index 40408ea343..1b282877f2 100644 --- a/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerReducer.kt @@ -13,9 +13,15 @@ private fun internalReduce(action: Action, state: AppState): DisclaimerState { if (action !is DisclaimerAction) return state.disclaimerState val disclaimerState = state.disclaimerState + return when (action) { - is DisclaimerAction.AcceptDisclaimer, DisclaimerAction.ShowAcceptedDisclaimer -> { - disclaimerState.copy(accepted = true) - } + is DisclaimerAction.UpdateState -> disclaimerState.copy( + type = action.type, + accepted = action.accepted, + ) + is DisclaimerAction.SetOnAcceptCallback -> disclaimerState.copy( + onAcceptCallback = action.onAcceptCallback, + ) + else -> disclaimerState } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerState.kt b/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerState.kt index b92252ae01..530a4afb78 100644 --- a/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerState.kt +++ b/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerState.kt @@ -1,7 +1,30 @@ package com.tangem.tap.features.disclaimer.redux +import android.net.Uri +import com.tangem.common.card.Card +import com.tangem.common.extensions.VoidCallback +import com.tangem.domain.common.ScanResponse +import com.tangem.domain.common.TapWorkarounds.isSaltPay import org.rekotlin.StateType data class DisclaimerState( - val accepted: Boolean = false + val accepted: Boolean = false, + val type: DisclaimerType = DisclaimerType.Tangem, + val onAcceptCallback: VoidCallback? = null, ) : StateType + +sealed class DisclaimerType( + val uri: Uri, +) { + object Tangem : DisclaimerType(Uri.parse("https://tangem.com/tangem_tos.html")) + object SaltPay : DisclaimerType(Uri.parse("https://tangem.com/soltpay_tos.html")) + + companion object { + fun get(scanResponse: ScanResponse): DisclaimerType = get(scanResponse.card) + + fun get(card: Card): DisclaimerType = when { + card.isSaltPay -> SaltPay + else -> Tangem + } + } +} \ No newline at end of file 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 abbd7bf8de..b13e5b588b 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 @@ -55,25 +55,25 @@ class DisclaimerFragment : Fragment(R.layout.fragment_disclaimer), binding.toolbar.setNavigationOnClickListener { store.dispatch(NavigationAction.PopBackTo()) } - initAndLoadTOS() - setOnClickListeners() + initViews() } - private fun initAndLoadTOS() { + private fun initViews() { binding.webView.configureSettings() - binding.webView.loadUrl("file:///android_asset/tos.html") } - private fun setOnClickListeners() { - binding.btnAccept.setOnClickListener { store.dispatch(DisclaimerAction.AcceptDisclaimer) } - } - - override fun newState(state: DisclaimerState) { + override fun newState(state: DisclaimerState) = with(binding) { if (activity == null || view == null) return if (state.accepted) { - binding.halfTransparentOverlay.hide() - binding.btnAccept.hide() + halfTransparentOverlay.hide() + btnAccept.hide() } + + btnAccept.setOnClickListener { + store.dispatch(DisclaimerAction.AcceptDisclaimer(state.type)) + } + + webView.loadUrl(state.type.uri.toString()) } } \ No newline at end of file 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 487362e5c1..839c7a3828 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 @@ -16,7 +16,6 @@ import com.tangem.tap.common.redux.navigation.NavigationAction import com.tangem.tap.features.home.compose.StoriesScreen import com.tangem.tap.features.home.redux.HomeAction import com.tangem.tap.features.home.redux.HomeState -import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction import com.tangem.tap.features.tokens.redux.TokensAction import com.tangem.tap.store import org.rekotlin.StoreSubscriber @@ -38,7 +37,6 @@ class HomeFragment : Fragment(), StoreSubscriber { ): View? { val context = container?.context ?: return null - store.dispatch(BackupAction.CheckForUnfinishedBackup) composeView = ComposeView(context).apply { setContent { AppCompatTheme { 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 c22bd6c043..0a276dff4f 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 @@ -4,15 +4,14 @@ import com.tangem.tap.common.entities.IndeterminateProgressButton import org.rekotlin.Action sealed class HomeAction : Action { + + object Init : HomeAction() + data class ShouldScanCardOnResume(val shouldScanCard: Boolean) : HomeAction() + // from ui object ReadCard : HomeAction() data class ScanInProgress(val scanInProgress: Boolean) : HomeAction() data class GoToShop(val userCountryCode: String?) : HomeAction() - // internal - data class ShouldScanCardOnResume(val shouldScanCard: Boolean) : HomeAction() - object Init : HomeAction() - - 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 7ecc17b56e..4a10d18d51 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,19 +1,23 @@ package com.tangem.tap.features.home.redux +import com.tangem.common.card.Card 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.operations.backup.BackupService 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.backupService +import com.tangem.tap.common.analytics.AnalyticsEventAnOld +import com.tangem.tap.common.analytics.AnalyticsParamAnOld +import com.tangem.tap.common.analytics.GetCardSourceParamsAnOld +import com.tangem.tap.common.analytics.events.AnalyticsParam 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.extensions.primaryCardIsSaltPayVisa import com.tangem.tap.common.postUiDelayBg import com.tangem.tap.common.redux.AppDialog import com.tangem.tap.common.redux.AppState @@ -21,6 +25,9 @@ import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.common.redux.navigation.AppScreen import com.tangem.tap.common.redux.navigation.FragmentShareTransition import com.tangem.tap.common.redux.navigation.NavigationAction +import com.tangem.tap.features.disclaimer.redux.DisclaimerAction +import com.tangem.tap.features.disclaimer.redux.DisclaimerType +import com.tangem.tap.features.disclaimer.redux.isAccepted 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 @@ -28,8 +35,8 @@ 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.redux.BackupAction +import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayExceptionHandler 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 @@ -65,8 +72,8 @@ private fun handleHomeAction(appState: () -> AppState?, action: Action, dispatch is HomeAction.Init -> { store.dispatch(GlobalAction.RestoreAppCurrency) store.dispatch(GlobalAction.ExchangeManager.Init) - store.dispatch(HomeAction.SetTermsOfUseState(preferencesStorage.wasDisclaimerAccepted())) store.dispatch(GlobalAction.FetchUserCountry) + store.dispatch(BackupAction.CheckForUnfinishedBackup) } is HomeAction.ShouldScanCardOnResume -> { if (action.shouldScanCard) { @@ -75,43 +82,97 @@ private fun handleHomeAction(appState: () -> AppState?, action: Action, dispatch } } 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) } - } + changeButtonState(ButtonState.PROGRESS) + val scanCardAction = GlobalAction.ScanCard( + onSuccess = { scanResponse -> + store.dispatch(HomeAction.ScanInProgress(false)) + checkForUnfinishedBackupForSaltPay( + backupService = backupService, + scanResponse = scanResponse, + nextHandler = { + showDisclaimerIfNeed( + scanResponse = scanResponse, + nextHandler = ::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), + store.state.globalState.analyticsHandler?.handleAnalyticsEvent( + event = AnalyticsEventAnOld.GET_CARD, + params = mapOf(AnalyticsParamAnOld.SOURCE.param to GetCardSourceParamsAnOld.WELCOME.param), ) } } } +/** + * It checks only the SaltPay cards. To check for unfinished backups for the standard Wallet cards + * see BackupAction.CheckForUnfinishedBackup + * If user touches card other than Visa SaltPay - show dialog and block next processing + */ +private fun checkForUnfinishedBackupForSaltPay( + backupService: BackupService, + scanResponse: ScanResponse, + nextHandler: (ScanResponse) -> Unit, +) { + if (!backupService.hasIncompletedBackup || !backupService.primaryCardIsSaltPayVisa()) { + nextHandler(scanResponse) + return + } + + fun isTheSamePrimaryCard(card: Card): Boolean { + return backupService.primaryCardId?.let { it == card.cardId } ?: false + } + + if (scanResponse.isSaltPayWallet() || !isTheSamePrimaryCard(scanResponse.card)) { + changeButtonState(ButtonState.ENABLED) + showSaltPayTapVisaLogoCardDialog() + } else { + nextHandler(scanResponse) + } +} + +private fun showDisclaimerIfNeed(scanResponse: ScanResponse, nextHandler: (ScanResponse) -> Unit) { + val disclaimerType = DisclaimerType.get(scanResponse) + store.dispatch(DisclaimerAction.SetDisclaimerType(disclaimerType)) + + if (disclaimerType.isAccepted()) { + nextHandler((scanResponse)) + } else { + changeButtonState(ButtonState.ENABLED) + store.dispatch( + DisclaimerAction.Show { + changeButtonState(ButtonState.PROGRESS) + nextHandler(scanResponse) + }, + ) + } +} + private fun onScanSuccess(scanResponse: ScanResponse) { val globalState = store.state.globalState val tapWalletManager = globalState.tapWalletManager tapWalletManager.updateConfigManager(scanResponse) - store.dispatch(HomeAction.ScanInProgress(false)) + globalState.analyticsHandler.attachToAllEvents(AnalyticsParam.BatchId, scanResponse.card.batchId) + store.dispatch(TwinCardsAction.IfTwinsPrepareState(scanResponse)) if (scanResponse.isSaltPay()) { if (scanResponse.isSaltPayVisa()) { scope.launch { - val result = OnboardingSaltPayHelper.isOnboardingCase(scanResponse) + val (manager, config) = OnboardingSaltPayState.initDependency(scanResponse) + val result = OnboardingSaltPayHelper.isOnboardingCase(scanResponse, manager) delay(500) withMainContext { when (result) { @@ -119,10 +180,8 @@ private fun onScanSuccess(scanResponse: ScanResponse) { 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) @@ -131,20 +190,7 @@ private fun onScanSuccess(scanResponse: 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", - ) - } - } + SaltPayExceptionHandler.handle(result.error) } } } @@ -152,12 +198,7 @@ private fun onScanSuccess(scanResponse: ScanResponse) { } 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, - ), - ) + showSaltPayTapVisaLogoCardDialog() } else { navigateTo(AppScreen.Wallet, null) scope.launch { store.onCardScanned(scanResponse) } @@ -180,6 +221,15 @@ private fun onScanSuccess(scanResponse: ScanResponse) { } } +private fun showSaltPayTapVisaLogoCardDialog() { + store.dispatchDialogShow( + AppDialog.SimpleOkDialogRes( + headerId = R.string.saltpay_error_empty_backup_title, + messageId = R.string.saltpay_error_empty_backup_message, + ), + ) +} + private fun onScanFailure(error: TangemError) { store.dispatch(HomeAction.ScanInProgress(false)) changeButtonState(ButtonState.ENABLED) diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingMenuProvider.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingMenuProvider.kt new file mode 100644 index 0000000000..2fff1d0fab --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingMenuProvider.kt @@ -0,0 +1,27 @@ +package com.tangem.tap.features.onboarding + +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import androidx.core.view.MenuProvider +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 OnboardingMenuProvider : MenuProvider { + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.menu_onboarding, menu) + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) { + R.id.menu_item_chat_support -> { + store.dispatch(GlobalAction.OpenChat(SupportInfo())) + true + } + else -> false + } +} \ No newline at end of file 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 index 8b56bbc65a..209fc789eb 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingSaltPayHelper.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingSaltPayHelper.kt @@ -3,10 +3,11 @@ 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 +import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayActivationManager +import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayActivationError +import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.SaltPayActivationStep +import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.update +import com.tangem.tap.preferencesStorage /** [REDACTED_AUTHOR] @@ -14,21 +15,36 @@ import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.toSaltPa class OnboardingSaltPayHelper { companion object { - suspend fun isOnboardingCase(scanResponse: ScanResponse): Result { + suspend fun isOnboardingCase( + scanResponse: ScanResponse, + manager: SaltPayActivationManager, + ): 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 } + var updatedStep = manager.update(SaltPayActivationStep.None, null).successOr { return it } - val isRegistrationOnboardingCase = registrationStep != SaltPayRegistrationStep.Finished - val isBackupOnboardingCase = scanResponse.card.backupStatus?.isActive == false - Result.Success(isRegistrationOnboardingCase || isBackupOnboardingCase) + val cardStorage = preferencesStorage.usedCardsPrefStorage + val activationIsFinished = cardStorage.isActivationFinished(scanResponse.card.cardId) + if (updatedStep == SaltPayActivationStep.Success && activationIsFinished) { + updatedStep = SaltPayActivationStep.Finished + } + val isActivationCase = updatedStep != SaltPayActivationStep.Finished + val isBackupCase = scanResponse.card.backupStatus?.isActive == false + Result.Success(isActivationCase || isBackupCase) } catch (ex: Exception) { Result.Failure(ex) - } catch (error: SaltPayRegistrationError){ + } catch (error: SaltPayActivationError) { Result.Failure(error) } } + + fun testProceedToOnboarding( + scanResponse: ScanResponse, + manager: SaltPayActivationManager, + ): Result = Result.Success(true) + + fun testProceedToMainScreen( + scanResponse: ScanResponse, + manager: SaltPayActivationManager, + ): Result = Result.Success(false) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/BaseOnboardingFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/BaseOnboardingFragment.kt index f29444abc5..032cce60d3 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/BaseOnboardingFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/BaseOnboardingFragment.kt @@ -1,9 +1,14 @@ package com.tangem.tap.features.onboarding.products +import android.os.Bundle +import android.view.View +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.MenuProvider import by.kirich1409.viewbindingdelegate.viewBinding import com.tangem.tap.common.extensions.show import com.tangem.tap.common.transitions.HomeToOnboardingTransition import com.tangem.tap.features.BaseStoreFragment +import com.tangem.tap.features.onboarding.OnboardingMenuProvider import com.tangem.wallet.R import com.tangem.wallet.databinding.FragmentOnboardingMainBinding import com.tangem.wallet.databinding.ViewOnboardingProgressBinding @@ -22,6 +27,12 @@ abstract class BaseOnboardingFragment : BaseStoreFragment(R.layout.fragment_o exitTransition = HomeToOnboardingTransition() } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + (activity as? AppCompatActivity)?.setSupportActionBar(binding.toolbar) + } + + override fun loadToolbarMenu(): MenuProvider? = OnboardingMenuProvider() protected fun showConfetti(show: Boolean) = with(binding.vConfetti) { lavConfetti.show(show) 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 a81bc28e87..a54a27bd28 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 @@ -1,19 +1,14 @@ package com.tangem.tap.features.onboarding.products.note -import android.graphics.Bitmap -import android.graphics.drawable.BitmapDrawable import android.os.Bundle import android.view.View import android.view.animation.OvershootInterpolator -import android.widget.ImageView import androidx.annotation.LayoutRes import androidx.constraintlayout.widget.ConstraintSet import androidx.core.view.isVisible import androidx.transition.TransitionManager import coil.load import com.tangem.blockchain.common.Blockchain -import com.tangem.tangem_sdk_new.extensions.fadeIn -import com.tangem.tangem_sdk_new.extensions.fadeOut import com.tangem.tap.common.extensions.getDrawableCompat import com.tangem.tap.common.extensions.stripZeroPlainString import com.tangem.tap.common.redux.navigation.ShareElement @@ -33,23 +28,18 @@ import com.tangem.wallet.R class OnboardingNoteFragment : BaseOnboardingFragment() { private var previousStep: OnboardingNoteStep = OnboardingNoteStep.None + private val mainBinding by lazy { binding.vMain } private lateinit var btnRefreshBalanceWidget: RefreshBalanceWidget - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - postponeEnterTransition() - } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) addBackPressHandler(this) - binding.onboardingTopContainer.imvFrontCard.transitionName = ShareElement.imvFrontCard - startPostponedEnterTransition() + mainBinding.onboardingTopContainer.imvFrontCard.transitionName = ShareElement.imvFrontCard binding.toolbar.setTitle(R.string.onboarding_title) - btnRefreshBalanceWidget = RefreshBalanceWidget(binding.onboardingTopContainer.onboardingWalletContainer) + btnRefreshBalanceWidget = RefreshBalanceWidget(mainBinding.onboardingTopContainer.onboardingWalletContainer) store.dispatch(OnboardingNoteAction.LoadCardArtwork) store.dispatch(OnboardingNoteAction.DetermineStepOfScreen) @@ -67,7 +57,7 @@ class OnboardingNoteFragment : BaseOnboardingFragment() { override fun newState(state: OnboardingNoteState) { if (activity == null || view == null) return - binding.onboardingTopContainer.imvFrontCard.load(state.cardArtworkUrl) { + mainBinding.onboardingTopContainer.imvFrontCard.load(state.cardArtworkUrl) { placeholder(R.drawable.card_placeholder_black) error(R.drawable.card_placeholder_black) fallback(R.drawable.card_placeholder_black) @@ -90,7 +80,7 @@ class OnboardingNoteFragment : BaseOnboardingFragment() { private fun setBalance(state: OnboardingNoteState) { if (state.walletBalance.currency.blockchain == Blockchain.Unknown) return - with (binding.onboardingTopContainer.onboardingTvBalance) { + with(mainBinding.onboardingTopContainer.onboardingTvBalance) { if (state.balanceCriticalError == null) { val balanceValue = state.walletBalance.value.stripZeroPlainString() tvBalanceValue.text = balanceValue @@ -102,7 +92,7 @@ class OnboardingNoteFragment : BaseOnboardingFragment() { } } - private fun setupNoneState() = with(binding.onboardingActionContainer) { + private fun setupNoneState() = with(mainBinding.onboardingActionContainer) { btnMainAction.isVisible = false btnAlternativeAction.isVisible = false btnRefreshBalanceWidget.show(false) @@ -116,7 +106,7 @@ class OnboardingNoteFragment : BaseOnboardingFragment() { } private fun setupCreateWalletState(state: OnboardingNoteState) = - with(binding.onboardingActionContainer) { + with(mainBinding.onboardingActionContainer) { btnMainAction.setText(R.string.onboarding_create_wallet_button_create_wallet) btnMainAction.setOnClickListener { store.dispatch(OnboardingNoteAction.CreateWallet) } btnAlternativeAction.setText(R.string.onboarding_button_what_does_it_mean) @@ -127,15 +117,15 @@ class OnboardingNoteFragment : BaseOnboardingFragment() { tvHeader.setText(R.string.onboarding_create_wallet_header) tvBody.setText(R.string.onboarding_create_wallet_body) - binding.onboardingTopContainer.imvCardBackground.setBackgroundDrawable( - requireContext().getDrawableCompat(R.drawable.shape_circle) + mainBinding.onboardingTopContainer.imvCardBackground.setBackgroundDrawable( + requireContext().getDrawableCompat(R.drawable.shape_circle), ) updateConstraints(state.currentStep, R.layout.lp_onboarding_create_wallet) btnAlternativeAction.isVisible = false // temporary } - private fun setupTopUpWalletState(state: OnboardingNoteState) = with(binding.onboardingActionContainer) { + private fun setupTopUpWalletState(state: OnboardingNoteState) = with(mainBinding.onboardingActionContainer) { if (state.isBuyAllowed) { btnMainAction.setText(R.string.onboarding_top_up_button_but_crypto) btnMainAction.setOnClickListener { @@ -163,7 +153,7 @@ class OnboardingNoteFragment : BaseOnboardingFragment() { state.walletBalance.amountToCreateAccount?.let { amount -> val tvBodyMessage = getString( R.string.onboarding_top_up_body_no_account_error, - amount, state.walletBalance.currency.currencySymbol + amount, state.walletBalance.currency.currencySymbol, ) tvBody.text = tvBodyMessage } @@ -175,12 +165,13 @@ class OnboardingNoteFragment : BaseOnboardingFragment() { store.dispatch(OnboardingNoteAction.Balance.Update) } } - binding.onboardingTopContainer.imvCardBackground - .setBackgroundDrawable(requireContext().getDrawableCompat(R.drawable.shape_rectangle_rounded_8)) + + val drawable = requireContext().getDrawableCompat(R.drawable.shape_rectangle_rounded_8) + mainBinding.onboardingTopContainer.imvCardBackground.setBackgroundDrawable(drawable) updateConstraints(state.currentStep, R.layout.lp_onboarding_topup_wallet) } - private fun setupDoneState(state: OnboardingNoteState) = with(binding.onboardingActionContainer) { + private fun setupDoneState(state: OnboardingNoteState) = with(mainBinding.onboardingActionContainer) { btnMainAction.setText(R.string.onboarding_done_button_continue) btnMainAction.setOnClickListener { showConfetti(false) @@ -188,15 +179,15 @@ class OnboardingNoteFragment : BaseOnboardingFragment() { } btnAlternativeAction.isVisible = false - btnAlternativeAction.setText("") + btnAlternativeAction.text = "" btnAlternativeAction.setOnClickListener { } btnRefreshBalanceWidget.mainView.setOnClickListener(null) tvHeader.setText(R.string.onboarding_done_header) tvBody.setText(R.string.onboarding_done_body) - binding.onboardingTopContainer.imvCardBackground.setBackgroundDrawable( - requireContext().getDrawableCompat(R.drawable.shape_rectangle_rounded_8) + mainBinding.onboardingTopContainer.imvCardBackground.setBackgroundDrawable( + requireContext().getDrawableCompat(R.drawable.shape_rectangle_rounded_8), ) updateConstraints(state.currentStep, R.layout.lp_onboarding_done_activation) } @@ -204,7 +195,7 @@ class OnboardingNoteFragment : BaseOnboardingFragment() { private fun updateConstraints(currentStep: OnboardingNoteStep, @LayoutRes layoutId: Int) { if (this.previousStep == currentStep) return - with(binding.onboardingTopContainer) { + with(mainBinding.onboardingTopContainer) { val constraintSet = ConstraintSet() constraintSet.clone(requireContext(), layoutId) constraintSet.applyTo(onboardingWalletContainer) @@ -213,13 +204,4 @@ class OnboardingNoteFragment : BaseOnboardingFragment() { TransitionManager.beginDelayedTransition(onboardingWalletContainer, transition) } } -} - -fun ImageView.swapToBitmapDrawable(bitmap: Bitmap?, duration: Long = 200) { - if (drawable is BitmapDrawable && (drawable as BitmapDrawable).bitmap == bitmap) return - - fadeOut(duration) { - setImageBitmap(bitmap) - fadeIn(duration) - } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt index 34c0d381b2..d990941bd5 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt @@ -4,7 +4,12 @@ import com.tangem.common.CompletionResult import com.tangem.common.extensions.guard import com.tangem.domain.common.extensions.withMainContext import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE -import com.tangem.tap.common.extensions.* +import com.tangem.tap.common.extensions.dispatchDialogShow +import com.tangem.tap.common.extensions.dispatchErrorNotification +import com.tangem.tap.common.extensions.dispatchOpenUrl +import com.tangem.tap.common.extensions.getAddressData +import com.tangem.tap.common.extensions.getToUpUrl +import com.tangem.tap.common.extensions.onCardScanned import com.tangem.tap.common.postUi import com.tangem.tap.common.redux.AppDialog import com.tangem.tap.common.redux.AppState @@ -115,11 +120,11 @@ private fun handleNoteAction(appState: () -> AppState?, action: Action, dispatch val balanceIsLoading = noteState.walletBalance.copy( currency = Currency.Blockchain( walletManager.wallet.blockchain, - walletManager.wallet.publicKey.derivationPath?.rawPath + walletManager.wallet.publicKey.derivationPath?.rawPath, ), state = ProgressState.Loading, error = null, - criticalError = null + criticalError = null, ) store.dispatch(OnboardingNoteAction.Balance.Set(balanceIsLoading)) 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 bb09b3d434..0ed23143da 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 @@ -24,17 +24,13 @@ import com.tangem.wallet.R */ class OnboardingOtherCardsFragment : BaseOnboardingFragment() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - postponeEnterTransition() - } + private val mainBinding by lazy { binding.vMain } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) addBackPressHandler(this) - binding.onboardingTopContainer.imvFrontCard.transitionName = ShareElement.imvFrontCard - startPostponedEnterTransition() + mainBinding.onboardingTopContainer.imvFrontCard.transitionName = ShareElement.imvFrontCard binding.toolbar.setTitle(R.string.onboarding_title) store.dispatch(OnboardingOtherCardsAction.LoadCardArtwork) @@ -54,7 +50,7 @@ class OnboardingOtherCardsFragment : BaseOnboardingFragment() { + private val mainBinding by lazy { binding.vMain } private var previousStep = TwinCardsStep.None private lateinit var twinsWidget: TwinsCardWidget @@ -47,7 +48,6 @@ class TwinsCardsFragment : BaseOnboardingFragment() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - postponeEnterTransition() store.dispatch(TwinCardsAction.Init) } @@ -66,30 +66,30 @@ class TwinsCardsFragment : BaseOnboardingFragment() { super.onViewCreated(view, savedInstanceState) addBackPressHandler(this) - reconfigureLayoutForTwins(binding.onboardingTopContainer) + reconfigureLayoutForTwins(mainBinding.onboardingTopContainer) val typedValue = TypedValue() resources.getValue(R.dimen.device_scale_factor_for_twins_welcome, typedValue, true) val deviceScaleFactorForWelcomeState = typedValue.float twinsWidget = TwinsCardWidget( - LeapfrogWidget(binding.onboardingTopContainer.cardsContainer), - deviceScaleFactorForWelcomeState + LeapfrogWidget(mainBinding.onboardingTopContainer.cardsContainer), + deviceScaleFactorForWelcomeState, ) { 285f * deviceScaleFactorForWelcomeState } btnRefreshBalanceWidget = - RefreshBalanceWidget(binding.onboardingTopContainer.onboardingWalletContainer) + RefreshBalanceWidget(mainBinding.onboardingTopContainer.onboardingWalletContainer) binding.toolbar.title = getText(R.string.twins_recreate_toolbar) - binding.onboardingTopContainer.imvTwinFrontCard.load(Artwork.TWIN_CARD_1) { + mainBinding.onboardingTopContainer.imvTwinFrontCard.load(Artwork.TWIN_CARD_1) { placeholder(R.drawable.card_placeholder_black) error(R.drawable.card_placeholder_black) fallback(R.drawable.card_placeholder_black) } - binding.onboardingTopContainer.imvTwinBackCard.load(Artwork.TWIN_CARD_2) { + mainBinding.onboardingTopContainer.imvTwinBackCard.load(Artwork.TWIN_CARD_2) { placeholder(R.drawable.card_placeholder_white) error(R.drawable.card_placeholder_white) fallback(R.drawable.card_placeholder_white) @@ -143,7 +143,7 @@ class TwinsCardsFragment : BaseOnboardingFragment() { private fun setBalance(state: TwinCardsState) { if (state.walletBalance.currency.blockchain == Blockchain.Unknown) return - with(binding.onboardingTopContainer.onboardingTvBalance) { + with(mainBinding.onboardingTopContainer.onboardingTvBalance) { if (state.balanceCriticalError == null) { val balanceValue = state.walletBalance.value.stripZeroPlainString() tvBalanceValue.text = balanceValue @@ -167,26 +167,26 @@ class TwinsCardsFragment : BaseOnboardingFragment() { } private fun setupWelcomeState(state: TwinCardsState, mainAction: VoidCallback) = - with(binding.onboardingActionContainer) { - twinsWidget.toWelcome(false) { startPostponedEnterTransition() } + with(mainBinding.onboardingActionContainer) { + twinsWidget.toWelcome(false) - binding.onboardingTopContainer.onboardingTwinsWelcomeBg.root.show() + mainBinding.onboardingTopContainer.onboardingTwinsWelcomeBg.root.show() pbBinding.pbState.hide() tvHeader.setText(R.string.twins_onboarding_subtitle) tvBody.text = getString( R.string.twins_onboarding_description_format, - state.cardNumber?.pairIndexNumber() + state.cardNumber?.pairIndexNumber(), ) btnMainAction.setText(R.string.common_continue) btnMainAction.setOnClickListener { mainAction() } } - private fun setupWarningState(state: TwinCardsState) = with(binding.onboardingActionContainer) { - twinsWidget.toWelcome(false) { startPostponedEnterTransition() } + private fun setupWarningState(state: TwinCardsState) = with(mainBinding.onboardingActionContainer) { + twinsWidget.toWelcome(false) - binding.onboardingTopContainer.onboardingTwinsWelcomeBg.root.hide() + mainBinding.onboardingTopContainer.onboardingTwinsWelcomeBg.root.hide() pbBinding.pbState.hide() chbUnderstand.show() @@ -203,184 +203,175 @@ class TwinsCardsFragment : BaseOnboardingFragment() { } } - private fun setupCreateFirstWalletState(state: TwinCardsState) = - with(binding.onboardingActionContainer) { - binding.onboardingTopContainer.onboardingTwinsWelcomeBg.root.hide() - binding.onboardingTopContainer.onboardingTwinsWelcomeBg.bgCircleLarge.hide() - binding.onboardingTopContainer.onboardingTwinsWelcomeBg.bgCircleMedium.hide() - binding.onboardingTopContainer.onboardingTwinsWelcomeBg.bgCircleMin.hide() - chbUnderstand.hide() - pbBinding.pbState.show() + private fun setupCreateFirstWalletState(state: TwinCardsState) = with(mainBinding.onboardingActionContainer) { + mainBinding.onboardingTopContainer.onboardingTwinsWelcomeBg.root.hide() + mainBinding.onboardingTopContainer.onboardingTwinsWelcomeBg.bgCircleLarge.hide() + mainBinding.onboardingTopContainer.onboardingTwinsWelcomeBg.bgCircleMedium.hide() + mainBinding.onboardingTopContainer.onboardingTwinsWelcomeBg.bgCircleMin.hide() + chbUnderstand.hide() + pbBinding.pbState.show() - binding.onboardingTopContainer.onboardingWalletContainer.beginDelayedTransition() + mainBinding.onboardingTopContainer.onboardingWalletContainer.beginDelayedTransition() - when (previousStep) { - TwinCardsStep.None -> { - twinsWidget.toLeapfrog(false) { - when (state.cardNumber) { - TwinCardNumber.First -> startPostponedEnterTransition() - TwinCardNumber.Second -> { - switchToCard( - state.cardNumber, - false - ) { startPostponedEnterTransition() } - } - } - } - } - TwinCardsStep.Welcome, TwinCardsStep.Warning -> { - twinsWidget.toLeapfrog(onEnd = { switchToCard(state.cardNumber) }) - } - } - - val twinIndexNumber = state.cardNumber?.indexNumber() - tvHeader.text = getString(R.string.twins_recreate_title_format, twinIndexNumber) - tvBody.setText(R.string.onboarding_twins_interrupt_warning) - - btnMainAction.text = getString(R.string.twins_recreate_button_format, twinIndexNumber) - btnMainAction.setOnClickListener { - store.dispatch( - TwinCardsAction.Wallet.LaunchFirstStep( - Message(getString(R.string.twins_recreate_title_format, twinIndexNumber)), - AndroidAssetReader(requireContext()), - ) - ) - } - - btnAlternativeAction.isVisible = false - btnAlternativeAction.isClickable = false - } - - private fun setupCreateSecondWalletState(state: TwinCardsState) = - with(binding.onboardingActionContainer) { - switchToCard(state.cardNumber?.pairNumber()) - - val twinPairIndexNumber = state.cardNumber?.pairIndexNumber() - tvHeader.text = getString(R.string.twins_recreate_title_format, twinPairIndexNumber) - tvBody.setText(R.string.onboarding_twins_interrupt_warning) - - btnMainAction.text = - getString(R.string.twins_recreate_button_format, twinPairIndexNumber) - btnMainAction.setOnClickListener { - store.dispatch( - TwinCardsAction.Wallet.LaunchSecondStep( - Message( - getString( - R.string.twins_recreate_title_format, - twinPairIndexNumber - ) - ), - Message(getString(R.string.twins_recreate_title_preparing)), - Message(getString(R.string.twins_recreate_title_creating_wallet)), - ) - ) - } - } - - private fun setupCreateThirdWalletState(state: TwinCardsState) = - with(binding.onboardingActionContainer) { - switchToCard(state.cardNumber) - - val twinIndexNumber = state.cardNumber?.indexNumber() - tvHeader.text = getString(R.string.twins_recreate_title_format, twinIndexNumber) - tvBody.setText(R.string.onboarding_twins_interrupt_warning) - - btnMainAction.text = getString(R.string.twins_recreate_button_format, twinIndexNumber) - btnMainAction.setOnClickListener { - store.dispatch( - TwinCardsAction.Wallet.LaunchThirdStep( - Message(getString(R.string.twins_recreate_title_format, twinIndexNumber)) - ) - ) - } - } - - - private fun setupTopUpWalletState(state: TwinCardsState) = - with(binding.onboardingActionContainer) { - when (previousStep) { - TwinCardsStep.None -> { + when (previousStep) { + TwinCardsStep.None -> { + twinsWidget.toLeapfrog(false) { when (state.cardNumber) { - TwinCardNumber.First -> { - twinsWidget.leapfrogWidget.unfold(false) { - twinsWidget.toActivate(false) { - startPostponedEnterTransition() - } - } - } + TwinCardNumber.First -> {} TwinCardNumber.Second -> { - switchToCard(state.cardNumber, false) { - twinsWidget.toActivate(false) - startPostponedEnterTransition() - } + switchToCard( + state.cardNumber, + false, + ) } } } - TwinCardsStep.CreateThirdWallet -> { - twinsWidget.toActivate() - } } - - if (state.isBuyAllowed) { - btnMainAction.setText(R.string.onboarding_top_up_button_but_crypto) - btnMainAction.setOnClickListener { - store.dispatch(TwinCardsAction.TopUp) - } - - btnAlternativeAction.isVisible = true - btnAlternativeAction.setText(R.string.onboarding_top_up_button_show_wallet_address) - btnAlternativeAction.setOnClickListener { - store.dispatch(TwinCardsAction.ShowAddressInfoDialog) - } - } else { - btnMainAction.setText(R.string.onboarding_button_receive_crypto) - btnMainAction.setOnClickListener { - store.dispatch(TwinCardsAction.ShowAddressInfoDialog) - } - - btnAlternativeAction.isVisible = false + TwinCardsStep.Welcome, TwinCardsStep.Warning -> { + twinsWidget.toLeapfrog(onEnd = { switchToCard(state.cardNumber) }) } - - tvHeader.setText(R.string.onboarding_top_up_header) - tvBody.setText(R.string.onboarding_top_up_body) - - btnRefreshBalanceWidget.changeState(state.walletBalance.state) - if (btnRefreshBalanceWidget.isShowing != true) { - btnRefreshBalanceWidget.mainView.setOnClickListener { - store.dispatch(TwinCardsAction.Balance.Update) - } - } - binding.onboardingTopContainer.imvCardBackground.setBackgroundDrawable( - requireContext().getDrawableCompat(R.drawable.shape_rectangle_rounded_8) - ) - updateConstraints(state.currentStep, R.layout.lp_onboarding_topup_wallet_twins) } - private fun setupDoneState(state: TwinCardsState) = - with(binding.onboardingActionContainer) { - btnMainAction.setText(R.string.onboarding_done_button_continue) + val twinIndexNumber = state.cardNumber?.indexNumber() + tvHeader.text = getString(R.string.twins_recreate_title_format, twinIndexNumber) + tvBody.setText(R.string.onboarding_twins_interrupt_warning) + + btnMainAction.text = getString(R.string.twins_recreate_button_format, twinIndexNumber) + btnMainAction.setOnClickListener { + store.dispatch( + TwinCardsAction.Wallet.LaunchFirstStep( + Message(getString(R.string.twins_recreate_title_format, twinIndexNumber)), + AndroidAssetReader(requireContext()), + ), + ) + } + + btnAlternativeAction.isVisible = false + btnAlternativeAction.isClickable = false + } + + private fun setupCreateSecondWalletState(state: TwinCardsState) = with(mainBinding.onboardingActionContainer) { + switchToCard(state.cardNumber?.pairNumber()) + + val twinPairIndexNumber = state.cardNumber?.pairIndexNumber() + tvHeader.text = getString(R.string.twins_recreate_title_format, twinPairIndexNumber) + tvBody.setText(R.string.onboarding_twins_interrupt_warning) + + btnMainAction.text = + getString(R.string.twins_recreate_button_format, twinPairIndexNumber) + btnMainAction.setOnClickListener { + store.dispatch( + TwinCardsAction.Wallet.LaunchSecondStep( + Message( + getString( + R.string.twins_recreate_title_format, + twinPairIndexNumber, + ), + ), + Message(getString(R.string.twins_recreate_title_preparing)), + Message(getString(R.string.twins_recreate_title_creating_wallet)), + ), + ) + } + } + + private fun setupCreateThirdWalletState(state: TwinCardsState) = with(mainBinding.onboardingActionContainer) { + switchToCard(state.cardNumber) + + val twinIndexNumber = state.cardNumber?.indexNumber() + tvHeader.text = getString(R.string.twins_recreate_title_format, twinIndexNumber) + tvBody.setText(R.string.onboarding_twins_interrupt_warning) + + btnMainAction.text = getString(R.string.twins_recreate_button_format, twinIndexNumber) + btnMainAction.setOnClickListener { + store.dispatch( + TwinCardsAction.Wallet.LaunchThirdStep( + Message(getString(R.string.twins_recreate_title_format, twinIndexNumber)), + ), + ) + } + } + + private fun setupTopUpWalletState(state: TwinCardsState) = with(mainBinding.onboardingActionContainer) { + when (previousStep) { + TwinCardsStep.None -> { + when (state.cardNumber) { + TwinCardNumber.First -> { + twinsWidget.leapfrogWidget.unfold(false) { + twinsWidget.toActivate(false) + } + } + TwinCardNumber.Second -> { + switchToCard(state.cardNumber, false) { + twinsWidget.toActivate(false) + } + } + } + } + TwinCardsStep.CreateThirdWallet -> { + twinsWidget.toActivate() + } + } + + if (state.isBuyAllowed) { + btnMainAction.setText(R.string.onboarding_top_up_button_but_crypto) btnMainAction.setOnClickListener { - store.dispatch(TwinCardsAction.Confetti.Hide) - store.dispatch(TwinCardsAction.Done) + store.dispatch(TwinCardsAction.TopUp) + } + + btnAlternativeAction.isVisible = true + btnAlternativeAction.setText(R.string.onboarding_top_up_button_show_wallet_address) + btnAlternativeAction.setOnClickListener { + store.dispatch(TwinCardsAction.ShowAddressInfoDialog) + } + } else { + btnMainAction.setText(R.string.onboarding_button_receive_crypto) + btnMainAction.setOnClickListener { + store.dispatch(TwinCardsAction.ShowAddressInfoDialog) } btnAlternativeAction.isVisible = false - btnAlternativeAction.isClickable = false - - tvHeader.setText(R.string.onboarding_done_header) - tvBody.setText(R.string.onboarding_done_body) - - val layout = when (state.mode) { - CreateTwinWalletMode.CreateWallet -> R.layout.lp_onboarding_done_activation_twins - CreateTwinWalletMode.RecreateWallet -> R.layout.lp_onboarding_done - } - updateConstraints(state.currentStep, layout) } + tvHeader.setText(R.string.onboarding_top_up_header) + tvBody.setText(R.string.onboarding_top_up_body) + + btnRefreshBalanceWidget.changeState(state.walletBalance.state) + if (btnRefreshBalanceWidget.isShowing != true) { + btnRefreshBalanceWidget.mainView.setOnClickListener { + store.dispatch(TwinCardsAction.Balance.Update) + } + } + mainBinding.onboardingTopContainer.imvCardBackground.setBackgroundDrawable( + requireContext().getDrawableCompat(R.drawable.shape_rectangle_rounded_8), + ) + updateConstraints(state.currentStep, R.layout.lp_onboarding_topup_wallet_twins) + } + + private fun setupDoneState(state: TwinCardsState) = with(mainBinding.onboardingActionContainer) { + btnMainAction.setText(R.string.onboarding_done_button_continue) + btnMainAction.setOnClickListener { + store.dispatch(TwinCardsAction.Confetti.Hide) + store.dispatch(TwinCardsAction.Done) + } + + btnAlternativeAction.isVisible = false + btnAlternativeAction.isClickable = false + + tvHeader.setText(R.string.onboarding_done_header) + tvBody.setText(R.string.onboarding_done_body) + + val layout = when (state.mode) { + CreateTwinWalletMode.CreateWallet -> R.layout.lp_onboarding_done_activation_twins + CreateTwinWalletMode.RecreateWallet -> R.layout.lp_onboarding_done + } + updateConstraints(state.currentStep, layout) + } + private fun updateConstraints(currentStep: TwinCardsStep, @LayoutRes layoutId: Int) { if (this.previousStep == currentStep) return - with(binding.onboardingTopContainer) { + with(mainBinding.onboardingTopContainer) { val constraintSet = ConstraintSet() constraintSet.clone(requireContext(), layoutId) constraintSet.applyTo(onboardingWalletContainer) @@ -390,11 +381,7 @@ class TwinsCardsFragment : BaseOnboardingFragment() { } } - private fun switchToCard( - cardNumber: TwinCardNumber?, - animate: Boolean = true, - onEnd: VoidCallback = {} - ) { + private fun switchToCard(cardNumber: TwinCardNumber?, animate: Boolean = true, onEnd: VoidCallback = {}) { val position = cardNumber?.number ?: return if (twinsWidget.leapfrogWidget.getViewPositionByIndex(1) != position - 1) { @@ -405,12 +392,14 @@ class TwinsCardsFragment : BaseOnboardingFragment() { } override fun handleOnBackPressed() { - store.dispatch(TwinCardsAction.Wallet.HandleOnBackPressed { should, popAction -> - store.dispatch(TwinCardsAction.Confetti.Hide) - showConfetti(false) - if (should) switchToCard(TwinCardNumber.First, true, popAction) - else popAction() - }) + store.dispatch( + TwinCardsAction.Wallet.HandleOnBackPressed { should, popAction -> + store.dispatch(TwinCardsAction.Confetti.Hide) + showConfetti(false) + if (should) switchToCard(TwinCardNumber.First, true, popAction) + else popAction() + }, + ) } } 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 ce9a6b6c41..885f57fa99 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,7 +5,7 @@ import org.rekotlin.Action sealed class OnboardingWalletAction : Action { object Init : OnboardingWalletAction() - object StartSaltPayCardActivation: OnboardingWalletAction() + object StartSaltPayCardActivation : OnboardingWalletAction() object GetToCreateWalletStep : OnboardingWalletAction() object GetToSaltPayStep : OnboardingWalletAction() object CreateWallet : OnboardingWalletAction() @@ -30,6 +30,11 @@ sealed class BackupAction : Action { object StartAddingPrimaryCard : BackupAction() object ScanPrimaryCard : BackupAction() + /** + * Check for unfinished backup of standard Wallet cards. + * For SaltPay cards unfinished backup resumed after scanning the card on HomeScreen through Onboarding.Start. + * See more Onboarding.Start, CheckForUnfinishedBackup, StartForUnfinishedBackup + */ object CheckForUnfinishedBackup : BackupAction() object StartAddingBackupCards : BackupAction() 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 745dee84fd..8438c8b760 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 @@ -3,13 +3,13 @@ package com.tangem.tap.features.onboarding.products.wallet.redux import com.tangem.blockchain.common.Blockchain 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.backupService +import com.tangem.tap.common.extensions.dispatchDialogShow import com.tangem.tap.common.extensions.dispatchOnMain +import com.tangem.tap.common.extensions.primaryCardIsSaltPayVisa import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.common.redux.navigation.AppScreen @@ -18,6 +18,7 @@ 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.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.wallet.redux.Artwork @@ -60,24 +61,26 @@ private fun handleWalletAction(action: Action, state: () -> AppState?, dispatch: when (action) { OnboardingWalletAction.Init -> { - // if (walletState.onboardingSaltPayState?.isTest == true) { - // store.dispatch(BackupAction.FinishBackup) - // return - // } - when { card == null -> { - // StartForUnfinishedBackup -> navigateTo(OnboardingWallet) -> OnboardingWalletAction.Init + // it's possible when found unfinished backup for standard Wallet cards 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) + when { + // check for unfinished backup for saltPay cards. See more + scanResponse.isSaltPay() && backupService.hasIncompletedBackup -> { + store.dispatch(OnboardingWalletAction.ResumeBackup) + } + scanResponse.isSaltPay() -> { + store.dispatch(OnboardingWalletAction.GetToSaltPayStep) + store.dispatch(BackupAction.FinishBackup) + } + else -> store.dispatch(BackupAction.FinishBackup) } - store.dispatch(BackupAction.FinishBackup) } else -> { store.dispatch(OnboardingWalletAction.GetToCreateWalletStep) @@ -87,10 +90,8 @@ private fun handleWalletAction(action: Action, state: () -> AppState?, dispatch: 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) - } + if (onboardingWalletState.isSaltPay && scanResponse != null) { + startCardActivation(scanResponse) } } is OnboardingWalletAction.LoadArtwork -> { @@ -128,7 +129,7 @@ private fun handleWalletAction(action: Action, state: () -> AppState?, dispatch: card = result.data.card, ), ) - onboardingManager.activationStarted(updatedResponse.card.cardId) + startCardActivation(updatedResponse) store.dispatch(OnboardingWalletAction.ResumeBackup) } is CompletionResult.Failure -> { @@ -141,13 +142,15 @@ private fun handleWalletAction(action: Action, state: () -> AppState?, dispatch: OnboardingWalletAction.FinishOnboarding -> { store.dispatch(GlobalAction.Onboarding.Stop) - //TODO: SaltPay: scanResponse if (scanResponse == null) { store.dispatch(NavigationAction.PopBackTo()) store.dispatch(HomeAction.ReadCard) } else { val backupState = store.state.onboardingWalletState.backupState val updatedScanResponse = updateScanResponseAfterBackup(scanResponse, backupState) + if (onboardingWalletState.isSaltPay) { + finishCardActivation(backupState, scanResponse.card) + } scope.launch { globalState.tapWalletManager.onCardScanned(updatedScanResponse) } store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Wallet)) } @@ -173,16 +176,9 @@ private fun handleWalletAction(action: Action, state: () -> AppState?, dispatch: newAction?.let { store.dispatch(it) } } OnboardingWalletAction.OnBackPressed -> { - when (onboardingWalletState.backupState.backupStep) { - BackupStep.InitBackup, BackupStep.Finished -> store.dispatch(NavigationAction.PopBackTo()) - BackupStep.ScanOriginCard, BackupStep.AddBackupCards, BackupStep.EnterAccessCode, - BackupStep.ReenterAccessCode, BackupStep.SetAccessCode, BackupStep.WritePrimaryCard, - -> { - store.dispatch(BackupAction.DiscardBackup) - store.dispatch(NavigationAction.PopBackTo()) - } - is BackupStep.WriteBackupCard -> - store.dispatch(GlobalAction.ShowDialog(BackupDialog.BackupInProgress)) + when { + onboardingWalletState.isSaltPay -> handleOnBackPressedSaltPay(onboardingWalletState) + else -> handleOnBackPressed(onboardingWalletState) } } } @@ -316,31 +312,24 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction) } } is BackupAction.FinishBackup -> { - (listOf(backupState.primaryCardId, card?.cardId) + backupState.backupCardIds) - .distinct().filterNotNull() - .forEach { cardId -> - preferencesStorage.usedCardsPrefStorage.activationFinished(cardId) - } + if (!onboardingWalletState.isSaltPay) { + finishCardActivation(backupState, card) + } } is BackupAction.DiscardBackup -> { backupService.discardSavedBackup() } is BackupAction.DiscardSavedBackup -> { - backupService.primaryCardId?.let { - preferencesStorage.usedCardsPrefStorage.activationFinished(it) - } + backupService.primaryCardId?.let { finishCardsActivationForDiscardedUnfinishedBackup(it) } backupService.discardSavedBackup() } is BackupAction.CheckForUnfinishedBackup -> { - if (backupService.hasIncompletedBackup) { + if (backupService.hasIncompletedBackup && !backupService.primaryCardIsSaltPayVisa()) { store.dispatch(GlobalAction.ShowDialog(BackupDialog.UnfinishedBackupFound)) } } is BackupAction.ResumeFoundUnfinishedBackup -> { - val isSaltPay = backupService.primaryCardId?.slice(0..3)?.let { - SaltPayWorkaround.isVisaBatchId(it) - } ?: false - store.dispatch(GlobalAction.Onboarding.StartForUnfinishedBackup(isSaltPay)) + store.dispatch(GlobalAction.Onboarding.StartForUnfinishedBackup) store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingWallet)) } is BackupAction.DismissBackup -> { @@ -350,6 +339,33 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction) } } +/** + * Standard Wallet cards start activation at OnboardingWalletAction.CreateWallet + * SaltPay cards start activation at OnboardingWalletAction.StartSaltPayCardActivation + */ +private fun startCardActivation(scanResponse: ScanResponse) { + preferencesStorage.usedCardsPrefStorage.activationStarted(scanResponse.card.cardId) +} + +/** + * Standard Wallet cards finish activation at BackupAction.FinishBackup + * SaltPay cards finish activation at OnboardingWalletAction.FinishOnboarding + */ +private fun finishCardActivation(backupState: BackupState, card: Card?) { + (listOf(backupState.primaryCardId, card?.cardId) + backupState.backupCardIds) + .distinct().filterNotNull() + .forEach { cardId -> + preferencesStorage.usedCardsPrefStorage.activationFinished(cardId) + } +} + +/** + * Not used for SaltPay cards, because discarding unfinished backup is not possible + */ +private fun finishCardsActivationForDiscardedUnfinishedBackup(cardId: String) { + preferencesStorage.usedCardsPrefStorage.activationFinished(cardId) +} + /** * SaltPay state maybe not initialized if backup resumed from ResumeFoundUnfinishedBackup action */ @@ -358,11 +374,38 @@ private fun initSaltPayOnBackupFinishedIfNeeded( 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) } +} + +private fun handleOnBackPressedSaltPay(state: OnboardingWalletState) { + when (state.backupState.backupStep) { + BackupStep.Finished -> { + store.dispatchDialogShow( + SaltPayDialog.Activation.TryToInterrupt( + onOk = { store.dispatch(NavigationAction.PopBackTo()) }, + onCancel = { /* do nothing */ }, + ), + ) + } + else -> handleOnBackPressed(state) + } +} + +private fun handleOnBackPressed(state: OnboardingWalletState) { + when (state.backupState.backupStep) { + BackupStep.InitBackup, BackupStep.Finished -> store.dispatch(NavigationAction.PopBackTo()) + BackupStep.ScanOriginCard, BackupStep.AddBackupCards, BackupStep.EnterAccessCode, + BackupStep.ReenterAccessCode, BackupStep.SetAccessCode, BackupStep.WritePrimaryCard, + -> { + store.dispatch(BackupAction.DiscardBackup) + store.dispatch(NavigationAction.PopBackTo()) + } + is BackupStep.WriteBackupCard -> + store.dispatch(GlobalAction.ShowDialog(BackupDialog.BackupInProgress)) + } } \ 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 0f79ba3da0..8cca79e9dd 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 @@ -38,26 +38,28 @@ 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() - ) + fun reduce(action: GlobalAction.Onboarding, state: OnboardingWalletState): OnboardingWalletState { + return 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 = 2, + canSkipBackup = false, + ), + isSaltPay = false, + ) + } + else -> state } - is GlobalAction.Onboarding.StartForUnfinishedBackup -> { - OnboardingWalletState( - backupState = state.backupState.copy( - maxBackupCards = if (action.isSaltPayBackup) 1 else 2, - canSkipBackup = false, - ), - isSaltPay = action.isSaltPayBackup, - ) - } - else -> state } } } @@ -74,7 +76,7 @@ private class BackupReducer { is BackupAction.IntroduceBackup -> BackupState( backupStep = BackupStep.InitBackup, maxBackupCards = if (isSaltPay) 1 else 2, - canSkipBackup = !isSaltPay, + canSkipBackup = state.canSkipBackup, ) BackupAction.StartAddingPrimaryCard -> state.copy(backupStep = BackupStep.ScanOriginCard) BackupAction.StartAddingBackupCards -> { 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 566cf6aaf3..6fc233d092 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 @@ -5,7 +5,7 @@ import com.tangem.common.CardFilter import com.tangem.domain.common.SaltPayWorkaround 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 com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.SaltPayActivationStep import org.rekotlin.StateType /** @@ -41,7 +41,6 @@ data class OnboardingWalletState( when (backupState.backupStep) { BackupStep.InitBackup -> 2 BackupStep.ScanOriginCard -> 3 - BackupStep.AddBackupCards -> 4 BackupStep.EnterAccessCode -> 4 BackupStep.ReenterAccessCode -> 4 @@ -52,13 +51,16 @@ data class OnboardingWalletState( } 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() + SaltPayActivationStep.None, SaltPayActivationStep.NoGas -> 0 + SaltPayActivationStep.NeedPin -> 7 + SaltPayActivationStep.CardRegistration -> 8 + SaltPayActivationStep.KycIntro, SaltPayActivationStep.KycStart, + SaltPayActivationStep.KycWaiting, SaltPayActivationStep.KycReject, + -> 9 + SaltPayActivationStep.Claim -> 10 + SaltPayActivationStep.ClaimInProgress -> 11 + SaltPayActivationStep.ClaimSuccess, SaltPayActivationStep.Success, SaltPayActivationStep.Finished, + -> getMaxProgress() } } step == OnboardingWalletStep.Done -> getMaxProgress() 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 index 5937e34621..771fca7d4b 100644 --- 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 @@ -7,6 +7,7 @@ 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.TransactionSigner import com.tangem.blockchain.common.WalletManager import com.tangem.blockchain.extensions.Result import com.tangem.blockchain.extensions.SimpleResult @@ -42,6 +43,8 @@ class GnosisRegistrator( else -> throw IllegalArgumentException("GnosisRegistrator supports only the SaltPay blockchain") } + private val addressTreasureSafe = "0x8e9260a049d3Aa9ac60D0d4F27017320E0e2396B" + private val atomicNonce = AtomicLong() suspend fun checkHasGas(): Result { @@ -142,6 +145,21 @@ class GnosisRegistrator( return Result.Success(compiledEthereumTransaction) } + suspend fun getAllowance(): Result { + return walletManager.getAllowance(addressTreasureSafe, token) + } + + suspend fun transferFrom(amountToClaim: BigDecimal, signer: TransactionSigner): Result { + val amount = Amount(token, amountToClaim) + val feeAmount = walletManager.getFeeToTransferFrom(amount, addressTreasureSafe) + .extractFeeAmount().successOr { return it } + + val transactionData = walletManager.createTransferFromTransaction(amount, feeAmount, addressTreasureSafe) + walletManager.transferFrom(transactionData, signer).successOr { return Result.Failure(it.error) } + + return Result.Success(Unit) + } + private fun Result>.extractFeeAmount(): Result { return when (this) { is Result.Success -> { 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/SaltPayActivationManager.kt similarity index 65% rename from app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/SaltPayRegistrationManager.kt rename to app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/SaltPayActivationManager.kt index 93e129737c..f62baf1b51 100644 --- 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/SaltPayActivationManager.kt @@ -2,6 +2,13 @@ package com.tangem.tap.features.onboarding.products.wallet.saltPay import android.net.Uri import com.tangem.blockchain.blockchains.ethereum.SignedEthereumTransaction +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.AmountType +import com.tangem.blockchain.common.BlockchainSdkError +import com.tangem.blockchain.common.TransactionSigner +import com.tangem.blockchain.extensions.successOr +import com.tangem.common.extensions.guard +import com.tangem.common.extensions.isZero import com.tangem.common.services.Result import com.tangem.domain.common.extensions.successOr import com.tangem.network.api.paymentology.AttestationResponse @@ -10,23 +17,24 @@ 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.network.api.paymentology.tryExtractError import com.tangem.operations.attestation.AttestWalletKeyResponse +import com.tangem.tap.common.extensions.safeUpdate +import com.tangem.tap.domain.getFirstToken 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 com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayActivationError import java.math.BigDecimal /** [REDACTED_AUTHOR] */ -class SaltPayRegistrationManager( +class SaltPayActivationManager( 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) @@ -35,18 +43,14 @@ class SaltPayRegistrationManager( 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) + Result.Failure(SaltPayActivationError.NoGas) } - is com.tangem.blockchain.extensions.Result.Failure -> Result.Failure(SaltPayRegistrationError.NoGas) + is com.tangem.blockchain.extensions.Result.Failure -> Result.Failure(hasGasResult.error as BlockchainSdkError) } } @@ -58,20 +62,27 @@ class SaltPayRegistrationManager( } suspend fun checkRegistration(): Result { - val registrationResponse = paymentologyService.checkRegistration(cardId, cardPublicKey).successOr { return it } + val response: RegistrationResponse = paymentologyService.checkRegistration(cardId, cardPublicKey) + .successOr { return it } + .tryExtractError() + .successOr { return it } return try { - if (!registrationResponse.success || registrationResponse.results.isEmpty()) { - throw SaltPayRegistrationError.EmptyResponse(registrationResponse.error) - } - Result.Success(registrationResponse.results[0]) - } catch (ex: SaltPayRegistrationError) { + if (response.results.isEmpty()) throw SaltPayActivationError.EmptyResponse + + val item = response.results[0] + if (item.error != null) throw Exception(response.makeErrorMessage()) + + Result.Success(item) + } catch (ex: Exception) { Result.Failure(ex) } } suspend fun requestAttestationChallenge(): Result { return paymentologyService.requestAttestationChallenge(cardId, cardPublicKey) + .successOr { return it } + .tryExtractError() } suspend fun sendTransactions( @@ -94,14 +105,41 @@ class SaltPayRegistrationManager( cardSignature = attestResponse.cardSignature ?: byteArrayOf(), pin = pinCode, ) + return paymentologyService.registerWallet(request) + .successOr { return it } + .tryExtractError() + } - val result = paymentologyService.registerWallet(request) + suspend fun getAmountToClaim(): Result { + val amount = gnosisRegistrator.getAllowance().successOr { + return Result.Failure(SaltPayActivationError.FailedToGetFundsToClaim) + } - registrationStorage.data = registrationStorage.data.copy( - transactionsSent = result is Result.Success, - ) + return if (amount.value == null || amount.value?.isZero() == true) { + Result.Failure(SaltPayActivationError.NoFundsToClaim) + } else { + Result.Success(amount) + } + } - return result + suspend fun claim(amountToClaim: BigDecimal, signer: TransactionSigner): Result { + val result = gnosisRegistrator.transferFrom(amountToClaim, signer).successOr { + return Result.Failure(SaltPayActivationError.ClaimTransactionFailed) + } + return Result.Success(Unit) + } + + suspend fun getTokenAmount(): Result { + val wallet = gnosisRegistrator.walletManager.safeUpdate().successOr { return it } + val token = wallet.getFirstToken().guard { + throw UnsupportedOperationException() + } + + val amountValue = wallet.amounts[AmountType.Token(token)]?.value.guard { + throw UnsupportedOperationException() + } + + return Result.Success(amountValue) } fun makeRegistrationTask(challenge: ByteArray): SaltPayRegistrationTask { @@ -122,11 +160,10 @@ class SaltPayRegistrationManager( ) companion object { - fun stub(): SaltPayRegistrationManager = SaltPayRegistrationManager( + fun stub(): SaltPayActivationManager = SaltPayActivationManager( kycProvider = SaltPayConfig.stub().kycProvider, paymentologyService = PaymentologyApiService.stub(), gnosisRegistrator = GnosisRegistrator.stub(), - registrationStorage = SaltPayRegistrationStorage.stub(), cardId = "", cardPublicKey = byteArrayOf(), walletPublicKey = byteArrayOf(), diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/SaltPayExceptionHandler.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/SaltPayExceptionHandler.kt new file mode 100644 index 0000000000..3b4ed41a05 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/SaltPayExceptionHandler.kt @@ -0,0 +1,46 @@ +package com.tangem.tap.features.onboarding.products.wallet.saltPay + +import com.tangem.blockchain.common.BlockchainSdkError +import com.tangem.common.core.TangemSdkError +import com.tangem.tap.common.extensions.dispatchDialogShow +import com.tangem.tap.common.redux.AppDialog +import com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog.SaltPayDialog +import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayActivationError +import com.tangem.tap.store + +/** +[REDACTED_AUTHOR] + */ +class SaltPayExceptionHandler { + companion object { + fun handle(throwable: Throwable) { + when (throwable) { + is SaltPayActivationError -> { + val dialog = when (throwable) { + is SaltPayActivationError.NoGas -> SaltPayDialog.Activation.NoGas + else -> SaltPayDialog.Activation.OnError(throwable) + } + store.dispatchDialogShow(dialog) + } + is TangemSdkError -> { + when (throwable) { + is TangemSdkError.NetworkError -> { + val message = (throwable).customMessage + store.dispatchDialogShow(AppDialog.SimpleOkErrorDialog(message)) + } + else -> { + // do nothing + } + } + } + is BlockchainSdkError -> { + store.dispatchDialogShow(AppDialog.SimpleOkErrorDialog(throwable.customMessage)) + } + else -> { + val message = throwable.localizedMessage ?: "SaltPay unknown error" + store.dispatchDialogShow(AppDialog.SimpleOkErrorDialog(message)) + } + } + } + } +} \ 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 index f7ddfbea66..831d097783 100644 --- 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 @@ -14,7 +14,7 @@ import com.tangem.wallet.R */ class RegistrationErrorDialog { companion object { - fun create(context: Context, dialog: SaltPayDialog.RegistrationError): Dialog { + fun create(context: Context, dialog: SaltPayDialog.Activation.OnError): Dialog { val convertedMessage = SaltPayErrorConverter(context).convert(dialog.error) as ConvertedDialogMessage return AlertDialog.Builder(context).apply { 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 index 5bd2e1aac0..fda1331a37 100644 --- 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 @@ -1,12 +1,16 @@ package com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog +import com.tangem.common.extensions.VoidCallback import com.tangem.tap.common.redux.StateDialog -import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayRegistrationError +import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayActivationError /** [REDACTED_AUTHOR] */ sealed class SaltPayDialog : StateDialog { - object NoFundsForActivation : SaltPayDialog() - data class RegistrationError(val error: SaltPayRegistrationError) : SaltPayDialog() + sealed class Activation : SaltPayDialog() { + object NoGas : SaltPayDialog() + data class OnError(val error: SaltPayActivationError) : SaltPayDialog() + data class TryToInterrupt(val onOk: VoidCallback, val onCancel: VoidCallback) : SaltPayDialog() + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/dialog/TryToInterruptRegistrationDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/dialog/TryToInterruptRegistrationDialog.kt new file mode 100644 index 0000000000..6f02df9120 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/dialog/TryToInterruptRegistrationDialog.kt @@ -0,0 +1,26 @@ +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.extensions.dispatchDialogHide +import com.tangem.tap.store +import com.tangem.wallet.R + +/** +[REDACTED_AUTHOR] + */ +class TryToInterruptRegistrationDialog { + companion object { + fun create(context: Context, dialog: SaltPayDialog.Activation.TryToInterrupt): Dialog { + return AlertDialog.Builder(context).apply { + setTitle(context.getString(R.string.onboarding_exit_alert_title)) + setMessage(context.getString(R.string.onboarding_exit_alert_message)) + setPositiveButton(R.string.common_ok) { _, _ -> dialog.onOk() } + setNegativeButton(R.string.common_cancel) { _, _ -> dialog.onCancel() } + setOnDismissListener { store.dispatchDialogHide() } + setCancelable(false) + }.create() + } + } +} \ 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 index 144647b876..6f2a5de7ad 100644 --- 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 @@ -20,7 +20,7 @@ sealed class SaltPayModuleError( } } -sealed class SaltPayRegistrationError( +sealed class SaltPayActivationError( subCode: Int, message: String? = null, override val data: Any? = null, @@ -28,22 +28,27 @@ sealed class SaltPayRegistrationError( subCode = ERROR_CODE_REGISTRATION + subCode, message = message ?: EMPTY_MESSAGE, ) { - object Unknown : SaltPayRegistrationError(4) - object Empty : SaltPayRegistrationError(4) + object Unknown : SaltPayActivationError(4) + object Empty : SaltPayActivationError(4) // gnosis - object FailedToMakeTxData : SaltPayRegistrationError(1) - object FailedToSendTx : SaltPayRegistrationError(2) + object FailedToMakeTxData : SaltPayActivationError(1) + object FailedToSendTx : SaltPayActivationError(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) // + object NeedPin : SaltPayActivationError(3) + object NoGas : SaltPayActivationError(5) // + object EmptyResponse : SaltPayActivationError(6) + object CardNotFound : SaltPayActivationError(8) + class CardDisabled(message: String?) : SaltPayActivationError(9, message) + class CardNotPassed(message: String?) : SaltPayActivationError(10, message) + object EmptyDynamicAttestResponse : SaltPayActivationError(11) + object EmptyBackupCardScanned : SaltPayActivationError(12) + object WeakPin : SaltPayActivationError(13) + + object FailedToGetFundsToClaim : SaltPayActivationError(14) + object NoFundsToClaim : SaltPayActivationError(15) + object ClaimTransactionFailed : SaltPayActivationError(16) + object NotClaimed : SaltPayActivationError(17) companion object { const val EMPTY_MESSAGE = "" 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 index 54876f0a64..009f2350ad 100644 --- 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 @@ -1,8 +1,10 @@ package com.tangem.tap.features.onboarding.products.wallet.saltPay.redux +import com.tangem.blockchain.common.Amount +import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayActivationManager import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayConfig -import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayRegistrationManager import org.rekotlin.Action +import java.math.BigDecimal /** [REDACTED_AUTHOR] @@ -10,24 +12,28 @@ import org.rekotlin.Action sealed class OnboardingSaltPayAction : Action { sealed class Init : OnboardingSaltPayAction() { data class SetDependencies( - val registrationManager: SaltPayRegistrationManager, + val registrationManager: SaltPayActivationManager, val saltPayConfig: SaltPayConfig, ) : Init() - - // only for tests - object DiscardBackupSteps : Init() } - data class SetIsBusy(val isBusy: Boolean) : OnboardingSaltPayAction() + data class SetInProgress(val isInProgress: Boolean) : OnboardingSaltPayAction() + data class SetClaimRefreshInProgress(val isInProgress: Boolean) : OnboardingSaltPayAction() + data class SetAccessCode(val accessCode: String?) : OnboardingSaltPayAction() object Update : OnboardingSaltPayAction() object RegisterCard : OnboardingSaltPayAction() - object KYCStart : OnboardingSaltPayAction() - object OnFinishKYC : OnboardingSaltPayAction() + object OpenUtorgKYC : OnboardingSaltPayAction() + object UtorgKYCRedirectSuccess : OnboardingSaltPayAction() + object RegisterKYC : OnboardingSaltPayAction() + object Claim : OnboardingSaltPayAction() + object RefreshClaim : OnboardingSaltPayAction() data class TrySetPin(val pin: String) : OnboardingSaltPayAction() data class SetPin(val pin: String) : OnboardingSaltPayAction() - data class SetStep(val newStep: SaltPayRegistrationStep? = null) : OnboardingSaltPayAction() + data class SetStep(val newStep: SaltPayActivationStep) : OnboardingSaltPayAction() + data class SetAmountToClaim(val amount: Amount?) : OnboardingSaltPayAction() + data class SetTokenBalance(val balanceValue: BigDecimal) : 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 index 16c7594774..51a593e33c 100644 --- 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 @@ -1,27 +1,31 @@ package com.tangem.tap.features.onboarding.products.wallet.saltPay.redux import com.tangem.Message +import com.tangem.blockchain.common.Amount 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.KYCStatus 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.analytics.GlobalAnalyticsEventHandler +import com.tangem.tap.common.analytics.events.Onboarding import com.tangem.tap.common.extensions.dispatchOnMain +import com.tangem.tap.common.extensions.isPositive import com.tangem.tap.common.redux.AppState -import com.tangem.tap.domain.TapError +import com.tangem.tap.common.redux.global.GlobalAction +import com.tangem.tap.domain.TangemSigner 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.features.onboarding.products.wallet.saltPay.SaltPayActivationManager +import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayExceptionHandler +import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayActivationError import com.tangem.tap.scope import com.tangem.tap.store +import com.tangem.tap.tangemSdk import com.tangem.tap.tangemSdkManager import com.tangem.wallet.R import kotlinx.coroutines.launch @@ -55,10 +59,27 @@ private fun handleOnboardingSaltPayAction(anyAction: Action, appState: () -> App fun getAppState(): AppState = appState()!! fun getOnboardingWalletState(): OnboardingWalletState = getAppState().onboardingWalletState fun getState(): OnboardingSaltPayState = getOnboardingWalletState().onboardingSaltPayState!! + val analyticsHandler = getAppState().globalState.analyticsHandler when (action) { + is OnboardingSaltPayAction.Update -> { + handleInProgress = true + + val state = getState() + scope.launch { + val updateStep = state.saltPayManager.update(state.step, state.amountToClaim).successOr { + onException(it.error) + return@launch + } + + handleInProgress = false + dispatchOnMain(OnboardingSaltPayAction.SetStep(updateStep)) + } + } is OnboardingSaltPayAction.RegisterCard -> { - handleIsBusy = true + analyticsHandler.handleAnalyticsEvent(Onboarding.ButtonConnect()) + + handleInProgress = true val state = getState() scope.launch { @@ -69,7 +90,7 @@ private fun handleOnboardingSaltPayAction(anyAction: Action, appState: () -> App } val pinCode = state.pinCode.guard { - onError(SaltPayRegistrationError.NeedPin) + onError(SaltPayActivationError.NeedPin) return@launch } @@ -78,11 +99,6 @@ private fun handleOnboardingSaltPayAction(anyAction: Action, appState: () -> App return@launch } - if (!attestationResponse.success) { - onError(SaltPayRegistrationError.CardNotFound(attestationResponse.error)) - return@launch - } - val registrationResponse = tangemSdkManager.runTaskAsync( runnable = saltPayManager.makeRegistrationTask(attestationResponse.challenge!!), cardId = saltPayManager.cardId, @@ -97,7 +113,7 @@ private fun handleOnboardingSaltPayAction(anyAction: Action, appState: () -> App } saltPayManager.sendTransactions(registrationResponse.signedTransactions).successOr { - onError(SaltPayRegistrationError.FailedToSendTx) + onError(SaltPayActivationError.FailedToSendTx) return@launch } @@ -106,184 +122,314 @@ private fun handleOnboardingSaltPayAction(anyAction: Action, appState: () -> App return@launch } - withMainContext { - handleIsBusy = false - store.dispatch(OnboardingSaltPayAction.SetStep(SaltPayRegistrationStep.KycIntro)) - } + handleInProgress = false + dispatchOnMain(OnboardingSaltPayAction.SetStep(SaltPayActivationStep.KycIntro)) } } - is OnboardingSaltPayAction.KYCStart -> { - store.dispatch(OnboardingSaltPayAction.SetStep(SaltPayRegistrationStep.KycStart)) + is OnboardingSaltPayAction.OpenUtorgKYC -> { + store.dispatch(OnboardingSaltPayAction.SetStep(SaltPayActivationStep.KycStart)) } - is OnboardingSaltPayAction.OnFinishKYC -> { + is OnboardingSaltPayAction.UtorgKYCRedirectSuccess -> { + store.dispatch(OnboardingSaltPayAction.RegisterKYC) + } + is OnboardingSaltPayAction.RegisterKYC -> { + val state = getState() + store.dispatch(OnboardingSaltPayAction.SetStep(SaltPayActivationStep.KycWaiting)) + handleInProgress = true + scope.launch { - registerKYCIfNeeded(getState()).successOr { + Timber.d("saltPayManager.registerKYC()") + state.saltPayManager.registerKYC().successOr { onException(it.error) + return@launch } + + handleInProgress = false + dispatchOnMain(OnboardingSaltPayAction.Update) } } is OnboardingSaltPayAction.TrySetPin -> { try { assertPinValid(action.pin, getState().pinLength) + analyticsHandler.handleAnalyticsEvent(Onboarding.PinCodeSet()) store.dispatch(OnboardingSaltPayAction.SetPin(action.pin)) - store.dispatch(OnboardingSaltPayAction.SetStep(SaltPayRegistrationStep.CardRegistration)) - } catch (error: SaltPayRegistrationError) { - handleError = error + store.dispatch(OnboardingSaltPayAction.SetStep(SaltPayActivationStep.CardRegistration)) + } catch (error: SaltPayActivationError) { + SaltPayExceptionHandler.handle(error) } } - is OnboardingSaltPayAction.Update -> { - handleIsBusy = true + is OnboardingSaltPayAction.Claim -> { + analyticsHandler.handleAnalyticsEvent(Onboarding.ButtonClaim()) + val state = getState() + handleInProgress = true + + scope.launch { + val amountToClaim = getAmountToClaimIfNeeded(state.saltPayManager, state.amountToClaim).guard { + onException(SaltPayActivationError.NoFundsToClaim) + return@launch + } + + val scanResponse = store.state.globalState.scanResponse + ?: store.state.globalState.onboardingState.onboardingManager?.scanResponse + val signer = TangemSigner( + card = scanResponse!!.card, + tangemSdk = tangemSdk, + initialMessage = Message(), + accessCode = state.accessCode, + ) { signResponse -> + store.dispatch( + GlobalAction.UpdateWalletSignedHashes( + walletSignedHashes = signResponse.totalSignedHashes, + walletPublicKey = state.saltPayManager.walletPublicKey, + remainingSignatures = signResponse.remainingSignatures, + ), + ) + } + state.saltPayManager.claim(amountToClaim.value!!, signer).successOr { + dispatchOnMain(OnboardingSaltPayAction.SetStep(SaltPayActivationStep.Claim)) + onException(it.error) + return@launch + } + + handleInProgress = false + analyticsHandler.handleAnalyticsEvent(Onboarding.ClaimWasSuccessfully()) + dispatchOnMain(OnboardingSaltPayAction.SetStep(SaltPayActivationStep.ClaimInProgress)) + dispatchOnMain(OnboardingSaltPayAction.RefreshClaim) + } + } + is OnboardingSaltPayAction.RefreshClaim -> { + handleClaimRefreshInProgress = true val state = getState() scope.launch { - checkGasIfNeeded(state.saltPayManager, state.step).successOr { - onException(it.error) - return@launch - } + when (val amountToClaimResult = state.saltPayManager.getAmountToClaim()) { + is Result.Success -> { + // need to re claim + handleClaimRefreshInProgress = false + dispatchOnMain(OnboardingSaltPayAction.SetAmountToClaim(amountToClaimResult.data)) + dispatchOnMain(OnboardingSaltPayAction.SetStep(SaltPayActivationStep.Claim)) + } + is Result.Failure -> { + when (amountToClaimResult.error) { + is SaltPayActivationError.NoFundsToClaim -> { + val tokenAmountValue = state.saltPayManager.getTokenAmount().successOr { + SaltPayExceptionHandler.handle(it.error) + handleClaimRefreshInProgress = false + 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)) + handleClaimRefreshInProgress = false + if (tokenAmountValue.isPositive()) { + dispatchOnMain(OnboardingSaltPayAction.SetTokenBalance(tokenAmountValue)) + dispatchOnMain(OnboardingSaltPayAction.SetStep(SaltPayActivationStep.ClaimSuccess)) + } + } + else -> { + handleClaimRefreshInProgress = false + SaltPayExceptionHandler.handle(amountToClaimResult.error) + } + } + } } } } + is OnboardingSaltPayAction.SetStep -> handleAnalytics(analyticsHandler, action.newStep) else -> { /* do nothing, only reduce */ } } } -private var handleIsBusy: Boolean = false - set(value) { - field = value - store.dispatchOnMain(OnboardingSaltPayAction.SetIsBusy(value)) +private fun handleAnalytics(analyticsHandler: GlobalAnalyticsEventHandler, step: SaltPayActivationStep) { + when (step) { + SaltPayActivationStep.KycStart -> analyticsHandler.handleAnalyticsEvent(Onboarding.KYCStarted()) + SaltPayActivationStep.KycWaiting -> analyticsHandler.handleAnalyticsEvent(Onboarding.KYCInProgress()) + SaltPayActivationStep.KycReject -> analyticsHandler.handleAnalyticsEvent(Onboarding.KYCRejected()) + SaltPayActivationStep.Claim -> analyticsHandler.handleAnalyticsEvent(Onboarding.ClaimScreenOpened()) + else -> {} + } +} + +suspend fun SaltPayActivationManager.update( + currentStep: SaltPayActivationStep, + currentAmountToClaim: Amount?, +): Result { + Timber.d("updateSaltPayStatus") + + val registrationResponse = checkRegistration().successOr { return it } + val amountToClaim = getAmountToClaimIfNeeded(this, currentAmountToClaim) + + val newStep = try { + determineStep(currentStep, amountToClaim, registrationResponse) + } catch (ex: Exception) { + return Result.Failure(ex) } -private suspend fun onError(error: SaltPayRegistrationError) { + checkGasIfNeeded(this, newStep).successOr { return it } + + Timber.d("update: success: %s", newStep) + return Result.Success(newStep) +} + +private fun determineStep( + currentStep: SaltPayActivationStep, + amountToClaim: Amount?, + response: RegistrationResponse.Item, +): SaltPayActivationStep { + fun getStepBaseOnAmountToClaim(amount: Amount?): SaltPayActivationStep = when (amount) { + null -> SaltPayActivationStep.Success + else -> SaltPayActivationStep.Claim + } + + return when { + response.passed != true -> throw SaltPayActivationError.CardNotPassed(response.error) + response.disabledByAdmin == true -> throw SaltPayActivationError.CardDisabled(response.error) + + // go to claim screen + response.active == true -> getStepBaseOnAmountToClaim(amountToClaim) + + // pinSet is false, go toPin screen + response.pinSet == false -> SaltPayActivationStep.NeedPin + + response.kycStatus != null -> { + when (currentStep) { + SaltPayActivationStep.KycWaiting -> when (response.kycStatus) { + KYCStatus.NOT_STARTED, KYCStatus.STARTED -> SaltPayActivationStep.KycIntro + KYCStatus.WAITING_FOR_APPROVAL -> SaltPayActivationStep.KycWaiting + KYCStatus.CORRECTION_REQUESTED, KYCStatus.REJECTED -> SaltPayActivationStep.KycReject + KYCStatus.APPROVED -> getStepBaseOnAmountToClaim(amountToClaim) + else -> SaltPayActivationStep.KycIntro + } + SaltPayActivationStep.KycReject -> when (response.kycStatus) { + KYCStatus.NOT_STARTED, KYCStatus.STARTED -> SaltPayActivationStep.KycStart + KYCStatus.WAITING_FOR_APPROVAL -> SaltPayActivationStep.KycWaiting + KYCStatus.CORRECTION_REQUESTED -> SaltPayActivationStep.KycStart + KYCStatus.REJECTED -> SaltPayActivationStep.KycStart + KYCStatus.APPROVED -> getStepBaseOnAmountToClaim(amountToClaim) + else -> SaltPayActivationStep.KycIntro + } + else -> when (response.kycStatus) { + KYCStatus.NOT_STARTED, KYCStatus.STARTED -> SaltPayActivationStep.KycIntro + KYCStatus.WAITING_FOR_APPROVAL -> SaltPayActivationStep.KycWaiting + KYCStatus.CORRECTION_REQUESTED, KYCStatus.REJECTED -> SaltPayActivationStep.KycReject + KYCStatus.APPROVED -> getStepBaseOnAmountToClaim(amountToClaim) + else -> SaltPayActivationStep.KycIntro + } + } + } + + // kycDate is set, go to kyc waiting screen + response.kycDate != null -> SaltPayActivationStep.KycWaiting + + else -> SaltPayActivationStep.KycIntro + } +} + +private suspend fun getAmountToClaimIfNeeded( + saltPayManager: SaltPayActivationManager, + amountToClaim: Amount?, +): Amount? { + Timber.d("getAmountToClaimIfNeeded: for amount: %s", amountToClaim) + if (amountToClaim != null) { + Timber.d("getAmountToClaimIfNeeded: checks no need, amount already persist") + return amountToClaim + } + + return when (val result = saltPayManager.getAmountToClaim()) { + is Result.Success -> { + Timber.d("getAmountToClaimIfNeeded: success: %s", result.data) + return result.data + } + is Result.Failure -> { + Timber.e(result.error, "getAmountToClaimIfNeeded: failure") + null + } + } +} + +private var handleClaimRefreshInProgress: Boolean = false + set(value) { + field = value + store.dispatchOnMain(OnboardingSaltPayAction.SetClaimRefreshInProgress(value)) + } + +private var handleInProgress: Boolean = false + set(value) { + field = value + store.dispatchOnMain(OnboardingSaltPayAction.SetInProgress(value)) + } + +private suspend fun onError(error: SaltPayActivationError) { withMainContext { - handleIsBusy = false - handleError = error + handleInProgress = false + SaltPayExceptionHandler.handle(error) } } private suspend fun onException(error: Throwable) { withMainContext { - handleIsBusy = false - handleException = error + handleInProgress = false + SaltPayExceptionHandler.handle(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, + saltPayManager: SaltPayActivationManager, + step: SaltPayActivationStep, ): Result { - if (step == SaltPayRegistrationStep.KycStart || - step == SaltPayRegistrationStep.KycWaiting || - step == SaltPayRegistrationStep.Finished - ) { + Timber.d("checkGasIfNeeded: for step: %s", step) + if (step.ordinal >= SaltPayActivationStep.KycIntro.ordinal) { + Timber.d("checkGasIfNeeded: no need to check for step: %s", step) return Result.Success(Unit) } - return saltPayManager.checkHasGas() + val result = saltPayManager.checkHasGas() + when (result) { + is Result.Success -> Timber.d("checkGasIfNeeded: has GAS") + is Result.Failure -> Timber.d("checkGasIfNeeded: has NO GAS") + } + return result } -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 { +@Throws(SaltPayActivationError::class) +fun RegistrationResponse.Item.toSaltPayStep(currentStep: SaltPayActivationStep): SaltPayActivationStep { return when { - passed != true -> throw SaltPayRegistrationError.CardNotPassed(this.error) - disabledByAdmin == true -> throw SaltPayRegistrationError.CardDisabled(this.error) + passed != true -> throw SaltPayActivationError.CardNotPassed(this.error) + disabledByAdmin == true -> throw SaltPayActivationError.CardDisabled(this.error) - // go to success screen - active == true -> SaltPayRegistrationStep.Finished + // go to claim screen + active == true -> SaltPayActivationStep.Claim // pinSet is false, go toPin screen - pinSet == false -> SaltPayRegistrationStep.NeedPin + pinSet == false -> SaltPayActivationStep.NeedPin + kycStatus != null -> { + when (kycStatus) { + KYCStatus.NOT_STARTED, KYCStatus.STARTED -> SaltPayActivationStep.KycIntro + KYCStatus.WAITING_FOR_APPROVAL -> SaltPayActivationStep.KycWaiting + KYCStatus.CORRECTION_REQUESTED, KYCStatus.REJECTED -> SaltPayActivationStep.KycReject + KYCStatus.APPROVED -> SaltPayActivationStep.Claim + null -> throw UnsupportedOperationException() + } + } // kycDate is set, go to kyc waiting screen - kycDate != null -> SaltPayRegistrationStep.KycWaiting + kycDate != null -> SaltPayActivationStep.KycWaiting - else -> SaltPayRegistrationStep.KycIntro + else -> SaltPayActivationStep.KycIntro } } -@Throws(SaltPayRegistrationError::class) +@Throws(SaltPayActivationError::class) private fun assertPinValid(pin: String, pinLength: Int) { val array = pin.toCharArray() - if (array.size < pinLength) throw SaltPayRegistrationError.WeakPin + if (array.size < pinLength) throw SaltPayActivationError.WeakPin val filters: List> = listOf( AllSymbolsTheSameFilter(), ) if (filters.any { it.filter(pin) }) { - throw SaltPayRegistrationError.WeakPin + throw SaltPayActivationError.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 index ac9ed3c67b..264829c157 100644 --- 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 @@ -1,8 +1,6 @@ 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 /** @@ -31,20 +29,6 @@ private fun internalReduce(anyAction: Action, onboardingWalletState: OnboardingW 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, @@ -62,19 +46,27 @@ private fun internalReduce(anyAction: Action, onboardingWalletState: OnboardingW ) } } - is OnboardingSaltPayAction.SetIsBusy -> { + is OnboardingSaltPayAction.SetAmountToClaim -> { oldState.copy( - isBusy = action.isBusy, + amountToClaim = action.amount, + ) + } + is OnboardingSaltPayAction.SetTokenBalance -> { + oldState.copy( + tokenAmount = oldState.tokenAmount.copy(value = action.balanceValue), + ) + } + is OnboardingSaltPayAction.SetInProgress -> { + oldState.copy( + inProgress = action.isInProgress, + ) + } + is OnboardingSaltPayAction.SetClaimRefreshInProgress -> { + oldState.copy( + claimInProgress = action.isInProgress, ) } 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) { 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 index 2ecaa4b382..6acbe4bdd2 100644 --- 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 @@ -1,45 +1,48 @@ package com.tangem.tap.features.onboarding.products.wallet.saltPay.redux +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.Blockchain 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.SaltPayWorkaround 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.SaltPayActivationManager 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 +import java.math.BigDecimal /** [REDACTED_AUTHOR] */ data class OnboardingSaltPayState( @Transient - val saltPayManager: SaltPayRegistrationManager = SaltPayRegistrationManager.stub(), + val saltPayManager: SaltPayActivationManager = SaltPayActivationManager.stub(), val saltPayConfig: SaltPayConfig = SaltPayConfig.stub(), val pinCode: String? = null, val accessCode: String? = null, - val step: SaltPayRegistrationStep = SaltPayRegistrationStep.NeedPin, + val amountToClaim: Amount? = null, + val tokenAmount: Amount = Amount(SaltPayWorkaround.tokenFrom(Blockchain.SaltPay), BigDecimal.ZERO), + val step: SaltPayActivationStep = SaltPayActivationStep.None, val saltPayCardArtworkUrl: String? = null, - val isBusy: Boolean = false, - val isTest: Boolean = false, + val inProgress: Boolean = false, + val claimInProgress: Boolean = false, ) { val mainButtonState: WidgetState - get() = if (isBusy) ProgressState.Loading else ProgressState.Done + get() = if (inProgress) ProgressState.Loading else ProgressState.Done + + fun readyToClaim(): Boolean = amountToClaim != null val pinLength: Int = 4 companion object { - fun initDependency(scanResponse: ScanResponse): Pair { + fun initDependency(scanResponse: ScanResponse): Pair { val globalState = store.state.globalState val saltPayConfig = globalState.configManager?.config?.saltPayConfig.guard { throw NullPointerException("SaltPayConfig is not initialized") @@ -52,32 +55,17 @@ data class OnboardingSaltPayState( 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 { + ): SaltPayActivationManager { if (!scanResponse.isSaltPay()) { throw IllegalArgumentException("Can't initialize the OnboardingSaltPayMiddleware if card is not SalPay") } @@ -85,14 +73,13 @@ data class OnboardingSaltPayState( throw NullPointerException("SaltPay card must have one wallet at least") } - return SaltPayRegistrationManager( + return SaltPayActivationManager( cardId = scanResponse.card.cardId, cardPublicKey = scanResponse.card.cardPublicKey, walletPublicKey = saltPaySingleWallet.publicKey, kycProvider = kycProvider, paymentologyService = paymentologyService, gnosisRegistrator = gnosisRegistrator, - registrationStorage = registrationStorage, ) } @@ -107,12 +94,18 @@ data class OnboardingSaltPayState( } } -enum class SaltPayRegistrationStep { +enum class SaltPayActivationStep { + None, NoGas, NeedPin, CardRegistration, KycIntro, KycStart, KycWaiting, + KycReject, + Claim, + ClaimInProgress, + ClaimSuccess, + Success, Finished; } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/ui/OnboardingSaltPayView.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/ui/OnboardingSaltPayView.kt new file mode 100644 index 0000000000..90321f426a --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/ui/OnboardingSaltPayView.kt @@ -0,0 +1,388 @@ +package com.tangem.tap.features.onboarding.products.wallet.saltPay.ui + +import android.view.View +import android.view.ViewGroup +import android.view.animation.OvershootInterpolator +import android.widget.ProgressBar +import androidx.annotation.LayoutRes +import androidx.annotation.StringRes +import androidx.constraintlayout.widget.ConstraintSet +import androidx.core.view.children +import androidx.transition.TransitionManager +import coil.load +import coil.size.Scale +import com.google.android.material.appbar.MaterialToolbar +import com.google.android.material.button.MaterialButton +import com.tangem.common.extensions.guard +import com.tangem.tap.common.compose.PinCodeWidget +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.getDrawableCompat +import com.tangem.tap.common.extensions.hide +import com.tangem.tap.common.extensions.setDrawable +import com.tangem.tap.common.extensions.show +import com.tangem.tap.common.extensions.stop +import com.tangem.tap.common.extensions.toFormattedCurrencyString +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.common.toggleWidget.RefreshBalanceWidget +import com.tangem.tap.common.transitions.InternalNoteLayoutTransition +import com.tangem.tap.features.onboarding.products.wallet.redux.BackupState +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.SaltPayActivationStep +import com.tangem.tap.features.onboarding.products.wallet.ui.OnboardingWalletFragment +import com.tangem.tap.features.wallet.redux.ProgressState +import com.tangem.tap.store +import com.tangem.wallet.R +import com.tangem.wallet.databinding.LayoutOnboardingContainerBottomBinding +import com.tangem.wallet.databinding.LayoutOnboardingContainerTopBinding +import com.tangem.wallet.databinding.LayoutOnboardingMainBinding + +/** +[REDACTED_AUTHOR] + */ +internal class OnboardingSaltPayView( + private val walletFragment: OnboardingWalletFragment, +) { + + private val toolbar: MaterialToolbar by lazy { walletFragment.binding.toolbar } + private val claimBinding: LayoutOnboardingMainBinding by lazy { walletFragment.bindingSaltPay.claim } + private val topContainer: LayoutOnboardingContainerTopBinding by lazy { claimBinding.onboardingTopContainer } + private val actionContainer: LayoutOnboardingContainerBottomBinding by lazy { claimBinding.onboardingActionContainer } + + private val btnRefreshBalanceWidget by lazy { + RefreshBalanceWidget(claimBinding.onboardingTopContainer.onboardingWalletContainer) + } + + private var progressButton: SaltPayProgressButton? = null + private var previousStep: SaltPayActivationStep = SaltPayActivationStep.None + + 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) + } + + fun showAddBackupCards(state: BackupState) = with(walletFragment.binding) { + 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) + } + 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) + } + else -> {} + } + } + + 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) { + SaltPayActivationStep.NoGas -> handleNoGas() + SaltPayActivationStep.NeedPin -> handleNeedPin() + SaltPayActivationStep.CardRegistration -> handleCardRegistration() + SaltPayActivationStep.KycIntro -> handleKycIntro() + SaltPayActivationStep.KycStart -> handleKycStart(state) + SaltPayActivationStep.KycWaiting -> handleKycWaiting() + SaltPayActivationStep.KycReject -> handleKycReject() + SaltPayActivationStep.Claim -> handleClaim(state) + SaltPayActivationStep.ClaimInProgress -> handleClaim(state) + SaltPayActivationStep.ClaimSuccess -> handleClaim(state) + SaltPayActivationStep.Success -> handleSuccess(state) + } + progressButton?.changeState(state.mainButtonState) + } + + private fun handleNoGas() { + // normally this shouldn't happen + store.dispatch(NavigationAction.PopBackTo(AppScreen.Home)) + store.dispatchDialogShow(SaltPayDialog.Activation.NoGas) + } + + private fun handleNeedPin() = with(walletFragment.bindingSaltPay) { + toolbar.title = getText(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 = getText(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 = getText(R.string.onboarding_navbar_kyc_start) + showOnlyView(verifyIdentity.root) { + progressButton = SaltPayProgressButton(verifyIdentity.root) + verifyIdentity.btnVerify.setOnClickListener { + store.dispatch(OnboardingSaltPayAction.OpenUtorgKYC) + } + } + } + + private fun handleKycStart(state: OnboardingSaltPayState) = with(walletFragment.bindingSaltPay) { + toolbar.title = getText(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.UtorgKYCRedirectSuccess) + }, + ) + webvVerifyIdentity.webChromeClient = UtorgWebChromeClient(walletFragment.requireActivity()) + webvVerifyIdentity.loadUrl(state.saltPayManager.kycUrlProvider.requestUrl) + } + } + + private fun handleKycWaiting() = with(walletFragment.bindingSaltPay.kycInProgress) { + toolbar.title = getText(R.string.onboarding_navbar_kyc_progress) + showOnlyView(root) + + imvInProgress.setDrawable(R.drawable.ic_in_progress) + tvHeader.text = getText(R.string.onboarding_title_kyc_waiting) + tvBody.text = getText(R.string.onboarding_subtitle_kyc_waiting) + + btnOpenSupportChat.hide() + btnOpenSupportChat.setOnClickListener { + store.dispatch(GlobalAction.OpenChat(SupportInfo())) + } + + btnKycAction.text = getText(R.string.onboarding_button_kyc_waiting) + btnKycAction.setOnClickListener { + store.dispatch(OnboardingSaltPayAction.Update) + } + progressButton = SaltPayProgressButton(root) + } + + private fun handleKycReject() = with(walletFragment.bindingSaltPay.kycInProgress) { + toolbar.title = getText(R.string.onboarding_navbar_kyc_progress) + showOnlyView(root) + + imvInProgress.setDrawable(R.drawable.ic_reject) + tvHeader.text = getText(R.string.onboarding_title_kyc_retry) + tvBody.text = getText(R.string.onboarding_subtitle_kyc_retry) + + btnOpenSupportChat.hide() + btnKycAction.text = getText(R.string.onboarding_button_kyc_start) + btnKycAction.setOnClickListener { + store.dispatch(OnboardingSaltPayAction.Update) + } + progressButton = SaltPayProgressButton(root) + } + + private fun handleClaim(state: OnboardingSaltPayState) = with(walletFragment.bindingSaltPay) { + toolbar.title = getText(R.string.onboarding_navbar_claim) + val btnMain = actionContainer.btnContainer.findViewById(R.id.btn_main_action) + val tvHeader = actionContainer.tvHeader + val tvBody = actionContainer.tvBody + + showOnlyView(claimBinding.root) { + // topContainer.progress.pbState.hide() + progressButton = SaltPayProgressButton(actionContainer.root) + topContainer.imvCardBackground.apply { + setBackgroundDrawable(context.getDrawableCompat(R.drawable.shape_rectangle_rounded_8)) + } + topContainer.imvFrontCard.load(R.drawable.img_salt_pay_visa) { + scale(Scale.FILL) + crossfade(enable = true) + } + actionContainer.btnAlternativeAction.hide() + updateConstraints(state.step, R.layout.lp_onboarding_topup_wallet) + } + + btnRefreshBalanceWidget.changeState(if (state.claimInProgress) ProgressState.Loading else ProgressState.Done) + if (btnRefreshBalanceWidget.isShowing != true) { + btnRefreshBalanceWidget.mainView.setOnClickListener { + store.dispatch(OnboardingSaltPayAction.RefreshClaim) + } + } + val tokenAmountString = tokenAmountString(state) + topContainer.onboardingTvBalance.tvBalanceValue.text = tokenAmountString ?: "" + topContainer.onboardingTvBalance.tvBalanceCurrency.text = "" + + when (state.step) { + SaltPayActivationStep.Claim -> { + claimValueString(state)?.let { + tvHeader.text = getText(R.string.onboarding_title_claim, it) + } + tvBody.text = getText(R.string.onboarding_subtitle_claim) + + btnMain.text = getText(R.string.onboarding_button_claim) + btnMain.isEnabled + btnMain.setOnClickListener { + store.dispatch(OnboardingSaltPayAction.Claim) + } + progressButton = SaltPayProgressButton(actionContainer.root) + progressButton?.isEnabled = true + } + SaltPayActivationStep.ClaimInProgress -> { + tvHeader.text = getText(R.string.onboarding_title_claim_progress) + tvBody.text = getText(R.string.onboarding_subtitle_claim_progress) + btnMain.text = getText(R.string.onboarding_button_claim) + progressButton = SaltPayProgressButton(actionContainer.root) + progressButton?.isEnabled = false + } + SaltPayActivationStep.ClaimSuccess -> { + tvHeader.setText(R.string.common_success) + tvBody.setText(R.string.onboarding_subtitle_success_claim) + + btnMain.setText(R.string.onboarding_button_continue_wallet) + btnMain.setOnClickListener { + walletFragment.showConfetti(false) + store.dispatch(OnboardingWalletAction.FinishOnboarding) + } + + btnRefreshBalanceWidget.mainView.setOnClickListener(null) + updateConstraints(state.step, R.layout.lp_onboarding_done_activation) + walletFragment.showConfetti(true) + progressButton = SaltPayProgressButton(actionContainer.root) + progressButton?.isEnabled = true + } + else -> {} + } + } + + private fun handleSuccess(state: OnboardingSaltPayState) = with(walletFragment.binding) { + walletFragment.bindingSaltPay.onboardingSaltpayContainer.hide() + onboardingWalletContainer.show() + walletFragment.showSuccess() + tvBody.text = getText(R.string.onboarding_subtitle_success_claim) + layoutButtonsCommon.btnWalletMainAction.text = getText(R.string.onboarding_button_continue_wallet) + } + + private fun tokenAmountString(state: OnboardingSaltPayState): String? { + return state.tokenAmount.value?.toFormattedCurrencyString( + decimals = state.tokenAmount.decimals, + currency = state.tokenAmount.currencySymbol, + ) + } + + private fun claimValueString(state: OnboardingSaltPayState): String? { + return state.amountToClaim?.value?.toFormattedCurrencyString( + decimals = state.amountToClaim.decimals, + currency = state.amountToClaim.currencySymbol, + ) + } + + private fun updateConstraints(currentStep: SaltPayActivationStep, @LayoutRes layoutId: Int) { + if (this.previousStep == currentStep) return + + with(claimBinding.onboardingTopContainer) { + val constraintSet = ConstraintSet() + constraintSet.clone(root.context, layoutId) + constraintSet.applyTo(onboardingWalletContainer) + val transition = InternalNoteLayoutTransition() + transition.interpolator = OvershootInterpolator() + TransitionManager.beginDelayedTransition(onboardingWalletContainer, transition) + } + } + + private fun showOnlyView(view: View, onShowListener: (() -> Unit)? = null) { + walletFragment.bindingSaltPay.onboardingSaltpayContainer.children + .filter { it.id != view.id } + .forEach { it.hide() } + view.show { onShowListener?.invoke() } + } + + private fun getText(@StringRes resId: Int): String { + return walletFragment.getString(resId) + } + + private fun getText(@StringRes resId: Int, vararg formatArgs: Any?): String { + return walletFragment.getString(resId, *formatArgs) + } + + 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/BackupAnimator.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/BackupAnimator.kt new file mode 100644 index 0000000000..d57205ce46 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/BackupAnimator.kt @@ -0,0 +1,306 @@ +package com.tangem.tap.features.onboarding.products.wallet.ui + +import com.google.android.material.button.MaterialButton +import com.tangem.tap.common.extensions.show +import com.tangem.tap.common.postUiDelayBg +import com.tangem.tap.features.onboarding.products.wallet.redux.BackupState +import com.tangem.tap.features.onboarding.products.wallet.ui.BackupCardType.FIRST_BACKUP +import com.tangem.tap.features.onboarding.products.wallet.ui.BackupCardType.ORIGIN +import com.tangem.tap.features.onboarding.products.wallet.ui.BackupCardType.SECOND_BACKUP +import com.tangem.wallet.databinding.FragmentOnboardingWalletBinding + +/** +[REDACTED_AUTHOR] + */ +interface BackupAnimator { + fun updateBackupState(backupState: BackupState) + fun setupCreateWalletState() + fun showBackupIntro(state: BackupState) + fun showScanOriginCard() + fun showAddBackupCards(state: BackupState, backupCardsCounts: Int) + fun showWritePrimaryCard(state: BackupState) + fun showWriteBackupCard(state: BackupState, backupCard: Int) + fun showSuccess(onEnd: () -> Unit) +} + +class WalletBackupAnimator( + private val cardsWidget: WalletCardsWidget, +) : BackupAnimator { + + private val viewState: State = when (cardsWidget.leapfrogWidget.getViewsCount()) { + 2 -> State.TwoCards + 3 -> State.TreeCards + else -> throw UnsupportedOperationException() + } + + private var currentStep = -1 + + override fun updateBackupState(backupState: BackupState) { + } + + override fun showBackupIntro(state: BackupState) { + currentStep = STEP_BACKUP_INTRO + + ORIGIN.show() + FIRST_BACKUP.show() + SECOND_BACKUP.show() + + cardsWidget.toWelcome() + } + + override fun setupCreateWalletState() { + currentStep = STEP_CREATE_WALLET + + cardsWidget.toFolded(false) + + ORIGIN.alpha(1f) + FIRST_BACKUP.alpha(1f) + SECOND_BACKUP.alpha(1f) + } + + override fun showScanOriginCard() { + currentStep = STEP_SCAN_ORIGIN_CARD + + cardsWidget.toFolded() + } + + override fun showAddBackupCards(state: BackupState, backupCardsCounts: Int) { + when (backupCardsCounts) { + 0 -> { + when (currentStep) { + STEP_CREATE_WALLET -> { + cardsWidget.toFolded(false) { cardsWidget.toFan() } + + FIRST_BACKUP.animateAlpha(0.6f, 0) + SECOND_BACKUP.animateAlpha(0.2f, 400) + } + STEP_BACKUP_INTRO -> { + cardsWidget.toFolded(duration = 300) { cardsWidget.toFan() } + FIRST_BACKUP.animateAlpha(0.6f, 400, 250) + SECOND_BACKUP.animateAlpha(0.2f, 400, 250) + } + STEP_SCAN_ORIGIN_CARD -> { + cardsWidget.toFan() + FIRST_BACKUP.animateAlpha(0.6f, 400, 250) + SECOND_BACKUP.animateAlpha(0.2f, 400, 250) + } + } + } + 1 -> { + cardsWidget.toFan(false) + FIRST_BACKUP.alpha(1f) + SECOND_BACKUP.alpha(0.2f) + } + 2 -> { + cardsWidget.toFan(false) + FIRST_BACKUP.alpha(1f) + SECOND_BACKUP.alpha(1f) + } + } + + currentStep = STEP_ADD_BACKUP_CARDS + } + + override fun showWritePrimaryCard(state: BackupState) { + currentStep = STEP_WRITE_PRIMARY_CARD + + cardsWidget.toLeapfrog( + onEndFold = { + FIRST_BACKUP.alpha(0.4f) + SECOND_BACKUP.alpha(0.2f) + }, + ) + } + + private var firstBackupCardAnimated = false + private var secondBackupCardAnimated = false + + override fun showWriteBackupCard(state: BackupState, backupCard: Int) { + when (viewState) { + State.TwoCards -> { + if (firstBackupCardAnimated) return + firstBackupCardAnimated = true + + val delay = if (currentStep == UNDEFINED) 700L else 0 + postUiDelayBg(delay) { + FIRST_BACKUP.alpha(1f) + cardsWidget.leapfrogWidget.leap { + ORIGIN.alpha(0.4f) + } + } + } + State.TreeCards -> { + when (backupCard) { + 1 -> { + if (firstBackupCardAnimated) return + firstBackupCardAnimated = true + FIRST_BACKUP.alpha(1f) + cardsWidget.leapfrogWidget.leap { + SECOND_BACKUP.alpha(0.4f) + ORIGIN.alpha(0.2f) + } + } + 2 -> { + if (secondBackupCardAnimated) return + secondBackupCardAnimated = true + SECOND_BACKUP.alpha(1f) + cardsWidget.leapfrogWidget.leap { + ORIGIN.alpha(0.4f) + FIRST_BACKUP.alpha(0.2f) + } + } + } + } + } + currentStep = STEP_WRITE_BACKUP_CARD + } + + override fun showSuccess(onEnd: () -> Unit) { + currentStep = STEP_SUCCESS + cardsWidget.leapfrogWidget.fold(onEnd = onEnd) + } + + private enum class State { + TreeCards, + TwoCards + } + + private fun BackupCardType.animateAlpha( + alpha: Float, + duration: Long = 0L, + startDelay: Long = 0L, + ) { + when (this) { + ORIGIN -> { + cardsWidget.getOriginCardView().animate().apply { + alpha(alpha) + this.startDelay = startDelay + this.duration = duration + } + } + FIRST_BACKUP -> { + cardsWidget.getFirstBackupCardView().animate().apply { + alpha(alpha) + this.startDelay = startDelay + this.duration = duration + } + } + SECOND_BACKUP -> { + if (viewState != State.TreeCards) return + cardsWidget.getSecondBackupCardView().animate().apply { + alpha(alpha) + this.startDelay = startDelay + this.duration = duration + } + } + } + } + + private fun BackupCardType.alpha(alpha: Float) { + when (this) { + ORIGIN -> cardsWidget.getOriginCardView().alpha = alpha + FIRST_BACKUP -> cardsWidget.getFirstBackupCardView().alpha = alpha + SECOND_BACKUP -> { + if (viewState != State.TreeCards) return + cardsWidget.getSecondBackupCardView().alpha = alpha + } + } + } + + private fun BackupCardType.show() { + when (this) { + ORIGIN -> cardsWidget.getOriginCardView().show() + FIRST_BACKUP -> cardsWidget.getFirstBackupCardView().show() + SECOND_BACKUP -> { + if (viewState != State.TreeCards) return + cardsWidget.getSecondBackupCardView().show() + } + } + } + + companion object { + private const val UNDEFINED = -1 + private const val STEP_CREATE_WALLET = 0 + private const val STEP_BACKUP_INTRO = 1 + private const val STEP_SCAN_ORIGIN_CARD = 2 + private const val STEP_ADD_BACKUP_CARDS = 3 + private const val STEP_WRITE_PRIMARY_CARD = 4 + private const val STEP_WRITE_BACKUP_CARD = 5 + private const val STEP_SUCCESS = 6 + } +} + +class TestBackupAnimation( + private val animator: BackupAnimator, + binding: FragmentOnboardingWalletBinding, +) : BackupAnimator { + + private lateinit var state: BackupState + private var steps = 0 + + init { + init(binding, this) + } + + fun setStep(step: Int, onStepUpdate: (Int) -> Unit = {}) { + steps = step + when (steps) { + 0 -> setupCreateWalletState() + 1 -> showBackupIntro(state) + 2 -> showScanOriginCard() + 3 -> showAddBackupCards(state, 0) + 4 -> showAddBackupCards(state, 1) + 5 -> showAddBackupCards(state, 2) + 6 -> showWritePrimaryCard(state) + 7 -> showWriteBackupCard(state, 1) + 8 -> showWriteBackupCard(state, 2) + else -> setStep(0) + } + onStepUpdate(steps) + } + + fun onNext(onStepUpdate: (Int) -> Unit) { + steps++ + setStep(steps, onStepUpdate) + } + + override fun updateBackupState(backupState: BackupState) { + this.state = backupState + } + + override fun setupCreateWalletState() { + animator.setupCreateWalletState() + } + + override fun showBackupIntro(state: BackupState) { + animator.showBackupIntro(state) + } + + override fun showScanOriginCard() { + animator.showScanOriginCard() + } + + override fun showAddBackupCards(state: BackupState, backupCardsCounts: Int) { + animator.showAddBackupCards(state, backupCardsCounts) + } + + override fun showWritePrimaryCard(state: BackupState) { + animator.showWritePrimaryCard(state) + } + + override fun showWriteBackupCard(state: BackupState, backupCard: Int) { + animator.showWriteBackupCard(state, backupCard) + } + + override fun showSuccess(onEnd: () -> Unit) { + animator.showSuccess(onEnd) + } + + companion object { + fun init(binding: FragmentOnboardingWalletBinding, animator: TestBackupAnimation) { + val button = MaterialButton(binding.onboardingRoot.context).apply { text = "Next" } + button.setOnClickListener { animator.onNext { button.text = "$it" } } + binding.onboardingRoot.addView(button, 0) + } + } +} \ 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 8a444373b5..47b6beb3aa 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 @@ -5,52 +5,34 @@ import android.util.TypedValue import android.view.View import android.view.ViewGroup import android.view.WindowManager +import android.widget.FrameLayout 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.core.view.MenuProvider 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.inflate 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.BaseFragment import com.tangem.tap.features.FragmentOnBackPressedHandler import com.tangem.tap.features.addBackPressHandler +import com.tangem.tap.features.onboarding.OnboardingMenuProvider import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction import com.tangem.tap.features.onboarding.products.wallet.redux.BackupState 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.saltPay.ui.OnboardingSaltPayView import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.AccessCodeDialog import com.tangem.tap.store import com.tangem.wallet.R @@ -59,23 +41,21 @@ import com.tangem.wallet.databinding.LayoutOnboardingSaltpayBinding import com.tangem.wallet.databinding.ViewOnboardingProgressBinding import org.rekotlin.StoreSubscriber -class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet), +class OnboardingWalletFragment : BaseFragment(R.layout.fragment_onboarding_wallet), StoreSubscriber, FragmentOnBackPressedHandler { private val pbBinding: ViewOnboardingProgressBinding by viewBinding(ViewOnboardingProgressBinding::bind) - private val binding: FragmentOnboardingWalletBinding by viewBinding(FragmentOnboardingWalletBinding::bind) - private val bindingSaltPay: LayoutOnboardingSaltpayBinding by lazy { binding.onboardingSaltpayContainer } + internal val binding: FragmentOnboardingWalletBinding by viewBinding(FragmentOnboardingWalletBinding::bind) + internal val bindingSaltPay: LayoutOnboardingSaltpayBinding by lazy { binding.onboardingSaltpayContainer } - private val saltPayView: SaltPayView = SaltPayView(this) + private val onboardingSaltPayView: OnboardingSaltPayView = OnboardingSaltPayView(this) private lateinit var cardsWidget: WalletCardsWidget private var accessCodeDialog: AccessCodeDialog? = null - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - postponeEnterTransition() - setHasOptionsMenu(true) + private lateinit var animator: BackupAnimator + override fun configureTransitions() { val inflater = TransitionInflater.from(requireContext()) enterTransition = inflater.inflateTransition(R.transition.fade) exitTransition = inflater.inflateTransition(R.transition.fade) @@ -84,16 +64,8 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet), override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - val typedValue = TypedValue() - resources.getValue(R.dimen.device_scale_factor_for_twins_welcome, typedValue, true) - val deviceScaleFactor = typedValue.float - - val leapfrogCalculator = PropertyCalculator( - yTranslationFactor = 25f * deviceScaleFactor, - ) - val leapfrog = LeapfrogWidget(binding.flCardsContainer, leapfrogCalculator) - cardsWidget = WalletCardsWidget(leapfrog, deviceScaleFactor) { 200f * deviceScaleFactor } - startPostponedEnterTransition() + val scaleFactor = getDeviceScaleFactor() + initCardsWidget(createLeapfrogWidget(binding.flCardsContainer, scaleFactor), scaleFactor) binding.viewPagerBackupInfo.adapter = BackupInfoAdapter() TabLayoutMediator( @@ -113,6 +85,39 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet), store.dispatch(OnboardingWalletAction.LoadArtwork) } + override fun loadToolbarMenu(): MenuProvider? = OnboardingMenuProvider() + + private fun reInitCardsWidget(backupCardsCounts: Int) = with(binding) { + val viewBackupCount = flCardsContainer.childCount - 1 + if (viewBackupCount <= 0) return@with + if (viewBackupCount == backupCardsCounts) return@with + + cardsWidget.toFolded(false) + + if (viewBackupCount > backupCardsCounts) { + flCardsContainer.removeViewAt(viewBackupCount - 1) + } else { + flCardsContainer.inflate(R.layout.view_onboarding_card, true) + } + + val scaleFactor = getDeviceScaleFactor() + initCardsWidget(createLeapfrogWidget(flCardsContainer, scaleFactor), scaleFactor) + } + + private fun createLeapfrogWidget(container: FrameLayout, deviceScaleFactor: Float): LeapfrogWidget { + val leapfrogCalculator = PropertyCalculator(yTranslationFactor = 25f * deviceScaleFactor) + return LeapfrogWidget(container, leapfrogCalculator) + } + + private fun initCardsWidget(leapfrogWidget: LeapfrogWidget, deviceScaleFactor: Float, isTest: Boolean = false) { + cardsWidget = WalletCardsWidget(leapfrogWidget, deviceScaleFactor) { 200f * deviceScaleFactor } + animator = if (isTest) { + TestBackupAnimation(WalletBackupAnimator(cardsWidget), binding) + } else { + WalletBackupAnimator(cardsWidget) + } + } + override fun onStart() { super.onStart() store.subscribe(this) { state -> @@ -129,14 +134,16 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet), override fun newState(state: OnboardingWalletState) { if (activity == null || view == null) return + if (state.isSaltPay) reInitCardsWidget(1) + animator.updateBackupState(state.backupState) requireActivity().invalidateOptionsMenu() pbBinding.pbState.max = state.getMaxProgress() pbBinding.pbState.progress = state.getProgressStep() if (state.isSaltPay) { - saltPayView.newState(state) + onboardingSaltPayView.newState(state) } else { loadImageIntoImageView(state.cardArtworkUrl, binding.imvFrontCard) loadImageIntoImageView(state.cardArtworkUrl, binding.imvFirstBackupCard) @@ -145,7 +152,7 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet), } } - private fun loadImageIntoImageView(url: String?, view: ImageView) { + internal fun loadImageIntoImageView(url: String?, view: ImageView) { view.load(url) { placeholder(R.drawable.card_placeholder_black) error(R.drawable.card_placeholder_black) @@ -153,7 +160,7 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet), } } - private fun handleOnboardingStep(state: OnboardingWalletState) { + internal fun handleOnboardingStep(state: OnboardingWalletState) { when (state.step) { OnboardingWalletStep.CreateWallet -> setupCreateWalletState() OnboardingWalletStep.Backup -> setBackupState(state.backupState, state.isSaltPay) @@ -162,16 +169,16 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet), } private fun setupCreateWalletState() = with(binding) { - layoutButtonsCommon.btnMainAction.setText(R.string.onboarding_create_wallet_button_create_wallet) - layoutButtonsCommon.btnMainAction.setOnClickListener { store.dispatch(OnboardingWalletAction.CreateWallet) } - layoutButtonsCommon.btnAlternativeAction.hide() + layoutButtonsCommon.btnWalletMainAction.setText(R.string.onboarding_create_wallet_button_create_wallet) + layoutButtonsCommon.btnWalletMainAction.setOnClickListener { store.dispatch(OnboardingWalletAction.CreateWallet) } + layoutButtonsCommon.btnWalletAlternativeAction.hide() toolbar.title = getText(R.string.onboarding_getting_started) tvHeader.setText(R.string.onboarding_create_wallet_header) tvBody.setText(R.string.onboarding_create_wallet_body) - cardsWidget.toFolded(false) { startPostponedEnterTransition() } + animator.setupCreateWalletState() } private fun setBackupState(state: BackupState, isSaltPay: Boolean) { @@ -192,52 +199,36 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet), imvFirstBackupCard.show() imvSecondBackupCard.show() - cardsWidget.toWelcome() - tvHeader.hide() tvBody.hide() viewPagerBackupInfo.show() tabLayoutBackupInfo.show() with(layoutButtonsCommon) { - btnMainAction.text = getText(R.string.onboarding_button_backup_now) - btnMainAction.setOnClickListener { store.dispatch(BackupAction.StartBackup) } + btnWalletMainAction.text = getText(R.string.onboarding_button_backup_now) + btnWalletMainAction.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) + btnWalletAlternativeAction.text = getText(R.string.onboarding_button_skip_backup) + btnWalletAlternativeAction.setOnClickListener { store.dispatch(BackupAction.DismissBackup) } + btnWalletAlternativeAction.show(state.canSkipBackup && !isSaltPay) } - - startPostponedEnterTransition() + animator.showBackupIntro(state) } private fun showScanOriginCard() = with(binding) { prepareBackupView() - - cardsWidget.toFolded() - tvHeader.text = getText(R.string.onboarding_title_scan_origin_card) tvBody.text = getString( R.string.onboarding_subtitle_scan_origin_card, ) with(layoutButtonsCommon) { - btnMainAction.text = getString(R.string.onboarding_button_scan_origin_card) - btnAlternativeAction.hide() - btnMainAction.setOnClickListener { store.dispatch(BackupAction.ScanPrimaryCard) } + btnWalletMainAction.text = getString(R.string.onboarding_button_scan_origin_card) + btnWalletAlternativeAction.hide() + btnWalletMainAction.setOnClickListener { store.dispatch(BackupAction.ScanPrimaryCard) } } - } - private fun prepareBackupView() = with(binding) { - toolbar.title = getText(R.string.onboarding_navbar_title_creating_backup) - - tvHeader.show() - tvBody.show() - viewPagerBackupInfo.hide() - tabLayoutBackupInfo.hide() - - imvFirstBackupCard.show() - imvSecondBackupCard.show() + animator.showScanOriginCard() } private fun showAddBackupCards(state: BackupState, isSaltPay: Boolean) = with(binding) { @@ -260,36 +251,38 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet), 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).duration = 200 - cardsWidget.getSecondBackupCardView().animate().alpha(0.2f).duration = 200 + onboardingSaltPayView.showAddBackupCards(state) + } else { + when (state.backupCardsNumber) { + 0 -> { + tvHeader.text = getText(R.string.onboarding_title_no_backup_cards) + tvBody.text = getText(R.string.onboarding_subtitle_no_backup_cards) } - tvHeader.text = getText(R.string.onboarding_title_no_backup_cards) - tvBody.text = getText(R.string.onboarding_subtitle_no_backup_cards) - } - 1 -> { - tvHeader.text = getText(R.string.onboarding_title_one_backup_card) - tvBody.text = getText(R.string.onboarding_subtitle_one_backup_card) - - cardsWidget.toFan(false) - cardsWidget.getFirstBackupCardView().animate().alpha(1f).duration = 400 - cardsWidget.getSecondBackupCardView().alpha = 0.2f - } - 2 -> { - tvHeader.text = getText(R.string.onboarding_title_two_backup_cards) - tvBody.text = getText(R.string.onboarding_subtitle_two_backup_cards) - - cardsWidget.toFan(false) - cardsWidget.getFirstBackupCardView().alpha = 1f - cardsWidget.getSecondBackupCardView().animate().alpha(1f).duration = 400 + 1 -> { + tvHeader.text = getText(R.string.onboarding_title_one_backup_card) + tvBody.text = getText(R.string.onboarding_subtitle_one_backup_card) + } + 2 -> { + tvHeader.text = getText(R.string.onboarding_title_two_backup_cards) + tvBody.text = getText(R.string.onboarding_subtitle_two_backup_cards) + } + else -> {} } } + + animator.showAddBackupCards(state, state.backupCardsNumber) + } + + private fun prepareBackupView() = with(binding) { + toolbar.title = getText(R.string.onboarding_navbar_title_creating_backup) + + tvHeader.show() + tvBody.show() + viewPagerBackupInfo.hide() + tabLayoutBackupInfo.hide() + + imvFirstBackupCard.show() + imvSecondBackupCard.show() } private fun showSetAccessCode() { @@ -323,89 +316,67 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet), private fun showWritePrimaryCard(state: BackupState, isSaltPay: Boolean) = with(binding) { accessCodeDialog?.dismiss() + reInitCardsWidget(state.backupCardsNumber) prepareViewForFinalizeStep() - cardsWidget.getSecondBackupCardView().show(state.backupCardsNumber == 2) - - cardsWidget.toLeapfrog { - cardsWidget.getFirstBackupCardView().alpha = 0.6f - cardsWidget.getSecondBackupCardView().alpha = 0.2f - } val cardIdFormatter = CardIdFormatter(CardIdDisplayFormat.LastMasked(4)) 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) + layoutButtonsCommon.btnWalletMainAction.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.btnWalletMainAction.text = getText(R.string.onboarding_button_backup_origin) } + layoutButtonsCommon.btnWalletMainAction.setOnClickListener { store.dispatch(BackupAction.WritePrimaryCard) } - layoutButtonsCommon.btnMainAction.setOnClickListener { store.dispatch(BackupAction.WritePrimaryCard) } - + animator.showWritePrimaryCard(state) } private fun prepareViewForFinalizeStep() = with(binding) { - layoutButtonsAddCards.root.hide() - layoutButtonsCommon.root.show() - - imvFirstBackupCard.show() - imvSecondBackupCard.show() - toolbar.title = getText(R.string.onboarding_button_finalize_backup) + layoutButtonsAddCards.root.hide() + layoutButtonsCommon.root.show() + layoutButtonsCommon.btnWalletAlternativeAction.hide() + imvCardBackground.hide() - - cardsWidget.toLeapfrog() - - layoutButtonsCommon.btnAlternativeAction.hide() + imvFirstBackupCard.show() + imvSecondBackupCard.show() } 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 { - cardsWidget.getOriginCardView().alpha = 0.4f - cardsWidget.getFirstBackupCardView().alpha = 0.2f - cardsWidget.getSecondBackupCardView().alpha = 1f - } - 2 -> cardsWidget.leapfrogWidget.leap { - cardsWidget.getOriginCardView().alpha = 0.4f - cardsWidget.getFirstBackupCardView().alpha = 0.2f - cardsWidget.getSecondBackupCardView().alpha = 1f - } - } - - val cardIdFormatter = CardIdFormatter(CardIdDisplayFormat.LastMasked(4)) 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) + layoutButtonsCommon.btnWalletMainAction.text = getString(R.string.onboarding_saltpay_title_backup_card) } else { + 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( + layoutButtonsCommon.btnWalletMainAction.text = getString( R.string.onboarding_button_backup_card_format, cardNumber, ) } - layoutButtonsCommon.btnMainAction.setOnClickListener { + layoutButtonsCommon.btnWalletMainAction.setOnClickListener { store.dispatch(BackupAction.WriteBackupCard(cardNumber)) } + + animator.showWriteBackupCard(state, cardNumber) } - private fun showSuccess() = with(binding) { + internal fun showSuccess() = with(binding) { toolbar.title = getString(R.string.onboarding_done_header) tvHeader.text = getText(R.string.onboarding_done_header) @@ -415,24 +386,30 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet), tabLayoutBackupInfo.hide() tvBody.text = getText(R.string.onboarding_subtitle_success_tangem_wallet_onboarding) - layoutButtonsCommon.btnMainAction.text = getText(R.string.onboarding_button_continue_wallet) - layoutButtonsCommon.btnAlternativeAction.hide() - layoutButtonsCommon.btnMainAction.setOnClickListener { - vConfetti.lavConfetti.cancelAnimation() - vConfetti.lavConfetti.hide() + layoutButtonsCommon.btnWalletMainAction.text = getText(R.string.onboarding_button_continue_wallet) + layoutButtonsCommon.btnWalletAlternativeAction.hide() + layoutButtonsCommon.btnWalletMainAction.setOnClickListener { + showConfetti(false) store.dispatch(OnboardingWalletAction.FinishOnboarding) } - cardsWidget.leapfrogWidget.fold { + animator.showSuccess { flCardsContainer.hide() imvCardBackground.hide() - vConfetti.lavConfetti.show() - vConfetti.lavConfetti.playAnimation() + showConfetti(true) imvSuccess.alpha = 0f imvSuccess.show() + imvSuccess.animate()?.alpha(1f)?.duration = 400 + } + } - imvSuccess.animate() - ?.alpha(1f)?.duration = 400 + internal fun showConfetti(show: Boolean) = with(binding.vConfetti) { + lavConfetti.show(show) + + if (show) { + lavConfetti.playAnimation() + } else { + lavConfetti.cancelAnimation() } } @@ -440,203 +417,9 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet), 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 - } - } - } + private fun getDeviceScaleFactor(): Float { + val typedValue = TypedValue() + resources.getValue(R.dimen.device_scale_factor_for_twins_welcome, typedValue, true) + return typedValue.float } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/WalletCardsWidget.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/WalletCardsWidget.kt index 833c3dfb81..7d4831c434 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/WalletCardsWidget.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/WalletCardsWidget.kt @@ -16,87 +16,100 @@ class WalletCardsWidget( val getTopOfAnchorViewForActivateState: () -> Float, ) { - val animDuration: Long = 400 + private val animDuration: Long = 400 var currentState: WidgetState? = null + private set - fun toWelcome(animate: Boolean = true, onEnd: () -> Unit = {}) { + fun toWelcome( + animate: Boolean = true, + onEnd: () -> Unit = {}, + ) { if (currentState == WidgetState.WELCOME) return currentState = WidgetState.WELCOME - val animator = createAnimator(animate, onEnd) - animator.playTogether( - createAnimator(BackupCardType.ORIGIN, createWelcomeProperties(BackupCardType.ORIGIN)), - createAnimator(BackupCardType.FIRST_BACKUP, createWelcomeProperties(BackupCardType.FIRST_BACKUP)), - createAnimator(BackupCardType.SECOND_BACKUP, createWelcomeProperties(BackupCardType.SECOND_BACKUP)), - ) + val animator = createAnimator(animate, onEnd = onEnd) + animator.playTogether(createAnimators(::createWelcomeProperties)) leapfrogWidget.fold { animator.start() } } - fun toFolded(animate: Boolean = true, onEnd: () -> Unit = {}) { + fun toFolded( + animate: Boolean = true, + duration: Long = animDuration, + onEnd: () -> Unit = {}, + ) { if (currentState == WidgetState.FOLDED) return currentState = WidgetState.FOLDED - val animator = createAnimator(animate, onEnd) - animator.playTogether( - createAnimator(BackupCardType.ORIGIN, createLeapfrogProperties(BackupCardType.ORIGIN)), - createAnimator(BackupCardType.FIRST_BACKUP, createLeapfrogProperties(BackupCardType.FIRST_BACKUP)), - createAnimator(BackupCardType.SECOND_BACKUP, createLeapfrogProperties(BackupCardType.SECOND_BACKUP)), - ) - leapfrogWidget.fold(animate) { animator.start() } + val animator = createAnimator(animate, duration) { + leapfrogWidget.fold(animate, onEnd) + } + animator.playTogether(createAnimators(::createLeapfrogProperties)) + animator.start() } - fun toFan(animate: Boolean = true, onEnd: () -> Unit = {}) { + fun toFan( + animate: Boolean = true, + onEnd: () -> Unit = {}, + ) { if (currentState == WidgetState.FAN) return currentState = WidgetState.FAN - val animator = createAnimator(animate, onEnd) - animator.playTogether( - createAnimator(BackupCardType.ORIGIN, createFanProperties(BackupCardType.ORIGIN)), - createAnimator(BackupCardType.FIRST_BACKUP, createFanProperties(BackupCardType.FIRST_BACKUP)), - createAnimator(BackupCardType.SECOND_BACKUP, createFanProperties(BackupCardType.SECOND_BACKUP)), - ) + val animator = createAnimator(animate, onEnd = onEnd) + animator.playTogether(createAnimators(::createFanProperties)) leapfrogWidget.fold { animator.start() } } - fun toLeapfrog(animate: Boolean = true, onEnd: () -> Unit = {}) { + fun toLeapfrog( + animate: Boolean = true, + onEndFold: () -> Unit = {}, + onEnd: () -> Unit = {}, + ) { + if (currentState == WidgetState.LEAPFROG) return + currentState = WidgetState.LEAPFROG - val animator = createAnimator(animate, onEnd) - animator.playTogether( - createAnimator(BackupCardType.ORIGIN, createLeapfrogProperties(BackupCardType.ORIGIN)), - createAnimator(BackupCardType.FIRST_BACKUP, createLeapfrogProperties(BackupCardType.FIRST_BACKUP)), - createAnimator(BackupCardType.SECOND_BACKUP, createLeapfrogProperties(BackupCardType.SECOND_BACKUP)), - ) - leapfrogWidget.fold { - animator.doOnEnd { + val onAnimatorEnd = { + leapfrogWidget.fold(animate) { leapfrogWidget.initViews() - leapfrogWidget.unfold() + onEndFold() + leapfrogWidget.unfold(animate, onEnd) } - animator.start() } + val animator = createAnimator(animate, onEnd = onAnimatorEnd) + animator.playTogether(createAnimators(::createLeapfrogProperties)) + animator.start() } - private fun createAnimator(animate: Boolean, onEnd: () -> Unit): AnimatorSet { + private fun createAnimators(propertyFactory: (BackupCardType) -> CardProperties): List { + return getCardTypesTypes().map { createCardAnimator(it, propertyFactory(it)) } + } + + private fun getCardTypesTypes(): List = when (leapfrogWidget.getViewsCount()) { + 2 -> listOf(BackupCardType.ORIGIN, BackupCardType.FIRST_BACKUP) + 3 -> listOf(BackupCardType.ORIGIN, BackupCardType.FIRST_BACKUP, BackupCardType.SECOND_BACKUP) + else -> throw UnsupportedOperationException() + } + + private fun createAnimator(animate: Boolean, duration: Long = animDuration, onEnd: () -> Unit): AnimatorSet { return AnimatorSet().apply { - duration = if (animate) animDuration else 0 + this.duration = if (animate) duration else 0 doOnEnd { onEnd() } } } - private fun createAnimator( + private fun createCardAnimator( cardType: BackupCardType, properties: CardProperties, ): ObjectAnimator { - val view = getLeapViewByCardNumber(cardType).view - val animator = - ObjectAnimator.ofPropertyValuesHolder( - view, - *properties.createValuesHolders().toTypedArray(), - ) + val view = getLeapViewByCard(cardType).view + val animator = ObjectAnimator.ofPropertyValuesHolder( + view, + *properties.createValuesHolders().toTypedArray(), + ) view.elevation = properties.elevation return animator } @@ -181,7 +194,7 @@ class WalletCardsWidget( private fun createActivateProperties(cardType: BackupCardType): CardProperties { val topOfAnchorView = getTopOfAnchorViewForActivateState() - val twinProperties = CardProperties.from(getLeapViewByCardNumber(cardType).state) + val twinProperties = CardProperties.from(getLeapViewByCard(cardType).state) return twinProperties.copy( xTranslation = twinProperties.xTranslation, yTranslation = twinProperties.yTranslation - topOfAnchorView, @@ -190,24 +203,40 @@ class WalletCardsWidget( ) } - private fun getLeapViewByCardNumber(cardType: BackupCardType): LeapView { - return when (cardType) { - BackupCardType.ORIGIN -> leapfrogWidget.getViewByPosition(0) - BackupCardType.FIRST_BACKUP -> leapfrogWidget.getViewByPosition(1) - BackupCardType.SECOND_BACKUP -> leapfrogWidget.getViewByPosition(2) - } - } - + @Throws(UnsupportedOperationException::class) fun getOriginCardView(): ImageView { - return getLeapViewByCardNumber(BackupCardType.ORIGIN).view as ImageView + return getLeapViewByCard(BackupCardType.ORIGIN).view as ImageView } + @Throws(UnsupportedOperationException::class) fun getFirstBackupCardView(): ImageView { - return getLeapViewByCardNumber(BackupCardType.FIRST_BACKUP).view as ImageView + return getLeapViewByCard(BackupCardType.FIRST_BACKUP).view as ImageView } + @Throws(UnsupportedOperationException::class) fun getSecondBackupCardView(): ImageView { - return getLeapViewByCardNumber(BackupCardType.SECOND_BACKUP).view as ImageView + return getLeapViewByCard(BackupCardType.SECOND_BACKUP).view as ImageView + } + + @Throws(UnsupportedOperationException::class) + private fun getLeapViewByCard(cardType: BackupCardType): LeapView { + fun getByIndex(index: Int): LeapView { + return leapfrogWidget.getViewByPosition(leapfrogWidget.getViewPositionByIndex(index)) + } + + return when (leapfrogWidget.getViewsCount()) { + 2 -> when (cardType) { + BackupCardType.ORIGIN -> getByIndex(1) + BackupCardType.FIRST_BACKUP -> getByIndex(0) + BackupCardType.SECOND_BACKUP -> throw UnsupportedOperationException() + } + 3 -> when (cardType) { + BackupCardType.ORIGIN -> getByIndex(2) + BackupCardType.FIRST_BACKUP -> getByIndex(1) + BackupCardType.SECOND_BACKUP -> getByIndex(0) + } + else -> throw UnsupportedOperationException() + } } enum class WidgetState { WELCOME, FOLDED, FAN, LEAPFROG } diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt index defca803e3..22cd5ad13a 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt @@ -17,13 +17,12 @@ import com.tangem.common.services.Result import com.tangem.domain.common.TapWorkarounds.isStart2Coin import com.tangem.domain.common.extensions.withMainContext import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE -import com.tangem.tap.common.analytics.Analytics -import com.tangem.tap.common.analytics.AnalyticsEvent -import com.tangem.tap.common.analytics.AnalyticsParam +import com.tangem.tap.common.analytics.AnalyticsAnOld +import com.tangem.tap.common.analytics.AnalyticsEventAnOld +import com.tangem.tap.common.analytics.AnalyticsParamAnOld 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.extensions.logSendTransactionError import com.tangem.tap.common.extensions.safeUpdate import com.tangem.tap.common.extensions.stripZeroPlainString import com.tangem.tap.common.redux.AppDialog @@ -222,8 +221,8 @@ private fun sendTransaction( when (sendResult) { is SimpleResult.Success -> { - store.state.globalState.analyticsHandlers?.triggerEvent( - event = AnalyticsEvent.TRANSACTION_IS_SENT, + store.state.globalState.analyticsHandler?.handleAnalyticsEvent( + event = AnalyticsEventAnOld.TRANSACTION_IS_SENT, card = card, blockchain = walletManager.wallet.blockchain.currency, ) @@ -251,10 +250,10 @@ private fun sendTransaction( feeAmount = feeAmount, destinationAddress = destinationAddress, ) - store.state.globalState.analyticsHandlers?.logSendTransactionError( + store.state.globalState.analyticsHandler?.handleBlockchainSdkErrorEvent( error = sendResult.error, - action = Analytics.ActionToLog.SendTransaction, - parameters = mapOf(AnalyticsParam.BLOCKCHAIN to walletManager.wallet.blockchain.currency), + action = AnalyticsAnOld.ActionToLog.SendTransaction, + params = mapOf(AnalyticsParamAnOld.BLOCKCHAIN to walletManager.wallet.blockchain.currency), card = card, ) diff --git a/app/src/main/java/com/tangem/tap/features/shop/redux/ShopMiddleware.kt b/app/src/main/java/com/tangem/tap/features/shop/redux/ShopMiddleware.kt index f3734950da..ceba9a5217 100644 --- a/app/src/main/java/com/tangem/tap/features/shop/redux/ShopMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/shop/redux/ShopMiddleware.kt @@ -121,7 +121,7 @@ private fun handle(action: Action) { scope.launch { shopService.waitForCheckout( productType = shopState.selectedProduct, - analyticsHandler = store.state.globalState.analyticsHandlers + analyticsHandler = store.state.globalState.analyticsHandler ) } } diff --git a/app/src/main/java/com/tangem/tap/features/tokens/ui/AddTokensFragment.kt b/app/src/main/java/com/tangem/tap/features/tokens/ui/AddTokensFragment.kt index 8abbc33a2b..9a9052115c 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/ui/AddTokensFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/ui/AddTokensFragment.kt @@ -30,10 +30,14 @@ import com.tangem.tap.store import com.tangem.wallet.R import com.tangem.wallet.databinding.FragmentAddTokensBinding import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import org.rekotlin.StoreSubscriber - class AddTokensFragment : Fragment(R.layout.fragment_add_tokens), StoreSubscriber { @@ -123,7 +127,7 @@ class AddTokensFragment : Fragment(R.layout.fragment_add_tokens), } override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - inflater.inflate(R.menu.popular_tokens, menu) + inflater.inflate(R.menu.menu_popular_tokens, menu) val addCustomTokenAllowed = store.state.tokensState.allowToAdd menu.findItem(R.id.menu_navigate_add_custom_token).isVisible = addCustomTokenAllowed 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 19d4a51cf0..ca2323f38e 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 @@ -11,7 +11,7 @@ import com.tangem.common.services.Result import com.tangem.domain.common.extensions.withMainContext import com.tangem.operations.attestation.Attestation import com.tangem.operations.attestation.OnlineCardVerifier -import com.tangem.tap.common.analytics.Analytics +import com.tangem.tap.common.analytics.AnalyticsAnOld import com.tangem.tap.common.extensions.copyToClipboard import com.tangem.tap.common.extensions.dispatchDebugErrorNotification import com.tangem.tap.common.extensions.dispatchOnMain @@ -186,9 +186,9 @@ class WalletMiddleware { } is CompletionResult.Failure -> { (result.error as? TangemSdkError)?.let { error -> - store.state.globalState.analyticsHandlers?.logCardSdkError( + store.state.globalState.analyticsHandler?.handleCardSdkErrorEvent( error, - Analytics.ActionToLog.CreateWallet, + AnalyticsAnOld.ActionToLog.CreateWallet, card = store.state.detailsState.scanResponse?.card ) } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WarningsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WarningsMiddleware.kt index 7aa45f997d..8bf72ef3c7 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WarningsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WarningsMiddleware.kt @@ -8,7 +8,7 @@ import com.tangem.common.card.Card import com.tangem.common.card.FirmwareVersion import com.tangem.domain.common.ScanResponse import com.tangem.domain.common.TapWorkarounds.isTestCard -import com.tangem.tap.common.analytics.AnalyticsEvent +import com.tangem.tap.common.analytics.AnalyticsEventAnOld import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.isGreaterThan import com.tangem.tap.common.redux.global.GlobalState @@ -25,10 +25,10 @@ import com.tangem.tap.preferencesStorage import com.tangem.tap.scope import com.tangem.tap.store import com.tangem.wallet.R -import java.math.BigDecimal import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import java.math.BigDecimal class WarningsMiddleware { fun handle(action: WalletAction.Warnings, globalState: GlobalState?) { @@ -81,7 +81,7 @@ class WarningsMiddleware { preferencesStorage.appRatingLaunchObserver.foundWalletWithFunds() } if (preferencesStorage.appRatingLaunchObserver.isReadyToShow()) { - store.state.globalState.analyticsHandlers?.triggerEvent(AnalyticsEvent.APP_RATING_DISPLAYED) + store.state.globalState.analyticsHandler?.handleAnalyticsEvent(AnalyticsEventAnOld.APP_RATING_DISPLAYED) addWarningMessage(WarningMessagesManager.appRatingWarning(), true) } } 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 7d124f2a41..9f2f76e6fd 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 @@ -116,6 +116,8 @@ class OnWalletLoadedReducer { private fun onSingleWalletLoaded(wallet: Wallet, walletState: WalletState): WalletState { if (wallet.blockchain != walletState.primaryBlockchain) return walletState + // val ratesRespository = store.state.globalState.tapWalletManager.ratesRepository + // val tokenCurrency = val fiatCurrencyName = store.state.globalState.appCurrency.code val token = wallet.getFirstToken() val tokenData = if (token != null) { diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt index db9173e486..281d4b3938 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt @@ -42,7 +42,7 @@ import com.tangem.tap.features.wallet.redux.WalletState.Companion.UNKNOWN_AMOUNT import com.tangem.tap.features.wallet.ui.adapters.PendingTransactionsAdapter import com.tangem.tap.features.wallet.ui.adapters.WalletDetailWarningMessagesAdapter import com.tangem.tap.features.wallet.ui.images.load -import com.tangem.tap.features.wallet.ui.test.TestWalletDetails +import com.tangem.tap.features.wallet.ui.test.TestWallet import com.tangem.tap.store import com.tangem.wallet.R import com.tangem.wallet.databinding.FragmentWalletDetailsBinding @@ -125,7 +125,7 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), view?.findViewById(R.id.l_balance)?.let { view -> TestActions.initFor( view = view, - actions = TestWalletDetails.solanaRentExemptWarning() + actions = TestWallet.solanaRentExemptWarning() ) } } @@ -345,7 +345,7 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), } override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - inflater.inflate(R.menu.wallet_details, menu) + inflater.inflate(R.menu.menu_wallet_details, menu) } private fun TextView.setWarningStatus(mainMessage: Int, error: String? = null) { 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 1f7d881721..87a31563fa 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 @@ -139,7 +139,7 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber { val checker = SolanaRentWarningActionEmitter() @@ -26,6 +30,80 @@ class TestWalletDetails { "BALANCE = 0.00089089" to { checker.setMoreThanRentExemptByOne() }, ) } + + fun getBlockchainBalanceActions(blockchainNetwork: BlockchainNetwork): List { + return getBlockchainBalanceActions( + getWalletManager(blockchainNetwork), + blockchainNetwork.blockchain.decimals(), + ) + } + + fun getTokenBalanceAction(blockchainNetwork: BlockchainNetwork, token: Token): List { + return getTokenBalanceAction(getWalletManager(blockchainNetwork), token) + } + + fun getBlockchainBalanceActions(walletManager: WalletManager?, decimals: Int): List { + val minValue = BigDecimal.ONE.movePointLeft(decimals) + val averageValue = BigDecimal.ONE.movePointRight(decimals) + .divide(BigDecimal(2)).movePointLeft(decimals) + val maxValue = BigDecimal(2).pow(32) + + return listOf( + "0.0" to { setBalance(walletManager, BigDecimal.ZERO) }, + "Min value" to { setBalance(walletManager, minValue) }, + "Average value" to { setBalance(walletManager, averageValue) }, + "Max value" to { setBalance(walletManager, maxValue) }, + "1234567890123.01234567890123" to { setBalance(walletManager, BigDecimal.ZERO) }, + ) + } + + fun getTokenBalanceAction(walletManager: WalletManager?, token: Token): List { + val minValue = BigDecimal.ONE.movePointLeft(token.decimals) + val averageValue = BigDecimal.ONE.movePointRight(token.decimals) + .divide(BigDecimal(2)).movePointLeft(token.decimals) + val maxValue = BigDecimal(2).pow(32) + + return listOf( + "0.0" to { setTokenBalance(walletManager, BigDecimal.ZERO, token) }, + "Min value" to { setTokenBalance(walletManager, minValue, token) }, + "Average value" to { setTokenBalance(walletManager, averageValue, token) }, + "Max value" to { setTokenBalance(walletManager, maxValue, token) }, + "1234567890123.01234567890123" to { setTokenBalance(walletManager, BigDecimal.ZERO, token) }, + ) + } + + fun setBalance(walletManager: WalletManager?, value: BigDecimal) { + val manager = walletManager.guard { + store.dispatchDebugErrorNotification("WalletManager not found") + return + } + val amount = manager.wallet.amounts[AmountType.Coin] ?: Amount(manager.wallet.blockchain) + setBalance(manager, amount, value) + } + + private fun setTokenBalance(walletManager: WalletManager?, value: BigDecimal, token: Token? = null) { + val manager = walletManager.guard { + store.dispatchDebugErrorNotification("WalletManager not found") + return + } + val token = token.guard { + store.dispatchDebugErrorNotification("Token not found") + return + } + + val amount = manager.wallet.amounts[AmountType.Token(token)] ?: Amount(token) + setBalance(manager, amount, value) + } + + private fun setBalance(walletManager: WalletManager, amount: Amount, value: BigDecimal) { + TestActions.testAmountInjectionForWalletManagerEnabled = true + walletManager.wallet.setAmount(amount.copy(value = value)) + store.dispatch(WalletAction.LoadData) + } + + private fun getWalletManager(blockchainNetwork: BlockchainNetwork): WalletManager? { + return store.state.walletState.getWalletManager(blockchainNetwork) + } } } diff --git a/app/src/main/java/com/tangem/tap/persistence/DisclaimerPrefStorage.kt b/app/src/main/java/com/tangem/tap/persistence/DisclaimerPrefStorage.kt new file mode 100644 index 0000000000..7c016eaa5d --- /dev/null +++ b/app/src/main/java/com/tangem/tap/persistence/DisclaimerPrefStorage.kt @@ -0,0 +1,25 @@ +package com.tangem.tap.persistence + +import android.content.SharedPreferences +import androidx.core.content.edit + +/** +[REDACTED_AUTHOR] + */ +class DisclaimerPrefStorage( + private val preferences: SharedPreferences, +) { + + var hasTangemTosAccepted: Boolean + get() = preferences.getBoolean(KEY_TANGEM_TOS, false) + set(value) = preferences.edit { putBoolean(KEY_TANGEM_TOS, value) } + + var hasSaltPayTosAccepted: Boolean + get() = preferences.getBoolean(KEY_SALT_PAY_TOS, false) + set(value) = preferences.edit { putBoolean(KEY_SALT_PAY_TOS, value) } + + companion object { + private const val KEY_TANGEM_TOS = "tangem_tos_accepted" + private const val KEY_SALT_PAY_TOS = "saltPay_tos_accepted" + } +} \ No newline at end of file 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 723af0df99..fad5cd8acd 100644 --- a/app/src/main/java/com/tangem/tap/persistence/PreferencesStorage.kt +++ b/app/src/main/java/com/tangem/tap/persistence/PreferencesStorage.kt @@ -7,15 +7,15 @@ import androidx.core.content.edit import com.tangem.common.json.MoshiJsonConverter import java.util.* - class PreferencesStorage(applicationContext: Application) { - private val preferences: SharedPreferences = applicationContext.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE) + private val preferences: SharedPreferences = + applicationContext.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE) val appRatingLaunchObserver: AppRatingLaunchObserver val usedCardsPrefStorage: UsedCardsPrefStorage val fiatCurrenciesPrefStorage: FiatCurrenciesPrefStorage - val saltPayRegistrationStorage: SaltPayRegistrationStorage + val disclaimerPrefStorage: DisclaimerPrefStorage init { incrementLaunchCounter() @@ -24,7 +24,7 @@ class PreferencesStorage(applicationContext: Application) { usedCardsPrefStorage.migrate() fiatCurrenciesPrefStorage = FiatCurrenciesPrefStorage(preferences, MoshiJsonConverter.INSTANCE) fiatCurrenciesPrefStorage.migrate() - saltPayRegistrationStorage = SaltPayRegistrationPrefStorage(applicationContext, MoshiJsonConverter.INSTANCE) + disclaimerPrefStorage = DisclaimerPrefStorage(preferences) } var chatFirstLaunchTime: Long? @@ -38,14 +38,6 @@ class PreferencesStorage(applicationContext: Application) { return usedCardsPrefStorage.wasScanned(cardId) } - fun saveDisclaimerAccepted() { - preferences.edit { putBoolean(DISCLAIMER_ACCEPTED_KEY, true) } - } - - fun wasDisclaimerAccepted(): Boolean { - return preferences.getBoolean(DISCLAIMER_ACCEPTED_KEY, false) - } - fun saveTwinsOnboardingShown() { preferences.edit { putBoolean(TWINS_ONBOARDING_SHOWN_KEY, true) } } @@ -61,12 +53,10 @@ class PreferencesStorage(applicationContext: Application) { companion object { private const val PREFERENCES_NAME = "tapPrefs" - private const val DISCLAIMER_ACCEPTED_KEY = "disclaimerAccepted" private const val TWINS_ONBOARDING_SHOWN_KEY = "twinsOnboardingShown" private const val APP_LAUNCH_COUNT_KEY = "launchCount" private const val CHAT_FIRST_LAUNCH_KEY = "chatFirstLaunchKey" } - } class AppRatingLaunchObserver( diff --git a/app/src/main/java/com/tangem/tap/persistence/SaltPayRegistrationPrefStorage.kt b/app/src/main/java/com/tangem/tap/persistence/SaltPayRegistrationPrefStorage.kt deleted file mode 100644 index 0fd6a6cac6..0000000000 --- a/app/src/main/java/com/tangem/tap/persistence/SaltPayRegistrationPrefStorage.kt +++ /dev/null @@ -1,82 +0,0 @@ -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_reject.xml b/app/src/main/res/drawable/ic_reject.xml new file mode 100644 index 0000000000..776aa18595 --- /dev/null +++ b/app/src/main/res/drawable/ic_reject.xml @@ -0,0 +1,22 @@ + + + + + + diff --git a/app/src/main/res/layout-h680dp/layout_onboarding_container_bottom.xml b/app/src/main/res/layout-h680dp/layout_onboarding_container_bottom.xml index e635e3ee6f..b14f8571de 100644 --- a/app/src/main/res/layout-h680dp/layout_onboarding_container_bottom.xml +++ b/app/src/main/res/layout-h680dp/layout_onboarding_container_bottom.xml @@ -13,7 +13,7 @@ android:layout_marginBottom="16dp" android:gravity="center" android:orientation="vertical" - app:layout_constraintBottom_toTopOf="@+id/fl_scan_card_container" + app:layout_constraintBottom_toTopOf="@+id/btn_container" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" @@ -39,7 +39,7 @@ android:layout_marginTop="10dp" android:layout_marginEnd="64dp" android:minLines="3" - app:layout_constraintBottom_toTopOf="@+id/fl_scan_card_container" + app:layout_constraintBottom_toTopOf="@+id/btn_container" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/tv_header" @@ -55,14 +55,14 @@ android:textStyle="bold" android:visibility="gone" app:buttonTint="@color/accent" - app:layout_constraintBottom_toTopOf="@+id/fl_scan_card_container" + app:layout_constraintBottom_toTopOf="@+id/btn_container" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/linearLayoutCompat" tools:visibility="visible" /> + tools:text="@string/home_button_scan" /> + + + app:layout_constraintTop_toBottomOf="@+id/guideline1" /> @@ -62,7 +67,7 @@ app:layout_constraintStart_toStartOf="parent"> diff --git a/app/src/main/res/layout/fragment_add_tokens.xml b/app/src/main/res/layout/fragment_add_tokens.xml index a845918beb..f7a8fa24dc 100644 --- a/app/src/main/res/layout/fragment_add_tokens.xml +++ b/app/src/main/res/layout/fragment_add_tokens.xml @@ -19,7 +19,7 @@ android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" - app:menu="@menu/popular_tokens" + app:menu="@menu/menu_popular_tokens" app:navigationIcon="@drawable/ic_baseline_arrow_back_24" app:title="@string/add_tokens_title" /> diff --git a/app/src/main/res/layout/fragment_disclaimer.xml b/app/src/main/res/layout/fragment_disclaimer.xml index 556960be96..604410e507 100644 --- a/app/src/main/res/layout/fragment_disclaimer.xml +++ b/app/src/main/res/layout/fragment_disclaimer.xml @@ -22,6 +22,7 @@ android:layout_height="?attr/actionBarSize" app:navigationIcon="@drawable/ic_baseline_arrow_back_24" app:title="@string/disclaimer_title" /> + - + app:layout_constraintTop_toTopOf="parent"> + + + + - - - - - - - + diff --git a/app/src/main/res/layout/fragment_wallet.xml b/app/src/main/res/layout/fragment_wallet.xml index 8c49e592c4..ab388fa0cd 100644 --- a/app/src/main/res/layout/fragment_wallet.xml +++ b/app/src/main/res/layout/fragment_wallet.xml @@ -21,7 +21,7 @@ android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" - app:menu="@menu/wallet" + app:menu="@menu/menu_wallet" app:navigationIcon="@drawable/ic_scan_card" app:navigationIconTint="@color/darkGray6" app:title="@string/wallet_title" diff --git a/app/src/main/res/layout/fragment_wallet_details.xml b/app/src/main/res/layout/fragment_wallet_details.xml index 04649efe6b..1467361b47 100644 --- a/app/src/main/res/layout/fragment_wallet_details.xml +++ b/app/src/main/res/layout/fragment_wallet_details.xml @@ -22,7 +22,7 @@ android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:theme="@style/ThemeOverlay.MyTheme.Toolbar" - app:menu="@menu/wallet_details" + app:menu="@menu/menu_wallet_details" app:navigationIcon="@drawable/ic_baseline_arrow_back_24" app:title=" " /> 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 9bd99de16a..f51dffd3cb 100644 --- a/app/src/main/res/layout/layout_onboarding_buttons_common.xml +++ b/app/src/main/res/layout/layout_onboarding_buttons_common.xml @@ -6,7 +6,7 @@ android:layout_height="wrap_content"> + tools:text="@string/home_button_scan" /> + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_onboarding_saltpay.xml b/app/src/main/res/layout/layout_onboarding_saltpay.xml index 193cf0753c..fbb3534b9b 100644 --- a/app/src/main/res/layout/layout_onboarding_saltpay.xml +++ b/app/src/main/res/layout/layout_onboarding_saltpay.xml @@ -16,7 +16,7 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_onboarding_saltpay_kyc_in_progress.xml b/app/src/main/res/layout/layout_onboarding_saltpay_kyc_in_progress.xml new file mode 100644 index 0000000000..580995fc6f --- /dev/null +++ b/app/src/main/res/layout/layout_onboarding_saltpay_kyc_in_progress.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + \ 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_kyc_start.xml similarity index 100% rename from app/src/main/res/layout/layout_onboarding_saltpay_verify_identity.xml rename to app/src/main/res/layout/layout_onboarding_saltpay_kyc_start.xml diff --git a/app/src/main/res/layout/view_onboarding_card.xml b/app/src/main/res/layout/view_onboarding_card.xml new file mode 100644 index 0000000000..e3086ff628 --- /dev/null +++ b/app/src/main/res/layout/view_onboarding_card.xml @@ -0,0 +1,10 @@ + + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_onboarding.xml b/app/src/main/res/menu/menu_onboarding.xml new file mode 100644 index 0000000000..bc09d08fae --- /dev/null +++ b/app/src/main/res/menu/menu_onboarding.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/popular_tokens.xml b/app/src/main/res/menu/menu_popular_tokens.xml similarity index 100% rename from app/src/main/res/menu/popular_tokens.xml rename to app/src/main/res/menu/menu_popular_tokens.xml diff --git a/app/src/main/res/menu/wallet.xml b/app/src/main/res/menu/menu_wallet.xml similarity index 100% rename from app/src/main/res/menu/wallet.xml rename to app/src/main/res/menu/menu_wallet.xml diff --git a/app/src/main/res/menu/wallet_details.xml b/app/src/main/res/menu/menu_wallet_details.xml similarity index 100% rename from app/src/main/res/menu/wallet_details.xml rename to app/src/main/res/menu/menu_wallet_details.xml diff --git a/app/src/main/res/values-de/strings_final.xml b/app/src/main/res/values-de/strings_final.xml index 6c801e756c..384c389dbe 100644 --- a/app/src/main/res/values-de/strings_final.xml +++ b/app/src/main/res/values-de/strings_final.xml @@ -121,7 +121,7 @@ In progress Set PIN code Register - Verify + Verify via Utorg Refresh Connect your card Verify your identity @@ -141,4 +141,16 @@ Prepare the SaltPay card Tap the SaltPay card Tap the Tangem card + Support + Getting started + Claim %s + To get started, simply claim wxDAI to your wallet + Claim + Congratulations! Your first payment crypto card has been activated + Claiming + It will take a few seconds + Something went wrong + Please check you email for further instructions + Do you want to exit the activation process? + In this case, you will need to start from the beginning. \ 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 6c801e756c..384c389dbe 100644 --- a/app/src/main/res/values-fr/strings_final.xml +++ b/app/src/main/res/values-fr/strings_final.xml @@ -121,7 +121,7 @@ In progress Set PIN code Register - Verify + Verify via Utorg Refresh Connect your card Verify your identity @@ -141,4 +141,16 @@ Prepare the SaltPay card Tap the SaltPay card Tap the Tangem card + Support + Getting started + Claim %s + To get started, simply claim wxDAI to your wallet + Claim + Congratulations! Your first payment crypto card has been activated + Claiming + It will take a few seconds + Something went wrong + Please check you email for further instructions + Do you want to exit the activation process? + In this case, you will need to start from the beginning. \ 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 6c801e756c..384c389dbe 100644 --- a/app/src/main/res/values-it/strings_final.xml +++ b/app/src/main/res/values-it/strings_final.xml @@ -121,7 +121,7 @@ In progress Set PIN code Register - Verify + Verify via Utorg Refresh Connect your card Verify your identity @@ -141,4 +141,16 @@ Prepare the SaltPay card Tap the SaltPay card Tap the Tangem card + Support + Getting started + Claim %s + To get started, simply claim wxDAI to your wallet + Claim + Congratulations! Your first payment crypto card has been activated + Claiming + It will take a few seconds + Something went wrong + Please check you email for further instructions + Do you want to exit the activation process? + In this case, you will need to start from the beginning. \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings_final.xml b/app/src/main/res/values-ru/strings_final.xml index add278e752..bcf57b6db6 100644 --- a/app/src/main/res/values-ru/strings_final.xml +++ b/app/src/main/res/values-ru/strings_final.xml @@ -119,7 +119,7 @@ В обработке Установить Код доступа Зарегистрироваться - Подтвердить + Верифицировать (Utorg) Обновить Подключите свою карту Подтвердите свою личность @@ -139,4 +139,16 @@ Приготовьте SaltPay карту Приложите SaltPay карту Приложите Tangem карту + Чат + Начало работы + Запросить %s + Для начала работы просто запросите начисление wxDai на свой кошелек + Запросить + Поздравляем! Ваша платежная крипто карта теперь активирована! + Запрашивается + Это займет несколько секунд + Что-то пошло не так + Более подробная информация отправлена на ваш адрес электронной почты. + Вы хотите выйти из процесса активации? + В этом случае вам будет необходимо начать процесс заново. diff --git a/app/src/main/res/values/strings_final.xml b/app/src/main/res/values/strings_final.xml index 5a47c7ed20..76dc5aa8dc 100644 --- a/app/src/main/res/values/strings_final.xml +++ b/app/src/main/res/values/strings_final.xml @@ -121,7 +121,7 @@ In progress Set PIN code Register - Verify + Verify via Utorg Refresh Connect your card Verify your identity @@ -141,4 +141,16 @@ Prepare the SaltPay card Tap the SaltPay card Tap the Tangem card + Support + Getting started + Claim %s + To get started, simply claim wxDAI to your wallet + Claim + Congratulations! Your first payment crypto card has been activated + Claiming + It will take a few seconds + Something went wrong + Please check you email for further instructions + Do you want to exit the activation process? + In this case, you will need to start from the beginning. diff --git a/common/src/main/java/com/tangem/common/Converter.kt b/common/src/main/java/com/tangem/common/Converter.kt new file mode 100644 index 0000000000..699f548ac3 --- /dev/null +++ b/common/src/main/java/com/tangem/common/Converter.kt @@ -0,0 +1,8 @@ +package com.tangem.common + +/** +[REDACTED_AUTHOR] + */ +interface Converter { + fun convert(value: I): O +} \ No newline at end of file diff --git a/dependencies.gradle b/dependencies.gradle index 7ecf2fc3a7..a7b0e1623d 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -3,7 +3,7 @@ ext.versions = [ build_gradle : '7.1.3', tangem_card_sdk : 'develop-165', // tangem_card_sdk : '0.0.1', - tangem_blockchain_sdk: 'develop-129', + tangem_blockchain_sdk: 'develop-130', // tangem_blockchain_sdk: '0.0.1', ] diff --git a/domain/src/main/java/com/tangem/domain/common/LogConfig.kt b/domain/src/main/java/com/tangem/domain/common/LogConfig.kt index ff11351635..c4f96f4b55 100644 --- a/domain/src/main/java/com/tangem/domain/common/LogConfig.kt +++ b/domain/src/main/java/com/tangem/domain/common/LogConfig.kt @@ -7,6 +7,7 @@ object LogConfig { val storeAction: Boolean = BuildConfig.DEBUG val zendesk: Boolean = false val network: NetworkLogConfig = NetworkLogConfig + val analyticsHandlers: AnalyticsHandlersLogConfig = AnalyticsHandlersLogConfig } object NetworkLogConfig { @@ -15,4 +16,10 @@ object NetworkLogConfig { val tangemTechService: Boolean = BuildConfig.DEBUG val paymentologyApiService: Boolean = BuildConfig.DEBUG val blockchainSdkNetwork: Boolean = BuildConfig.DEBUG +} + +object AnalyticsHandlersLogConfig { + val firebase: Boolean = false + val appsFlyer: Boolean = false + val amplitude: Boolean = false } \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/common/SaltPayWorkaround.kt b/domain/src/main/java/com/tangem/domain/common/SaltPayWorkaround.kt index cd122d1a34..c397160c2d 100644 --- a/domain/src/main/java/com/tangem/domain/common/SaltPayWorkaround.kt +++ b/domain/src/main/java/com/tangem/domain/common/SaltPayWorkaround.kt @@ -10,7 +10,7 @@ object SaltPayWorkaround { return when (blockchain) { Blockchain.SaltPay -> Token( name = "WXDAI", - symbol = "WXDAI", + symbol = "wxDAI", contractAddress = "0x4346186e7461cB4DF06bCFCB4cD591423022e417", decimals = 18, id = "wrapped-xdai", diff --git a/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenState.kt b/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenState.kt index 2ecd595e8e..28b821f2ab 100644 --- a/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenState.kt +++ b/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenState.kt @@ -7,6 +7,7 @@ import com.tangem.common.json.MoshiJsonConverter import com.tangem.domain.AddCustomTokenError import com.tangem.domain.DomainWrapped import com.tangem.domain.common.TapWorkarounds.isTestCard +import com.tangem.domain.common.extensions.isSupportedInApp import com.tangem.domain.common.extensions.supportedBlockchains import com.tangem.domain.common.extensions.supportedTokens import com.tangem.domain.common.form.CustomTokenValidator @@ -50,7 +51,7 @@ data class AddCustomTokenState( val foundToken: CoinsResponse.Coin? = null, val warnings: Set = emptySet(), val screenState: ScreenState = createInitialScreenState(), - val tangemTechServiceManager: AddCustomTokenService? = null + val tangemTechServiceManager: AddCustomTokenService? = null, ) : StateType { inline fun getField(id: FieldId): T = form.getField(id) as T @@ -171,7 +172,7 @@ data class AddCustomTokenState( internal fun getDerivationPath( mainNetwork: Blockchain, derivationNetwork: Blockchain, - derivationStyle: DerivationStyle? + derivationStyle: DerivationStyle?, ): com.tangem.common.hdWallet.DerivationPath? { // If we allow user to select derivations, we need to provide different derivations // (Legacy style derivations). @@ -239,9 +240,10 @@ data class AddCustomTokenState( } private fun getSupportedDerivations(card: Card): List { - val evmBlockchains = Blockchain.values().filter { - card.isTestCard == it.isTestnet() && it.isEvm() - } + val evmBlockchains = Blockchain.values() + .filter { card.isTestCard == it.isTestnet() && it.isEvm() } + .filter { it.isSupportedInApp() } + return (listOf(Blockchain.Unknown) + evmBlockchains).sortByName() } @@ -266,14 +268,18 @@ data class AddCustomTokenState( val action = (action as? AddCustomTokenAction) ?: return null val state = stateHolder.addCustomTokensState - val fieldConverter = FieldToJsonConverter(listOf( - ContractAddress, - Network, - Name, - Symbol, - Decimals, - DerivationPath, - ), jsonConverter) + val fieldConverter = + FieldToJsonConverter( + listOf( + ContractAddress, + Network, + Name, + Symbol, + Decimals, + DerivationPath, + ), + jsonConverter, + ) state.visitDataConverter(fieldConverter) val errors = state.formErrors.map { "${it.key}: ${it.value::class.java.simpleName}" diff --git a/network/src/main/java/com/tangem/network/api/paymentology/Responses.kt b/network/src/main/java/com/tangem/network/api/paymentology/Responses.kt index e70fdd3b1b..b60324b42d 100644 --- a/network/src/main/java/com/tangem/network/api/paymentology/Responses.kt +++ b/network/src/main/java/com/tangem/network/api/paymentology/Responses.kt @@ -2,16 +2,32 @@ package com.tangem.network.api.paymentology import com.squareup.moshi.Json import com.tangem.common.extensions.calculateHashCode +import com.tangem.common.services.Result /** [REDACTED_AUTHOR] */ +interface ResponseError { + val success: Boolean + val error: String? + val errorCode: Int? + + fun makeErrorMessage(): String { + return error ?: "unknown error" + } +} + +inline fun ResponseError.tryExtractError(): Result = when (success) { + true -> Result.Success(this as T) + else -> Result.Failure(Throwable(makeErrorMessage())) +} + data class RegistrationResponse( val results: List = listOf(), - val success: Boolean, - val error: String?, - val errorCode: Int?, -) { + override val success: Boolean, + override val error: String?, + override val errorCode: Int?, +) : ResponseError { data class Item( @Json(name = "CID") @@ -28,18 +44,29 @@ data class RegistrationResponse( val kycProvider: String?, @Json(name = "kyc_date") val kycDate: String?, + @Json(name = "kyc_status") + val kycStatus: KYCStatus?, @Json(name = "disabled_by_admin") val disabledByAdmin: Boolean?, val error: String?, ) } +enum class KYCStatus { + NOT_STARTED, + STARTED, + WAITING_FOR_APPROVAL, + CORRECTION_REQUESTED, + REJECTED, + APPROVED, +} + data class AttestationResponse( val challenge: ByteArray?, - val success: Boolean, - val error: String?, - val errorCode: Int?, -) { + override val success: Boolean, + override val error: String?, + override val errorCode: Int?, +) : ResponseError { override fun equals(other: Any?): Boolean { if (this === other) return true @@ -64,7 +91,7 @@ data class AttestationResponse( } data class RegisterWalletResponse( - val success: Boolean, - val error: String?, - val errorCode: Int?, -) \ No newline at end of file + override val success: Boolean, + override val error: String?, + override val errorCode: Int?, +) : ResponseError \ No newline at end of file