diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..29a3a50 --- /dev/null +++ b/.gitignore @@ -0,0 +1,43 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..8973d6c --- /dev/null +++ b/.metadata @@ -0,0 +1,30 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "761747bfc538b5af34aa0d3fac380f1bc331ec49" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49 + base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49 + - platform: android + create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49 + base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f4aa842 --- /dev/null +++ b/LICENSE @@ -0,0 +1,619 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..ca6069e --- /dev/null +++ b/README.md @@ -0,0 +1,101 @@ +# Haveno App + + + +## Table of Contents +1. [Prerequisites](#prerequisites) +4. [Project Status](#project-status) + - [Network Endorsements](#network-endorsements) +6. [Project Activity](#project-activity) +7. [Roadmap](#roadmap) +8. [Contributing](#contributing) + +--- + +## Prerequisites (For Testing) + +Before you begin, you'll need to set up a testing environment: + +- **Android Device or Emulator:** You can test on a physical Android phone or use an Android simulator via [Android Studio](https://studio.android.com) for advanced users. [BlueStacks](https://www.bluestacks.com/download.html) is another option with a gentler learning curve. +- **Latest Pre-release Builds:** Obtain the latest pre-release builds from the [Releases](https://github.com/KewbitXMR/haveno-app/releases) page. These are typically updated weekly. + +**Important:** Follow the instructions in this guide carefully for Haveno Plus to function correctly. + +## Setup Your Mobile Device + +### Install Tor VPN Relay + +To ensure all traffic is securely routed through Tor, you must install and activate a Tor VPN relay on your mobile device. The recommended apps are: + +- **[Orbot](https://play.google.com/store/apps/details?id=org.torproject.android):** Officially supported by The Tor Project. + - [Sourcecode & Releases](https://github.com/guardianproject/orbot/releases/tag/17.3.2-RC-1-tor-0.4.8.12) +- **[InviZible](https://play.google.com/store/apps/details?id=pan.alexander.tordnscrypt.gp):** A popular community alternative. + - [Sourcecode & Releases](https://github.com/Gedsh/InviZible/releases/tag/v2.3.0-beta) + +**Steps:** +1. Download [Orbot on Google Play](https://play.google.com/store/apps/details?id=org.torproject.android) or [InviZible on Google Play](https://play.google.com/store/apps/details?id=pan.alexander.tordnscrypt.gp). + - Alternatively, download [InviZible on F-Droid](https://f-droid.org/packages/pan.alexander.tordnscrypt.stable/). +2. Open the app of your choice and follow the on-screen instructions to activate it. Ensure that Tor is enabled and the VPN is activated. +3. Configure the VPN relay to route your Haveno Plus app traffic through Tor. The app will not load if a VPN relay is not configured first, by design, for your security. + +### Haveno Install Guide + +The Haveno Plus app is available as alpha pre-release builds for Android and Windows. Download the app from the [Releases](https://github.com/KewbitXMR/haveno-app/releases) page. The desktop clients are designed to be user-friendly, with custom installers for quick setup. + +**Note:** Haveno Plus is currently configured to use the stagenet (a test network) for at least the next 2 months. It is not intended for real-life trading. + +## Setup Your Desktop or Server + +- **Windows:** (Coming soon) +- **MacOS:** (Coming soon) +- **Android** Alpha (testing) +- **iOS** (Coming soon) +- **Linux:** Alpha (testing) +- **Docker:** (Coming soon) + + +### Step-by-Step Guides +1. [How to Install Haveno on Desktop](https://haveno.com/documentation/installing-haveno-on-desktop/) +2. [How to Install Haveno on Mobile](https://haveno.com/documentation/install-haveno-on-a-mobile-device/) +3. [How to Install Haveno on Server with Docker](https://haveno.com/documentation/installing-the-haveno-daemon-with-docker-securely/) +4. [How to Setup your own Haveno Network](https://haveno.com/documentation/setup-a-custom-haveno-network-seednode-with-docker/) + + +## Project Status + +Milestone 1: Protocol Interface ✅ +Milestone 2: Complete UI + Providers + lots more ✅ +Extras not in CCS: + - Caching system to ease the load on the daemon SQLite + - AES encryption on shared shared preferences and DB (not tested, will including on wallet too if nessesary) + +The project is currently currently in the testing peroid of Milestone 2 having completed it. + +### Network Endorsements + +Haveno does not endorse or denounce any particular network. The choice of network will be available upon official release. + +## Roadmap + +- Dart SDK API ✅ [Haveno Dart SDK](https://pub.dev/packages/haveno) +- Complete UI ✅ (tweaks needed) +- Linux desktop support ✅ +- Windows desktop Support +- MacOS desktop support +- Android mobile Support +- Complete full arbitration scope. +- Add client authentication for onion-hosted daemons. +- iOS support. +- Easy whitelisting and fund transfers to Cake Wallet or similar. +- Biometric security for mobile devices, with PIN or password protection for those without biometric options. +- Standalone version not requiring desktop or server (considerable work; community support may be needed). +- Support for Monero Atomic Swaps + +## Contributing + +Testing on old phones or laptops and providing high-quality feedback is the best way to contribute. A discussion section will be set up for initial feedback and contributions. + +## Disclaimer +Kewbit the maintainers blog is at [Kewbit.org](https://kewbit.org/) official sources for this are located at [https://git.haveno.com/haveno/](Haveno.com's Gitlab). **HAVENO.COM represents the official haveno app website and services as a client to a Haveno Daemon only**, and **HAVENO.EXCHANGE represents everything else, including not not limited to the p2p server network protocol, daemon nodes and pricenodes**, there are now also lots of app-specific guides located at [haveno documentation](https://haveno.com/documentation/) section of the site, which are atuned towards the new app. + +None of the code in this repository (haveno-app) is intrinically holding custody of philosophy in what may be considered 'crypto-assets' OR transmitting any such 'crypto-assets' or other financial services across the the wire, network or the general internet. diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..ace0476 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,31 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +analyzer: + errors: + avoid_print: ignore +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..6f56801 --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,13 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties +**/*.keystore +**/*.jks diff --git a/android/app/build.gradle b/android/app/build.gradle new file mode 100644 index 0000000..fdedd5a --- /dev/null +++ b/android/app/build.gradle @@ -0,0 +1,67 @@ +plugins { + id "com.android.application" + id "kotlin-android" + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id "dev.flutter.flutter-gradle-plugin" +} + +def localProperties = new Properties() +def localPropertiesFile = rootProject.file("local.properties") +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader("UTF-8") { reader -> + localProperties.load(reader) + } +} + +def flutterVersionCode = localProperties.getProperty("flutter.versionCode") +if (flutterVersionCode == null) { + flutterVersionCode = "1" +} + +def flutterVersionName = localProperties.getProperty("flutter.versionName") +if (flutterVersionName == null) { + flutterVersionName = "1.0" +} + +android { + namespace = "com.haveno.haveno" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + splits { + abi { + enable false + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.haveno.haveno" + // You can update the following values to match your application needs. + // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutterVersionCode.toInteger() + versionName = flutterVersionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.debug + shrinkResources false + zipAlignEnabled false + minifyEnabled false + } + } +} + +flutter { + source = "../.." +} diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..f102a89 --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/kotlin/com/example/haveno_flutter_app/MainActivity.kt b/android/app/src/main/kotlin/com/example/haveno_flutter_app/MainActivity.kt new file mode 100644 index 0000000..a471477 --- /dev/null +++ b/android/app/src/main/kotlin/com/example/haveno_flutter_app/MainActivity.kt @@ -0,0 +1,5 @@ +package com.haveno.haveno + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/drawable-v214e19131 b/android/app/src/main/res/drawable-v214e19131 new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/android/app/src/main/res/drawable-v214e19131 @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 0000000..d2ffbff --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,18 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = "../build" +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean", Delete) { + delete rootProject.buildDir +} diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..99ceb86 --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,4 @@ +org.gradle.jvmargs=-Xmx4G -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true +android.enableJetifier=true +android.bundle.enableUncompressedNativeLibs=false \ No newline at end of file diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..e1ca574 --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip diff --git a/android/settings.gradle b/android/settings.gradle new file mode 100644 index 0000000..0f79dad --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1,25 @@ +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + }() + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "7.3.0" apply false + id "org.jetbrains.kotlin.android" version "1.9.20" apply false +} + +include ":app" diff --git a/assets/arbitration-logo.png b/assets/arbitration-logo.png new file mode 100644 index 0000000..3c31963 Binary files /dev/null and b/assets/arbitration-logo.png differ diff --git a/assets/config/android/i2p/i2pd.conf b/assets/config/android/i2p/i2pd.conf new file mode 100644 index 0000000..b865df0 --- /dev/null +++ b/assets/config/android/i2p/i2pd.conf @@ -0,0 +1,94 @@ +## Configuration file for a typical i2pd user +## See https://i2pd.readthedocs.io/en/latest/user-guide/configuration/ +## for more options you can use in this file. + +#logfile = /sdcard/i2pd/i2pd.log +loglevel = none +#tunnelsdir = /sdcard/i2pd/tunnels.d + +# host = 1.2.3.4 +# port = 4567 + +ipv4 = true +ipv6 = false + +ssu = false + +bandwidth = L +# share = 100 + +# notransit = true +# floodfill = true + +[ntcp2] +enabled = true + +[ssu2] +enabled = true +published = true + +[http] +enabled = true +address = 127.0.0.1 +port = 7070 +# auth = true +# user = i2pd +# pass = changeme + +[httpproxy] +enabled = true +address = 127.0.0.1 +port = 4444 +inbound.length = 1 +inbound.quantity = 5 +outbound.length = 1 +outbound.quantity = 5 +signaturetype=7 +i2cp.leaseSetType=3 +i2cp.leaseSetEncType=0,4 +keys = proxy-keys.dat +# addresshelper = true +# outproxy = http://false.i2p +## httpproxy section also accepts I2CP parameters, like "inbound.length" etc. + +[socksproxy] +enabled = true +address = 127.0.0.1 +port = 4447 +keys = proxy-keys.dat +# outproxy.enabled = false +# outproxy = 127.0.0.1 +# outproxyport = 9050 +## socksproxy section also accepts I2CP parameters, like "inbound.length" etc. + +[sam] +enabled = false +# address = 127.0.0.1 +# port = 7656 + +[precomputation] +elgamal = false + +[upnp] +enabled = true +# name = I2Pd + +[reseed] +verify = true +## Path to local reseed data file (.su3) for manual reseeding +# file = /path/to/i2pseeds.su3 +## or HTTPS URL to reseed from +# file = https://legit-website.com/i2pseeds.su3 +## Path to local ZIP file or HTTPS URL to reseed from +# zipfile = /path/to/netDb.zip +## If you run i2pd behind a proxy server, set proxy server for reseeding here +## Should be http://address:port or socks://address:port +# proxy = http://127.0.0.1:8118 +## Minimum number of known routers, below which i2pd triggers reseeding. 25 by default +# threshold = 25 + +[limits] +transittunnels = 50 + +[persist] +profiles = false diff --git a/assets/config/android/i2p/tunnels.conf b/assets/config/android/i2p/tunnels.conf new file mode 100644 index 0000000..c39a022 --- /dev/null +++ b/assets/config/android/i2p/tunnels.conf @@ -0,0 +1,33 @@ +#[IRC-IRC2P] +#type = client +#address = 127.0.0.1 +#port = 6668 +#destination = irc.postman.i2p +#destinationport = 6667 +#keys = irc-keys.dat + +#[IRC-ILITA] +#type = client +#address = 127.0.0.1 +#port = 6669 +#destination = irc.ilita.i2p +#destinationport = 6667 +#keys = irc-keys.dat + +#[SMTP] +#type = client +#address = 127.0.0.1 +#port = 7659 +#destination = smtp.postman.i2p +#destinationport = 25 +#keys = smtp-keys.dat + +#[POP3] +#type = client +#address = 127.0.0.1 +#port = 7660 +#destination = pop.postman.i2p +#destinationport = 110 +#keys = pop3-keys.dat + +# see more examples at https://i2pd.readthedocs.io/en/latest/user-guide/tunnels/ diff --git a/assets/config/default/torrc b/assets/config/default/torrc new file mode 100644 index 0000000..7052019 --- /dev/null +++ b/assets/config/default/torrc @@ -0,0 +1,22 @@ +DataDirectory ./data +HiddenServiceDir ../daemon_service +HiddenServicePort 80 127.0.0.1:3201 +HTTPTunnelPort 8888 +SocksPort 9066 +ControlPort 9077 +HashedControlPassword 16:7FD95C8D50BA159760AA90C98DEA2924F0539C6B939F8A07AA68265FF4 +Log notice stdout +#Log notice file ./logs/notice.log +KeepalivePeriod 60 +ConstrainedSockets 1 +ConstrainedSockSize 8192 +#GeoIPFile ./lib/geoip +#GeoIPv6File ./lib/geoip6 +ClientDNSRejectInternalAddresses 1 +Nickname HAx000000 +ContactInfo uuidv4@something.com +UseEntryGuards 1 +NumEntryGuards 3 + +### EXPERIMENTAL FUTURE STUFF DONT ENABLE THESE YET IT COULD BE DISASTEROUS +#HiddenServiceSingleHopMode 1 \ No newline at end of file diff --git a/assets/config/macos/org.kewbit.havenoDaemon.plist b/assets/config/macos/org.kewbit.havenoDaemon.plist new file mode 100644 index 0000000..e69de29 diff --git a/assets/config/macos/org.kewbit.havenoTorDaemon.plist b/assets/config/macos/org.kewbit.havenoTorDaemon.plist new file mode 100644 index 0000000..e69de29 diff --git a/assets/getting-started-logo.png b/assets/getting-started-logo.png new file mode 100644 index 0000000..a98a524 Binary files /dev/null and b/assets/getting-started-logo.png differ diff --git a/assets/haveno-logo.png b/assets/haveno-logo.png new file mode 100644 index 0000000..770bb68 Binary files /dev/null and b/assets/haveno-logo.png differ diff --git a/assets/icon/app_icon.ico b/assets/icon/app_icon.ico new file mode 100644 index 0000000..64c802f Binary files /dev/null and b/assets/icon/app_icon.ico differ diff --git a/assets/icon/app_icon.png b/assets/icon/app_icon.png new file mode 100644 index 0000000..c031b77 Binary files /dev/null and b/assets/icon/app_icon.png differ diff --git a/assets/icon/app_icon_smaller.png b/assets/icon/app_icon_smaller.png new file mode 100644 index 0000000..92f0a25 Binary files /dev/null and b/assets/icon/app_icon_smaller.png differ diff --git a/assets/libraries/windows/sqlite3.dll b/assets/libraries/windows/sqlite3.dll new file mode 100644 index 0000000..1544c2d Binary files /dev/null and b/assets/libraries/windows/sqlite3.dll differ diff --git a/assets/tor-logo.png b/assets/tor-logo.png new file mode 100644 index 0000000..26de062 Binary files /dev/null and b/assets/tor-logo.png differ diff --git a/assets/versions.json b/assets/versions.json new file mode 100644 index 0000000..86f6508 --- /dev/null +++ b/assets/versions.json @@ -0,0 +1,17 @@ +{ + "haveno-core": { + "default": "" + }, + "tor": { + "default": "14.5a1", + "otherwise_default_model": "tor_find_latest" + }, + "monero": { + "default": "0.18.3.4", + "otherwise_default_model": "" + }, + "java": { + "default": "21.0.4+7", + "otherwise_default_model": "" + } +} \ No newline at end of file diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 0000000..fa0b357 --- /dev/null +++ b/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/lib/background_services.dart b/lib/background_services.dart new file mode 100644 index 0000000..d696ab5 --- /dev/null +++ b/lib/background_services.dart @@ -0,0 +1,304 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'dart:async'; +import 'dart:convert'; +import 'package:flutter_background_service/flutter_background_service.dart'; +import 'package:haveno/enums.dart'; +import 'package:haveno/haveno_client.dart'; +import 'package:haveno/haveno_service.dart'; +import 'package:haveno_app/models/haveno_daemon_config.dart'; +import 'package:haveno_app/models/schema.dart'; +import 'package:haveno_app/services/local_notification_service.dart'; +import 'package:haveno_app/services/secure_storage_service.dart'; + +// This is for Mobile background services only (for now) fuck off if you wanted something else. But we could run native Tor on Desktop too! + +class BackgroundServiceListener { + final FlutterBackgroundService service = FlutterBackgroundService(); + final EventDispatcher dispatcher; + + BackgroundServiceListener(this.dispatcher); + + Future startListening() async { + service.on('updateTorStatus').listen((event) { + if (event != null) { + dispatcher.dispatch(BackgroundEvent( + type: BackgroundEventType.updateTorStatus, + data: event, + )); + } + }); + + service.on('updateDaemonStatus').listen((event) { + if (event != null) { + dispatcher.dispatch(BackgroundEvent( + type: BackgroundEventType.updateDaemonStatus, + data: event, + )); + } + }); + + service.on('torStdOutLog').listen((event) { + if (event != null) { + dispatcher.dispatch(BackgroundEvent( + type: BackgroundEventType.torStdOutLog, + data: event, + )); + } + }); + + service.on('torStdErrLog').listen((event) { + if (event != null) { + dispatcher.dispatch(BackgroundEvent( + type: BackgroundEventType.torStdErrLog, + data: event, + )); + } + }); + } +} + + +void startBackgroundService() { + final service = FlutterBackgroundService(); + service.startService(); +} + +void stopBackgroundService() { + final service = FlutterBackgroundService(); + service.invoke("stop"); +} + +void markServiceNotificationSeen() { + final service = FlutterBackgroundService(); // just notify the UI to show the notificiation instead??? + service.invoke("mark_notification_as_seen"); +} + +Future startLongRunningForegroundService(ServiceInstance service) async { + //await _startTor(service); + LocalNotificationsService localNotificationsService = LocalNotificationsService(); + localNotificationsService.init(); + + NotificationsService notificationsService = + NotificationsService(); + + notificationsService.addListener( + NotificationMessage_NotificationType.APP_INITIALIZED, + (object) { + localNotificationsService.updateForegroundServiceNotification( + title: 'Status', + body: 'Connected', + ); + }, + ); + + notificationsService.addListener( + NotificationMessage_NotificationType.TRADE_UPDATE, + (object) { + localNotificationsService.showNotification( + id: object.hashCode, + title: 'Trade Update', + body: object.message, + payload: jsonEncode({ + 'action': 'route_to_active_trades_screen', + 'tradeProtobufAsJson': jsonEncode(object.trade.toProto3Json()) + })); + }, + ); + + notificationsService.addListener( + NotificationMessage_NotificationType.CHAT_MESSAGE, + (object) { + var payload = { + 'action': 'route_to_chat_screen', + 'chatMessageProtobufAsJson': + jsonEncode(object.chatMessage.toProto3Json()) + }; + print(jsonEncode(object.trade.toProto3Json())); + + localNotificationsService.showNotification( + id: object.hashCode, + title: object.chatMessage.type == SupportType.TRADE + ? 'New Message' + : 'New Support Message', + body: object.chatMessage.message, + payload: jsonEncode(payload)); + }, + ); + + notificationsService.listen(); + + localNotificationsService.updateForegroundServiceNotification( + title: 'Status', + body: 'Attempting to connect...', + ); + + //_connectToHavenoDaemonHeadless(service); + + service.on("stop").listen((event) { + // We may need to just trigger this when the app is opened, and when its closed, enable it again + service.stopSelf(); + // Set something in database for this or in secure storage, if it stops we need to make sure ios background fetch is running at least + }); +} + +// For iOS to update notifications and state (mainly notifications) +Future startShortLivedBackgroundFetch(ServiceInstance service) async { + LocalNotificationsService localNotificationsService = LocalNotificationsService(); + localNotificationsService.init(); // This whole thing might not complete in time for iOS +// if (!Tor.instance.enabled) { +// await _startTor(service); +// } else { + // +// } +// await _connectToHavenoDaemonHeadless(service, retryUntilSuccess: true, failWhenNoDaemonConfig: true); + + // Check for new trades and chat messages only then send notification + // TODO +} + +//Future _startTor(ServiceInstance service) async { +// try { +// if (!Tor.instance.started) { +// service.invoke("updateTorStatus", { +// "status": "intializing", +// "details": "Tor is is now initializing..." +// }); +// await Tor.init(); +// service.invoke("updateTorStatus", { +// "status": "initialized", +// "details": "Tor has now been initialized. (Not yet started)" +// }); +// service.invoke("updateTorStatus", { +// "status": "starting", +// "details": "Start is now starting..." +// } +// ); +// if (!Tor.instance.enabled) { +// await Tor.instance.enable(); +// } +// await Tor.instance.start(); +// while (!Tor.instance.bootstrapped) { +// await Future.delayed(const Duration(seconds: 1)); +// } +// service.invoke("updateTorStatus", { +// "status": "started", +// "port": Tor.instance.port, +// "details": "Tor service successfully started on port ${Tor.instance.port}" +// }); +// } else { +// service.invoke("updateTorStatus", { +// "status": "started", +// "port": Tor.instance.port, +// "details": "Tor service successfully started on port ${Tor.instance.port}" +// }); +// } +// } catch (e) { +// service.invoke("updateTorStatus", { +// "status": "error", +// "details": e.toString() +// }); +// rethrow; +// } +// +// StreamSubscription subscription = Tor.instance.events.stream.listen( /// might have to make sure this is close or reopenable on closure automatically +// (event) { +// // Handle each event +// service.invoke("torStdOutLog", { +// "details": event.toString(), +// }); +// }, +// onError: (error) { +// // Handle any errors that occur during the stream +// service.invoke("torStdErrLog", { +// "details": error.toString(), +// }); +// }, +// onDone: () { +// // Handle when the stream is closed or completed +// service.invoke("updateTorStatus", { +// "status": "Tor stream closed", +// }); +// }, +// cancelOnError: false, // If true, the stream will cancel on the first error +// ); + +//} + +Future _connectToHavenoDaemonHeadless(ServiceInstance service, {bool retryUntilSuccess = true, bool failWhenNoDaemonConfig = false}) async { + HavenoChannel havenoService = HavenoChannel(); + HavenoDaemonConfig? daemonConfig; + while (daemonConfig == null && retryUntilSuccess) { + service.invoke("updateDaemonStatus", { + "status": "aquiringRemoteDaemonConfig", + "details": "Aquiring remote daemon configuration from shared preferences..." + }); + daemonConfig = await SecureStorageService().readHavenoDaemonConfig(); + if (daemonConfig == null) { + service.invoke("updateDaemonStatus", { + "status": "remoteDaemonConfigNotFound", + "details": "No remote daemon configuration in shared preferences..." + }); + if (failWhenNoDaemonConfig) { + service.invoke("updateDaemonStatus", { + "status": "stopped", + "details": "No daemon config found and it is configured to fail in this case and not retry..." + }); + } else { + service.invoke("updateDaemonStatus", { + "status": "retryingConfig", + "details": "Scheduled to check for config again in 20 seconds..." + }); + await Future.delayed(const Duration(seconds: 10)); + } + } else { + service.invoke("updateDaemonStatus", { + "status": "foundDaemonConfig", + "details": "Found remote daemon configuration from shared preferences..." + }); + if (havenoService.isConnected) { + break; + } else { + try { + service.invoke("updateDaemonStatus", { + "status": "connecting", + "details": "Connecting to the daemon..." + }); + await havenoService.connect(daemonConfig.host, daemonConfig.port, + daemonConfig.clientAuthPassword); + service.invoke("updateDaemonStatus", { + "status": "connected", + "details": "Connects to the daemon..." + }); + } catch (e) { + service.invoke("updateDaemonStatus", { + "status": "unknownError", + "details": "Failure: ${e.toString()}" + }); + + await Future.delayed(const Duration(seconds: 10)); + } + } + } + } +} diff --git a/lib/background_tasks/desktop/fetch_data.dart b/lib/background_tasks/desktop/fetch_data.dart new file mode 100644 index 0000000..94d3fd6 --- /dev/null +++ b/lib/background_tasks/desktop/fetch_data.dart @@ -0,0 +1,20 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . \ No newline at end of file diff --git a/lib/background_tasks/mobile_notification_workers/check_daemon_connection.dart b/lib/background_tasks/mobile_notification_workers/check_daemon_connection.dart new file mode 100644 index 0000000..94d3fd6 --- /dev/null +++ b/lib/background_tasks/mobile_notification_workers/check_daemon_connection.dart @@ -0,0 +1,20 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . \ No newline at end of file diff --git a/lib/background_tasks/mobile_notification_workers/check_if_buyer_payment_time.dart b/lib/background_tasks/mobile_notification_workers/check_if_buyer_payment_time.dart new file mode 100644 index 0000000..94d3fd6 --- /dev/null +++ b/lib/background_tasks/mobile_notification_workers/check_if_buyer_payment_time.dart @@ -0,0 +1,20 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . \ No newline at end of file diff --git a/lib/background_tasks/mobile_notification_workers/check_if_seller_confirm_payment_time.dart b/lib/background_tasks/mobile_notification_workers/check_if_seller_confirm_payment_time.dart new file mode 100644 index 0000000..94d3fd6 --- /dev/null +++ b/lib/background_tasks/mobile_notification_workers/check_if_seller_confirm_payment_time.dart @@ -0,0 +1,20 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . \ No newline at end of file diff --git a/lib/background_tasks/mobile_notification_workers/check_new_dispute_messages.dart b/lib/background_tasks/mobile_notification_workers/check_new_dispute_messages.dart new file mode 100644 index 0000000..94d3fd6 --- /dev/null +++ b/lib/background_tasks/mobile_notification_workers/check_new_dispute_messages.dart @@ -0,0 +1,20 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . \ No newline at end of file diff --git a/lib/background_tasks/mobile_notification_workers/check_new_disputes.dart b/lib/background_tasks/mobile_notification_workers/check_new_disputes.dart new file mode 100644 index 0000000..94d3fd6 --- /dev/null +++ b/lib/background_tasks/mobile_notification_workers/check_new_disputes.dart @@ -0,0 +1,20 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . \ No newline at end of file diff --git a/lib/background_tasks/mobile_notification_workers/check_new_offers.dart b/lib/background_tasks/mobile_notification_workers/check_new_offers.dart new file mode 100644 index 0000000..94d3fd6 --- /dev/null +++ b/lib/background_tasks/mobile_notification_workers/check_new_offers.dart @@ -0,0 +1,20 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . \ No newline at end of file diff --git a/lib/background_tasks/mobile_notification_workers/check_new_trade_messages.dart b/lib/background_tasks/mobile_notification_workers/check_new_trade_messages.dart new file mode 100644 index 0000000..ff27339 --- /dev/null +++ b/lib/background_tasks/mobile_notification_workers/check_new_trade_messages.dart @@ -0,0 +1,61 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* import 'package:haveno_app/background_tasks/mobile_notification_workers/schema.dart'; +import 'package:haveno_app/proto/compiled/pb.pb.dart'; +import 'package:haveno_app/proto/compiled/grpc.pb.dart'; +import 'package:haveno_app/services/haveno_grpc_clients/trades_client_service.dart'; + +class CheckNewTradeMessagesTask extends MobileTask { + @override + Future run() async { + TradesClientService tradesClient = TradesClientService(havenoService: havenoService); + List? trades = await tradesClient.getTrades(); + + if (trades != null) { + for (var trade in trades) { + List? chatMessages = await tradesClient.getChatMessages(trade.tradeId); + for (var message in chatMessages!) { + bool isNewMessage = await db.isTradeChatMessageNew(message.uid); + await db.insertTradeChatMessage(message, trade.tradeId); + + if (!trade.role.contains('buyer') && (trade.tradePeerNodeAddress != trade.contract.buyerNodeAddress)) { + if (isNewMessage) { + await sendNotification( + id: message.uid.hashCode, + title: 'New Trade Message', + body: message.message, + ); + } + } + } + //await Future.delayed(const Duration(minutes: 1)); + } + } + } + + @override + Future updateState() { + // Implement state update logic if needed + throw UnimplementedError(); + } +} + */ \ No newline at end of file diff --git a/lib/background_tasks/mobile_notification_workers/check_new_trades.dart b/lib/background_tasks/mobile_notification_workers/check_new_trades.dart new file mode 100644 index 0000000..4dd8dcd --- /dev/null +++ b/lib/background_tasks/mobile_notification_workers/check_new_trades.dart @@ -0,0 +1,61 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* // mobile_tasks/check_new_trades.dart + +import 'package:haveno_app/background_tasks/mobile_notification_workers/schema.dart'; + +import 'package:haveno_app/proto/compiled/grpc.pb.dart'; +import 'package:haveno_app/services/haveno_grpc_clients/trades_client_service.dart'; +import 'package:haveno_app/utils/payment_utils.dart'; + +class CheckNewTradesTask extends MobileTask { + + CheckNewTradesTask() : super(minIntervalDuration: const Duration(minutes: 10)); + + @override + Future run() async { + TradesClientService tradesClient = TradesClientService(havenoService: havenoService); + List? trades = await tradesClient.getTrades(); + + if (trades != null) { + for (var trade in trades) { + bool isNewTrade = await db.isTradeNew(trade.tradeId); + await db.insertTrade(trade); + + if (isNewTrade) { + await sendNotification( + id: trade.tradeId.hashCode, + title: 'New Trade', + body: 'You have a new trade for ${formatXmr(trade.amount)} XMR', + ); + } + } + } + } + + @override + Future updateState() { + // Implement state update logic if needed + throw UnimplementedError(); + } +} + */ \ No newline at end of file diff --git a/lib/background_tasks/mobile_notification_workers/check_pending_background_service_closure.dart b/lib/background_tasks/mobile_notification_workers/check_pending_background_service_closure.dart new file mode 100644 index 0000000..94d3fd6 --- /dev/null +++ b/lib/background_tasks/mobile_notification_workers/check_pending_background_service_closure.dart @@ -0,0 +1,20 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . \ No newline at end of file diff --git a/lib/background_tasks/mobile_notification_workers/check_tor_connection.dart b/lib/background_tasks/mobile_notification_workers/check_tor_connection.dart new file mode 100644 index 0000000..94d3fd6 --- /dev/null +++ b/lib/background_tasks/mobile_notification_workers/check_tor_connection.dart @@ -0,0 +1,20 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . \ No newline at end of file diff --git a/lib/background_tasks/mobile_notification_workers/check_trade_payout_state.dart b/lib/background_tasks/mobile_notification_workers/check_trade_payout_state.dart new file mode 100644 index 0000000..94d3fd6 --- /dev/null +++ b/lib/background_tasks/mobile_notification_workers/check_trade_payout_state.dart @@ -0,0 +1,20 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . \ No newline at end of file diff --git a/lib/background_tasks/mobile_notification_workers/check_wallet_txn_received.dart b/lib/background_tasks/mobile_notification_workers/check_wallet_txn_received.dart new file mode 100644 index 0000000..94d3fd6 --- /dev/null +++ b/lib/background_tasks/mobile_notification_workers/check_wallet_txn_received.dart @@ -0,0 +1,20 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . \ No newline at end of file diff --git a/lib/background_tasks/mobile_notification_workers/schema.dart b/lib/background_tasks/mobile_notification_workers/schema.dart new file mode 100644 index 0000000..95fef83 --- /dev/null +++ b/lib/background_tasks/mobile_notification_workers/schema.dart @@ -0,0 +1,106 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import 'package:haveno/haveno_client.dart'; +import 'package:haveno_app/models/haveno_daemon_config.dart'; +import 'package:haveno_app/services/connection_checker_service.dart'; +import 'package:haveno_app/services/secure_storage_service.dart'; +import 'package:haveno_app/services/local_notification_service.dart'; +import 'package:haveno_app/utils/database_helper.dart'; + +abstract class AbstractMobileTask { + late Duration minIntervalDuration; + + Future run(); + Future sendNotification({required int id, required String title, required String body}); + Future updateState(); +} + +class MobileTask implements AbstractMobileTask { + final SecureStorageService secureStorage = SecureStorageService(); + final HavenoChannel havenoChannel = HavenoChannel(); + late final DatabaseHelper db; + HavenoDaemonConfig? havenoDaemonConfig; + + @override + Duration minIntervalDuration; + + // Constructor with a default value for minIntervalDuration + MobileTask({this.minIntervalDuration = const Duration(minutes: 5)}) { + init(); + } + + // Common initialization logic + Future init() async { + // Get remote daemon config + havenoDaemonConfig = await secureStorage.readHavenoDaemonConfig(); + + // Init database + db = DatabaseHelper.instance; + + // Check if connected to Tor + while (true) { + try { + if ((!await ConnectionCheckerService().isTorConnected())) { + throw Exception("Not yet connected to Tor..."); + } + } catch (e) { + print(e.toString()); + } + break; + } + while (true) { + try { + if ((!havenoChannel.isConnected)) { + await havenoChannel.connect( + havenoDaemonConfig!.host, + havenoDaemonConfig!.port, + havenoDaemonConfig!.clientAuthPassword, + ); + } + } catch (e) { + print("Failed to connect to Haveno instance: $e"); + } + break; + } + } + + @override + Future run() async { + throw UnimplementedError("Subclasses should implement this method."); + } + + @override + Future sendNotification({required int id, required String title, required String body}) async { + // Default implementation of sendNotification + LocalNotificationsService().showNotification( + id: id, + title: title, + body: body, + ); + } + + @override + Future updateState() async { + // Default implementation of updateState + // Can be overridden by subclasses if needed + } +} diff --git a/lib/data/crypto_currencies.json b/lib/data/crypto_currencies.json new file mode 100644 index 0000000..2d4f4bb --- /dev/null +++ b/lib/data/crypto_currencies.json @@ -0,0 +1,10 @@ +[ + { + "code": "BTC", + "label": "Bitcoin" + }, + { + "code": "LTC", + "label": "Litecoin" + } +] \ No newline at end of file diff --git a/lib/data/fiat_currencies.json b/lib/data/fiat_currencies.json new file mode 100644 index 0000000..e69de29 diff --git a/lib/data/mock_data.dart b/lib/data/mock_data.dart new file mode 100644 index 0000000..ba36f53 --- /dev/null +++ b/lib/data/mock_data.dart @@ -0,0 +1,645 @@ +import 'package:interactive_chart/interactive_chart.dart'; + +class MockDataTesla { + static const List _rawData = [ + // (Price data for Tesla Inc, taken from Yahoo Finance) + // timestamp, open, high, low, close, volume + [1555939800, 53.80, 53.94, 52.50, 52.55, 60735500], + [1556026200, 52.03, 53.12, 51.15, 52.78, 54719500], + [1556112600, 52.77, 53.06, 51.60, 51.73, 53637500], + [1556199000, 51.00, 51.80, 49.21, 49.53, 109247000], + [1556285400, 49.30, 49.34, 46.23, 47.03, 111803500], + [1556544600, 47.17, 48.80, 46.43, 48.29, 83572500], + [1556631000, 48.41, 48.84, 47.40, 47.74, 47323000], + [1556717400, 47.77, 48.00, 46.30, 46.80, 53522000], + [1556803800, 49.10, 49.43, 47.54, 48.82, 90796500], + [1556890200, 48.77, 51.32, 48.70, 51.01, 118534000], + [1557149400, 50.00, 51.67, 49.70, 51.07, 54169500], + [1557235800, 51.36, 51.44, 49.02, 49.41, 50657000], + [1557322200, 49.39, 50.12, 48.84, 48.97, 30882000], + [1557408600, 48.40, 48.74, 47.39, 48.40, 33557000], + [1557495000, 47.95, 48.40, 47.20, 47.90, 35041500], + [1557754200, 46.40, 46.49, 44.90, 45.40, 54174000], + [1557840600, 45.86, 46.90, 45.60, 46.46, 36262000], + [1557927000, 45.86, 46.49, 45.05, 46.39, 36480000], + [1558013400, 45.90, 46.20, 45.30, 45.67, 37416500], + [1558099800, 44.39, 44.45, 41.78, 42.21, 88933500], + [1558359000, 40.56, 41.20, 39.05, 41.07, 102631000], + [1558445400, 39.55, 41.48, 39.21, 41.02, 90019500], + [1558531800, 39.82, 40.79, 38.36, 38.55, 93426000], + [1558618200, 38.87, 39.89, 37.24, 39.10, 132735500], + [1558704600, 39.97, 40.00, 37.75, 38.13, 70683000], + [1559050200, 38.24, 39.00, 37.57, 37.74, 51564500], + [1559136600, 37.42, 38.48, 37.01, 37.97, 59843000], + [1559223000, 37.75, 38.45, 37.40, 37.64, 39632500], + [1559309400, 37.02, 37.98, 36.82, 37.03, 52033500], + [1559568600, 37.10, 37.34, 35.40, 35.79, 65322000], + [1559655000, 36.22, 38.80, 35.92, 38.72, 69037500], + [1559741400, 39.74, 40.26, 38.37, 39.32, 67554000], + [1559827800, 40.89, 42.20, 40.36, 41.19, 101211000], + [1559914200, 41.00, 42.17, 40.70, 40.90, 80017500], + [1560173400, 42.05, 43.39, 41.80, 42.58, 52925000], + [1560259800, 43.83, 44.18, 42.70, 43.42, 58267500], + [1560346200, 44.59, 44.68, 41.80, 41.85, 75987500], + [1560432600, 42.08, 42.98, 41.50, 42.78, 40841500], + [1560519000, 42.25, 43.33, 42.08, 42.98, 37167000], + [1560778200, 43.10, 45.40, 42.85, 45.01, 61584000], + [1560864600, 45.74, 46.95, 44.51, 44.95, 63579000], + [1560951000, 45.02, 45.55, 44.21, 45.29, 32875500], + [1561037400, 44.60, 45.38, 43.27, 43.92, 59317500], + [1561123800, 43.24, 44.44, 43.10, 44.37, 41010500], + [1561383000, 44.65, 45.17, 44.20, 44.73, 28754000], + [1561469400, 44.88, 45.07, 43.90, 43.95, 30910500], + [1561555800, 44.06, 45.45, 43.62, 43.85, 42536000], + [1561642200, 43.89, 44.58, 43.47, 44.57, 31698500], + [1561728600, 44.20, 45.03, 44.16, 44.69, 34257000], + [1561987800, 46.04, 46.62, 45.26, 45.43, 41067000], + [1562074200, 45.78, 45.83, 44.44, 44.91, 46295000], + [1562160600, 47.88, 48.31, 46.90, 46.98, 71005500], + [1562333400, 46.91, 47.09, 46.16, 46.62, 35328500], + [1562592600, 46.25, 46.45, 45.73, 46.07, 29402500], + [1562679000, 45.79, 46.20, 45.46, 46.01, 30954000], + [1562765400, 46.83, 47.79, 46.63, 47.78, 45728500], + [1562851800, 47.63, 48.30, 47.16, 47.72, 37572000], + [1562938200, 47.95, 49.08, 47.94, 49.02, 46002500], + [1563197400, 49.60, 50.88, 48.97, 50.70, 55000500], + [1563283800, 49.86, 50.71, 49.59, 50.48, 40745000], + [1563370200, 51.13, 51.66, 50.67, 50.97, 48823500], + [1563456600, 51.01, 51.15, 50.38, 50.71, 23793000], + [1563543000, 51.14, 51.99, 50.92, 51.64, 35242000], + [1563802200, 51.75, 52.43, 50.84, 51.14, 34212000], + [1563888600, 51.34, 52.10, 50.90, 52.03, 25115500], + [1563975000, 51.83, 53.21, 51.63, 52.98, 55364000], + [1564061400, 46.70, 46.90, 45.11, 45.76, 112091500], + [1564147800, 45.38, 46.05, 44.45, 45.61, 50138500], + [1564407000, 45.42, 47.19, 45.21, 47.15, 46366500], + [1564493400, 46.58, 48.67, 46.44, 48.45, 40545000], + [1564579800, 48.60, 49.34, 47.33, 48.32, 45891000], + [1564666200, 48.53, 48.90, 46.35, 46.77, 41297500], + [1564752600, 46.27, 47.25, 45.85, 46.87, 30682500], + [1565011800, 45.92, 46.27, 45.16, 45.66, 35141500], + [1565098200, 46.38, 46.50, 45.15, 46.15, 27821000], + [1565184600, 45.30, 46.71, 45.16, 46.68, 23882500], + [1565271000, 46.89, 47.96, 46.53, 47.66, 26371500], + [1565357400, 47.21, 47.79, 46.76, 47.00, 19491000], + [1565616600, 46.60, 47.15, 45.75, 45.80, 23319500], + [1565703000, 45.76, 47.20, 45.51, 47.00, 24240500], + [1565789400, 46.24, 46.30, 43.34, 43.92, 47813000], + [1565875800, 44.17, 44.31, 42.31, 43.13, 40798000], + [1565962200, 43.33, 44.45, 43.20, 43.99, 25492500], + [1566221400, 44.84, 45.57, 44.34, 45.37, 26548000], + [1566307800, 45.52, 45.82, 44.91, 45.17, 20626000], + [1566394200, 44.40, 44.64, 43.52, 44.17, 38971500], + [1566480600, 44.56, 45.08, 43.64, 44.43, 32795000], + [1566567000, 43.99, 44.23, 42.20, 42.28, 42693000], + [1566826200, 42.72, 43.00, 42.31, 43.00, 25259500], + [1566912600, 43.15, 43.76, 42.41, 42.82, 27081000], + [1566999000, 42.74, 43.45, 42.46, 43.12, 16127500], + [1567085400, 43.80, 44.68, 43.60, 44.34, 25897500], + [1567171800, 45.83, 46.49, 44.84, 45.12, 46603000], + [1567517400, 44.82, 45.79, 44.63, 45.00, 26770500], + [1567603800, 45.38, 45.69, 43.84, 44.14, 28805000], + [1567690200, 44.50, 45.96, 44.17, 45.92, 36976500], + [1567776600, 45.44, 45.93, 45.03, 45.49, 20947000], + [1568035800, 46.00, 46.75, 45.85, 46.36, 24013500], + [1568122200, 46.16, 47.11, 45.79, 47.11, 24418500], + [1568208600, 47.48, 49.63, 47.20, 49.42, 50214000], + [1568295000, 49.54, 50.70, 48.88, 49.17, 42906000], + [1568381400, 49.39, 49.69, 48.97, 49.04, 26565500], + [1568640600, 49.20, 49.49, 48.23, 48.56, 23640500], + [1568727000, 48.49, 49.12, 48.07, 48.96, 19327000], + [1568813400, 49.00, 49.63, 48.47, 48.70, 20851000], + [1568899800, 49.20, 49.59, 48.97, 49.32, 23979000], + [1568986200, 49.30, 49.39, 47.63, 48.12, 31765000], + [1569245400, 48.00, 49.04, 47.84, 48.25, 21701000], + [1569331800, 48.30, 48.40, 44.52, 44.64, 64457500], + [1569418200, 44.91, 45.80, 43.67, 45.74, 47135500], + [1569504600, 46.13, 48.66, 45.48, 48.51, 59422500], + [1569591000, 48.44, 49.74, 47.75, 48.43, 55582000], + [1569850200, 48.60, 48.80, 47.22, 48.17, 29399000], + [1569936600, 48.30, 49.19, 47.83, 48.94, 30813000], + [1570023000, 48.66, 48.93, 47.89, 48.63, 28157000], + [1570109400, 46.37, 46.90, 44.86, 46.61, 75422500], + [1570195800, 46.32, 46.96, 45.61, 46.29, 39975000], + [1570455000, 45.96, 47.71, 45.71, 47.54, 40321000], + [1570541400, 47.17, 48.79, 46.90, 48.01, 43391000], + [1570627800, 48.26, 49.46, 48.13, 48.91, 34472000], + [1570714200, 49.06, 49.86, 48.32, 48.95, 31416500], + [1570800600, 49.43, 50.22, 49.36, 49.58, 42377000], + [1571059800, 49.58, 51.71, 49.43, 51.39, 51025000], + [1571146200, 51.54, 52.00, 50.82, 51.58, 32164000], + [1571232600, 51.48, 52.42, 51.38, 51.95, 33420500], + [1571319000, 52.50, 52.96, 52.03, 52.39, 23846500], + [1571405400, 52.14, 52.56, 51.02, 51.39, 28749000], + [1571664600, 51.67, 51.90, 50.04, 50.70, 25101500], + [1571751000, 50.86, 51.67, 50.17, 51.12, 23004000], + [1571837400, 50.90, 51.23, 50.27, 50.94, 26305500], + [1571923800, 59.67, 60.99, 57.84, 59.94, 148604500], + [1572010200, 59.54, 66.00, 59.22, 65.63, 150030500], + [1572269400, 65.51, 68.17, 64.52, 65.54, 94351500], + [1572355800, 64.00, 64.86, 62.95, 63.24, 63421500], + [1572442200, 62.60, 63.76, 61.99, 63.00, 48209000], + [1572528600, 62.62, 63.80, 62.60, 62.98, 25335000], + [1572615000, 63.26, 63.30, 61.96, 62.66, 31919500], + [1572877800, 62.96, 64.39, 61.85, 63.49, 43935000], + [1572964200, 63.92, 64.70, 63.22, 63.44, 34717000], + [1573050600, 63.60, 65.34, 62.90, 65.32, 39704500], + [1573137000, 65.83, 68.30, 65.60, 67.11, 72336500], + [1573223400, 66.90, 67.49, 66.50, 67.43, 30346000], + [1573482600, 68.79, 69.84, 68.40, 69.02, 49933500], + [1573569000, 69.38, 70.07, 68.81, 69.99, 36797000], + [1573655400, 71.00, 71.27, 69.04, 69.22, 42100500], + [1573741800, 69.22, 70.77, 68.58, 69.87, 32324500], + [1573828200, 70.13, 70.56, 69.67, 70.43, 24045000], + [1574087400, 70.58, 70.63, 69.22, 70.00, 22002000], + [1574173800, 70.35, 72.00, 69.56, 71.90, 38624000], + [1574260200, 72.00, 72.24, 69.91, 70.44, 33625500], + [1574346600, 70.90, 72.17, 70.80, 70.97, 30550000], + [1574433000, 68.03, 68.20, 66.00, 66.61, 84353000], + [1574692200, 68.86, 68.91, 66.89, 67.27, 61697500], + [1574778600, 67.05, 67.10, 65.42, 65.78, 39737000], + [1574865000, 66.22, 66.79, 65.71, 66.26, 27778000], + [1575037800, 66.22, 66.25, 65.50, 65.99, 12328000], + [1575297000, 65.88, 67.28, 65.74, 66.97, 30372500], + [1575383400, 66.52, 67.58, 66.44, 67.24, 32868500], + [1575469800, 67.55, 67.57, 66.57, 66.61, 27665000], + [1575556200, 66.57, 66.88, 65.45, 66.07, 18623000], + [1575642600, 67.00, 67.77, 66.95, 67.18, 38062000], + [1575901800, 67.32, 68.89, 67.02, 67.91, 45115500], + [1575988200, 67.99, 70.15, 67.86, 69.77, 44141500], + [1576074600, 70.38, 71.44, 70.22, 70.54, 34489000], + [1576161000, 70.98, 72.55, 70.65, 71.94, 38819500], + [1576247400, 72.21, 73.04, 70.93, 71.68, 32854500], + [1576506600, 72.51, 76.72, 72.50, 76.30, 90871000], + [1576593000, 75.80, 77.10, 75.18, 75.80, 42484000], + [1576679400, 76.13, 79.04, 76.12, 78.63, 70605000], + [1576765800, 79.46, 81.37, 79.30, 80.81, 90535500], + [1576852200, 82.06, 82.60, 80.04, 81.12, 73763500], + [1577111400, 82.36, 84.40, 82.00, 83.84, 66598000], + [1577197800, 83.67, 85.09, 82.54, 85.05, 40273500], + [1577370600, 85.58, 86.70, 85.27, 86.19, 53169500], + [1577457000, 87.00, 87.06, 85.22, 86.08, 49728500], + [1577716200, 85.76, 85.80, 81.85, 82.94, 62932000], + [1577802600, 81.00, 84.26, 80.42, 83.67, 51428500], + [1577975400, 84.90, 86.14, 84.34, 86.05, 47660500], + [1578061800, 88.10, 90.80, 87.38, 88.60, 88892500], + [1578321000, 88.09, 90.31, 88.00, 90.31, 50665000], + [1578407400, 92.28, 94.33, 90.67, 93.81, 89410500], + [1578493800, 94.74, 99.70, 93.65, 98.43, 155721500], + [1578580200, 99.42, 99.76, 94.57, 96.27, 142202000], + [1578666600, 96.36, 96.99, 94.74, 95.63, 64797500], + [1578925800, 98.70, 105.13, 98.40, 104.97, 132588000], + [1579012200, 108.85, 109.48, 104.98, 107.58, 144981000], + [1579098600, 105.95, 107.57, 103.36, 103.70, 86844000], + [1579185000, 98.75, 102.89, 98.43, 102.70, 108683500], + [1579271400, 101.52, 103.13, 100.63, 102.10, 68145500], + [1579617000, 106.05, 109.72, 105.68, 109.44, 89017500], + [1579703400, 114.38, 118.90, 111.82, 113.91, 156845000], + [1579789800, 112.85, 116.40, 111.12, 114.44, 98255000], + [1579876200, 114.13, 114.77, 110.85, 112.96, 71768000], + [1580135400, 108.40, 112.89, 107.86, 111.60, 68040500], + [1580221800, 113.70, 115.36, 111.62, 113.38, 58942500], + [1580308200, 115.14, 117.96, 113.49, 116.20, 89007500], + [1580394600, 126.48, 130.18, 123.60, 128.16, 145028500], + [1580481000, 128.00, 130.60, 126.50, 130.11, 78596500], + [1580740200, 134.74, 157.23, 134.70, 156.00, 235325000], + [1580826600, 176.59, 193.80, 166.78, 177.41, 304694000], + [1580913000, 164.65, 169.20, 140.82, 146.94, 242119000], + [1580999400, 139.98, 159.17, 137.40, 149.79, 199404000], + [1581085800, 146.11, 153.95, 146.00, 149.61, 85317500], + [1581345000, 160.00, 164.00, 150.48, 154.26, 123446000], + [1581431400, 153.76, 156.70, 151.60, 154.88, 58487500], + [1581517800, 155.57, 157.95, 152.67, 153.46, 60112500], + [1581604200, 148.37, 163.60, 147.00, 160.80, 131446500], + [1581690600, 157.44, 162.59, 157.10, 160.01, 78468500], + [1582036200, 168.32, 172.00, 166.47, 171.68, 81908500], + [1582122600, 184.70, 188.96, 180.20, 183.48, 127115000], + [1582209000, 182.39, 182.40, 171.99, 179.88, 88174500], + [1582295400, 181.40, 182.61, 176.09, 180.20, 71574000], + [1582554600, 167.80, 172.70, 164.44, 166.76, 75961000], + [1582641000, 169.80, 171.32, 157.40, 159.98, 86452500], + [1582727400, 156.50, 162.66, 155.22, 155.76, 70427500], + [1582813800, 146.00, 147.95, 133.80, 135.80, 121386000], + [1582900200, 125.94, 138.10, 122.30, 133.60, 121114500], + [1583159400, 142.25, 148.74, 137.33, 148.72, 100975000], + [1583245800, 161.00, 161.40, 143.22, 149.10, 128920000], + [1583332200, 152.79, 153.30, 144.95, 149.90, 75245000], + [1583418600, 144.75, 149.15, 143.61, 144.91, 54263500], + [1583505000, 138.00, 141.40, 136.85, 140.70, 63314500], + [1583760600, 121.08, 132.60, 121.00, 121.60, 85368500], + [1583847000, 131.89, 133.60, 121.60, 129.07, 77972000], + [1583933400, 128.04, 130.72, 122.60, 126.85, 66612500], + [1584019800, 116.18, 118.90, 109.25, 112.11, 94545500], + [1584106200, 119.00, 121.51, 100.40, 109.32, 113201500], + [1584365400, 93.90, 98.97, 88.43, 89.01, 102447500], + [1584451800, 88.00, 94.37, 79.20, 86.04, 119973000], + [1584538200, 77.80, 80.97, 70.10, 72.24, 118931000], + [1584624600, 74.94, 90.40, 71.69, 85.53, 150977500], + [1584711000, 87.64, 95.40, 85.16, 85.51, 141427500], + [1584970200, 86.72, 88.40, 82.10, 86.86, 82272500], + [1585056600, 95.46, 102.74, 94.80, 101.00, 114476000], + [1585143000, 109.05, 111.40, 102.22, 107.85, 106113500], + [1585229400, 109.48, 112.00, 102.45, 105.63, 86903500], + [1585315800, 101.00, 105.16, 98.81, 102.87, 71887000], + [1585575000, 102.05, 103.33, 98.25, 100.43, 59990500], + [1585661400, 100.25, 108.59, 99.40, 104.80, 88857500], + [1585747800, 100.80, 102.79, 95.02, 96.31, 66766000], + [1585834200, 96.21, 98.85, 89.28, 90.89, 99292000], + [1585920600, 101.90, 103.10, 93.68, 96.00, 112810500], + [1586179800, 102.24, 104.20, 99.59, 103.25, 74509000], + [1586266200, 109.00, 113.00, 106.47, 109.09, 89599000], + [1586352600, 110.84, 111.44, 106.67, 109.77, 63280000], + [1586439000, 112.42, 115.04, 111.42, 114.60, 68250000], + [1586784600, 118.03, 130.40, 116.11, 130.19, 112377000], + [1586871000, 139.79, 148.38, 138.49, 141.98, 152882500], + [1586957400, 148.40, 150.63, 142.00, 145.97, 117885000], + [1587043800, 143.39, 151.89, 141.34, 149.04, 103289500], + [1587130200, 154.46, 154.99, 149.53, 150.78, 65641000], + [1587389400, 146.54, 153.11, 142.44, 149.27, 73733000], + [1587475800, 146.02, 150.67, 134.76, 137.34, 101045500], + [1587562200, 140.80, 146.80, 137.74, 146.42, 70827500], + [1587648600, 145.52, 146.80, 140.63, 141.13, 66183500], + [1587735000, 142.16, 146.15, 139.64, 145.03, 66060000], + [1587994200, 147.52, 159.90, 147.00, 159.75, 103407000], + [1588080600, 159.13, 161.00, 151.34, 153.82, 76110000], + [1588167000, 158.03, 160.64, 156.63, 160.10, 81080000], + [1588253400, 171.04, 173.96, 152.70, 156.38, 142359500], + [1588339800, 151.00, 154.55, 136.61, 140.26, 162659000], + [1588599000, 140.20, 152.40, 139.60, 152.24, 96185500], + [1588685400, 157.96, 159.78, 152.44, 153.64, 84958500], + [1588771800, 155.30, 157.96, 152.22, 156.52, 55616000], + [1588858200, 155.44, 159.28, 154.47, 156.01, 57638500], + [1588944600, 158.75, 164.80, 157.40, 163.88, 80432500], + [1589203800, 158.10, 164.80, 157.00, 162.26, 82598000], + [1589290200, 165.40, 168.66, 161.60, 161.88, 79534500], + [1589376600, 164.17, 165.20, 152.66, 158.19, 95327500], + [1589463000, 156.00, 160.67, 152.80, 160.67, 68411000], + [1589549400, 158.07, 161.01, 157.31, 159.83, 52592000], + [1589808600, 165.56, 166.94, 160.78, 162.73, 58329000], + [1589895000, 163.03, 164.41, 161.22, 161.60, 48182500], + [1589981400, 164.10, 165.20, 162.36, 163.11, 36546500], + [1590067800, 163.20, 166.50, 159.20, 165.52, 61273000], + [1590154200, 164.43, 166.36, 162.40, 163.38, 49937500], + [1590499800, 166.90, 166.92, 163.14, 163.77, 40448500], + [1590586200, 164.17, 165.54, 157.00, 164.05, 57747500], + [1590672600, 162.70, 164.95, 160.34, 161.16, 36278000], + [1590759000, 161.75, 167.00, 160.84, 167.00, 58822500], + [1591018200, 171.60, 179.80, 170.82, 179.62, 74697500], + [1591104600, 178.94, 181.73, 174.20, 176.31, 67828000], + [1591191000, 177.62, 179.59, 176.02, 176.59, 39747500], + [1591277400, 177.98, 179.15, 171.69, 172.88, 44438500], + [1591363800, 175.57, 177.30, 173.24, 177.13, 39059500], + [1591623000, 183.80, 190.00, 181.83, 189.98, 70873500], + [1591709400, 188.00, 190.89, 184.79, 188.13, 56941000], + [1591795800, 198.38, 205.50, 196.50, 205.01, 92817000], + [1591882200, 198.04, 203.79, 194.40, 194.57, 79582500], + [1591968600, 196.00, 197.60, 182.52, 187.06, 83817000], + [1592227800, 183.56, 199.77, 181.70, 198.18, 78486000], + [1592314200, 202.37, 202.58, 192.48, 196.43, 70255500], + [1592400600, 197.54, 201.00, 196.51, 198.36, 49454000], + [1592487000, 200.60, 203.84, 198.89, 200.79, 48759500], + [1592573400, 202.56, 203.19, 198.27, 200.18, 43398500], + [1592832600, 199.99, 201.78, 198.00, 198.86, 31812000], + [1592919000, 199.78, 202.40, 198.80, 200.36, 31826500], + [1593005400, 198.82, 200.18, 190.63, 192.17, 54798000], + [1593091800, 190.85, 197.20, 187.43, 197.20, 46272500], + [1593178200, 198.96, 199.00, 190.97, 191.95, 44274500], + [1593437400, 193.80, 202.00, 189.70, 201.87, 45132000], + [1593523800, 201.30, 217.54, 200.75, 215.96, 84592500], + [1593610200, 216.60, 227.07, 216.10, 223.93, 66634500], + [1593696600, 244.30, 245.60, 237.12, 241.73, 86250500], + [1594042200, 255.34, 275.56, 253.21, 274.32, 102849500], + [1594128600, 281.00, 285.90, 267.34, 277.97, 107448500], + [1594215000, 281.00, 283.45, 262.27, 273.18, 81556500], + [1594301400, 279.40, 281.71, 270.26, 278.86, 58588000], + [1594387800, 279.20, 309.78, 275.20, 308.93, 116688000], + [1594647000, 331.80, 359.00, 294.22, 299.41, 194927000], + [1594733400, 311.20, 318.00, 286.20, 303.36, 117090500], + [1594819800, 308.60, 310.00, 291.40, 309.20, 81839000], + [1594906200, 295.43, 306.34, 293.20, 300.13, 71504000], + [1594992600, 302.69, 307.50, 298.00, 300.17, 46650000], + [1595251800, 303.80, 330.00, 297.60, 328.60, 85607000], + [1595338200, 327.99, 335.00, 311.60, 313.67, 80536000], + [1595424600, 319.80, 325.28, 312.40, 318.47, 70805500], + [1595511000, 335.79, 337.80, 296.15, 302.61, 121642500], + [1595597400, 283.20, 293.00, 273.31, 283.40, 96983000], + [1595856600, 287.00, 309.59, 282.60, 307.92, 80243500], + [1595943000, 300.80, 312.94, 294.88, 295.30, 79043500], + [1596029400, 300.20, 306.96, 297.40, 299.82, 47134500], + [1596115800, 297.60, 302.65, 294.20, 297.50, 38105000], + [1596202200, 303.00, 303.41, 284.20, 286.15, 61041000], + [1596461400, 289.84, 301.96, 288.88, 297.00, 44046500], + [1596547800, 299.00, 305.48, 292.40, 297.40, 42075000], + [1596634200, 298.60, 299.97, 293.66, 297.00, 24739000], + [1596720600, 298.17, 303.46, 295.45, 297.92, 29961500], + [1596807000, 299.91, 299.95, 283.00, 290.54, 44482000], + [1597066200, 289.60, 291.50, 277.17, 283.71, 37611500], + [1597152600, 279.20, 284.00, 273.00, 274.88, 43129000], + [1597239000, 294.00, 317.00, 287.00, 310.95, 109147000], + [1597325400, 322.20, 330.24, 313.45, 324.20, 102126500], + [1597411800, 333.00, 333.76, 325.33, 330.14, 62888000], + [1597671000, 335.40, 369.17, 334.57, 367.13, 101211500], + [1597757400, 379.80, 384.78, 369.02, 377.42, 82372500], + [1597843800, 373.00, 382.20, 368.24, 375.71, 61026500], + [1597930200, 372.14, 404.40, 371.41, 400.37, 103059000], + [1598016600, 408.95, 419.10, 405.01, 410.00, 107448000], + [1598275800, 425.26, 425.80, 385.50, 402.84, 100318000], + [1598362200, 394.98, 405.59, 393.60, 404.67, 53294500], + [1598448600, 412.00, 433.20, 410.73, 430.63, 71197000], + [1598535000, 436.09, 459.12, 428.50, 447.75, 118465000], + [1598621400, 459.02, 463.70, 437.30, 442.68, 100406000], + [1598880600, 444.61, 500.14, 440.11, 498.32, 118374400], + [1598967000, 502.14, 502.49, 470.51, 475.05, 89841100], + [1599053400, 478.99, 479.04, 405.12, 447.37, 96176100], + [1599139800, 407.23, 431.80, 402.00, 407.00, 87596100], + [1599226200, 402.81, 428.00, 372.02, 418.32, 110321900], + [1599571800, 356.00, 368.74, 329.88, 330.21, 115465700], + [1599658200, 356.60, 369.00, 341.51, 366.28, 79465800], + [1599744600, 386.21, 398.99, 360.56, 371.34, 84930600], + [1599831000, 381.94, 382.50, 360.50, 372.72, 60717500], + [1600090200, 380.95, 420.00, 373.30, 419.62, 83020600], + [1600176600, 436.56, 461.94, 430.70, 449.76, 97298200], + [1600263000, 439.87, 457.79, 435.31, 441.76, 72279300], + [1600349400, 415.60, 437.79, 408.00, 423.43, 76779200], + [1600435800, 447.94, 451.00, 428.80, 442.15, 86406800], + [1600695000, 453.13, 455.68, 407.07, 449.39, 109476800], + [1600781400, 429.60, 437.76, 417.60, 424.23, 79580800], + [1600867800, 405.16, 412.15, 375.88, 380.36, 95074200], + [1600954200, 363.80, 399.50, 351.30, 387.79, 96561100], + [1601040600, 393.47, 408.73, 391.30, 407.34, 67208500], + [1601299800, 424.62, 428.08, 415.55, 421.20, 49719600], + [1601386200, 416.00, 428.50, 411.60, 419.07, 50219300], + [1601472600, 421.32, 433.93, 420.47, 429.01, 48145600], + [1601559000, 440.76, 448.88, 434.42, 448.16, 50741500], + [1601645400, 421.39, 439.13, 415.00, 415.09, 71430000], + [1601904600, 423.35, 433.64, 419.33, 425.68, 44722800], + [1601991000, 423.79, 428.78, 406.05, 413.98, 49146300], + [1602077400, 419.87, 429.90, 413.85, 425.30, 43127700], + [1602163800, 438.44, 439.00, 425.30, 425.92, 40421100], + [1602250200, 430.13, 434.59, 426.46, 434.00, 28925700], + [1602509400, 442.00, 448.74, 438.58, 442.30, 38791100], + [1602595800, 443.35, 448.89, 436.60, 446.65, 34463700], + [1602682200, 449.78, 465.90, 447.35, 461.30, 47879700], + [1602768600, 450.31, 456.57, 442.50, 448.88, 35672400], + [1602855000, 454.44, 455.95, 438.85, 439.67, 32775900], + [1603114200, 446.24, 447.00, 428.87, 430.83, 36287800], + [1603200600, 431.75, 431.75, 419.05, 421.94, 31656300], + [1603287000, 422.70, 432.95, 421.25, 422.64, 32370500], + [1603373400, 441.92, 445.23, 424.51, 425.79, 39993200], + [1603459800, 421.84, 422.89, 407.38, 420.63, 33717000], + [1603719000, 411.63, 425.76, 410.00, 420.28, 28239200], + [1603805400, 423.76, 430.50, 420.10, 424.68, 22686500], + [1603891800, 416.48, 418.60, 406.00, 406.02, 25451400], + [1603978200, 409.96, 418.06, 406.46, 410.83, 22655300], + [1604064600, 406.90, 407.59, 379.11, 388.04, 42511300], + [1604327400, 394.00, 406.98, 392.30, 400.51, 29021100], + [1604413800, 409.73, 427.77, 406.69, 423.90, 34351700], + [1604500200, 430.62, 435.40, 417.10, 420.98, 32143100], + [1604586600, 428.30, 440.00, 424.00, 438.09, 28414500], + [1604673000, 436.10, 436.57, 424.28, 429.95, 21706000], + [1604932200, 439.50, 452.50, 421.00, 421.26, 34833000], + [1605018600, 420.09, 420.09, 396.03, 410.36, 30284200], + [1605105000, 416.45, 418.70, 410.58, 417.13, 17357700], + [1605191400, 415.05, 423.00, 409.52, 411.76, 19855100], + [1605277800, 410.85, 412.53, 401.66, 408.50, 19771100], + [1605537000, 408.93, 412.45, 404.09, 408.09, 26838600], + [1605623400, 460.17, 462.00, 433.01, 441.61, 61188300], + [1605709800, 448.35, 496.00, 443.50, 486.64, 78044000], + [1605796200, 492.00, 508.61, 487.57, 499.27, 62475300], + [1605882600, 497.99, 502.50, 489.06, 489.61, 32911900], + [1606141800, 503.50, 526.00, 501.79, 521.85, 50260300], + [1606228200, 540.40, 559.99, 526.20, 555.38, 53648500], + [1606314600, 550.06, 574.00, 545.37, 574.00, 48930200], + [1606487400, 581.16, 598.78, 578.45, 585.76, 37561100], + [1606746600, 602.21, 607.80, 554.51, 567.60, 63003100], + [1606833000, 597.59, 597.85, 572.05, 584.76, 40103500], + [1606919400, 556.44, 571.54, 541.21, 568.82, 47775700], + [1607005800, 590.02, 598.97, 582.43, 593.38, 42552000], + [1607092200, 591.01, 599.04, 585.50, 599.04, 29401300], + [1607351400, 604.92, 648.79, 603.05, 641.76, 56309700], + [1607437800, 625.51, 651.28, 618.50, 649.88, 64265000], + [1607524200, 653.69, 654.32, 588.00, 604.48, 71291200], + [1607610600, 574.37, 627.75, 566.34, 627.07, 67083200], + [1607697000, 615.01, 624.00, 596.80, 609.99, 46475000], + [1607956200, 619.00, 642.75, 610.20, 639.83, 52040600], + [1608042600, 643.28, 646.90, 623.80, 633.25, 45071500], + [1608129000, 628.23, 632.50, 605.00, 622.77, 42095800], + [1608215400, 628.19, 658.82, 619.50, 655.90, 56270100], + [1608301800, 668.90, 695.00, 628.54, 695.00, 222126200], + [1608561000, 666.24, 668.50, 646.07, 649.86, 58045300], + [1608647400, 648.00, 649.88, 614.23, 640.34, 51716000], + [1608733800, 632.20, 651.50, 622.57, 645.98, 33173000], + [1608820200, 642.99, 666.09, 641.00, 661.77, 22865600], + [1609165800, 674.51, 681.40, 660.80, 663.69, 32278600], + [1609252200, 661.00, 669.90, 655.00, 665.99, 22910800], + [1609338600, 672.00, 696.60, 668.36, 694.78, 42846000], + [1609425000, 699.99, 718.72, 691.12, 705.67, 49649900], + [1609770600, 719.46, 744.49, 717.19, 729.77, 48638200], + [1609857000, 723.66, 740.84, 719.20, 735.11, 32245200], + [1609943400, 758.49, 774.00, 749.10, 755.98, 44700000], + [1610029800, 777.63, 816.99, 775.20, 816.04, 51498900], + [1610116200, 856.00, 884.49, 838.39, 880.02, 75055500], + [1610375400, 849.40, 854.43, 803.62, 811.19, 59301600], + [1610461800, 831.00, 868.00, 827.34, 849.44, 46270700], + [1610548200, 852.76, 860.47, 832.00, 854.41, 33312500], + [1610634600, 843.39, 863.00, 838.75, 845.00, 31266300], + [1610721000, 852.00, 859.90, 819.10, 826.16, 38777600], + [1611066600, 837.80, 850.00, 833.00, 844.55, 25367000], + [1611153000, 858.74, 859.50, 837.28, 850.45, 25665900], + [1611239400, 855.00, 855.72, 841.42, 844.99, 20521100], + [1611325800, 834.31, 848.00, 828.62, 846.64, 20066500], + [1611585000, 855.00, 900.40, 838.82, 880.80, 41173400], + [1611671400, 891.38, 895.90, 871.60, 883.09, 23131600], + [1611757800, 870.35, 891.50, 858.66, 864.16, 27334000], + [1611844200, 820.00, 848.00, 801.00, 835.43, 26378000], + [1611930600, 830.00, 842.41, 780.10, 793.53, 34990800], + [1612189800, 814.29, 842.00, 795.56, 839.81, 25391400], + [1612276200, 844.68, 880.50, 842.20, 872.79, 24346200], + [1612362600, 877.02, 878.08, 853.06, 854.69, 18343500], + [1612449000, 855.00, 856.50, 833.42, 849.99, 15812700], + [1612535400, 845.00, 864.77, 838.97, 852.23, 18566600], + [1612794600, 869.67, 877.77, 854.75, 863.42, 20161700], + [1612881000, 855.12, 859.80, 841.75, 849.46, 15157700], + [1612967400, 843.64, 844.82, 800.02, 804.82, 36216100], + [1613053800, 812.44, 829.88, 801.73, 811.66, 21622800], + [1613140200, 801.26, 817.33, 785.33, 816.12, 23768300], + [1613485800, 818.00, 821.00, 792.44, 796.22, 19802300], + [1613572200, 779.09, 799.84, 762.01, 798.15, 25996500], + [1613658600, 780.90, 794.69, 776.27, 787.38, 17957100], + [1613745000, 795.00, 796.79, 777.37, 781.30, 18958300], + [1614004200, 762.64, 768.50, 710.20, 714.50, 37269700], + [1614090600, 662.13, 713.61, 619.00, 698.84, 66606900], + [1614177000, 711.85, 745.00, 694.17, 742.02, 36767000], + [1614263400, 726.15, 737.21, 670.58, 682.22, 39023900], + [1614349800, 700.00, 706.70, 659.51, 675.50, 41089200], + [1614609000, 690.11, 719.00, 685.05, 718.43, 27136200], + [1614695400, 718.28, 721.11, 685.00, 686.44, 23732200], + [1614781800, 687.99, 700.70, 651.71, 653.20, 30208000], + [1614868200, 655.80, 668.45, 600.00, 621.44, 65919500], + [1614954600, 626.06, 627.84, 539.49, 597.95, 89396500], + [1615213800, 600.55, 620.13, 558.79, 563.00, 51787000], + [1615300200, 608.18, 678.09, 595.21, 673.58, 67523300], + [1615386600, 700.30, 717.85, 655.06, 668.06, 60605700], + [1615473000, 699.40, 702.50, 677.18, 699.60, 36253900], + [1615559400, 670.00, 694.88, 666.14, 693.73, 33583800], + [1615815000, 694.09, 713.18, 684.04, 707.94, 29335600], + [1615901400, 703.35, 707.92, 671.00, 676.88, 32195700], + [1615987800, 656.87, 703.73, 651.01, 701.81, 40372500], + [1616074200, 684.29, 689.23, 652.00, 653.16, 33224800], + [1616160600, 646.60, 657.23, 624.62, 654.87, 42894000], + [1616419800, 684.59, 699.62, 668.75, 670.00, 39512200], + [1616506200, 675.77, 677.80, 657.51, 662.16, 30491900], + [1616592600, 667.91, 668.02, 630.11, 630.27, 33795200], + [1616679000, 613.00, 645.50, 609.50, 640.39, 39224900], + [1616765400, 641.87, 643.82, 599.89, 618.71, 33852800], + [1617024600, 615.64, 616.48, 596.02, 611.29, 28637000], + [1617111000, 601.75, 637.66, 591.01, 635.62, 39432400], + [1617197400, 646.62, 672.00, 641.11, 667.93, 33337300], + [1617283800, 688.37, 692.42, 659.42, 661.75, 35298400], + [1617629400, 707.71, 708.16, 684.70, 691.05, 41842800], + [1617715800, 690.30, 696.55, 681.37, 691.62, 28271800], + [1617802200, 687.00, 691.38, 667.84, 670.97, 26309400], + [1617888600, 677.38, 689.55, 671.65, 683.80, 23924300], + [1617975000, 677.77, 680.97, 669.43, 677.02, 21437100], + [1618234200, 685.70, 704.80, 682.09, 701.98, 29135700], + [1618320600, 712.70, 763.00, 710.66, 762.32, 44652800], + [1618407000, 770.70, 780.79, 728.03, 732.23, 49017400], + [1618493400, 743.10, 743.69, 721.31, 738.85, 27848900], + [1618579800, 728.65, 749.41, 724.60, 739.78, 27979500], + [1618839000, 719.60, 725.40, 691.80, 714.63, 39686200], + [1618925400, 717.42, 737.25, 710.69, 718.99, 35609000], + [1619011800, 704.77, 744.84, 698.00, 744.12, 31215500], + [1619098200, 741.50, 753.77, 718.04, 719.69, 35590300], + [1619184600, 719.80, 737.36, 715.46, 729.40, 28370000], + [1619443800, 741.00, 749.30, 732.61, 738.20, 31038500], + [1619530200, 717.96, 724.00, 703.35, 704.74, 29437000], + [1619616600, 696.41, 708.50, 693.60, 694.40, 22271000], + [1619703000, 699.51, 702.25, 668.50, 677.00, 28845400], + [1619789400, 667.59, 715.47, 666.14, 709.44, 40758700], + [1620048600, 703.80, 706.00, 680.50, 684.90, 27043100], + [1620135000, 678.94, 683.45, 657.70, 673.60, 29739300], + [1620221400, 681.06, 685.30, 667.34, 670.94, 21901900], + [1620307800, 680.76, 681.02, 650.00, 663.54, 27784600], + [1620394200, 665.80, 690.00, 660.22, 672.37, 23469200], + [1620653400, 664.90, 665.05, 627.61, 629.04, 31392400], + [1620739800, 599.24, 627.10, 595.60, 617.20, 46503900], + [1620826200, 602.49, 620.41, 586.77, 589.89, 33823600], + [1620912600, 601.54, 606.46, 559.65, 571.69, 44184900], + [1620999000, 583.41, 592.87, 570.46, 589.74, 33370900], + [1621258200, 575.55, 589.73, 561.20, 576.83, 32390400], + [1621344600, 568.00, 596.25, 563.38, 577.87, 36830600], + [1621431000, 552.55, 566.21, 546.98, 563.46, 39578400], + [1621517400, 575.00, 588.85, 571.07, 586.78, 30821100], + [1621603800, 596.11, 596.68, 580.00, 580.88, 26030600], + [1621863000, 581.60, 614.48, 573.65, 606.44, 34558100], + [1621949400, 607.31, 613.99, 595.71, 604.69, 28005900], + [1622035800, 607.56, 626.17, 601.50, 619.13, 28639300], + [1622122200, 620.24, 631.13, 616.21, 630.85, 26370600], + [1622208600, 628.50, 635.59, 622.38, 625.22, 22737000], + [1622554200, 627.80, 633.80, 620.55, 623.90, 18084900], + [1622640600, 620.13, 623.36, 599.14, 605.12, 23302800], + [1622727000, 601.80, 604.55, 571.22, 572.84, 30111900], + [1622813400, 579.71, 600.61, 577.20, 599.05, 24036900], + [1623072600, 591.83, 610.00, 582.88, 605.13, 22543700], + [1623159000, 623.01, 623.09, 595.50, 603.59, 26053400], + [1623245400, 602.17, 611.79, 597.63, 598.78, 16584600], + [1623331800, 603.88, 616.59, 600.50, 610.12, 23919600], + [1623418200, 610.23, 612.56, 601.52, 609.89, 16205300], + [1623677400, 612.23, 625.49, 609.18, 617.69, 20424000], + [1623763800, 616.69, 616.79, 598.23, 599.36, 17764100], + [1623850200, 597.54, 608.50, 593.50, 604.87, 22144100], + [1623936600, 601.89, 621.47, 601.34, 616.60, 22701400], + [1624023000, 613.37, 628.35, 611.80, 623.31, 24560900], + [1624282200, 624.48, 631.39, 608.88, 620.83, 24812700], + [1624368600, 618.25, 628.57, 615.50, 623.71, 19158900], + [1624455000, 632.00, 657.20, 630.04, 656.57, 31099200], + [1624541400, 674.99, 697.62, 667.61, 679.82, 45982400], + [1624627800, 689.58, 693.81, 668.70, 671.87, 32496700], + [1624887000, 671.64, 694.70, 670.32, 688.72, 21628200], + [1624973400, 684.65, 687.51, 675.89, 680.76, 17381300], + [1625059800, 679.77, 692.81, 678.14, 679.70, 18924900], + [1625146200, 683.92, 687.99, 672.80, 677.92, 18634500], + [1625232600, 678.98, 700.00, 673.26, 678.90, 27054500], + [1625578200, 681.71, 684.00, 651.40, 659.58, 23284500], + [1625664600, 664.27, 665.70, 638.32, 644.65, 18792000], + [1625751000, 628.37, 654.43, 620.46, 652.81, 22773300], + [1625837400, 653.18, 658.91, 644.69, 656.95, 18140500], + [1626096600, 662.20, 687.24, 662.16, 685.70, 25927000], + [1626183000, 686.32, 693.28, 666.30, 668.54, 20966100], + [1626269400, 670.75, 678.61, 652.84, 653.38, 21641200], + [1626355800, 658.39, 666.14, 637.88, 650.60, 20209600], + [1626442200, 654.68, 656.70, 642.20, 644.22, 16371000], + [1626701400, 629.89, 647.20, 621.29, 646.22, 21297100], + [1626787800, 651.99, 662.39, 640.50, 660.50, 15487100], + [1626874200, 659.61, 664.86, 650.29, 655.29, 13953300], + [1626960600, 656.44, 662.17, 644.60, 649.26, 15105700], + [1627047000, 646.36, 648.80, 637.30, 643.38, 14604900], + [1627306200, 650.97, 668.20, 647.11, 657.62, 25336600], + [1627392600, 663.40, 666.50, 627.24, 644.78, 32813300], + [1627479000, 647.00, 654.97, 639.40, 646.98, 16006600], + [1627565400, 649.79, 683.69, 648.80, 677.35, 30394600], + [1627651800, 671.76, 697.53, 669.00, 687.20, 29600500], + [1627911000, 700.00, 726.94, 698.40, 709.67, 33615800], + [1627997400, 719.00, 722.65, 701.01, 709.74, 21620300], + [1628083800, 711.00, 724.90, 708.93, 710.92, 17002600], + [1628170200, 716.00, 720.95, 711.41, 714.63, 12919600], + [1628256600, 711.90, 716.33, 697.63, 699.10, 15576200], + [1628515800, 710.17, 719.03, 705.13, 713.76, 14715300], + [1628602200, 713.99, 716.59, 701.88, 709.99, 13432300], + [1628688600, 712.71, 715.18, 704.21, 707.82, 9800600], + [1628775000, 706.34, 722.80, 699.40, 722.25, 17459100], + [1628861400, 723.71, 729.90, 714.34, 717.17, 16698900], + [1629120600, 705.07, 709.50, 676.40, 686.17, 22677400], + [1629207000, 672.66, 674.58, 648.84, 665.71, 23721300], + [1629293400, 669.75, 695.77, 669.35, 688.99, 20349400], + [1629379800, 678.21, 686.55, 667.59, 673.47, 14313500], + [1629466200, 682.85, 692.13, 673.70, 680.26, 14781800], + [1629725400, 685.44, 712.13, 680.75, 706.30, 20264900], + [1629811800, 710.68, 715.22, 702.64, 708.49, 13083100], + [1629898200, 707.03, 716.97, 704.00, 711.20, 12645600], + [1629984600, 708.31, 715.40, 697.62, 701.16, 13214300], + [1630071000, 705.00, 715.00, 702.10, 711.92, 13762100], + [1630330200, 714.72, 731.00, 712.73, 730.91, 18604200], + [1630416600, 733.00, 740.39, 726.44, 735.72, 20855400], + [1630503000, 734.08, 741.99, 731.27, 734.09, 13204300], + [1630589400, 734.50, 740.97, 730.54, 732.39, 12777300], + [1630675800, 732.25, 734.00, 724.20, 733.57, 15246100], + [1631021400, 740.00, 760.20, 739.26, 752.92, 20039800], + [1631107800, 761.58, 764.45, 740.77, 753.87, 18793000], + [1631194200, 753.41, 762.10, 751.63, 754.86, 14077700], + [1631280600, 759.60, 762.61, 734.52, 736.27, 15114300], + [1631539800, 740.21, 744.78, 708.85, 743.00, 22952500], + [1631626200, 742.57, 754.47, 736.40, 744.49, 18524900], + [1631712600, 745.00, 756.86, 738.36, 755.83, 15357700], + [1631799000, 752.83, 758.91, 747.61, 756.99, 13923400], + [1631885400, 757.15, 761.04, 750.00, 759.49, 28204200], + [1632144600, 734.56, 742.00, 718.62, 730.17, 24757700], + [1632231000, 734.79, 744.74, 730.44, 739.38, 16330700], + [1632317400, 743.53, 753.67, 739.12, 751.94, 15126300], + [1632403800, 755.00, 758.20, 747.92, 753.64, 11947500], + [1632490200, 745.89, 774.80, 744.56, 774.39, 21373000], + [1632749400, 773.12, 799.00, 769.31, 791.36, 28070700], + [1632835800, 787.20, 795.64, 766.18, 777.56, 25381400], + [1632922200, 779.80, 793.50, 770.68, 781.31, 20942900], + [1633008600, 781.00, 789.13, 775.00, 775.48, 17956000], + [1633095000, 778.40, 780.78, 763.59, 775.22, 17031400], + [1633354200, 796.50, 806.97, 776.12, 781.53, 30483300], + [1633440600, 784.80, 797.31, 774.20, 780.59, 18432600], + [1633527000, 776.20, 786.66, 773.22, 782.75, 14632800], + [1633613400, 785.46, 805.00, 783.38, 793.61, 19195800], + [1633699800, 796.21, 796.38, 780.91, 785.49, 16711100], + [1633959000, 787.65, 801.24, 785.50, 791.94, 14175800], + [1634064472, 800.93, 812.32, 796.57, 811.41, 17289281], + ]; + + static List get candles => _rawData + .map((row) => CandleData( + timestamp: row[0] * 1000, + open: row[1]?.toDouble(), + high: row[2]?.toDouble(), + low: row[3]?.toDouble(), + close: row[4]?.toDouble(), + volume: row[5]?.toDouble(), + )) + .toList(); +} diff --git a/lib/data/public_monero_nodes.json b/lib/data/public_monero_nodes.json new file mode 100644 index 0000000..3104198 --- /dev/null +++ b/lib/data/public_monero_nodes.json @@ -0,0 +1,64 @@ +{ + "nodes": [ + "http://xmrnode2fjwmltxetalulcryyvdos437fnyoj5gzg3ecn22uhnwxgyad.onion:18081", + "http://moneronodeuomqv3jcb2jqslmcnrtkiqslv6hwh3cu57ugk6t55xchid.onion:18081", + "http://sneed5kqm3m2jlt2j3shagzz7zmsppicmuf4fmfwryy45kmsrvjfpdid.onion:18089", + "http://monerob2hm5lns5m4maiv3dauroadpofdrpftp5wd2cfqt332ydyeyid.onion:18089", + "http://3qnnb5zicqahxtn7bie6sizdjru3pmtwmzzgwhei7v66s36tafdjp7ad.onion:18081", + "http://l72pup26odlrbhsx543m3f5s4m6opwpkeklvbddddwo6b4qyche7zcyd.onion:18081", + "http://rd4mzz6vxhchnbhz66w6u76632yhgkzypmvpcfbgrfbacmr5ob2f4zqd.onion:18089", + "http://aclc4e2jhhtr44guufbnwk5bzwhaecinax4yip4wr4tjn27sjsfg6zqd.onion:18089", + "http://n7jagir3cwgylxhhwc63hu5ptlj6kunofckf25fv5pizx2rmjvlholid.onion:18089", + "http://monerof5eqykw3hfrjmb66v7r4ft4hlilfzjw5q4huwolttutjuc5dyd.onion:18089", + "http://zqitog3olj5jmeu76jcpthuksplnzlcoucyhska74ny2vdcyeln33bid.onion:18089", + "http://gtbjuomix62tppqmpnb65ak2vqv2fxaca4g3ijmxboy5j6bseu5d4sad.onion:18089", + "http://mexymskdcpunttj3kozziuw62aryumjggulew2kafk7rmvkmegrrgfyd.onion:18081", + "http://fkb5pynm4nr5mb54fw6eep3os7ux5ax7v36jgjcrq7cpilehwd5lgiad.onion:18083", + "http://4egxv4e3idoh2xihko5wkhx5s2i5mmrw3cc3bia3cgc5ttwgqhtgi7ad.onion:18089", + "http://xrft3jrcdrfwvry4aocxcswvwturaqxelesmtdk34j4646tajmc6rxyd.onion:18089", + "http://lrtrju7tz72422sjmwakygfu7xgskaawiqmfulmssfzx7aofatfkmvid.onion:18089", + "http://monerujods7mbghwe6cobdr6ujih6c22zu5rl7zshmizz2udf7v7fsad.onion:18081", + "http://hn3lysds52kplnso5eyiimbmakr67c3owqhakimc7xio5nzb5plbl6yd.onion:18089", + "http://so55gc5hiftcat5xagflyiuyaw3uc7jtj7ornxf757xwxhyea2clvzqd.onion:18089", + "http://fz2lbxvjob6ifeonngaep2xvf2ypxjjn23i3ncblcxjreovev56ubyyd.onion:18089", + "https://libertytmtitynvmnto2k42liys5fenb3wabaozmmmksyrc7jvgmjiqd.onion:18089", + "http://glnoqirjvs4cofkj3zy3ttq2s7fzcj67dfcletshrh6gwpkkhtbulpyd.onion:18089", + "http://libertytmtitynvmnto2k42liys5fenb3wabaozmmmksyrc7jvgmjiqd.onion:18089", + "http://ncbkcouzbqzcskr3dy26bvgzztpt7ywtjd2ly2bpv5c5zfxoht7gthad.onion:18081", + "http://moneroexnovtlp4datcwbgjznnulgm7q34wcl6r4gcvccruhkceb2xyd.onion:18089", + "http://plowsof3t5hogddwabaeiyrno25efmzfxyro2vligremt7sxpsclfaid.onion:18089", + "http://plowsoffjexmxalw73tkjmf422gq6575fc7vicuu4javzn2ynnte6tyd.onion:18089", + "http://ode6i5zdrm4xjqeubacrwmgnihzpfsfvpdf4kvcphhnrqkmrhq5idxyd.onion:18089", + "http://r6ou6dckycorsauaelpej2k4z2e2jkk62pbkskco34anmnkgl2tlaiqd.onion:18081", + "http://ulmlrardljg3r6urejlm6c2tikp3krvkupmdnueahmj33vl5ztez7jqd.onion:18081", + "http://2wi527wss2ysexkmyjveki7ypaikz5x567eiqnnl76om2zuyd4d3dpqd.onion:18081", + "http://43n3jitkea7hvx72xjj5ey2yxhevdlwmv3nail5fp7wzigt6ptega5qd.onion:18089", + "http://sneedxmr3mur3ypoto7o3xzqyvodidto5zlosxeesu4upunax5xnskid.onion:18089", + "https://plowsof3t5hogddwabaeiyrno25efmzfxyro2vligremt7sxpsclfaid.onion:18089", + "http://v5zluoefmjk6c24r3o4qsjzzyux233c5v332vrozu454xoohron5moid.onion:18089", + "http://zkvdadvagi5r757w7ewvpomhjgxab3d4xh3npo5rcxc4d7mwuee635qd.onion:18081", + "http://daturab6drmkhyeia4ch5gvfc2f3wgo6bhjrv3pz6n7kxmvoznlkq4yd.onion:18081", + "http://plowsofe6cleftfmk2raiw5h2x66atrik3nja4bfd3zrfa2hdlgworad.onion:18089", + "http://3brwzfhssrqacnufjwvr2vpbawpx6ifzqhd7nzfwa3twaivn443z4nad.onion:18089", + "http://6jvn5tinwxnp723vnsvekroaniq7qkag7nkdqmcwlbinxnpeonaowayd.onion:18089", + "http://kbs3vzc6cwxmrw7ig5r4wgt5brxl6km2ccr7y2mqf3aznriz42pvwgyd.onion:18089", + "http://waovyhgbeorma64p6cbomev4zfydua4s35gd3xdc5igtjqetzvwbtkyd.onion:18089", + "http://twiy7ceov5gskxlsqfsmlubbivse5duo3ro7xmrimrgfp3whwfmzi4id.onion:18081", + "http://rlz6pa5tfs6hxbnsvzad32gt3ck3ahvgs5a73clfztezz42eo664mzid.onion:18089", + "http://azj7425hcyxzw6jxtvf3a4aw6t4rbdazywngq7jtrsoysk24btfycvad.onion:18089", + "http://dtrnd4in2igrtfx2c45ghf2drns3doddmcsfy6b5gjw5iinukd33slqd.onion:18081", + "http://moneronkvv2hu2anvcc5b4qd5y7strnc2ob6khqsrtikmhocyvjpdjyd.onion:18089", + "http://xmrpoker6fqd7kh6gxv4dtr4kaue3pdmuzbaqorvlan4as3oh3f2ftqd.onion:18081", + "http://vfp7tc36qcyd2x7r3k4twsixkrubvipp3zm5zt7wgzrjrbaon7vwgkyd.onion:18081", + "http://nj4lnfbo2mkguxv6wvzpmnjwibz2jipy7kyma4qkposr6qef7suqf3ad.onion:18081", + "http://csxmritzk2qdgqmou2vwyrwu65xabimvmeniestaartks4fhlocfoeyd.onion:18081", + "http://qvb4rowgpqzz2m4vayovmtx2ag6l2lsdjhhbzb2nvg3ikybclb4ttdad.onion:18081", + "http://monero3x5yrb7tsalxx64tr2qhfw54xy3eudhswvpaskfvsdk2tzb3id.onion:18089", + "http://6dsdenp6vjkvqzy4wzsnzn6wixkdzihx3khiumyzieauxuxslmcaeiad.onion:18081", + "http://h5oepjpydwyacbks5vnanx2qw4rboqz53eksljflbkn5zrwcz2575xqd.onion:18089", + "http://hashvaultsvg2rinvxz7kos77hdfm6zrd5yco3tx2yh2linsmusfwyad.onion:18081", + "http://nrf3tly3ypsow73lfijhpx4cqll3l4xiuf3r4xbfzkdkgzy4gq642sad.onion:18089", + "http://27ihnchx3loqzue7hekggodxpazfb3npcosfxzrar5fom4zapd6rgcad.onion:18081", + "http://4kzkwyooeth3lxajxs7pmcriq6cgvnbex2vojum2uqflczsci4dlreyd.onion:18089" + ] + } \ No newline at end of file diff --git a/lib/haveno_app.dart b/lib/haveno_app.dart new file mode 100644 index 0000000..9659195 --- /dev/null +++ b/lib/haveno_app.dart @@ -0,0 +1,155 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:flutter_background_service/flutter_background_service.dart'; +import 'package:haveno_app/main.dart'; +import 'package:haveno_app/services/secure_storage_service.dart'; +import 'package:haveno_app/views/desktop_lifecycle.dart'; +import 'package:haveno_app/views/mobile_lifecycle.dart'; +import 'package:haveno_app/views/screens/onboarding_screen.dart'; +import 'package:haveno_app/views/screens/seednode_setup_screen.dart'; + +class HavenoApp extends StatefulWidget { + const HavenoApp({super.key}); + + @override + _HavenoAppState createState() => _HavenoAppState(); +} + +class _HavenoAppState extends State with WidgetsBindingObserver { + bool? _onboardingComplete; // Nullable + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + + // Load the onboarding status asynchronously and update the state + SecureStorageService().readOnboardingStatus().then((onboardingComplete) { + setState(() { + _onboardingComplete = onboardingComplete ?? false; + }); + }); + } + + Widget _buildAppContent() { + // Check if _onboardingComplete is null (while loading) + if (_onboardingComplete == null) { + return const Center(child: CircularProgressIndicator()); + } + + return MaterialApp( + debugShowCheckedModeBanner: false, + navigatorKey: navigatorKey, + theme: ThemeData( + useMaterial3: true, + colorScheme: ColorScheme.fromSeed( + primary: const Color(0xFFF4511E), + seedColor: const Color(0xFFF4511E), + brightness: Brightness.dark, + ), + scaffoldBackgroundColor: const Color(0xFF303030), + appBarTheme: const AppBarTheme( + backgroundColor: Color(0xFF303030), + ), + drawerTheme: const DrawerThemeData( + backgroundColor: Color(0xFF303030), + ), + navigationBarTheme: NavigationBarThemeData( + backgroundColor: const Color(0xFF303030).withOpacity(0.5), + indicatorShape: const StadiumBorder(), + ), + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFF4511E), + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5), + ), + ), + ), + cardTheme: const CardTheme( + color: Color(0xFF424242), + ), + ), + // Use _onboardingComplete to decide the home widget + home: _onboardingComplete == true ? SeedNodeSetupScreen() : OnboardingScreen(), + ); + } + + @override + Widget build(BuildContext context) { + if (Platform.isAndroid || Platform.isIOS) { + return MobileLifecycleWidget( + child: _buildAppContent(), + builder: (context, child) => child, + ); + } else if (Platform.isMacOS || Platform.isLinux || Platform.isWindows) { + return DesktopLifecycleWidget( + child: _buildAppContent(), + builder: (context, child) => child, + ); + } else { + return const Center(child: CircularProgressIndicator()); + } + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + + @override + Future didChangeAppLifecycleState(AppLifecycleState state) async { + + switch (state) { + case AppLifecycleState.resumed: + if (Platform.isAndroid || Platform.isIOS) { + final service = FlutterBackgroundService(); + bool isRunning = await service.isRunning(); + if (!isRunning) { + service.startService(); + } + } + print("App resumed"); + break; + case AppLifecycleState.paused: + print("App paused"); + break; + case AppLifecycleState.inactive: + print("App inactive"); + break; + case AppLifecycleState.detached: + print("App detached"); + break; + case AppLifecycleState.hidden: + print("App hidden"); + break; + } + + // Ensure the method always returns a Future + return Future.value(); + } +} diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 0000000..6ee90d7 --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,195 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import 'dart:io'; +import 'dart:ui'; +import 'package:flutter/material.dart'; +import 'package:flutter_background_service/flutter_background_service.dart'; +import 'package:haveno/haveno_client.dart'; +import 'package:haveno_app/haveno_app.dart'; +import 'package:haveno_app/background_services.dart'; +import 'package:haveno_app/models/schema.dart'; +import 'package:haveno_app/providers/haveno_client_providers/xmr_connections_provider.dart'; +import 'package:haveno_app/providers/haveno_daemon_provider.dart'; +import 'package:haveno_app/providers/haveno_client_providers/dispute_agents_provider.dart'; +import 'package:haveno_app/providers/haveno_client_providers/disputes_provider.dart'; +import 'package:haveno_app/providers/haveno_client_providers/offers_provider.dart'; +import 'package:haveno_app/providers/haveno_client_providers/payment_accounts_provider.dart'; +import 'package:haveno_app/providers/haveno_client_providers/price_provider.dart'; +import 'package:haveno_app/providers/haveno_client_providers/trade_statistics_provider.dart'; +import 'package:haveno_app/providers/haveno_providers/settings_provider.dart'; +import 'package:haveno_app/providers/haveno_client_providers/trades_provider.dart'; +import 'package:haveno_app/providers/haveno_client_providers/wallets_provider.dart'; +import 'package:haveno_app/services/local_notification_service.dart'; +import 'package:haveno_app/services/secure_storage_service.dart'; +import 'package:haveno_app/system_tray.dart'; +import 'package:haveno_app/versions.dart'; +import 'package:provider/provider.dart'; +import 'package:haveno_app/providers/haveno_client_providers/version_provider.dart'; +import 'package:haveno_app/providers/haveno_client_providers/account_provider.dart'; +import 'dart:async'; +import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:flutter_socks_proxy/socks_proxy.dart'; + +final GlobalKey navigatorKey = GlobalKey(); + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + + await Versions().load(); + + // Initialize event dispatcher + final eventDispatcher = EventDispatcher(); + + // Initialize the tor status service + //final torStatusService = TorStatusService(); + //torStatusService.startListening(eventDispatcher); + + // Initialise the tor log service + //final torLogService = TorLogService(); + //torLogService.startListening(eventDispatcher); + + // Initializt notifications + await LocalNotificationsService().init(); + + // Set the default for desktop (overrites later when connected for mobile) + + if (!Platform.isIOS) { + SocksProxy.initProxy(proxy: 'SOCKS5 127.0.0.1:9050'); + } + + // Start Orbot on iOS + //if (Platform.isIOS) { + // await OrbotApi().startOrbot(); + //} + + + if (Platform.isLinux || Platform.isMacOS || Platform.isWindows) { + intializeSystemTray(); + } + + // Setup background/foreground services (for fetching data for notifications and state updates in SQL) + if (Platform.isIOS || Platform.isAndroid) { + FlutterBackgroundService? mobileBackgroundService; + mobileBackgroundService = FlutterBackgroundService(); + + // Start the background service listener + final backgroundServiceListener = BackgroundServiceListener(eventDispatcher); + backgroundServiceListener.startListening(); + + await mobileBackgroundService.configure( + iosConfiguration: IosConfiguration( + autoStart: true, + //onForeground: onStart, + onBackground: onIosBackground, + ), + androidConfiguration: AndroidConfiguration( + autoStart: true, + onStart: onStart, + isForegroundMode: true, + autoStartOnBoot: true, + ), + ); + } + + final secureStorageService = SecureStorageService(); + final havenoChannel = HavenoChannel(); + + await SentryFlutter.init((options) { + options.dsn = + 'https://ddf883d1a885ae8d619a923d1c80350f@o4507901830299648.ingest.us.sentry.io/4507901840457728'; + options.tracesSampleRate = 1.0; + options.profilesSampleRate = 1.0; + if (Platform.isAndroid || Platform.isIOS) { + options.proxy = SentryProxy(type: SentryProxyType.socks, host: '127.0.0.1', port: 9050); + } else { + options.proxy = SentryProxy(type: SentryProxyType.socks, host: '127.0.0.1', port: 9066); + } + }, + appRunner: () => runApp( + MultiProvider( + providers: [ + Provider(create: (_) => havenoChannel), + //ChangeNotifierProvider( + // create: (context) => TorStatusProvider(torStatusService), + //), + //ChangeNotifierProvider( + // create: (context) => TorLogProvider(torLogService), + //), + ChangeNotifierProvider( + create: (context) => + HavenoDaemonProvider(secureStorageService), + ), + ChangeNotifierProvider( + create: (context) => SettingsProvider(secureStorageService), + ), + ChangeNotifierProvider( + create: (context) => GetVersionProvider(havenoChannel), + ), + ChangeNotifierProvider( + create: (context) => AccountProvider(), + ), + ChangeNotifierProvider( + create: (context) => WalletsProvider(havenoChannel), + ), + ChangeNotifierProvider( + create: (context) => OffersProvider(havenoChannel), + ), + ChangeNotifierProvider( + create: (context) => TradesProvider(havenoChannel), + ), + ChangeNotifierProvider( + create: (context) => PaymentAccountsProvider(havenoChannel), + ), + ChangeNotifierProvider( + create: (context) => PricesProvider(havenoChannel), + ), + ChangeNotifierProvider( + create: (context) => TradeStatisticsProvider(havenoChannel), + ), + ChangeNotifierProvider( + create: (context) => DisputesProvider(), + ), + ChangeNotifierProvider( + create: (context) => DisputeAgentsProvider(), + ), + ChangeNotifierProvider( + create: (context) => XmrConnectionsProvider(), + ), + ], + child: HavenoApp(), + ), + )); +} + +@pragma('vm:entry-point') +Future onIosBackground(ServiceInstance service) async { + WidgetsFlutterBinding.ensureInitialized(); + DartPluginRegistrant.ensureInitialized(); + startShortLivedBackgroundFetch(service); + return true; +} + +@pragma('vm:entry-point') +void onStart(ServiceInstance service) async { + WidgetsFlutterBinding.ensureInitialized(); + startLongRunningForegroundService(service); +} diff --git a/lib/models/candlestick.dart b/lib/models/candlestick.dart new file mode 100644 index 0000000..525fbd9 --- /dev/null +++ b/lib/models/candlestick.dart @@ -0,0 +1,138 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +/* import 'dart:convert'; +import 'package:fixnum/fixnum.dart'; +import 'package:haveno_app/proto/compiled/pb.pb.dart'; + +class CandlestickData { + final int timestamp; + final int open; + final int high; + final int low; + final int close; + final int volume; + + CandlestickData({ + required this.timestamp, + required this.open, + required this.high, + required this.low, + required this.close, + required this.volume, + }); +} + +Future> getCandlestickData(String period) async { + final db = await instance.database; + + // Determine the start and end timestamps for the specified period + int startTime; + int endTime; + + DateTime now = DateTime.now(); + + switch (period.toLowerCase()) { + case 'daily': + startTime = DateTime(now.year, now.month, now.day).millisecondsSinceEpoch; + endTime = DateTime(now.year, now.month, now.day + 1).millisecondsSinceEpoch - 1; + break; + case 'weekly': + startTime = DateTime(now.year, now.month, now.day - now.weekday + 1).millisecondsSinceEpoch; + endTime = DateTime(now.year, now.month, now.day - now.weekday + 8).millisecondsSinceEpoch - 1; + break; + case 'monthly': + startTime = DateTime(now.year, now.month, 1).millisecondsSinceEpoch; + endTime = DateTime(now.year, now.month + 1, 1).millisecondsSinceEpoch - 1; + break; + case 'yearly': + startTime = DateTime(now.year, 1, 1).millisecondsSinceEpoch; + endTime = DateTime(now.year + 1, 1, 1).millisecondsSinceEpoch - 1; + break; + default: + throw ArgumentError('Invalid period specified: $period'); + } + + // Query the database for trade statistics within the specified period + final List> maps = await db.query( + 'trade_statistics', + where: 'date >= ? AND date <= ?', + whereArgs: [startTime, endTime], + ); + + // Group the trade statistics by the period and calculate OHLC data + Map> groupedData = {}; + + for (var map in maps) { + final tradeStatisticJson = map['data']; + final tradeStatistic = TradeStatistics3.create() + ..mergeFromProto3Json(jsonDecode(tradeStatisticJson)); + + // Determine the period timestamp (e.g., start of the day, week, etc.) + DateTime date = DateTime.fromMillisecondsSinceEpoch(tradeStatistic.date.toInt()); + int periodTimestamp; + + switch (period.toLowerCase()) { + case 'daily': + periodTimestamp = DateTime(date.year, date.month, date.day).millisecondsSinceEpoch; + break; + case 'weekly': + periodTimestamp = DateTime(date.year, date.month, date.day - date.weekday + 1).millisecondsSinceEpoch; + break; + case 'monthly': + periodTimestamp = DateTime(date.year, date.month, 1).millisecondsSinceEpoch; + break; + case 'yearly': + periodTimestamp = DateTime(date.year, 1, 1).millisecondsSinceEpoch; + break; + default: + throw ArgumentError('Invalid period specified: $period'); + } + + groupedData.putIfAbsent(periodTimestamp, () => []).add(tradeStatistic); + } + + // Calculate OHLC data for each group + List candlestickData = groupedData.entries.map((entry) { + final trades = entry.value; + final sortedTrades = trades..sort((a, b) => a.date.compareTo(b.date)); + + Int64 open = sortedTrades.first.price; + Int64 close = sortedTrades.last.price; + Int64 high = sortedTrades.map((trade) => trade.price).reduce((a, b) => a > b ? a : b); + Int64 low = sortedTrades.map((trade) => trade.price).reduce((a, b) => a < b ? a : b); + Int64 volume = sortedTrades.map((trade) => trade.amount).reduce((a, b) => a + b); + + return CandlestickData( + timestamp: entry.key, + open: open.toInt(), + high: open., + high: , + low: low, + close: close, + volume: volume, + ); + }).toList(); + + return candlestickData; +} + */ \ No newline at end of file diff --git a/lib/models/haveno/p2p/haveno_network b/lib/models/haveno/p2p/haveno_network new file mode 100644 index 0000000..6b6eec4 --- /dev/null +++ b/lib/models/haveno/p2p/haveno_network @@ -0,0 +1,48 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. + +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +class HavenoNetwork { + final String name; + final String description; + + HavenoNetwork({ + required this.name, + required this.description, + }); + + Map toJson() { + return { + 'name': name, + 'description': description, + }; + } + + factory HavenoNetwork.fromJson(Map json) { + return HavenoNetwork( + name: json['name'], + description: json['description'], + ); + } + + factory HavenoNetwork.getDefault() { + return HavenoNetwork( + name: '', + description: 'A new Haveno network', + ); + } +} \ No newline at end of file diff --git a/lib/models/haveno/p2p/haveno_node.dart b/lib/models/haveno/p2p/haveno_node.dart new file mode 100644 index 0000000..05c4de2 --- /dev/null +++ b/lib/models/haveno/p2p/haveno_node.dart @@ -0,0 +1,17 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. + +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . \ No newline at end of file diff --git a/lib/models/haveno/p2p/haveno_peer.dart b/lib/models/haveno/p2p/haveno_peer.dart new file mode 100644 index 0000000..94d3fd6 --- /dev/null +++ b/lib/models/haveno/p2p/haveno_peer.dart @@ -0,0 +1,20 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . \ No newline at end of file diff --git a/lib/models/haveno/p2p/haveno_seednode.dart b/lib/models/haveno/p2p/haveno_seednode.dart new file mode 100644 index 0000000..2763ac0 --- /dev/null +++ b/lib/models/haveno/p2p/haveno_seednode.dart @@ -0,0 +1,57 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. + +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import 'package:haveno_app/models/haveno/p2p/haveno_network'; + +class HavenoSeedNode { + final String onionHost; + final int port; + final HavenoNetwork network; + + HavenoSeedNode({ + required this.onionHost, + required this.port, + required this.network, + }) { + _validateOnionAddress(onionHost); + } + + static void _validateOnionAddress(String host) { + final pattern = RegExp(r'^[a-zA-Z0-9]{56}\.onion$'); + if (!pattern.hasMatch(host)) { + throw const FormatException('Invalid v3 onion address'); + } + } + + Map toJson() { + return { + 'onionHost': onionHost, + 'port': port, + 'network': network.toJson(), + }; + } + + factory HavenoSeedNode.fromJson(Map json) { + return HavenoSeedNode( + onionHost: json['onionHost'], + port: json['port'], + network: HavenoNetwork.fromJson(json['network']), + ); + } + +} \ No newline at end of file diff --git a/lib/models/haveno_daemon_config.dart b/lib/models/haveno_daemon_config.dart new file mode 100644 index 0000000..8fea2a0 --- /dev/null +++ b/lib/models/haveno_daemon_config.dart @@ -0,0 +1,83 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +class HavenoDaemonConfig { + final String host; + final int port; + final String clientAuthPassword; + final Uri fullUri; + bool isVerified; // Has had at least one successful GRPC call + + HavenoDaemonConfig({ + required this.fullUri, + String? clientAuthPassword, + }) : clientAuthPassword = clientAuthPassword ?? _parsePassword(fullUri), + host = _parseHost(fullUri), + port = _parsePort(fullUri), + isVerified = false { + _validateOnionAddress(fullUri); + } + + static String _parseHost(Uri uri) { + return uri.host; + } + + static int _parsePort(Uri uri) { + return uri.hasPort ? uri.port : 80; + } + + static String _parsePassword(Uri uri) { + final password = uri.queryParameters['password']; + if (password == null) { + throw const FormatException('Missing password query parameter'); + } + return password; + } + + static void _validateOnionAddress(Uri uri) { + final pattern = RegExp(r'^[a-zA-Z0-9]{56}\.onion$'); + if (!pattern.hasMatch(uri.host)) { + throw const FormatException('Invalid v3 onion address'); + } + } + + void setVerified(bool verified) { + isVerified = verified; + } + + Map toJson() { + return { + 'host': host, + 'port': port, + 'fullUri': fullUri.toString(), + 'clientAuthPassword': clientAuthPassword, + 'isVerified': isVerified + }; + } + + factory HavenoDaemonConfig.fromJson(Map json) { + return HavenoDaemonConfig( + fullUri: Uri.parse(json['fullUri']), + clientAuthPassword: json['clientAuthPassword'], + ); + } +} \ No newline at end of file diff --git a/lib/models/monero/monero_node.dart b/lib/models/monero/monero_node.dart new file mode 100644 index 0000000..107f538 --- /dev/null +++ b/lib/models/monero/monero_node.dart @@ -0,0 +1,208 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +class MoneroNode { + final int adjustedTime; + final int altBlocksCount; + final int blockSizeLimit; + final int blockSizeMedian; + final int blockWeightLimit; + final int blockWeightMedian; + final String bootstrapDaemonAddress; + final bool busySyncing; + final int credits; + final int cumulativeDifficulty; + final int cumulativeDifficultyTop64; + final int databaseSize; + final int difficulty; + final int difficultyTop64; + final int freeSpace; + final int greyPeerlistSize; + final int height; + final int heightWithoutBootstrap; + final int incomingConnectionsCount; + final bool mainnet; + final String nettype; + final bool offline; + final int outgoingConnectionsCount; + final bool restricted; + final int rpcConnectionsCount; + final bool stagenet; + final int startTime; + final String status; + final bool synchronized; + final int target; + final int targetHeight; + final bool testnet; + final String topBlockHash; + final String topHash; + final int txCount; + final int txPoolSize; + final bool untrusted; + final bool updateAvailable; + final String version; + final bool wasBootstrapEverUsed; + final int whitePeerlistSize; + final String wideCumulativeDifficulty; + final String wideDifficulty; + + MoneroNode({ + required this.adjustedTime, + required this.altBlocksCount, + required this.blockSizeLimit, + required this.blockSizeMedian, + required this.blockWeightLimit, + required this.blockWeightMedian, + required this.bootstrapDaemonAddress, + required this.busySyncing, + required this.credits, + required this.cumulativeDifficulty, + required this.cumulativeDifficultyTop64, + required this.databaseSize, + required this.difficulty, + required this.difficultyTop64, + required this.freeSpace, + required this.greyPeerlistSize, + required this.height, + required this.heightWithoutBootstrap, + required this.incomingConnectionsCount, + required this.mainnet, + required this.nettype, + required this.offline, + required this.outgoingConnectionsCount, + required this.restricted, + required this.rpcConnectionsCount, + required this.stagenet, + required this.startTime, + required this.status, + required this.synchronized, + required this.target, + required this.targetHeight, + required this.testnet, + required this.topBlockHash, + required this.topHash, + required this.txCount, + required this.txPoolSize, + required this.untrusted, + required this.updateAvailable, + required this.version, + required this.wasBootstrapEverUsed, + required this.whitePeerlistSize, + required this.wideCumulativeDifficulty, + required this.wideDifficulty, + }); + + factory MoneroNode.fromJson(Map json) { + return MoneroNode( + adjustedTime: json['adjusted_time'], + altBlocksCount: json['alt_blocks_count'], + blockSizeLimit: json['block_size_limit'], + blockSizeMedian: json['block_size_median'], + blockWeightLimit: json['block_weight_limit'], + blockWeightMedian: json['block_weight_median'], + bootstrapDaemonAddress: json['bootstrap_daemon_address'], + busySyncing: json['busy_syncing'], + credits: json['credits'], + cumulativeDifficulty: json['cumulative_difficulty'], + cumulativeDifficultyTop64: json['cumulative_difficulty_top64'], + databaseSize: json['database_size'], + difficulty: json['difficulty'], + difficultyTop64: json['difficulty_top64'], + freeSpace: json['free_space'], + greyPeerlistSize: json['grey_peerlist_size'], + height: json['height'], + heightWithoutBootstrap: json['height_without_bootstrap'], + incomingConnectionsCount: json['incoming_connections_count'], + mainnet: json['mainnet'], + nettype: json['nettype'], + offline: json['offline'], + outgoingConnectionsCount: json['outgoing_connections_count'], + restricted: json['restricted'], + rpcConnectionsCount: json['rpc_connections_count'], + stagenet: json['stagenet'], + startTime: json['start_time'], + status: json['status'], + synchronized: json['synchronized'], + target: json['target'], + targetHeight: json['target_height'], + testnet: json['testnet'], + topBlockHash: json['top_block_hash'], + topHash: json['top_hash'], + txCount: json['tx_count'], + txPoolSize: json['tx_pool_size'], + untrusted: json['untrusted'], + updateAvailable: json['update_available'], + version: json['version'], + wasBootstrapEverUsed: json['was_bootstrap_ever_used'], + whitePeerlistSize: json['white_peerlist_size'], + wideCumulativeDifficulty: json['wide_cumulative_difficulty'], + wideDifficulty: json['wide_difficulty'], + ); + } + + Map toJson() { + return { + 'adjusted_time': adjustedTime, + 'alt_blocks_count': altBlocksCount, + 'block_size_limit': blockSizeLimit, + 'block_size_median': blockSizeMedian, + 'block_weight_limit': blockWeightLimit, + 'block_weight_median': blockWeightMedian, + 'bootstrap_daemon_address': bootstrapDaemonAddress, + 'busy_syncing': busySyncing, + 'credits': credits, + 'cumulative_difficulty': cumulativeDifficulty, + 'cumulative_difficulty_top64': cumulativeDifficultyTop64, + 'database_size': databaseSize, + 'difficulty': difficulty, + 'difficulty_top64': difficultyTop64, + 'free_space': freeSpace, + 'grey_peerlist_size': greyPeerlistSize, + 'height': height, + 'height_without_bootstrap': heightWithoutBootstrap, + 'incoming_connections_count': incomingConnectionsCount, + 'mainnet': mainnet, + 'nettype': nettype, + 'offline': offline, + 'outgoing_connections_count': outgoingConnectionsCount, + 'restricted': restricted, + 'rpc_connections_count': rpcConnectionsCount, + 'stagenet': stagenet, + 'start_time': startTime, + 'status': status, + 'synchronized': synchronized, + 'target': target, + 'target_height': targetHeight, + 'testnet': testnet, + 'top_block_hash': topBlockHash, + 'top_hash': topHash, + 'tx_count': txCount, + 'tx_pool_size': txPoolSize, + 'untrusted': untrusted, + 'update_available': updateAvailable, + 'version': version, + 'was_bootstrap_ever_used': wasBootstrapEverUsed, + 'white_peerlist_size': whitePeerlistSize, + 'wide_cumulative_difficulty': wideCumulativeDifficulty, + 'wide_difficulty': wideDifficulty, + }; + } +} diff --git a/lib/models/schema.dart b/lib/models/schema.dart new file mode 100644 index 0000000..888e5dc --- /dev/null +++ b/lib/models/schema.dart @@ -0,0 +1,248 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:haveno/grpc_models.dart'; +import 'package:haveno/profobuf_models.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +abstract class DeviceManagerAutoInitialization { + Future init(); +} + +abstract class PollingProvider with ChangeNotifier { + Timer? _timer; + bool _isPolling = false; + final Duration maxPollingInterval; + + PollingProvider(this.maxPollingInterval); + + // Method to be implemented by subclasses to define the polling action + Future pollAction(); + + // Method to start polling with a custom interval + void startPolling([Duration? interval]) { + if (_isPolling) return; // Prevent multiple polling tasks + + _isPolling = true; + _timer = Timer.periodic(interval ?? maxPollingInterval, (Timer t) async { + await pollAction(); + }); + } + + // Method to stop polling + void stopPolling() { + _timer?.cancel(); + _isPolling = false; + } + + @override + void dispose() { + stopPolling(); + super.dispose(); + } +} + +class SyncTask { + final Future Function() taskFunction; + final Duration cooldown; + final List dependencies; + DateTime _lastRun; + + SyncTask({ + required this.taskFunction, + required this.cooldown, + this.dependencies = const [], // Default to no dependencies + }) : _lastRun = DateTime.fromMillisecondsSinceEpoch(0); // Initialize to a time far in the past + + bool shouldRun() { + // Check if all dependencies have been run + bool dependenciesMet = dependencies.every((task) => task.hasRun); + + // Check if cooldown period has passed and dependencies are met + return dependenciesMet && DateTime.now().difference(_lastRun) >= cooldown; + } + + Future run() async { + if (shouldRun()) { + await taskFunction(); + _lastRun = DateTime.now(); + } + } + + bool get hasRun => _lastRun.isAfter(DateTime.fromMillisecondsSinceEpoch(0)); +} + +class SyncManager { + final List _tasks = []; + final Duration checkInterval; + Timer? _timer; + + SyncManager({required this.checkInterval}); + + void addTask(SyncTask task) { + _tasks.add(task); + } + + void start() { + _timer = Timer.periodic(checkInterval, (timer) async { + // Iterate over tasks, making sure dependencies are resolved + for (var task in _tasks) { + await task.run(); + } + }); + } + + void stop() { + _timer?.cancel(); + } +} + +mixin CooldownMixin { + final Map _cooldownDurations = {}; + + // Initialize cooldown durations + void setCooldownDurations(Map durations) { + _cooldownDurations.addAll(durations); + } + + // Check if the cooldown is valid by comparing the current time with the stored last run time + Future isCooldownValid(String key) async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + final lastRunTimestamp = prefs.getInt('cooldown_$key'); + + if (lastRunTimestamp == null) return false; // No previous run recorded, so it's invalid. + + final lastRunTime = DateTime.fromMillisecondsSinceEpoch(lastRunTimestamp); + final duration = _cooldownDurations[key] ?? const Duration(minutes: 5); + + return DateTime.now().isBefore(lastRunTime.add(duration)); // Valid if current time is before cooldown expires. + } + + // Update the cooldown with the current time and store it in shared_preferences + Future updateCooldown(String key) async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + final now = DateTime.now(); + final duration = _cooldownDurations[key] ?? const Duration(minutes: 5); // Default to 5 minutes if not defined + + final cooldownEndTime = now.add(duration); + await prefs.setInt('cooldown_$key', cooldownEndTime.millisecondsSinceEpoch); // Store the expiration time + } + + // Optionally, you can add a method to clear cooldowns for testing or reset purposes + Future clearCooldown(String key) async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + await prefs.remove('cooldown_$key'); + } +} + + +abstract class PlatformLifecycleWidget extends StatefulWidget { + final Widget child; + final Widget Function(BuildContext context, Widget child) builder; + + const PlatformLifecycleWidget({ + super.key, + required this.child, + required this.builder, + }); +} + +abstract class PlatformLifecycleState extends State { + Future initPlatform(); + + @override + void initState() { + super.initState(); + initPlatform(); + } + + @override + Widget build(BuildContext context) { + return widget.builder(context, widget.child); + } +} + +enum BackgroundEventType { + updateTorStatus, + updateDaemonStatus, + torStdOutLog, + torStdErrLog, +} + +class BackgroundEvent { + final BackgroundEventType type; + final Map data; + + BackgroundEvent({required this.type, required this.data}); +} + +class EventDispatcher { + final Map)>> _listeners = {}; + + void subscribe(BackgroundEventType eventType, Function(Map) callback) { + _listeners[eventType] ??= []; + _listeners[eventType]!.add(callback); + } + + void unsubscribe(BackgroundEventType eventType, Function(Map) callback) { + _listeners[eventType]?.remove(callback); + } + + void dispatch(BackgroundEvent event) { + if (_listeners[event.type] != null) { + for (var listener in _listeners[event.type]!) { + listener(event.data); + } + } + } +} + +mixin StreamListenerProviderMixin on ChangeNotifier { + StreamSubscription? _subscription; + + // A helper function to manage stream subscriptions + void listenToStream(Stream stream, VoidCallback onData) { + _subscription = stream.listen((_) { + onData(); // Perform action when data arrives (e.g., notifyListeners) + }); + } + + @override + void dispose() { + _subscription?.cancel(); + super.dispose(); + } +} + + + +// Notification callbacks +typedef NewChatMessageCallback = void Function(ChatMessage chatMessage); +typedef TradeUpdateCallback = void Function(TradeInfo trade, bool isNewTrade); + +// Background service callbacks +typedef UpdateTorStatusBackgroundCallback = void Function(String status, String detail); +typedef UpdateDaemonStatusBackgroundCallback = void Function(String status, String detail); +typedef TorStdOutLogBackgroundCallback = void Function(String details); +typedef TorStdErrLogBackgroundCallback = void Function(String details); diff --git a/lib/models/system_processes/base_system_process.dart b/lib/models/system_processes/base_system_process.dart new file mode 100644 index 0000000..7356342 --- /dev/null +++ b/lib/models/system_processes/base_system_process.dart @@ -0,0 +1,377 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import 'dart:async'; +import 'dart:io'; +import 'package:archive/archive_io.dart'; +import 'package:flutter/services.dart'; +import 'package:path/path.dart' as path; +import 'package:path_provider/path_provider.dart'; + +abstract class SystemProcess { + final String key; + final String displayName; + final String? bundleAssetKey; + final String? windowsInstallationPath; + final String? linuxInstallationPath; + final String? macOsInstallationPath; + final bool useInstallationPathAsWorkingDirectory; + final String? windowsExecutableName; + final String? linuxExecutableName; + final String? macOsExecutableName; + final bool startOnLaunch; + final String versionMinor; + final String versionMajor; + List? executionArgs = ['']; + final Uri downloadUrl; + final bool runAsDaemon; + final int? internalPort; + final int? externalPort; + final bool installedByDistribution; + final String? pidFilePath; + Process? process; + String? processStartPath; + + SystemProcess( + {required this.key, + required this.displayName, + required this.bundleAssetKey, + required this.windowsInstallationPath, + required this.linuxInstallationPath, + required this.macOsInstallationPath, + required this.useInstallationPathAsWorkingDirectory, + required this.windowsExecutableName, + required this.linuxExecutableName, + required this.macOsExecutableName, + required this.startOnLaunch, + required this.versionMinor, + required this.versionMajor, + required this.executionArgs, + required this.downloadUrl, + required this.runAsDaemon, + required this.internalPort, + required this.externalPort, + required this.installedByDistribution, + required this.pidFilePath}); + + Future getApplicationDirectory() async { + Directory appDir = await getApplicationSupportDirectory(); + if (Platform.isWindows) { + appDir = Directory('${appDir.path}\\$key'); + } else if (Platform.isMacOS) { + appDir = Directory('${appDir.path}/$key'); + } else if (Platform.isLinux) { + appDir = Directory('${appDir.path}/$key'); + } + + if (!await appDir.exists()) { + await appDir.create(recursive: true); + } + return appDir; + } + + String? get executableName { + if (Platform.isWindows) { + return windowsExecutableName; + } else if (Platform.isLinux) { + return linuxExecutableName; + } else if (Platform.isMacOS) { + return macOsExecutableName; + } else { + throw Exception("Operating system not supported for this service."); + } + } + + String? get installationPath { + if (Platform.isWindows) { + return windowsInstallationPath; + } else if (Platform.isLinux) { + return linuxInstallationPath; + } else if (Platform.isMacOS) { + return macOsInstallationPath; + } else { + throw Exception("Operating system not supported for this service."); + } + } + + Future isInstalled() async { + print("Is installed was called"); + final directory = await getApplicationDirectory(); + final defaultSystemProcessDirectory = path.join(directory.path, key); + + String executablePath; + if (Platform.isWindows) { + executablePath = windowsInstallationPath != null && + windowsInstallationPath!.isNotEmpty + ? path.join(windowsInstallationPath!, windowsExecutableName!) + : path.join(defaultSystemProcessDirectory, windowsExecutableName!); + } else if (Platform.isLinux) { + executablePath = + linuxInstallationPath != null && linuxInstallationPath!.isNotEmpty + ? path.join(linuxInstallationPath!, linuxExecutableName!) + : path.join(defaultSystemProcessDirectory, linuxExecutableName!); + } else if (Platform.isMacOS) { + executablePath = + macOsInstallationPath != null && macOsInstallationPath!.isNotEmpty + ? path.join(macOsInstallationPath!, macOsExecutableName!) + : path.join(defaultSystemProcessDirectory, macOsExecutableName!); + } else { + throw Exception("Operating system not supported for this service."); + } + print("Path to system process $executablePath"); + return File(executablePath).existsSync(); + } + + Future install() async { + if (installedByDistribution) { + throw Exception( + "Installation is not supported where installed by distribution, you may try to upgrade."); + } + + if (downloadUrl.toString().isNotEmpty || + (bundleAssetKey != null && bundleAssetKey!.isNotEmpty)) { + String? specifiedInstallPath; + if (Platform.isWindows) { + if (windowsInstallationPath != null && + windowsInstallationPath!.isNotEmpty) { + specifiedInstallPath = windowsInstallationPath; + } + } else if (Platform.isMacOS) { + if (macOsInstallationPath != null && + macOsInstallationPath!.isNotEmpty) { + specifiedInstallPath = macOsInstallationPath; + } + } else if (Platform.isLinux) { + if (linuxInstallationPath != null && + linuxInstallationPath!.isNotEmpty) { + specifiedInstallPath = linuxInstallationPath; + } + } + + final Directory installPath; + if (specifiedInstallPath == null || specifiedInstallPath.isEmpty) { + final directory = await getApplicationDirectory(); + final defaultInstallPath = path.join(directory.path, key); + installPath = Directory(defaultInstallPath); + } else { + installPath = Directory(specifiedInstallPath); + } + + final filePath = path.join(installPath.path, executableName!); + + // Check if the main executable already exists + final file = File(filePath); + if (file.existsSync()) { + print('$displayName is already installed.'); + print('Main executable was found at $filePath'); + throw Exception("Already installed"); + } else { + // Either download or extract from rootBundle + if (downloadUrl.isScheme('HTTP') || downloadUrl.isScheme('HTTPS')) { + // Handle file download (you need to implement this part) + await _downloadAndExtractFile(downloadUrl, installPath); + } else if (bundleAssetKey != null && bundleAssetKey!.isNotEmpty) { + await _extractAssetBundle(bundleAssetKey!, installPath); + } + } + } else { + throw Exception("No source or remote file defined for install"); + } + + return true; + } + + Future _downloadAndExtractFile( + Uri downloadUrl, Directory installPath) async { + // Implement the download and extraction logic + } + + Future _extractAssetBundle( + String assetKey, Directory installPath) async { + ByteData byteData; + try { + byteData = await rootBundle.load(assetKey); + } catch (e) { + throw Exception( + "Invalid bundle asset key supplied, the file could not be found. $e"); + } + + String? archiveType; + if (assetKey.endsWith('.zip')) { + archiveType = 'ZIP'; + } else if (assetKey.endsWith('.7z')) { + throw Exception("The installer cannot support .7z files currently"); + } else if (assetKey.endsWith('.tar.gz')) { + archiveType = 'TARGZ'; + } + + if (archiveType != null) { + await _extractArchive(byteData, archiveType, installPath); + } else { + await _writeSingleFile(byteData, installPath, path.basename(assetKey)); + } + } + + Future _extractArchive( + ByteData byteData, String archiveType, Directory installPath) async { + if (archiveType == 'ZIP') { + final archive = ZipDecoder().decodeBytes(byteData.buffer + .asUint8List(byteData.offsetInBytes, byteData.lengthInBytes)); + extractArchiveToDisk(archive, installPath.absolute.path); + } else if (archiveType == 'TARGZ') { + final tarBytes = GZipDecoder().decodeBytes(byteData.buffer + .asUint8List(byteData.offsetInBytes, byteData.lengthInBytes)); + final archive = TarDecoder().decodeBytes(tarBytes); + extractArchiveToDisk(archive, installPath.absolute.path); + } else { + throw Exception("Unsupported archive type: $archiveType"); + } + } + + Future _writeSingleFile( + ByteData byteData, Directory installPath, String fileName) async { + final file = File(path.join(installPath.path, fileName)); + try { + await file.writeAsBytes(byteData.buffer + .asUint8List(byteData.offsetInBytes, byteData.lengthInBytes)); + print("Successfully installed ${file.absolute.path}"); + } catch (e) { + print( + "Failed to write a single binary file as bytes to the installation path ${file.absolute.path}"); + rethrow; + } + } + + Future update() async { + // #TODO + } + + Future start() async { + print("$displayName service is starting..."); + final defaultApplicationDirectory = await getApplicationDirectory(); + final defaultSystemProcessDirectory = defaultApplicationDirectory.path; + process = null; + File pidFile; + + if (Platform.isWindows) { + if (windowsInstallationPath != null && + windowsInstallationPath!.isNotEmpty && + windowsExecutableName != null) { + processStartPath = + path.join(windowsInstallationPath!, windowsExecutableName!); + if (useInstallationPathAsWorkingDirectory) { + process = await Process.start(processStartPath!, executionArgs!, + mode: ProcessStartMode.detachedWithStdio, + workingDirectory: windowsInstallationPath); + } else { + process = await Process.start(processStartPath!, executionArgs!, + mode: ProcessStartMode.detachedWithStdio); + } + pidFile = File(path.join(windowsInstallationPath!, "$key.pid")); + await pidFile.writeAsString(process!.pid.toString()); + } else { + processStartPath = + path.join(defaultSystemProcessDirectory, windowsExecutableName!); + if (useInstallationPathAsWorkingDirectory) { + process = await Process.start(processStartPath!, executionArgs!, + mode: ProcessStartMode.detachedWithStdio, + workingDirectory: defaultSystemProcessDirectory); + } else { + process = await Process.start(processStartPath!, executionArgs!, + mode: ProcessStartMode.detachedWithStdio); + } + pidFile = File(path.join(defaultSystemProcessDirectory, "$key.pid")); + await pidFile.writeAsString(process!.pid.toString()); + } + } else if (Platform.isLinux) { + if (linuxInstallationPath != null && + linuxInstallationPath!.isNotEmpty && + linuxExecutableName != null) { + processStartPath = + path.join(linuxInstallationPath!, linuxExecutableName!); + if (useInstallationPathAsWorkingDirectory) { + process = await Process.start(processStartPath!, executionArgs!, + mode: ProcessStartMode.detachedWithStdio, + workingDirectory: linuxInstallationPath); + } else { + process = await Process.start(processStartPath!, executionArgs!, + mode: ProcessStartMode.detachedWithStdio); + } + pidFile = File(path.join(linuxInstallationPath!, "$key.pid")); + await pidFile.writeAsString(process!.pid.toString()); + } else { + processStartPath = + path.join(defaultSystemProcessDirectory, linuxExecutableName!); + if (useInstallationPathAsWorkingDirectory) { + process = await Process.start(processStartPath!, executionArgs!, + mode: ProcessStartMode.detachedWithStdio, + workingDirectory: defaultSystemProcessDirectory); + } else { + process = await Process.start(processStartPath!, executionArgs!, + mode: ProcessStartMode.detachedWithStdio); + } + pidFile = File(path.join(defaultSystemProcessDirectory, "$key.pid")); + await pidFile.writeAsString(process!.pid.toString()); + } + } else if (Platform.isMacOS) { + if (macOsInstallationPath != null && + macOsInstallationPath!.isNotEmpty && + macOsExecutableName != null) { + processStartPath = + path.join(macOsInstallationPath!, macOsExecutableName!); + if (useInstallationPathAsWorkingDirectory) { + process = await Process.start(processStartPath!, executionArgs!, + mode: ProcessStartMode.detachedWithStdio, + workingDirectory: macOsInstallationPath); + } else { + process = await Process.start(processStartPath!, executionArgs!, + mode: ProcessStartMode.detachedWithStdio); + } + pidFile = File(path.join(macOsInstallationPath!, "$key.pid")); + await pidFile.writeAsString(process!.pid.toString()); + } else { + processStartPath = + path.join(defaultSystemProcessDirectory, macOsExecutableName!); + if (useInstallationPathAsWorkingDirectory) { + process = await Process.start(processStartPath!, executionArgs!, + mode: ProcessStartMode.normal, + workingDirectory: defaultSystemProcessDirectory); + } else { + process = await Process.start(processStartPath!, executionArgs!, + mode: ProcessStartMode.normal); + } + pidFile = File(path.join(defaultSystemProcessDirectory, "$key.pid")); + await pidFile.writeAsString(process!.pid.toString()); + } + } else { + throw Exception( + "Operating system not supported for running this service, desktops only."); + } + if (process != null) { + print( + "The start script for $displayName was created with PID ${process?.pid.toString()} using command '$processStartPath ${executionArgs!.join(' ').trimRight()}'"); + return process; + } else { + print("The process for $displayName was not created"); + return null; + } + } +} diff --git a/lib/models/system_processes/haveno_daemon_system_process.dart b/lib/models/system_processes/haveno_daemon_system_process.dart new file mode 100644 index 0000000..1e212bb --- /dev/null +++ b/lib/models/system_processes/haveno_daemon_system_process.dart @@ -0,0 +1,181 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import 'dart:io'; +import 'package:haveno_app/utils/nssm_manager.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:path/path.dart' as path; + +import 'base_system_process.dart'; + +class HavenoDaemonSystemProcess extends SystemProcess { + final String? password; + + HavenoDaemonSystemProcess({this.password}) + : super( + key: 'Haveno Daemon', + displayName: 'Haveno Daemon', + bundleAssetKey: '', // Specify the correct asset path here + windowsInstallationPath: + '', // Define specific installation paths if required + linuxInstallationPath: + '', // Define specific installation paths if required + macOsInstallationPath: + '', // Define specific installation paths if required + useInstallationPathAsWorkingDirectory: true, + windowsExecutableName: 'daemon-all.jar', + linuxExecutableName: 'daemon-all.jar', + macOsExecutableName: 'daemon-all.jar', + startOnLaunch: true, + versionMinor: '0.17.1.9', + versionMajor: '0.17', + executionArgs: [ + '--baseCurrencyNetwork=XMR_STAGENET', + '--useLocalhostForP2P=false', + '--useDevPrivilegeKeys=false', + '--nodePort=9999', + '--apiPort=3201', + '--appName=haveno_app_stagenet', + '--useNativeXmrWallet=false', + '--torControlHost=127.0.0.1', + '--torControlPort=9077', + '--torControlPassword=boner', + '--appDataDir=data/' + ], + downloadUrl: Uri.parse(''), // Specify the download URL if required + runAsDaemon: true, + internalPort: 9050, + externalPort: null, + installedByDistribution: false, + pidFilePath: null) { + if (password != null && password!.isNotEmpty) { + executionArgs!.add('--apiPassword=$password'); + //executionArgs!.add('--passwordRequired=true'); + } else { + //executionArgs!.add('--passwordRequired=false'); + } + } + + @override + Future start() async { + print("$displayName service is starting..."); + final defaultApplicationDirectory = await getApplicationDirectory(); + final defaultSystemProcessDirectory = defaultApplicationDirectory.path; + process = null; + File? javaBinaryFile = await getJavaBinaryDirectory(); + File? havenoDaemonJarFile = await getHavenoDaemonJarFile(); + + if (javaBinaryFile == null) { + print("The java binary file was not found, cannot start the daemon"); + return null; + } + if (havenoDaemonJarFile == null) { + print( + "The haveno daemon file was not found, cannot start the daemon."); + return null; + } + + // Get the paths + String javaPath = javaBinaryFile.path; + String jarPath = havenoDaemonJarFile.path; + + Map environment = { + 'JAVA_HOME': path.dirname(path.dirname(javaPath)), + 'PATH': Platform.environment['PATH'] ?? '', + }; + + if (Platform.isWindows) { + Directory appSupportDirectory = await getApplicationSupportDirectory(); + + String nssmPath = path.join(appSupportDirectory.path, 'nssm.exe'); + + print("We are trying to load NSSM from: $nssmPath"); + + var nssmManager = NSSMServiceManager(nssmPath); + try { + await nssmManager.setServiceParameters('HavenoPlusDaemonService', + ['-jar', '"$jarPath"', ...executionArgs!]); + print("Set service parameters for HavenoPlusDaemonService"); + await Future.delayed(Duration(seconds: 2)); + await nssmManager.serviceStop('HavenoPlusDaemonService'); + // Wait 5 seconds + await Future.delayed(Duration(seconds: 2)); // Added wait period + await nssmManager.serviceStart('HavenoPlusDaemonService'); + print("Restarted HavenoPlusDaemonService"); + } catch (e) { + print("Error setting service parameters or restarting: $e"); + } + return null; + } + + if (Platform.isLinux) { + process = await Process.start( + javaPath, + ['-jar', jarPath, ...executionArgs!], + mode: ProcessStartMode.normal, + environment: environment, + ); + } else if (Platform.isMacOS) { + process = await Process.start( + javaPath, + ['-jar', jarPath, ...executionArgs!], + mode: ProcessStartMode.normal, + workingDirectory: defaultSystemProcessDirectory, + environment: environment, + ); + } + return process; + } + + Future stop() async { + if (Platform.isWindows) { + Directory appSupportDirectory = await getApplicationSupportDirectory(); + String nssmPath = path.join(appSupportDirectory.path, 'nssm.exe'); + await NSSMServiceManager(nssmPath).serviceStop('HavenoPlusDaemonService'); + } + } + + Future getJavaBinaryDirectory() async { + Directory appSupportDirectory = await getApplicationSupportDirectory(); + File javaBinaryFile; + + if (Platform.isWindows) { + javaBinaryFile = File(path.join( + appSupportDirectory.path, 'Java', 'jdk-21.0.4', 'bin', 'java.exe')); + } else if (Platform.isMacOS) { + javaBinaryFile = File(path.join(appSupportDirectory.path, 'Java', + '21.0.4+7', 'Contents', 'Home', 'bin', 'java')); + } else if (Platform.isLinux) { + return null; + } else { + return null; + } + + return await javaBinaryFile.exists() ? javaBinaryFile : null; + } + + Future getHavenoDaemonJarFile() async { + Directory havenoHomeDirectory = await getApplicationDirectory(); + String havenoJarFile = path.join(havenoHomeDirectory.path, executableName); + File file = File(havenoJarFile); + return await file.exists() ? file : null; + } +} diff --git a/lib/models/system_processes/java_system_process.dart b/lib/models/system_processes/java_system_process.dart new file mode 100644 index 0000000..4fc0677 --- /dev/null +++ b/lib/models/system_processes/java_system_process.dart @@ -0,0 +1,48 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import 'base_system_process.dart'; + +class JavaSystemProcess extends SystemProcess { + JavaSystemProcess() + : super( + key: 'Java', + displayName: 'Java', + bundleAssetKey: null, + windowsInstallationPath: '', + linuxInstallationPath: '', + macOsInstallationPath: '', + useInstallationPathAsWorkingDirectory: false, + windowsExecutableName: 'java.exe', + linuxExecutableName: 'java', + macOsExecutableName: 'java', + startOnLaunch: false, + versionMinor: '21.0.4+7', + versionMajor: '21', + executionArgs: [''], + downloadUrl: Uri.parse(''), // Add the appropriate download URL if needed + runAsDaemon: false, + internalPort: null, + externalPort: null, + installedByDistribution: true, + pidFilePath: null + ); +} \ No newline at end of file diff --git a/lib/models/system_processes/tor_system_process.dart b/lib/models/system_processes/tor_system_process.dart new file mode 100644 index 0000000..92cd79a --- /dev/null +++ b/lib/models/system_processes/tor_system_process.dart @@ -0,0 +1,48 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import 'base_system_process.dart'; + +class TorSystemProcess extends SystemProcess { + TorSystemProcess() + : super( + key: 'Tor', + displayName: 'Tor Daemon', + bundleAssetKey: '', // Specify the correct asset path here + windowsInstallationPath: '', // Define specific installation paths if required + linuxInstallationPath: '', // Define specific installation paths if required + macOsInstallationPath: '', // Define specific installation paths if required + useInstallationPathAsWorkingDirectory: true, // Assuming the working directory should be the installation path + windowsExecutableName: 'tor.exe', + linuxExecutableName: 'tor', + macOsExecutableName: 'tor', + startOnLaunch: true, + versionMinor: '0.17.1.9', + versionMajor: '0.17', + executionArgs: ['-f', 'torrc'], + downloadUrl: Uri.parse(''), // Specify the download URL if required + runAsDaemon: true, + internalPort: 9050, + externalPort: null, + installedByDistribution: true, + pidFilePath: 'tor.pid' + ); +} \ No newline at end of file diff --git a/lib/models/tor/hsv3_onion_config.dart b/lib/models/tor/hsv3_onion_config.dart new file mode 100644 index 0000000..758ebfc --- /dev/null +++ b/lib/models/tor/hsv3_onion_config.dart @@ -0,0 +1,54 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import 'package:cryptography/cryptography.dart'; + +class HSV3OnionConfig { + final List privateKeyBytes; + final SimplePublicKey publicKey; + final int internalPort; + final int externalPort; + + HSV3OnionConfig( + {required this.privateKeyBytes, + required this.publicKey, + required this.internalPort, + required this.externalPort}); + + get privateKey => null; + + Map toJson() { + return { + 'privateKeyBytes': privateKeyBytes, + 'publicKey': publicKey, + 'internalPort': internalPort, + 'externalPort': externalPort + }; + } + + factory HSV3OnionConfig.fromJson(Map json) { + return HSV3OnionConfig( + privateKeyBytes: json['privateKeyBytes'], + publicKey: json['publicKey'], + internalPort: json['internalPort'], + externalPort: json['externalPortal']); + } +} diff --git a/lib/models/tor/tor_daemon_config.dart b/lib/models/tor/tor_daemon_config.dart new file mode 100644 index 0000000..080469d --- /dev/null +++ b/lib/models/tor/tor_daemon_config.dart @@ -0,0 +1,75 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +class TorDaemonConfig { + final String host; + final int controlPort; + final List socks5ProxyPorts; + final List httpProxyPorts; + final String? hashedPassword; + + TorDaemonConfig({ + required this.host, + required this.controlPort, + this.socks5ProxyPorts = const [9050], + this.httpProxyPorts = const [8118], + this.hashedPassword, + }) { + _validateOnionAddress(host); + } + + static void _validateOnionAddress(String host) { + print(host); + final pattern = RegExp(r'^[a-zA-Z0-9]{56}\.onion$'); + if (!pattern.hasMatch(host)) { + throw const FormatException('Invalid v3 onion address'); + } + } + + Map toJson() { + return { + 'host': host, + 'controlPort': controlPort, + 'socks5ProxyPorts': socks5ProxyPorts, + 'httpProxyPorts': httpProxyPorts, + 'hashedPassword': hashedPassword, + }; + } + + factory TorDaemonConfig.fromJson(Map json) { + return TorDaemonConfig( + host: json['host'], + controlPort: json['controlPort'], + socks5ProxyPorts: List.from(json['socks5ProxyPorts'] ?? [9050]), + httpProxyPorts: List.from(json['httpProxyPorts'] ?? [8118]), + hashedPassword: json['hashedPassword'], + ); + } + + factory TorDaemonConfig.getDefault() { + return TorDaemonConfig( + host: '127.0.0.1', + controlPort: 9051, + socks5ProxyPorts: [9050], + httpProxyPorts: [8118], + ); + } +} \ No newline at end of file diff --git a/lib/providers/haveno_client_providers/account_provider.dart b/lib/providers/haveno_client_providers/account_provider.dart new file mode 100644 index 0000000..aeee188 --- /dev/null +++ b/lib/providers/haveno_client_providers/account_provider.dart @@ -0,0 +1,78 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import 'package:flutter/material.dart'; +import 'package:haveno/haveno_client.dart'; +import 'package:haveno/haveno_service.dart'; + +class AccountProvider with ChangeNotifier { + final HavenoChannel _havenoChannel = HavenoChannel(); + + bool? _accountExists; + DateTime? _lastCreatedAccount; + + AccountProvider(); + + bool? get accountExists => _accountExists; + DateTime? get lastCreatedAccount => _lastCreatedAccount; + + Future requestAccountExists() async { + await _havenoChannel.onConnected; + try { + _accountExists = await AccountService().accountExists(); + _lastCreatedAccount = DateTime.now(); + notifyListeners(); + } catch (e) { + print("Failed to check if account exists: $e"); + } + } + + Future sendCreateAccount(password) async { + await _havenoChannel.onConnected; + try { + await AccountService().createAccount(password); + } catch (e) { + print("Error while creating account: $e"); + rethrow; + } + } +} + +class PasswordProvider with ChangeNotifier { + final HavenoChannel _havenoChannel; + DateTime? _lastPasswordChange = DateTime.now(); + + PasswordProvider(this._havenoChannel); + + DateTime? get lastPasswordChange => _lastPasswordChange; + + Future sendChangePassword(oldPassword, newPassword) async { + await _havenoChannel.onConnected; + try { + await AccountService().changePassword(oldPassword, newPassword); + _lastPasswordChange = DateTime.now(); + notifyListeners(); + } catch (e) { + print("Error changing account password: $e"); + rethrow; + } + } +} diff --git a/lib/providers/haveno_client_providers/dispute_agents_provider.dart b/lib/providers/haveno_client_providers/dispute_agents_provider.dart new file mode 100644 index 0000000..3f0396a --- /dev/null +++ b/lib/providers/haveno_client_providers/dispute_agents_provider.dart @@ -0,0 +1,60 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:haveno/haveno_client.dart'; +import 'package:haveno/haveno_service.dart'; + +class DisputeAgentsProvider with ChangeNotifier { + final HavenoChannel _havenoChannel = HavenoChannel(); + + DisputeAgentsProvider(); + + Future registerDisputeAgent(String disputeAgentType, String registrationKey) async { + try { + await _havenoChannel.onConnected; + + await DisputeAgentService().registerDisputeAgent( + disputeAgentType, + registrationKey + ); + + } catch (e) { + print("Failed to register dispute agent: $e"); + } + } + + Future unregisterDisputeAgent(String disputeAgentType) async { + try { + await _havenoChannel.onConnected; + + await DisputeAgentService().unregisterDisputeAgent( + disputeAgentType + ); + + } catch (e) { + print("Failed to unregister dispute agent: $e"); + } + } + +} \ No newline at end of file diff --git a/lib/providers/haveno_client_providers/disputes_provider.dart b/lib/providers/haveno_client_providers/disputes_provider.dart new file mode 100644 index 0000000..001f1e0 --- /dev/null +++ b/lib/providers/haveno_client_providers/disputes_provider.dart @@ -0,0 +1,358 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import 'dart:async'; +import 'package:fixnum/fixnum.dart'; +import 'package:flutter/widgets.dart'; +import 'package:haveno/haveno_client.dart'; +import 'package:haveno/haveno_service.dart'; +import 'package:haveno/profobuf_models.dart'; + +class DisputesProvider with ChangeNotifier { + final HavenoChannel _havenoChannel = HavenoChannel(); + List _disputes = []; + + // Map to hold StreamControllers for each chat + final Map>> _chatControllers = {}; + + // Map to store unique chat messages for each dispute + final Map> _chatMessages = {}; + + // Map to store tradeId to disputeId mapping + final Map _disputeToTradeIdMap = {}; + + // TradeID to dispute map + final Map _tradeIdToDisputeMap = {}; + + DisputesProvider(); //: super(const Duration(minutes: 1)); + + @override + void dispose() { + // Close all StreamControllers when the provider is disposed + _chatControllers.forEach((_, controller) => controller.close()); + super.dispose(); + } + + // Method to get or create a StreamController for a specific chat + Stream> chatMessagesStream(String disputeId) { + if (!_chatControllers.containsKey(disputeId)) { + _chatControllers[disputeId] = StreamController>.broadcast(); + + // Start polling for new messages (if needed) + _startPolling(disputeId); + } + return _chatControllers[disputeId]!.stream; + } + + // Load initial messages independently of stream creation + Future loadInitialMessages(String disputeId) async { + // Retrieve the tradeId from the mapping + final tradeId = _disputeToTradeIdMap[disputeId]; + if (tradeId == null) return; + + // Retrieve the dispute from the _disputes list using the disputeId + Dispute? dispute = _disputes.firstWhere((d) => d!.id == disputeId, orElse: () => null); + + if (dispute != null) { + print("Setting chat messages for dispute: $disputeId with ${dispute.chatMessage.length} messages"); + + // Store the chat messages in _chatMessages for this disputeId + _chatMessages[disputeId] = dispute.chatMessage; + + // If a stream controller exists for this disputeId, add the messages to the stream + if (_chatControllers.containsKey(disputeId)) { + _chatControllers[disputeId]!.add(_chatMessages[disputeId]!); + } + } else { + // Handle case where no dispute is found + print("No dispute found for ID: $disputeId"); + _chatMessages[disputeId] = []; + } + } + + List getInitialChatMessages(String disputeId) { + print("Getting initial chat messages for dispute: $disputeId"); + return _chatMessages[disputeId] ?? []; + } + +/// Probsblt just redo the polling when not tired + void _startPolling(String disputeId) { + Timer.periodic(const Duration(minutes: 2), (timer) async { + print("POLLIN FROM DISPUTES PROVIER"); + // Get the tradeId from the mapping using disputeId + final tradeId = _disputeToTradeIdMap[disputeId]; + if (tradeId == null) { + print("No tradeId found for disputeId: $disputeId"); + return; + } + + Dispute? dispute = await getDispute(tradeId); + if (dispute != null && dispute.id == disputeId) { + for (var message in dispute.chatMessage) { + if (message.senderIsTrader) { + return; + } + if (!_chatMessages[disputeId]!.contains(message)) { + _chatMessages[disputeId]!.add(message); + if (_chatControllers.containsKey(disputeId)) { + _chatControllers[disputeId]!.add(_chatMessages[disputeId]!); + } + } + } + } + }); + } + +void debugPrintTradeIdToDisputeMap() { + if (_tradeIdToDisputeMap.isEmpty) { + print("The _tradeIdToDisputeMap is currently empty."); + } else { + //_tradeIdToDisputeMap.forEach((tradeId, dispute) { + //print('Trade ID: $tradeId, Dispute ID: ${dispute.id}'); + //}); + } +} +Dispute? getDisputeByTradeId(String tradeId) { + print("Attempting to retrieve dispute for Trade ID: $tradeId"); + + if (_tradeIdToDisputeMap.containsKey(tradeId)) { + print("Dispute found for Trade ID: $tradeId"); + debugPrintTradeIdToDisputeMap(); // Print the entire map for debugging + return _tradeIdToDisputeMap[tradeId]; + } else { + print("No dispute found for Trade ID: $tradeId"); + //debugPrintTradeIdToDisputeMap(); // Print the entire map for debugging + return null; + } +} + +Future?> getDisputes() async { + List disputes = []; + // Attempt to retrieve disputes from the service + try { + var disputeClient = DisputeService(); + disputes = await disputeClient.getDisputes(); + } catch (e) { + return null; + } + + // Extract the list of disputes + List disputesList = disputes; + + // Check if the disputes list is empty + if (disputesList.isEmpty) { + print("No disputes found."); + } else { + // Iterate through each dispute and map the tradeId to the dispute + for (var dispute in disputesList) { + _disputeToTradeIdMap[dispute.id] = dispute.tradeId; + _tradeIdToDisputeMap[dispute.tradeId] = dispute; + + // Debugging output to verify the mapping + print("Mapping added: Trade ID ${dispute.tradeId} -> Dispute ID ${dispute.id}"); + print("Current _tradeIdToDisputeMap contents:"); + _tradeIdToDisputeMap.forEach((tradeId, mappedDispute) { + print("Trade ID: $tradeId, Dispute ID: ${mappedDispute.id}"); + }); + } + } + + // Assign disputes to the internal list and notify listeners + _disputes = disputesList; + notifyListeners(); + return null; +} + +Future getDispute(String tradeId) async { + try { + await _havenoChannel.onConnected; + Dispute? dispute; + var disputeClient = DisputeService(); + dispute = await disputeClient.getDispute(tradeId); + + Dispute? newDispute = dispute; + + if (newDispute != null) { + _disputeToTradeIdMap[newDispute.id] = tradeId; + _tradeIdToDisputeMap[newDispute.tradeId] = newDispute; + + // Update the _disputes list or map if necessary + final int existingIndex = _disputes.indexWhere((d) => d!.id == newDispute.id); + + if (existingIndex != -1) { + _disputes[existingIndex] = newDispute; + } else { + _disputes.add(newDispute); + } + + notifyListeners(); + return newDispute; + } + return null; + } catch (e) { + print("Failed to get dispute: $e"); + return null; + } +} + + Future resolveDispute(String tradeId, DisputeResult_Winner? winner, DisputeResult_Reason? reason, String? summaryNotes, Int64? customPayoutAmount) async { + await _havenoChannel.onConnected; + + Dispute? dispute = await getDispute(tradeId); + if (!dispute!.isOpener) { + throw Exception("You can't close a dispute you didn't open!"); + } + + try { + DisputeService().resolveDispute( + tradeId, + winner, + reason, + summaryNotes, + customPayoutAmount, + ); + + getDisputes(); + } catch (e) { + print("Failed to resolve dispute: $e"); + rethrow; + } + } + + Future openDispute(String tradeId) async { + await _havenoChannel.onConnected; + try { + await DisputeService().openDispute(tradeId); + await getDisputes(); + Dispute? dispute = _disputes.firstWhere((d) => d!.tradeId == tradeId, orElse: () => null); + return dispute; + } catch (e) { + print("Failed to open dispute: $e"); + rethrow; + } + } + + Future sendDisputeChatMessage(String disputeId, String message, Iterable attachments) async { + await _havenoChannel.onConnected; + try { + await DisputeService().sendDisputeChatMessage( + disputeId, + message, + attachments, + ); + + // Might need to create a fake protobuf message and add it to the stream controller just to conform (so the message appears instantly) + + } catch (e) { + print("Failed to send dispute chat message: $e"); + rethrow; + } + } + + // Optional: Method to close a specific chat stream + void closeChat(String disputeId) { + if (_chatControllers.containsKey(disputeId)) { + _chatControllers[disputeId]!.close(); + _chatControllers.remove(disputeId); + _chatMessages.remove(disputeId); + _disputeToTradeIdMap.remove(disputeId); + } + } + + void addChatMessage(ChatMessage chatMessage) { + // do something special!!! + } + +// @override +// Future pollAction() async { +// getDisputes(); +// } + +} + + + +class DisputeChatProvider with ChangeNotifier { + final HavenoChannel _havenoChannel; + + // Map to store StreamControllers for each chat + final Map>> _chatControllers = {}; + + // Map to store chat messages + final Map> _chatMessages = {}; + + DisputeChatProvider(this._havenoChannel); + + // Get or create a StreamController for specific chat + Stream> chatMessagesStream(String disputeId) { + if (!_chatControllers.containsKey(disputeId)) { + _chatControllers[disputeId] = StreamController>.broadcast(); + // Load initial messages and start polling if needed + _startPolling(disputeId); + } + return _chatControllers[disputeId]!.stream; + } + + // Load initial messages for the dispute + Future loadInitialMessages(String disputeId, Dispute dispute) async { + _chatMessages[disputeId] = dispute.chatMessage; + if (_chatControllers.containsKey(disputeId)) { + _chatControllers[disputeId]!.add(_chatMessages[disputeId]!); + } + } + + // Add a new message + void addChatMessage(String disputeId, ChatMessage message) { + if (!_chatMessages.containsKey(disputeId)) { + _chatMessages[disputeId] = []; + } + _chatMessages[disputeId]!.add(message); + _chatControllers[disputeId]?.add(_chatMessages[disputeId]!); + } + + Future sendDisputeChatMessage(String disputeId, String message, Iterable attachments) async { + await _havenoChannel.onConnected; + try { + await DisputeService().sendDisputeChatMessage( + disputeId, + message, + attachments, + ); + } catch (e) { + print("Failed to send dispute chat message: $e"); + rethrow; + } + } + + void _startPolling(String disputeId) { + Timer.periodic(const Duration(minutes: 2), (timer) async { + // Implement polling logic to fetch new messages periodically + print("Polling for new messages for dispute: $disputeId"); + // Fetch dispute or chat messages and update the stream + }); + } + + @override + void dispose() { + _chatControllers.forEach((_, controller) => controller.close()); + super.dispose(); + } +} \ No newline at end of file diff --git a/lib/providers/haveno_client_providers/help_provider.dart b/lib/providers/haveno_client_providers/help_provider.dart new file mode 100644 index 0000000..8d3cc5e --- /dev/null +++ b/lib/providers/haveno_client_providers/help_provider.dart @@ -0,0 +1,43 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'dart:async'; +import 'package:flutter/widgets.dart'; +import 'package:haveno/haveno_client.dart'; +import 'package:haveno/haveno_service.dart'; + +class HelpProvider with ChangeNotifier { + final HavenoChannel _havenoChannel = HavenoChannel(); + + HelpProvider(); //: super(const Duration(minutes: 1)); + + Future getMethodHelp(String methodName) async { + await _havenoChannel.onConnected; + try { + final helpService = HelpService(); + return await helpService.getMethodHelp(methodName); + } catch (e) { + print("Failed get method help: $e"); + return null; + } + } +} diff --git a/lib/providers/haveno_client_providers/notificiations_provider.dart b/lib/providers/haveno_client_providers/notificiations_provider.dart new file mode 100644 index 0000000..af28620 --- /dev/null +++ b/lib/providers/haveno_client_providers/notificiations_provider.dart @@ -0,0 +1,55 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'dart:async'; +import 'package:flutter/widgets.dart'; +import 'package:grpc/grpc.dart'; +import 'package:haveno/grpc_models.dart'; +import 'package:haveno/haveno_service.dart'; + +class NotificationsProvider with ChangeNotifier { + + NotificationsProvider(); + + Future listen() async { + ResponseStream responseStream = NotificationsService() as ResponseStream; + + // Listen to the stream of notifications + responseStream.listen( + (notification) { + // Handle the notification, for example, print it to the debug console + print('Received notification: ${notification.toString()}'); + + // You can also add custom logic to handle different types of notifications here + }, + onError: (error) { + // Handle errors from the stream + print('Error receiving notifications: $error'); + }, + onDone: () { + // Handle the completion of the stream + print('Notification stream closed'); + }, + cancelOnError: true, // Optionally cancel the subscription if an error occurs + ); + } +} diff --git a/lib/providers/haveno_client_providers/offers_provider.dart b/lib/providers/haveno_client_providers/offers_provider.dart new file mode 100644 index 0000000..ebfa5db --- /dev/null +++ b/lib/providers/haveno_client_providers/offers_provider.dart @@ -0,0 +1,225 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'dart:async'; // Import the async library for Timer +import 'package:flutter/material.dart'; +import 'package:grpc/grpc.dart'; +import 'package:haveno/grpc_models.dart'; +import 'package:haveno/haveno_client.dart'; +import 'package:haveno_app/models/schema.dart'; +import 'package:fixnum/fixnum.dart' as fixnum; +import 'package:haveno_app/utils/database_helper.dart'; + +class OffersProvider with ChangeNotifier, CooldownMixin { + final HavenoChannel _havenoChannel; + final DatabaseHelper _databaseHelper = DatabaseHelper.instance; + List _offers = []; + OfferInfo? _lastCreatedOffer; + String? _lastCancelledOfferId; + List _myOffers = []; // Timer to periodically call getOffers + + OffersProvider(this._havenoChannel) { + setCooldownDurations({ + 'getOffers': const Duration(minutes: 1), // 2 minutes cooldown for getOffers + 'getMyOffers': const Duration(minutes: 2), // 2 minutes seconds cooldown for getMyOffers + }); + } + + List? get offers => _offers; + List? get marketBuyOffers => + _offers.where((offer) => offer.direction == 'SELL' && !offer.isMyOffer).toList(); + List? get marketSellOffers => + _offers.where((offer) => offer.direction == 'BUY' && !offer.isMyOffer).toList(); + OfferInfo? get lastCreatedOffer => _lastCreatedOffer; + String? get lastCancelledOffer => _lastCancelledOfferId; + List? get myOffers => _myOffers; + List? get mySellOffers => + _myOffers.where((offer) => offer.direction == 'SELL' && offer.isMyOffer).toList(); // It's reversed because if an offer is your own, then it's not a sell offer to you + List? get myBuyOffers => + _myOffers.where((offer) => offer.direction == 'BUY' && offer.isMyOffer).toList(); // It's reversed because if an offer is your own, then it's not a buy offer to you + + + Future getAllOffers() async { + await _havenoChannel.onConnected; + await getOffers(); + await Future.delayed(const Duration(seconds: 3)); + await getMyOffers(); + } + +Future> getOffers() async { + await _havenoChannel.onConnected; + + // Check if the cooldown has expired, if so, fetch from server + if (!await isCooldownValid('getOffers')) { + try { + // Fetch from the server + final getOffersReply = await _havenoChannel.offersClient!.getOffers(GetOffersRequest()); + final fetchedOffers = getOffersReply.offers; + + if (fetchedOffers.isEmpty) { + await _databaseHelper.deleteOffers(null, isMyOffer: false); + return []; + } + + // Save to local database and update the local cache + _offers = fetchedOffers; + List peerTradeOffers = []; + for (var offer in fetchedOffers) { + if (offer.hasIsMyOffer() && offer.isMyOffer != true) { + offer.isMyOffer = false; + } + peerTradeOffers.add(offer); + } + print("Returning ${fetchedOffers.length} offers from peers found on the daemon"); + + await _databaseHelper.deleteOffers(null, isMyOffer: false); + await _databaseHelper.insertOffers(peerTradeOffers); + + updateCooldown('getOffers'); // Update the cooldown after fetching + notifyListeners(); // Notify listeners if applicable + return _offers; + } catch (e) { + updateCooldown('getOffers'); + print("Failed to fetch peer offers from server: $e"); + } + } + + // If cooldown is active or no offers were fetched, return offers from the database or cache + if (_offers.isEmpty) { + _offers = await _databaseHelper.getOffers(); + print("Returning ${_offers.length} of my own offers from the local database."); + } else { + print("Returning ${_offers.length} of my own offers from cache due to cooldown."); + } + + notifyListeners(); // Notify listeners if applicable + return _offers; +} + + Future> getMyOffers() async { + await _havenoChannel.onConnected; + + // Check if cooldown has expired, if so, fetch from server + if (!await isCooldownValid('getMyOffers')) { + // Fetch from the server + final getMyOffersReply = await _havenoChannel.offersClient!.getMyOffers(GetMyOffersRequest()); + _myOffers = getMyOffersReply.offers; + updateCooldown('getMyOffers'); + + if (_myOffers.isNotEmpty) { + // Save to local database + List myTradeOffers = []; + try { + for (var myOffer in _myOffers) { + myOffer.isMyOffer = true; + myTradeOffers.add(myOffer); + } + print("Returning ${_myOffers.length} of my own offers requested from the daemon."); + + await _databaseHelper.deleteOffers(null, isMyOffer: true); + await _databaseHelper.insertOffers(myTradeOffers); + } catch (e) { + updateCooldown('getMyOffers'); + print("Failed to save one or more of my own offers from the daemon locally to DB: ${e.toString()}"); + } + } else { + print("None of my own offers found on daemon..."); + } + } else { + print("Returning ${_myOffers.length} my own offers from the database or cache due to cooldown"); + } + + // Return the offers, whether fetched or from the cache + return _myOffers; + } + + Future postOffer({ + required String currencyCode, + required String direction, + required String price, + required bool useMarketBasedPrice, + double? marketPriceMarginPct, + required fixnum.Int64 amount, + required fixnum.Int64 minAmount, + required double buyerSecurityDepositPct, + String? triggerPrice, + required bool reserveExactAmount, + required String paymentAccountId, + }) async { + try { + final postOfferResponse = await _havenoChannel.offersClient!.postOffer( + PostOfferRequest( + currencyCode: currencyCode, + direction: direction, + price: price, + useMarketBasedPrice: useMarketBasedPrice, + marketPriceMarginPct: marketPriceMarginPct, + amount: amount, + minAmount: minAmount, + buyerSecurityDepositPct: buyerSecurityDepositPct, + triggerPrice: triggerPrice, + reserveExactAmount: reserveExactAmount, + paymentAccountId: paymentAccountId, + ), + ); + final postedOffer = postOfferResponse.offer; + postedOffer.isMyOffer = true; + debugPrint(postedOffer.state); + debugPrint("IsActive = postedOffer.isActivated"); + _lastCreatedOffer = postedOffer; + _myOffers.add(postedOffer); + await _databaseHelper.insertOffer(postedOffer); + notifyListeners(); + } on GrpcError catch (e) { + print("Failed to post offer: $e"); + rethrow; + } + } + + Future cancelOffer(String offerId) async { + try { + await _havenoChannel.onConnected; + + await _havenoChannel.offersClient + !.cancelOffer(CancelOfferRequest(id: offerId)); + _lastCancelledOfferId = offerId; + _myOffers.removeWhere((offer) => offer.id == offerId); + notifyListeners(); + } catch (e) { + print("Failed to cancel offer: $e"); + rethrow; + } + } + + Future editOffer({required String offerId, double? marketPriceMarginPct, String? triggerPrice}) async { + await _havenoChannel.onConnected; + try { + //not implemented at daemon + } catch(e) { + //not implemented at daemon + } + } + + String offerToString(OfferInfo offer) { + return 'Offer(id: ${offer.id}, direction: ${offer.direction}, price: ${offer.price}, amount: ${offer.amount}, minAmount: ${offer.minAmount}, volume: ${offer.volume}, minVolume: ${offer.minVolume}, baseCurrencyCode: ${offer.baseCurrencyCode}, date: ${offer.date}, state: ${offer.state}, paymentAccountId: ${offer.paymentAccountId}, paymentMethodId: ${offer.paymentMethodId})'; + } +} diff --git a/lib/providers/haveno_client_providers/payment_accounts_provider.dart b/lib/providers/haveno_client_providers/payment_accounts_provider.dart new file mode 100644 index 0000000..9625a9c --- /dev/null +++ b/lib/providers/haveno_client_providers/payment_accounts_provider.dart @@ -0,0 +1,222 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'package:flutter/material.dart'; +import 'package:haveno/grpc_models.dart'; +import 'package:haveno/haveno_client.dart'; +import 'package:haveno/profobuf_models.dart'; +import 'package:haveno_app/models/schema.dart'; +import 'package:haveno_app/utils/database_helper.dart'; + +class PaymentAccountsProvider with ChangeNotifier, CooldownMixin { + final HavenoChannel _havenoChannel; + final DatabaseHelper _databaseHelper = DatabaseHelper.instance; + List _paymentMethods = []; + List _cryptoCurrencyPaymentMethods = []; + List _paymentAccounts = []; + final List _paymentAccountForms = []; + final Map _paymentMethodIdToPaymentAccountFormIdMap = {}; + final Map _paymentAccountFormIdToPaymentAccountFormMap = {}; + PaymentAccountsProvider(this._havenoChannel) { + setCooldownDurations({ + 'getPaymentAccounts': const Duration(minutes: 1), // 2 minutes cooldown for getOffers + }); + } + + List get paymentMethods => _paymentMethods; + List get cryptoCurrencyPaymentMethods => _cryptoCurrencyPaymentMethods; + List get paymentAccounts => _paymentAccounts; + + List? get paymentAccountForms => _paymentAccountForms.isNotEmpty ? _paymentAccountForms : null; + + Future?> getPaymentMethods() async { + await _havenoChannel.onConnected; + + try { + // Attempt to load payment methods from the local database + _paymentMethods = await _databaseHelper.getAllPaymentMethods(); + + if (_paymentMethods.isNotEmpty) { + return _paymentMethods; + } + } catch (e) { + print("Failed to load payment methods from the local database: $e"); + } + + // If the local database does not have the data, fetch it from the remote service + try { + final getPaymentMethodsReply = await _havenoChannel.paymentAccountsClient! + .getPaymentMethods(GetPaymentMethodsRequest()); + _paymentMethods = getPaymentMethodsReply.paymentMethods; + + // Optionally, save the retrieved payment methods to the local database + if (_paymentMethods.isNotEmpty) { + for (var paymentMethod in _paymentMethods) { + await _databaseHelper.insertPaymentMethod(paymentMethod); + } + } + } catch (e) { + print("Failed to get payment methods from the remote service: $e"); + rethrow; + } + + return _paymentMethods; + } + + Future> getPaymentAccounts() async { + await _havenoChannel.onConnected; + if (await isCooldownValid('getPaymentAccounts')) { + try { + // Attempt to load payment accounts from the local database + _paymentAccounts = await _databaseHelper.getAllPaymentAccounts(); + if (_paymentAccounts.isNotEmpty) { + return _paymentAccounts; + } + } catch (e) { + print("Failed to load payment accounts from the local database: $e"); + } + } else { + + try { + final getPaymentAccountsReply = await _havenoChannel + .paymentAccountsClient! + .getPaymentAccounts(GetPaymentAccountsRequest()); + _paymentAccounts = getPaymentAccountsReply.paymentAccounts; + print("Found ${_paymentAccounts.length} payment accounts on the daemon, will try caching them..."); + updateCooldown('getPaymentAccounts'); + } catch (e) { + updateCooldown('getPaymentAccounts'); + print("Failed to get payment accounts from daemon: $e"); + rethrow; + } + + try { + for (var paymentAccount in _paymentAccounts) { + await _databaseHelper.insertPaymentAccount(paymentAccount); + } + print("Successfully synced ${_paymentAccounts.length} payment accounts from the daemon to local storage"); + } catch (e) { + print("Could not add the payment accounts from the daemon to the local database: ${e.toString()}"); + } + return _paymentAccounts; + } + return []; + } + + + Future?> getCryptoCurrencyPaymentMethods() async { + await _havenoChannel.onConnected; + try { + final getCryptoCurrencyPaymentMethodsReply = await _havenoChannel + .paymentAccountsClient! + .getCryptoCurrencyPaymentMethods( + GetCryptoCurrencyPaymentMethodsRequest()); + _cryptoCurrencyPaymentMethods = getCryptoCurrencyPaymentMethodsReply.paymentMethods; + } catch (e) { + print("Failed to get payment accounts: $e"); + } + return paymentMethods; + } + + Future getPaymentAccountForm(String paymentMethodId) async { + await _havenoChannel.onConnected; + PaymentAccountForm? paymentAccountForm; + bool didLoadFromDb = false; + + try { + // Attempt to load payment account form from the local database + paymentAccountForm = await _databaseHelper.getPaymentAccountFormByPaymentMethodId(paymentMethodId); + if (paymentAccountForm != null) { + didLoadFromDb = true; + print("Loaded payment account form from local database for paymentMethod ID: $paymentMethodId."); + return paymentAccountForm; + } + } catch (e) { + print("Failed to load payment account form from the local database: $e"); + } + + if (!didLoadFromDb) { + try { + // Fetch from the remote service if not found locally + final paymentAccountFormReply = await _havenoChannel.paymentAccountsClient!.getPaymentAccountForm( + GetPaymentAccountFormRequest(paymentMethodId: paymentMethodId), + ); + + if (paymentAccountFormReply.hasPaymentAccountForm()) { + paymentAccountForm = paymentAccountFormReply.paymentAccountForm; + print("Loaded payment account form from remote service for paymentMethod ID: $paymentMethodId."); + + // Store the fetched form in the local database for future use + await _databaseHelper.insertPaymentAccountForm(paymentMethodId, paymentAccountForm); + + // Add a delay to respect network cooldown limitations, but this needs improving #TODO CooldownManager, or + // EVEN BETTER, remove cooldowns from the daemon completely, I'mm a + await Future.delayed(const Duration(seconds: 4)); + } else { + return null; + } + } catch (e) { + print("Failed to get the payment form from remote service: $e"); + rethrow; + } + } + + return paymentAccountForm; + } + + Future?> getAllPaymentAccountForms() async { + await _havenoChannel.onConnected; + if (_paymentMethods.isEmpty) { + _paymentMethods = (await getPaymentMethods())!; + } + + for (var paymentMethod in _paymentMethods) { + var paymentAccountForm = await getPaymentAccountForm(paymentMethod.id); + if (paymentAccountForm == null) continue; + + if (!_paymentAccountForms.contains(paymentAccountForm)) { + _paymentAccountForms.add(paymentAccountForm); + } + } + + print("There are currently ${(await _databaseHelper.getAllPaymentAccountForms())?.length ?? 'an unknown number of'} payment account forms in the database."); + return _paymentAccountForms; + } + + Future createPaymentAccount(String paymentMethodId, PaymentAccountForm form) async { + await _havenoChannel.onConnected; + try { + notifyListeners(); + final createdPaymentAccount = await _havenoChannel.paymentAccountsClient! + .createPaymentAccount( + CreatePaymentAccountRequest(paymentAccountForm: form)); + var paymentAccount = createdPaymentAccount.paymentAccount; + print("Created Payment Account: $paymentAccount"); + notifyListeners(); + return paymentAccount; + } catch (e) { + print("Failed to create payment account: $e"); + rethrow; + } + } +} + diff --git a/lib/providers/haveno_client_providers/price_provider.dart b/lib/providers/haveno_client_providers/price_provider.dart new file mode 100644 index 0000000..57fa486 --- /dev/null +++ b/lib/providers/haveno_client_providers/price_provider.dart @@ -0,0 +1,55 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:haveno/grpc_models.dart'; +import 'package:haveno/haveno_client.dart'; + +class PricesProvider with ChangeNotifier { + final HavenoChannel _havenoChannel; + List _marketPrices = []; + Timer? _timer; + + PricesProvider(this._havenoChannel); + + List get prices => _marketPrices; + + Future getXmrMarketPrices() async { + await _havenoChannel.onConnected; + try { + final getMarketPricesReply = await _havenoChannel.priceClient + !.getMarketPrices(MarketPricesRequest()); + _marketPrices = getMarketPricesReply.marketPrice; + notifyListeners(); + } catch (e) { + print("Failed to get prices: $e"); + rethrow; + } + } + + @override + void dispose() { + _timer?.cancel(); + super.dispose(); + } +} diff --git a/lib/providers/haveno_client_providers/trade_statistics_provider.dart b/lib/providers/haveno_client_providers/trade_statistics_provider.dart new file mode 100644 index 0000000..869152b --- /dev/null +++ b/lib/providers/haveno_client_providers/trade_statistics_provider.dart @@ -0,0 +1,67 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'package:flutter/material.dart'; +import 'package:haveno/haveno_client.dart'; +import 'package:haveno/haveno_service.dart'; +import 'package:haveno/profobuf_models.dart'; +import 'package:haveno_app/utils/database_helper.dart'; + +class TradeStatisticsProvider with ChangeNotifier { + final HavenoChannel _havenoChannel; + final DatabaseHelper _databaseHelper = DatabaseHelper.instance; + List _tradeStatistics = []; + + TradeStatisticsProvider(this._havenoChannel); + + List? get tradeStatisticsList => _tradeStatistics; + + Future getTradeStatistics() async { + try { + await _havenoChannel.onConnected; + + if (_tradeStatistics.isEmpty) { + _tradeStatistics = await _databaseHelper.getTradeStatistics(null); + } + + TradeStatisticsService? tradeStatisticsService = TradeStatisticsService(); + List? tradeStatistics3 = await tradeStatisticsService.getTradeStatistics(); + + if (tradeStatistics3 != null && tradeStatistics3.isNotEmpty) { + _tradeStatistics = tradeStatistics3; + try { + for (var tradeStatistic in _tradeStatistics) { + _databaseHelper.insertTradeStatistic(tradeStatistic); + } + } catch (e) { + print( + "Error adding one ore more trade statistic records to the database: ${e.toString()}"); + return; + } + } + + notifyListeners(); + } catch (e) { + print("Failed to get trade statistics: $e"); + } + } +} diff --git a/lib/providers/haveno_client_providers/trades_provider.dart b/lib/providers/haveno_client_providers/trades_provider.dart new file mode 100644 index 0000000..a58086f --- /dev/null +++ b/lib/providers/haveno_client_providers/trades_provider.dart @@ -0,0 +1,325 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'dart:async'; +import 'package:fixnum/fixnum.dart' as fixnum; +import 'package:flutter/material.dart'; +import 'package:haveno/grpc_models.dart'; +import 'package:haveno/haveno_client.dart'; +import 'package:haveno/haveno_service.dart'; +import 'package:haveno/profobuf_models.dart'; +import 'package:haveno_app/models/schema.dart'; +import 'package:haveno_app/utils/database_helper.dart'; +import 'package:collection/collection.dart'; + +class TradesProvider with ChangeNotifier, CooldownMixin { + final HavenoChannel _havenoChannel; + final DatabaseHelper _databaseHelper = DatabaseHelper.instance; + TradeUpdateCallback? onTradeUpdate; + NewChatMessageCallback? onNewChatMessage; + List _trades = []; + TradeInfo? _currentTrade; + final Map> _chatMessages = {}; + final Map _estimatedConfirmationTimes = {}; + + TradesProvider(this._havenoChannel) { + setCooldownDurations({ + 'getTrades': + const Duration(seconds: 10), // 10 seconds cooldown for getTrades + 'getTrade': const Duration(seconds: 10) + }); + } + + List? _newUnreadTrades; + + // Getters + List? get newUnreadTrades => _newUnreadTrades; + + List get trades => _trades; + List get activeTrades => _trades + .where((trade) => + trade.state != 'SELLER_SENT_PAYMENT_RECEIVED_MSG' && + trade.state != 'SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG') + .toList(); + List? get completedTrades => _trades + .where((trade) => + trade.state == 'SELLER_SENT_PAYMENT_RECEIVED_MSG' || + trade.state == 'SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG') + .toList(); + List? get cancelledTrades => _trades; + List? get expiredTrades => + _trades.where((trade) => trade.disputeState == 'something').toList(); + List? get disputedTrades => + _trades.where((trade) => trade.disputeState != 'NO_DISPUTE').toList(); + TradeInfo? get currentTrade => _currentTrade; + Map> get chatMessages => _chatMessages; + Map get estimatedConfirmationTimes => + _estimatedConfirmationTimes; + + Future getTrades() async { + await _havenoChannel.onConnected; + if (!await isCooldownValid('getTrades')) { + await updateCooldown('getTrades'); + try { + final tradesClient = TradesService(); + final fetchedTrades = await tradesClient.getTrades(); + + // Deep compare the list of trades + final tradesAreEqual = + const DeepCollectionEquality().equals(_trades, fetchedTrades); + + if (!tradesAreEqual) { + _trades = fetchedTrades!; + + // Insert or update trades in the database + await _databaseHelper.insertTrades( + fetchedTrades); // Assuming this handles insert or update + notifyListeners(); + } + } catch (e) { + print("Failed to get trades: $e"); + rethrow; + } + } else { + var tradesBefore = _trades; + _trades = await _databaseHelper.getAllTrades(); + + final tradesAreEqual = + const DeepCollectionEquality().equals(tradesBefore, _trades); + + if (!tradesAreEqual) { + notifyListeners(); + } + print( + "Returned ${_trades.length} trades from the server since cooldown is active"); + } + } + + Future getTrade(String tradeId) async { + await _havenoChannel.onConnected; + + TradeInfo? existingTrade; + + // Check if the cooldown is valid + if (!await isCooldownValid('getTrade')) { + await updateCooldown('getTrade'); + try { + final getTradeReply = await _havenoChannel.tradesClient! + .getTrade(GetTradeRequest(tradeId: tradeId)); + final fetchedTrade = getTradeReply.trade; + + // Find the corresponding trade in the current list of trades + try { + existingTrade = + _trades.firstWhere((trade) => trade.tradeId == tradeId); + } catch (e) { + print( + "No existing trade for $tradeId, very odd we're fetching it? $e"); + existingTrade = null; + } + + // Compare the existing trade with the fetched one + if (!const DeepCollectionEquality() + .equals(existingTrade, fetchedTrade)) { + // Update the trade in the _trades list + _trades = _trades + .map((trade) => trade.tradeId == tradeId ? fetchedTrade : trade) + .toList(); + + // Update the trade in the database + await _databaseHelper.insertTrade( + fetchedTrade); // Assuming this handles insert or update + onTradeUpdate?.call(fetchedTrade, false); + // Update cooldown and notify listeners + notifyListeners(); + } + } catch (e) { + print("Failed to get trade: $e"); + } + } else { + // If cooldown is active, fetch the trade from the database + final tradeFromDb = await _databaseHelper.getTradeById(tradeId); + + if (tradeFromDb != null) { + // Find the corresponding trade in the current list of trades + try { + existingTrade = + _trades.firstWhere((trade) => trade.tradeId == tradeId); + } catch (e) { + existingTrade = null; + } + + if (!const DeepCollectionEquality() + .equals(existingTrade, tradeFromDb)) { + // Update the _trades list and notify listeners if there's a change + _trades = _trades.map((trade) { + return trade.tradeId == tradeId ? tradeFromDb : trade; + }).toList(); + + notifyListeners(); + } + } + + print( + "Returned trade $tradeId from the database since cooldown is active"); + } + } + + Future takeOffer( + String? offerId, String? paymentAccountId, fixnum.Int64 amount) async { + await _havenoChannel.onConnected; + try { + final takeOfferReply = await _havenoChannel.tradesClient!.takeOffer( + TakeOfferRequest( + offerId: offerId, + paymentAccountId: paymentAccountId, + amount: amount)); + _currentTrade = takeOfferReply.trade; + notifyListeners(); + return _currentTrade; + } catch (e) { + print("Failed to take offer: $e"); + rethrow; + } + } + + Future sendChatMessage(String? tradeId, String? message) async { + await _havenoChannel.onConnected; + try { + await _havenoChannel.tradesClient!.sendChatMessage( + SendChatMessageRequest(tradeId: tradeId, message: message)); + } catch (e) { + print("Failed to send trade chat message: $e"); + rethrow; + } + } + + Future getChatMessages(String tradeId) async { + await _havenoChannel.onConnected; + try { + final getChatMessagesReply = await _havenoChannel.tradesClient! + .getChatMessages(GetChatMessagesRequest(tradeId: tradeId)); + _chatMessages[tradeId] = getChatMessagesReply.message; + notifyListeners(); + } catch (e) { + print("Failed to get trade chat messages: $e"); + } + } + + Future confirmPaymentSent(String tradeId) async { + await _havenoChannel.onConnected; + try { + await _havenoChannel.tradesClient! + .confirmPaymentSent(ConfirmPaymentSentRequest(tradeId: tradeId)); + await getTrade(tradeId); + notifyListeners(); + } catch (e) { + print("Failed to confirm payment sent: $e"); + rethrow; + } + } + + Future confirmPaymentReceived(String tradeId) async { + await _havenoChannel.onConnected; + try { + await _havenoChannel.tradesClient!.confirmPaymentReceived( + ConfirmPaymentReceivedRequest(tradeId: tradeId)); + await getTrade(tradeId); + notifyListeners(); + } catch (e) { + print("Failed to confirm payment received: $e"); + rethrow; + } + } + + Future completeTrade(String? tradeId) async { + await _havenoChannel.onConnected; + try { + await _havenoChannel.tradesClient! + .completeTrade(CompleteTradeRequest(tradeId: tradeId)); + } catch (e) { + print("Failed to complete trade: $e"); + rethrow; + } + } + + Future withdrawFunds( + String? tradeId, String? address, String? memo) async { + await _havenoChannel.onConnected; + try { + await _havenoChannel.tradesClient!.withdrawFunds( + WithdrawFundsRequest(tradeId: tradeId, address: address, memo: memo)); + } catch (e) { + print("Failed to withdraw funds from trade: $e"); + rethrow; + } + } + + // Used by the listener to add remote messages, dont use to post message to network + void addChatMessage(ChatMessage chatMessage) { + // Ensure the tradeId exists in the _chatMessages map + if (_chatMessages.containsKey(chatMessage.tradeId)) { + // Check if a message with the same UID already exists + bool messageExists = _chatMessages[chatMessage.tradeId]! + .any((msg) => msg.uid == chatMessage.uid); + + // If the message does not exist, add it to the list + if (!messageExists) { + _chatMessages[chatMessage.tradeId]!.add(chatMessage); + onNewChatMessage?.call(chatMessage); + notifyListeners(); + } + } else { + // If the tradeId does not exist, create a new entry + _chatMessages[chatMessage.tradeId] = [chatMessage]; + onNewChatMessage?.call(chatMessage); + notifyListeners(); + } + } + + // Used by the listener + void createOrUpdateTrade(TradeInfo trade) { + // Find the index of the trade with the same tradeId in the _trades list + final index = _trades + .indexWhere((existingTrade) => existingTrade.tradeId == trade.tradeId); + + if (index != -1) { + // If the trade exists, update it + _trades[index] = trade; + } else { + // If the trade does not exist, add it to the list + _trades.add(trade); + } + + // Update the trade in the database + _databaseHelper.insertTrade(trade); + + // Get deep trade + getTrade(trade.tradeId); + + // Trigger the callback + onTradeUpdate?.call(trade, !(index != -1)); + + // Notify listeners that the trade list has been updated + notifyListeners(); + } +} diff --git a/lib/providers/haveno_client_providers/version_provider.dart b/lib/providers/haveno_client_providers/version_provider.dart new file mode 100644 index 0000000..1ca3bb1 --- /dev/null +++ b/lib/providers/haveno_client_providers/version_provider.dart @@ -0,0 +1,45 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'package:flutter/material.dart'; +import 'package:haveno/haveno_client.dart'; +import 'package:haveno/haveno_service.dart'; + +class GetVersionProvider with ChangeNotifier { + final HavenoChannel _havenoChannel; + String? _version; + + GetVersionProvider(this._havenoChannel); + + String? get version => _version; + + Future fetchVersion() async { + try { + await _havenoChannel.onConnected; + final getVersionClient = GetVersionService(); + _version = await getVersionClient.fetchVersion(); + notifyListeners(); + } catch (e) { + print("Failed to get Haveno version: $e"); + } + } +} diff --git a/lib/providers/haveno_client_providers/wallets_provider.dart b/lib/providers/haveno_client_providers/wallets_provider.dart new file mode 100644 index 0000000..cabe1c8 --- /dev/null +++ b/lib/providers/haveno_client_providers/wallets_provider.dart @@ -0,0 +1,180 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'package:flutter/material.dart'; +import 'package:haveno/grpc_models.dart'; +import 'package:haveno/haveno_client.dart'; +import 'package:haveno/haveno_service.dart'; +import 'package:haveno_app/models/schema.dart'; + +class WalletsProvider with ChangeNotifier, CooldownMixin { + final HavenoChannel _havenoChannel; + final WalletsService _walletsService = WalletsService(); + BalancesInfo? _balances; + List _xmrTxs = []; + String? _xmrPrimaryAddress; + List _xmrIncomingTransfers = []; + List _xmrOutgoingTransfers = []; + + WalletsProvider(this._havenoChannel) { + setCooldownDurations({ + 'getBalances': const Duration(seconds: 15), + }); + } + + BalancesInfo? get balances => _balances; + String? get xmrPrimaryAddress => _xmrPrimaryAddress; + List? get xmrTxs => _xmrTxs; + List get xmrIncomingTransfers => _xmrIncomingTransfers; + List get xmrOutgoingTransfers => _xmrOutgoingTransfers; + + Future getBalances() async { + await _havenoChannel.onConnected; + if (!await isCooldownValid('getBalances')) { + try { + _balances = await _walletsService.getBalances(); + updateCooldown('getBalances'); + print("Loaded balances..."); + notifyListeners(); + return; + } catch (e) { + print("Failed to get balances: $e"); + } + } else { + print("Tried to get getBalances when cooldown is active"); + return; + } + } + + Future getXmrPrimaryAddress() async { + await _havenoChannel.onConnected; + try { + final getXmrPrimaryAddressReply = await _havenoChannel.walletsClient! + .getXmrPrimaryAddress(GetXmrPrimaryAddressRequest()); + _xmrPrimaryAddress = getXmrPrimaryAddressReply.primaryAddress; + print("Primary Address: $_xmrPrimaryAddress"); + notifyListeners(); + } catch (e) { + print("Failed to get primary address: $e"); + } + } + + Future getXmrTxs() async { + await _havenoChannel.onConnected; + try { + final getXmrTxsReply = + await _havenoChannel.walletsClient!.getXmrTxs(GetXmrTxsRequest()); + _xmrTxs = getXmrTxsReply.txs; + _xmrIncomingTransfers = []; + _xmrOutgoingTransfers = []; + + for (var xmrTx in _xmrTxs) { + _xmrIncomingTransfers.addAll(xmrTx.incomingTransfers); + if (xmrTx.hasOutgoingTransfer()) { + _xmrOutgoingTransfers.add(xmrTx.outgoingTransfer); + } + } + + notifyListeners(); + } catch (e) { + print("Failed to get XMR transactions: $e"); + } + } + + Future createXmrTx(Iterable destinations) async { + await _havenoChannel.onConnected; + try { + final createXmrTxsReply = await _havenoChannel.walletsClient! + .createXmrTx(CreateXmrTxRequest(destinations: destinations)); + var tx = createXmrTxsReply.tx; + // Check if tx with the same hash already exists + bool exists = _xmrTxs.any((existingTx) => existingTx.hash == tx.hash); + if (!exists) { + _xmrTxs.add(tx); + notifyListeners(); + } + } catch (e) { + print("Failed to create an XMR transaction: $e"); + } + } + + Future relayXmrTx(String metadata) async { + await _havenoChannel.onConnected; + try { + await _havenoChannel.walletsClient! + .relayXmrTx(RelayXmrTxRequest(metadata: metadata)); + } catch (e) { + print("Error relaying transaciton: $e"); + } + } + + Future getXmrSeed() async { + await _havenoChannel.onConnected; + try { + final getXmrSeedReply = + await _havenoChannel.walletsClient!.getXmrSeed(GetXmrSeedRequest()); + return getXmrSeedReply.seed; + } catch (e) { + print("Error getting seed phrase: $e"); + return null; + } + } + + Future setWalletPassword(String newPassword, String? password) async { + await _havenoChannel.onConnected; + try { + await _havenoChannel.walletsClient!.setWalletPassword( + SetWalletPasswordRequest( + password: password, newPassword: newPassword)); + } catch (e) { + print("Error setting wallet password: $e"); + } + } + + Future lockWallet(String password) async { + await _havenoChannel.onConnected; + try { + await _havenoChannel.walletsClient!.lockWallet(LockWalletRequest()); + } catch (e) { + print("Error setting wallet password: $e"); + } + } + + Future unlockWallet(String password) async { + await _havenoChannel.onConnected; + try { + await _havenoChannel.walletsClient!.unlockWallet(UnlockWalletRequest()); + } catch (e) { + print("Error setting wallet password: $e"); + } + } + + Future removeWalletPassword(String password) async { + await _havenoChannel.onConnected; + try { + await _havenoChannel.walletsClient!.removeWalletPassword( + RemoveWalletPasswordRequest(password: password)); + } catch (e) { + print("Error removing wallet password: $e"); + } + } +} diff --git a/lib/providers/haveno_client_providers/xmr_connections_provider.dart b/lib/providers/haveno_client_providers/xmr_connections_provider.dart new file mode 100644 index 0000000..3768651 --- /dev/null +++ b/lib/providers/haveno_client_providers/xmr_connections_provider.dart @@ -0,0 +1,155 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'package:flutter/material.dart'; +import 'package:haveno/grpc_models.dart'; +import 'package:haveno/haveno_client.dart'; +import 'package:haveno/haveno_service.dart'; + +class XmrConnectionsProvider with ChangeNotifier { + final HavenoChannel _havenoChannel = HavenoChannel(); + + List _xmrUrlConnections = []; + UrlConnection? _xmrActiveConnection; + + XmrConnectionsProvider(); + + // Expose the list of connections and the active node + List get xmrNodeConnections => _xmrUrlConnections; + UrlConnection? get xmrActiveConnection => _xmrActiveConnection; + + // Fetch connections from the server + Future getXmrConnectionSettings() async { + await _havenoChannel.onConnected; + try { + final xmrConnectionsClient = XmrConnectionsService(); + final response = await xmrConnectionsClient.getXmrConnectionSettings(); + _xmrUrlConnections = response; + notifyListeners(); + } catch (e) { + print("Failed to fetch connections: $e"); + } + } + + // Fetch connections from the server + Future checkConnections() async { + await _havenoChannel.onConnected; + try { + final response = await _havenoChannel.xmrConnectionsClient!.checkConnections(CheckConnectionsRequest()); + _xmrUrlConnections = response.connections; + notifyListeners(); + } catch (e) { + print("Failed to check connections: $e"); + } + } + + // Set the active node on the server and locally + Future setConnection(UrlConnection connection) async { + await _havenoChannel.onConnected; + try { + await _havenoChannel.xmrConnectionsClient!.setConnection(SetConnectionRequest(connection: connection)); + _xmrActiveConnection = connection; // Set the new active node locally + notifyListeners(); // Notify the UI to update + } catch (e) { + print("Failed to set active connection: $e"); + } + } + + // Fetch the currently active connection from the server + Future getActiveConnection() async { + await _havenoChannel.onConnected; + try { + final response = await _havenoChannel.xmrConnectionsClient!.getConnection(GetConnectionRequest()); + _xmrActiveConnection = response.connection; + notifyListeners(); + } catch (e) { + print("Failed to get active connection: $e"); + } + } + + // Method to check if the selected connection is online (optional, if you need this) + Future checkConnection() async { + await _havenoChannel.onConnected; + try { + final response = await _havenoChannel.xmrConnectionsClient!.checkConnection(CheckConnectionRequest()); + return response.connection.onlineStatus == UrlConnection_OnlineStatus.ONLINE; + } catch (e) { + print("Failed to check connection status: $e"); + return false; + } + } + + + // Add a connection to the list and notify + Future addConnection(UrlConnection connection) async { + await _havenoChannel.onConnected; + try { + await _havenoChannel.xmrConnectionsClient!.addConnection(AddConnectionRequest(connection: connection)); + _xmrUrlConnections.add(connection); // Add to local list + notifyListeners(); // Notify listeners/UI + return true; + } catch (e) { + print("Failed to add connection: $e"); + return false; + } + } + + // Remove a connection from the list and notify + Future removeConnection(String url) async { + await _havenoChannel.onConnected; + try { + await _havenoChannel.xmrConnectionsClient!.removeConnection(RemoveConnectionRequest(url: url)); + _xmrUrlConnections.removeWhere((connection) => connection.url == url); // Remove from local list + notifyListeners(); // Notify listeners/UI + return true; + } catch (e) { + print("Failed to remove connection: $e"); + return false; + } + } + + // Remove a connection from the list and notify + Future setAutoSwitchBestConnection(bool autoSwitch) async { + await _havenoChannel.onConnected; + try { + await _havenoChannel.xmrConnectionsClient!.setAutoSwitch(SetAutoSwitchRequest(autoSwitch: autoSwitch)); + notifyListeners(); + return true; + } catch (e) { + print("Failed to remove connection: $e"); + return false; + } + } + + // Remove a connection from the list and notify + Future getAutoSwitchBestConnection() async { + await _havenoChannel.onConnected; + try { + final getAutoSwitchReply = await _havenoChannel.xmrConnectionsClient!.getAutoSwitch(GetAutoSwitchRequest()); + return getAutoSwitchReply.autoSwitch; + } catch (e) { + print("Failed to remove connection: $e"); + return false; + } + } + +} diff --git a/lib/providers/haveno_client_providers/xmr_node_provider.dart b/lib/providers/haveno_client_providers/xmr_node_provider.dart new file mode 100644 index 0000000..b92a7e6 --- /dev/null +++ b/lib/providers/haveno_client_providers/xmr_node_provider.dart @@ -0,0 +1,51 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'package:flutter/material.dart'; +import 'package:haveno/grpc_models.dart'; +import 'package:haveno/haveno_client.dart'; +import 'package:haveno/profobuf_models.dart'; + +class XmrNodeProvider with ChangeNotifier { + final HavenoChannel _havenoChannel = HavenoChannel(); + + XmrNodeSettings? _xmrNodeSettings; + + XmrNodeProvider(); + + // Getters + XmrNodeSettings? get xmrNodeSettings => _xmrNodeSettings; + + Future getXmrNodeSettings() async { + await _havenoChannel.onConnected; + try { + final getXmrNodeSettingsReply = + await _havenoChannel.xmrNodeClient!.getXmrNodeSettings(GetXmrNodeSettingsRequest()); + _xmrNodeSettings = getXmrNodeSettingsReply.settings; + print("Getting XMR node settings from daemon..."); + notifyListeners(); + } catch (e) { + print("Failed to get XMR node settings: $e"); + rethrow; + } + } +} diff --git a/lib/providers/haveno_daemon_provider.dart b/lib/providers/haveno_daemon_provider.dart new file mode 100644 index 0000000..43cea16 --- /dev/null +++ b/lib/providers/haveno_daemon_provider.dart @@ -0,0 +1,84 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'dart:async'; +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:haveno_app/models/haveno_daemon_config.dart'; +import 'package:haveno_app/services/secure_storage_service.dart'; + + +class HavenoDaemonProvider with ChangeNotifier { + final SecureStorageService _secureStorageService; + + HavenoDaemonConfig? _currentDaemon; + // Getters + HavenoDaemonConfig? get currentDaemon => _currentDaemon; + bool get isPaired => _currentDaemon!.isVerified && _currentDaemon?.host != null; + + HavenoDaemonProvider(this._secureStorageService) { + _initializeSettings(); + } + + get lastPing => null; + + Future _initializeSettings() async { + _currentDaemon = await _secureStorageService.readHavenoDaemonConfig(); + notifyListeners(); + } + + // Returns how many seconds ago the last ping to Haveno Daemon was + Future ping() async { + if (_currentDaemon != null) { + try { + var httpClient = HttpClient(); + var request = await httpClient.headUrl(Uri.http('${_currentDaemon!.host}:${_currentDaemon!.port}', '/')); + var response = await request.close(); + httpClient.close(); + + if (response.statusCode == HttpStatus.ok) { + // If the response status is OK, the server is available + _currentDaemon!.setVerified(true); + _secureStorageService.writeHavenoDaemonConfig(_currentDaemon!); + return DateTime.now().millisecondsSinceEpoch ~/ 1000; + } else { + // Handle non-OK response statuses if necessary + return null; + } + } on SocketException catch (e) { + // Handle socket exceptions (e.g., server not available) + print('SocketException: $e'); + return null; + } on HttpException catch (e) { + // Handle HTTP exceptions + print('HttpException: $e'); + return null; + } catch (e) { + // Handle any other exceptions + print('Exception: $e'); + return null; + } + } + return null; + } + +} diff --git a/lib/providers/haveno_providers/mobile_wallet_provider.dart b/lib/providers/haveno_providers/mobile_wallet_provider.dart new file mode 100644 index 0000000..3e2283c --- /dev/null +++ b/lib/providers/haveno_providers/mobile_wallet_provider.dart @@ -0,0 +1,20 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . diff --git a/lib/providers/haveno_providers/settings_provider.dart b/lib/providers/haveno_providers/settings_provider.dart new file mode 100644 index 0000000..ebf5925 --- /dev/null +++ b/lib/providers/haveno_providers/settings_provider.dart @@ -0,0 +1,137 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:haveno_app/services/secure_storage_service.dart'; + +class SettingsProvider extends ChangeNotifier { + final SecureStorageService _secureStorageService; + + // Preferences + String? _preferredLanguage; + String? _country; + String? _preferredCurrency; + String? _blockchainExplorer; + String? _maxDeviationFromMarketPrice; + + // Display Options + bool _hideNonSupportedPaymentMethods = false; + bool _sortMarketListsByNumberOfOffersTrades = false; + bool _useDarkMode = false; + bool _autoWithdrawToNewStealthAddress = false; + + // Supported currencies + final List _supportedCurrencies = [ + 'CHF', 'MXN', 'CLP', 'ZAR', 'VND', 'AUD', 'ILS', 'IDR', 'TRY', 'AED', 'HKD', 'TWD', 'EUR', 'DKK', + 'BCH', 'CAD', 'MYR', 'MMK', 'NOK', 'GEL', 'BTC', 'LKR', 'NGN', 'CZK', 'PKR', 'SEK', 'LTC', 'UAH', + 'BHD', 'ARS', 'SAR', 'INR', 'CNY', 'THB', 'KRW', 'JPY', 'BDT', 'PLN', 'GBP', 'BMD', 'HUF', 'KWD', + 'PHP', 'RUB', 'USD', 'SGD', 'ETH', 'NZD', 'BRL' + ]; + + // Getters + String? get preferredLanguage => _preferredLanguage; + String? get country => _country; + String? get preferredCurrency => _preferredCurrency; + String? get blockchainExplorer => _blockchainExplorer; + String? get maxDeviationFromMarketPrice => _maxDeviationFromMarketPrice; + List get supportedCurrencies => _supportedCurrencies; + + bool get hideNonSupportedPaymentMethods => _hideNonSupportedPaymentMethods; + bool get sortMarketListsByNumberOfOffersTrades => _sortMarketListsByNumberOfOffersTrades; + bool get useDarkMode => _useDarkMode; + bool get autoWithdrawToNewStealthAddress => _autoWithdrawToNewStealthAddress; + + SettingsProvider(this._secureStorageService) { + _initializeSettings(); + } + + Future _initializeSettings() async { + _preferredLanguage = await _secureStorageService.readSettingsPreferredLanguage(); + _country = await _secureStorageService.readSettingsCountry(); + _preferredCurrency = await _secureStorageService.readSettingsPreferredCurrency() ?? 'USD'; + _blockchainExplorer = await _secureStorageService.readSettingsBlockchainExplorer(); + _maxDeviationFromMarketPrice = await _secureStorageService.readSettingsMaxDeviationFromMarketPrice(); + + _hideNonSupportedPaymentMethods = await _secureStorageService.readSettingsHideNonSupportedPaymentMethods() ?? false; + _sortMarketListsByNumberOfOffersTrades = await _secureStorageService.readSettingsSortMarketListsByNumberOfOffersTrades() ?? false; + _useDarkMode = await _secureStorageService.readSettingsUseDarkMode() ?? false; + _autoWithdrawToNewStealthAddress = await _secureStorageService.readSettingsAutoWithdrawToNewStealthAddress() ?? false; + + notifyListeners(); + } + + // Setters + Future setPreferredLanguage(String languageCode) async { + await _secureStorageService.writeSettingsPreferredLanguage(languageCode); + _preferredLanguage = languageCode; + notifyListeners(); + } + + Future setCountry(String country) async { + await _secureStorageService.writeSettingsCountry(country); + _country = country; + notifyListeners(); + } + + Future setPreferredCurrency(String currencyCode) async { + await _secureStorageService.writeSettingsPreferredCurrency(currencyCode); + _preferredCurrency = currencyCode; + notifyListeners(); + } + + Future setBlockchainExplorer(String explorer) async { + await _secureStorageService.writeSettingsBlockchainExplorer(explorer); + _blockchainExplorer = explorer; + notifyListeners(); + } + + Future setMaxDeviationFromMarketPrice(String deviation) async { + await _secureStorageService.writeSettingsMaxDeviationFromMarketPrice(deviation); + _maxDeviationFromMarketPrice = deviation; + notifyListeners(); + } + + Future setHideNonSupportedPaymentMethods(bool value) async { + await _secureStorageService.writeSettingsHideNonSupportedPaymentMethods(value); + _hideNonSupportedPaymentMethods = value; + notifyListeners(); + } + + Future setSortMarketListsByNumberOfOffersTrades(bool value) async { + await _secureStorageService.writeSettingsSortMarketListsByNumberOfOffersTrades(value); + _sortMarketListsByNumberOfOffersTrades = value; + notifyListeners(); + } + + Future setUseDarkMode(bool value) async { + await _secureStorageService.writeSettingsUseDarkMode(value); + _useDarkMode = value; + notifyListeners(); + } + + Future setAutoWithdrawToNewStealthAddress(bool value) async { + await _secureStorageService.writeSettingsAutoWithdrawToNewStealthAddress(value); + _autoWithdrawToNewStealthAddress = value; + notifyListeners(); + } +} \ No newline at end of file diff --git a/lib/providers/haveno_providers/tor_log_provider.dart b/lib/providers/haveno_providers/tor_log_provider.dart new file mode 100644 index 0000000..ca4ac32 --- /dev/null +++ b/lib/providers/haveno_providers/tor_log_provider.dart @@ -0,0 +1,39 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +/* import 'package:flutter/material.dart'; +import 'package:haveno_app/models/schema.dart'; +import 'package:haveno_app/services/tor/tor_log_service.dart'; + +class TorLogProvider with ChangeNotifier, StreamListenerProviderMixin { + final TorLogService _torLogService; + + List get stdOutLogs => _torLogService.stdOutLogs; + List get stdErrLogs => _torLogService.stdErrLogs; + + TorLogProvider(this._torLogService) { + // Listen for stdout and stderr log streams + listenToStream(_torLogService.stdOutStream, notifyListeners); + listenToStream(_torLogService.stdErrStream, notifyListeners); + } +} + */ \ No newline at end of file diff --git a/lib/providers/haveno_providers/tor_status_provder.dart b/lib/providers/haveno_providers/tor_status_provder.dart new file mode 100644 index 0000000..6af1975 --- /dev/null +++ b/lib/providers/haveno_providers/tor_status_provder.dart @@ -0,0 +1,38 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +/* import 'package:flutter/material.dart'; +import 'package:haveno_app/models/schema.dart'; +import 'package:haveno_app/services/tor/tor_status_service.dart'; + +class TorStatusProvider with ChangeNotifier, StreamListenerProviderMixin { + final TorStatusService _torStatusService; + + TorStatusProvider(this._torStatusService) { + listenToStream(_torStatusService.torStatusStream, notifyListeners); + } + + int? get port => _torStatusService.torPort; + String get torStatus => _torStatusService.torStatus; + String get torDetails => _torStatusService.torDetails; +} + */ \ No newline at end of file diff --git a/lib/services/connection_checker_service.dart b/lib/services/connection_checker_service.dart new file mode 100644 index 0000000..ddab57a --- /dev/null +++ b/lib/services/connection_checker_service.dart @@ -0,0 +1,84 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import 'package:haveno/haveno_client.dart'; +import 'package:haveno_app/services/secure_storage_service.dart'; +import 'package:haveno_app/services/tor_interface.dart'; +import 'package:internet_connection_checker_plus/internet_connection_checker_plus.dart'; + +class ConnectionCheckerService { + // Private named constructor + ConnectionCheckerService._internal(); + + // The single instance of the class + static final ConnectionCheckerService _instance = ConnectionCheckerService._internal(); + + // Factory constructor to return the same instance + factory ConnectionCheckerService() { + return _instance; + } + + final SecureStorageService secureStorageService = SecureStorageService(); + final HavenoChannel havenoService = HavenoChannel(); + + // Cache for isTorConnected status + bool? _torConnectedCache; + DateTime? _torCacheTimestamp; + final Duration _torCacheDuration = const Duration(minutes: 3); + + // Method to check if the cache is still valid + bool _isTorCacheValid() { + if (_torCacheTimestamp == null) return false; + return DateTime.now().difference(_torCacheTimestamp!) < _torCacheDuration; + } + + // Cached version of isTorConnected + Future isTorConnected() async { + // If cache is valid, return the cached value + if (_isTorCacheValid() && _torConnectedCache == true) { + print("Returning cached Tor connection status: $_torConnectedCache"); + return _torConnectedCache!; + } + + // Otherwise, perform the check + bool isConnected = await TorService.isTorConnected(); + + // If Tor is connected, cache the result and timestamp + if (isConnected) { + _torConnectedCache = true; + _torCacheTimestamp = DateTime.now(); + print("Caching successful Tor connection status."); + } else { + // If not connected, invalidate the cache + _torConnectedCache = false; + _torCacheTimestamp = null; + } + + return isConnected; + } + + Future isHavenoDaemonConnected() async { + print("Haveno Daemon Connection Status: ${havenoService.isConnected}"); + return havenoService.isConnected; + } + + Future isInternetConnected() async => await InternetConnection().hasInternetAccess; +} diff --git a/lib/services/desktop_manager_service.dart b/lib/services/desktop_manager_service.dart new file mode 100644 index 0000000..f465648 --- /dev/null +++ b/lib/services/desktop_manager_service.dart @@ -0,0 +1,86 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'dart:io'; +import 'package:haveno/haveno_client.dart'; +import 'package:haveno_app/models/haveno/p2p/haveno_seednode.dart'; +import 'package:haveno_app/services/platform_system_service/schema.dart'; +import 'package:haveno_app/services/secure_storage_service.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:path/path.dart' as path; + +class DesktopManagerService { + final SecureStorageService secureStorageService = SecureStorageService(); + final HavenoChannel havenoService = HavenoChannel(); + Uri? _desktopDaemonNodeUri; + late PlatformService platformService; + late String daemonPassword; + + DesktopManagerService(); + + Future getDesktopDaemonNodeUri() async { + String? daemonPassword = + await secureStorageService.readHavenoDaemonPassword(); + try { + Directory applicationSupportDirectory = + await getApplicationSupportDirectory(); + String torPath = path.join(applicationSupportDirectory.path, 'Tor', + 'daemon_service', 'hostname'); + File hiddenServiceHostnameFile = File(torPath); + if (await hiddenServiceHostnameFile.exists()) { + List hostnameFileLines = + await hiddenServiceHostnameFile.readAsLines(); + String? hostname = hostnameFileLines.first; + _desktopDaemonNodeUri = Uri.parse(hostname); + + if (daemonPassword != null && daemonPassword.isNotEmpty) { + _desktopDaemonNodeUri = + _desktopDaemonNodeUri!.replace(queryParameters: { + ..._desktopDaemonNodeUri!.queryParameters, + 'password': daemonPassword, + }); + } + + return _desktopDaemonNodeUri; + } else { + return null; + } + } catch (e) { + print("Error getting daemon node address: $e"); + return null; + } + } + + Future isSeednodeConfigured() async { + List storedSeedNodes = await secureStorageService.readHavenoSeedNodes(); + if (storedSeedNodes.isNotEmpty) { + return true; + } else { + return false; + } + } + + Future setInitialSeednode(HavenoSeedNode seedNode) async { + await secureStorageService.writeHavenoSeedNodes([seedNode]); + } + +} diff --git a/lib/services/http_service.dart b/lib/services/http_service.dart new file mode 100644 index 0000000..7f6a8dc --- /dev/null +++ b/lib/services/http_service.dart @@ -0,0 +1,56 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'dart:convert'; +import 'dart:io'; + +import 'package:socks5_proxy/socks_client.dart'; + +class HttpService { + final HttpClient _client; + + HttpService({String proxyHost = '127.0.0.1', int proxyPort = 8118}) + : _client = HttpClient() { + SocksTCPClient.assignToHttpClient(_client, [ + ProxySettings(InternetAddress(proxyHost), proxyPort), + ]); + } + + Future request(String method, String url, + {Map? headers, dynamic body}) async { + final request = await _client.openUrl(method, Uri.parse(url)); + + headers?.forEach((key, value) { + request.headers.set(key, value); + }); + + if (body != null) { + request.add(utf8.encode(json.encode(body))); + } + + return await request.close(); + } + + void close() { + _client.close(); + } +} diff --git a/lib/services/local_notification_service.dart b/lib/services/local_notification_service.dart new file mode 100644 index 0000000..8057c93 --- /dev/null +++ b/lib/services/local_notification_service.dart @@ -0,0 +1,249 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:haveno/profobuf_models.dart'; +import 'package:haveno_app/main.dart'; +import 'package:haveno_app/views/screens/dispute_chat_screen.dart'; +import 'package:haveno_app/views/screens/home_screen.dart'; +import 'package:haveno_app/views/screens/trade_chat_screen.dart'; +import 'package:timezone/timezone.dart' as tz; +import 'package:timezone/data/latest_all.dart' as tz; + +class LocalNotificationsService { + final FlutterLocalNotificationsPlugin _flutterLocalNotificationsPlugin = + FlutterLocalNotificationsPlugin(); + final int foregroundServiceNotificationId = 888; + bool _initialized = false; + + static final LocalNotificationsService _instance = LocalNotificationsService._internal(); + + factory LocalNotificationsService() => _instance; + + LocalNotificationsService._internal(); + +Future init() async { + if (_initialized) return; // Prevent re-initialization + _initialized = true; + + print("Initializing Timezones..."); + tz.initializeTimeZones(); + print("Timezones Initialized."); + + const LinuxInitializationSettings initializationSettingsLinux = + LinuxInitializationSettings(defaultActionName: 'Haveno'); + + const AndroidInitializationSettings initializationSettingsAndroid = + AndroidInitializationSettings('@mipmap/ic_launcher'); + + const DarwinInitializationSettings initializationSettingsDarwin = + DarwinInitializationSettings( + requestAlertPermission: true, + requestBadgePermission: true, + requestSoundPermission: true, + ); + + const DarwinInitializationSettings macOsInitializationSettingsDarwin = + DarwinInitializationSettings( + requestAlertPermission: false, + requestBadgePermission: false, + requestSoundPermission: false, + ); + + const InitializationSettings initializationSettings = InitializationSettings( + android: initializationSettingsAndroid, + iOS: initializationSettingsDarwin, + macOS: macOsInitializationSettingsDarwin, + linux: initializationSettingsLinux + ); + + print("Initializing FlutterLocalNotificationsPlugin..."); + await _flutterLocalNotificationsPlugin.initialize( + initializationSettings, + onDidReceiveNotificationResponse: _onNotificationResponse, + ); + print("FlutterLocalNotificationsPlugin Initialized."); + } + + Future showForegroundServiceNotification({ + required String title, + required String body, + String? payload, + }) async { + const AndroidNotificationDetails androidPlatformChannelSpecifics = + AndroidNotificationDetails( + 'haveno', + 'Haveno Plus Service', + channelDescription: 'Haveno service running in the background', + importance: Importance.max, + priority: Priority.high, + ticker: 'ticker', + ongoing: true, + ); + + const DarwinNotificationDetails iOSPlatformChannelSpecifics = DarwinNotificationDetails(); + + const DarwinNotificationDetails macOSPlatformChannelSpecifics = DarwinNotificationDetails(); + + const NotificationDetails platformChannelSpecifics = NotificationDetails( + android: androidPlatformChannelSpecifics, + iOS: iOSPlatformChannelSpecifics, + macOS: macOSPlatformChannelSpecifics + ); + + await _flutterLocalNotificationsPlugin.show( + foregroundServiceNotificationId, + title, + body, + platformChannelSpecifics, + payload: payload, + ); + } + + Future updateForegroundServiceNotification({ + required String title, + required String body, + String? payload, + }) async { + await showForegroundServiceNotification(title: title, body: body, payload: payload); + } + + Future showNotification({ + required int id, + required String title, + required String body, + String? payload, + }) async { + const AndroidNotificationDetails androidPlatformChannelSpecifics = + AndroidNotificationDetails( + 'haveno_notifications', + 'Haveno Plus Notifications', + channelDescription: 'Notifications for Haveno Plus events', + importance: Importance.max, + priority: Priority.high, + ticker: 'ticker', + ); + + const DarwinNotificationDetails iOSPlatformChannelSpecifics = DarwinNotificationDetails(); + + const DarwinNotificationDetails macOSPlatformChannelSpecifics = DarwinNotificationDetails( + presentAlert: true, + presentSound: true + ); + + const NotificationDetails platformChannelSpecifics = NotificationDetails( + android: androidPlatformChannelSpecifics, + iOS: iOSPlatformChannelSpecifics, + macOS: macOSPlatformChannelSpecifics + ); + + await _flutterLocalNotificationsPlugin.show( + id, + title, + body, + platformChannelSpecifics, + payload: payload, + ); + } + + Future scheduleNotification({ + required int id, + required String title, + required String body, + required DateTime scheduledDate, + String? payload, + }) async { + final tz.TZDateTime tzScheduledDate = tz.TZDateTime.from(scheduledDate, tz.local); + + const AndroidNotificationDetails androidPlatformChannelSpecifics = + AndroidNotificationDetails( + 'haveno_notifications', + 'Haveno Plus Notifications', + channelDescription: 'Notifications for Haveno Plus events', + importance: Importance.max, + priority: Priority.high, + ticker: 'ticker', + ); + + const DarwinNotificationDetails iOSPlatformChannelSpecifics = + DarwinNotificationDetails(); + + const NotificationDetails platformChannelSpecifics = NotificationDetails( + android: androidPlatformChannelSpecifics, + iOS: iOSPlatformChannelSpecifics, + ); + + await _flutterLocalNotificationsPlugin.zonedSchedule( + id, + title, + body, + tzScheduledDate, + platformChannelSpecifics, + payload: payload, + androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle, + uiLocalNotificationDateInterpretation: UILocalNotificationDateInterpretation.absoluteTime, + ); + } + + Future cancelNotification(int id) async { + await _flutterLocalNotificationsPlugin.cancel(id); + } + + Future cancelAllNotifications() async { + await _flutterLocalNotificationsPlugin.cancelAll(); + } + + void _onNotificationResponse(NotificationResponse notificationResponse) async { + print('Notification tapped with payload: ${notificationResponse.payload}'); + var object = jsonDecode(notificationResponse.payload!); + + switch (object['action']) { + case 'route_to_chat_screen': + //print(object['chateProtobufAsJson']); + var chatMessage = ChatMessage()..mergeFromProto3Json(jsonDecode(object['chatMessageProtobufAsJson'])); + if (chatMessage.tradeId.isNotEmpty) { + + if (chatMessage.type != SupportType.TRADE) { + navigatorKey.currentState?.push( + MaterialPageRoute(builder: (context) => DisputeChatScreen(tradeId: chatMessage.tradeId)) + ); + } else { + navigatorKey.currentState?.push( + MaterialPageRoute(builder: (context) => TradeChatScreen(tradeId: chatMessage.tradeId)) + ); + } + break; + } + case 'route_to_active_trades_screen': + navigatorKey.currentState?.push( + MaterialPageRoute(builder: (context) => HomeScreen(initialIndex: 3)) + ); + break; + + default: + print('Unknown action: ${object['action']}'); + break; + } + } +} \ No newline at end of file diff --git a/lib/services/mobile_manager_service.dart b/lib/services/mobile_manager_service.dart new file mode 100644 index 0000000..60a021b --- /dev/null +++ b/lib/services/mobile_manager_service.dart @@ -0,0 +1,87 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import 'dart:convert'; +import 'package:haveno/grpc_models.dart'; +import 'package:haveno/haveno_client.dart'; +import 'package:haveno_app/models/haveno_daemon_config.dart'; +import 'package:haveno_app/services/platform_system_service/schema.dart'; +import 'package:haveno_app/services/secure_storage_service.dart'; +import 'package:haveno_app/services/security.dart'; + +class MobileManagerService { + final SecureStorageService secureStorageService = SecureStorageService(); + final HavenoChannel havenoChannel = HavenoChannel(); + HavenoDaemonConfig? _remoteHavenoDaemonNodeConfig; + bool _hasHavenoDaemonNodeConfig = false; + late PlatformService platformService; + + MobileManagerService(); + + Future getRemoteHavenoDaemonNode() async { + try { + _remoteHavenoDaemonNodeConfig = await secureStorageService.readHavenoDaemonConfig(); + return _remoteHavenoDaemonNodeConfig; + } catch (e) { + print("Failed to read Haveno Daemon config: $e"); + return null; + } + } + + Future setHavenoDaemonNodeConfig(Uri onionUri) async { + HavenoDaemonConfig havenoDaemonConfig = HavenoDaemonConfig(fullUri: onionUri); + print(jsonEncode(havenoDaemonConfig.toJson())); + try { + havenoChannel.connect(havenoDaemonConfig.host, havenoDaemonConfig.port, havenoDaemonConfig.clientAuthPassword); + } catch (e) { + print("Couldn't connect to the URI provided ${e.toString()}"); + return null; + } + try { + await havenoChannel.versionClient?.getVersion(GetVersionRequest()); + havenoDaemonConfig.setVerified(true); + } catch (e) { + print('Error fetching version ${e.toString()}'); + return null; + } + try { + await secureStorageService.writeHavenoDaemonConfig(havenoDaemonConfig); + _hasHavenoDaemonNodeConfig = true; + return havenoDaemonConfig; + } catch (e) { + print("Couldn't set the new haveno daemon config: $e"); + rethrow; + } + } + + Future logout() async { + try { + await SecurityService().resetAppData(); + _hasHavenoDaemonNodeConfig = false; + return true; + } catch (e) { + print("Failed to logout: $e"); + _hasHavenoDaemonNodeConfig = false; + return false; + } + } + +} diff --git a/lib/services/platform_system_service/android_platform_service.dart b/lib/services/platform_system_service/android_platform_service.dart new file mode 100644 index 0000000..7d8d80f --- /dev/null +++ b/lib/services/platform_system_service/android_platform_service.dart @@ -0,0 +1,43 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import 'schema.dart'; + +class AndroidPlatformService implements PlatformService { + @override + Future init() { + // TODO: implement init + throw UnimplementedError(); + } + + @override + Future setupHavenoDaemon(String? password) { + // TODO: implement setupHavenoDaemon + throw UnimplementedError(); + } + + @override + Future setupTorDaemon() { + // TODO: implement setupTorDaemon + throw UnimplementedError(); + } + +} diff --git a/lib/services/platform_system_service/factory.dart b/lib/services/platform_system_service/factory.dart new file mode 100644 index 0000000..dd95a01 --- /dev/null +++ b/lib/services/platform_system_service/factory.dart @@ -0,0 +1,42 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'dart:io'; +import 'package:haveno_app/services/platform_system_service/android_platform_service.dart'; +import 'package:haveno_app/services/platform_system_service/linux_platform_service.dart'; +import 'package:haveno_app/services/platform_system_service/schema.dart'; + +Future getPlatformService() async { + + late PlatformService platformService; + + if (Platform.isAndroid) { + platformService = AndroidPlatformService(); + } else if (Platform.isLinux) { + platformService = LinuxPlatformService(); + } else { + throw UnsupportedError('Unsupported platform'); + } + + await platformService.init(); // Ensure init is called + return platformService; +} diff --git a/lib/services/platform_system_service/linux_platform_service.dart b/lib/services/platform_system_service/linux_platform_service.dart new file mode 100644 index 0000000..151ced0 --- /dev/null +++ b/lib/services/platform_system_service/linux_platform_service.dart @@ -0,0 +1,197 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'dart:convert'; +import 'dart:io'; +import 'package:haveno_app/services/platform_system_service/schema.dart'; +import 'package:haveno_app/services/secure_storage_service.dart'; +import 'package:haveno_app/utils/kill.dart'; +import 'package:haveno_app/utils/dependancy_helper.dart'; +import 'package:haveno_app/versions.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:path/path.dart' as path; + +class LinuxPlatformService implements PlatformService { + final SecureStorageService secureStorageService = SecureStorageService(); + List executionArgs = [ + '--baseCurrencyNetwork=XMR_MAINNET', + '--useLocalhostForP2P=false', + '--useDevPrivilegeKeys=false', + '--nodePort=9999', + '--apiPort=3201', + '--appName=haveno_app_mainnet', + '--useNativeXmrWallet=false', + '--torControlHost=127.0.0.1', + '--torControlPort=9077', + '--torControlPassword=boner', + '--seedNodes=5i6blbmuflq4s4im6zby26a7g22oef6kyp7vbwyru6oq5e36akzo3ayd.onion:2001,dx4ktxyiemjc354imehuaswbhqlidhy62b4ifzigk5p2rb37lxqbveqd.onion:2002' + ]; + late Directory? applicationSupportDirectory; + late File? torBinaryFile; + late File? javaBinaryFile; + late File? havenoJarFile; + late String? daemonPassword; + late String moneroWalletDir; + Process? torDaemonProcess; + Process? havenoDaemonProcess; + List? allProcesses = []; + + @override + Future init() async { + applicationSupportDirectory = await getApplicationSupportDirectory(); + //daemonPassword = await secureStorageService.readHavenoDaemonPassword(); + await checkShouldDownloadTor('Tor'); + await checkShouldDownloadJava('Java'); + await checkShouldDownloadHavenoDaemon('Haveno Daemon'); + await checkShouldDownloadMonero('Haveno Daemon/data'); + torBinaryFile = await _getTorBinaryFile(); + javaBinaryFile = await _getJavaBinaryFile(); + havenoJarFile = await _getHavenoDaemonJarFile(); + moneroWalletDir = await _getMoneroWalletDir(); + _killProcesses(); + } + + @override + Future setupTorDaemon() async { + torDaemonProcess = await _startTorService(); + if (torDaemonProcess != null) { + allProcesses?.add(torDaemonProcess!); + await _updateStoredPids(); + + // Monitor STDOUT + torDaemonProcess!.stdout.transform(utf8.decoder).listen((data) { + if (data.contains("Bootstrapped 100% (done)")) { + print('Tor stdout: $data'); + } + print('Tor stdout: $data'); + }); + + // Monitor STDERR + torDaemonProcess!.stderr.transform(utf8.decoder).listen((data) { + print('Proc(Tor): $data'); + }); + + return true; + } + return false; + } + + @override + Future setupHavenoDaemon(String? password) async { + daemonPassword = password; + + await _startHavenoService(); + + if (havenoDaemonProcess != null) { + allProcesses?.add(torDaemonProcess!); + await _updateStoredPids(); + havenoDaemonProcess!.stdout.transform(utf8.decoder).listen((data) { + print('Haveno Daemon stdout: $data'); + }); + + havenoDaemonProcess!.stderr.transform(utf8.decoder).listen((data) { + print('Haveno Daemon stderr: $data'); + }); + + return true; + } + return false; + } + + Future _killProcesses() async { + try { + // Kill existing PIDs for the same process + List pids = await secureStorageService.readPids(); + killPids(pids); + } catch (e) { + print("No processes to kill, probable already dead..."); + } + } + + Future _updateStoredPids() async { + List pids = allProcesses!.map((process) => process.pid).toList(); + await secureStorageService.writePids(pids); + } + + Future _getHavenoDaemonJarFile() async { + Directory appSupportDirectory = await getApplicationSupportDirectory(); + String havenoJarFile = + path.join(appSupportDirectory.path, 'Haveno Daemon', 'daemon-all.jar'); + File file = File(havenoJarFile); + print("Haveno Jar: ${file.path}"); + return await file.exists() ? file : null; + } + + Future _getMoneroWalletDir() async { + Directory appSupportDirectory = await getApplicationSupportDirectory(); + moneroWalletDir = path.join(appSupportDirectory.path, 'Monero Wallet RPC', 'wallets'); + print("Monero Wallet Dir: $moneroWalletDir"); + return moneroWalletDir; + } + + Future _getJavaBinaryFile() async { + File javaBinaryFile = File(path.join(applicationSupportDirectory!.path, + 'Java', '21.0.4+7', 'bin', 'java')); + print("Java Binary: ${javaBinaryFile.path}"); + return await javaBinaryFile.exists() ? javaBinaryFile : null; + } + + Future _getTorBinaryFile() async { + File torBinary = + File(path.join(applicationSupportDirectory!.path, 'Tor', Versions().getVersion('tor'), 'tor')); + print("Tor Binary: ${torBinary.path}"); + return torBinary; + } + + Future _startHavenoService() async { + if (daemonPassword != null) { + executionArgs.add('--apiPassword=$daemonPassword'); + } + String havenoAppDirectory = + path.join(applicationSupportDirectory!.path, 'Haveno Daemon', 'data'); + executionArgs.add('--appDataDir=$havenoAppDirectory'); + + Map environment = { + 'JAVA_HOME': path.join(applicationSupportDirectory!.path, 'Java', + '21.0.4+7') + }; + + return await Process.start( + javaBinaryFile!.path, + ['-jar', havenoJarFile!.path, ...executionArgs], + mode: ProcessStartMode.normal, + workingDirectory: + path.join(applicationSupportDirectory!.path, 'Haveno Daemon'), + environment: environment, + ); + } + + Future _startTorService() async { + return await Process.start( + torBinaryFile!.path, + ['-f', '../torrc'], + mode: ProcessStartMode.normal, + workingDirectory: path.join(applicationSupportDirectory!.path, 'Tor', Versions().getVersion("tor")), + //environment: environment, + ); + } +} diff --git a/lib/services/platform_system_service/schema.dart b/lib/services/platform_system_service/schema.dart new file mode 100644 index 0000000..b28d375 --- /dev/null +++ b/lib/services/platform_system_service/schema.dart @@ -0,0 +1,32 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +abstract class PlatformService { + Future init(); + Future setupTorDaemon(); + Future setupHavenoDaemon(String? password); +} + +abstract class MobilePlatformService { + Future init(); + Future intializeHavenoConnection(String host, String port, String password); +} \ No newline at end of file diff --git a/lib/services/secure_storage_service.dart b/lib/services/secure_storage_service.dart new file mode 100644 index 0000000..9de8cfa --- /dev/null +++ b/lib/services/secure_storage_service.dart @@ -0,0 +1,480 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'dart:convert'; +import 'package:flutter/foundation.dart'; +import 'package:haveno_app/models/haveno/p2p/haveno_seednode.dart'; +import 'package:haveno_app/models/haveno_daemon_config.dart'; +import 'package:haveno_app/models/tor/hsv3_onion_config.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class SecureStorageService { + static final SecureStorageService _instance = + SecureStorageService._internal(); + + // Factory constructor to return the same instance every time + factory SecureStorageService() { + return _instance; + } + + SecureStorageService._internal(); // Private constructor for singleton + + Future get _prefs async => + await SharedPreferences.getInstance(); + + // Should basically never used this, so dont... unless it's like temporary + Future init() async { + // Initialize anything here if needed + } + + Future writeUserPassword(String userPassword) async { + try { + final prefs = await _prefs; + await prefs.setString('user_password', userPassword); + } catch (e) { + print("Failed to set user password: $e"); + rethrow; + } + } + + Future readUserPassword() async { + try { + final prefs = await _prefs; + return prefs.getString('user_password'); + } catch (e) { + print("Failed to read user password: $e"); + rethrow; + } + } + + // Preferred Language + Future writeSettingsPreferredLanguage(String languageCode) async { + try { + final prefs = await _prefs; + await prefs.setString('settings.preferred_language', languageCode); + } catch (e) { + print("Failed to set preferred language: $e"); + rethrow; + } + } + + Future readSettingsPreferredLanguage() async { + try { + final prefs = await _prefs; + return prefs.getString('settings.preferred_language'); + } catch (e) { + print("Failed to get preferred language: $e"); + rethrow; + } + } + + // Country + Future writeSettingsCountry(String country) async { + try { + final prefs = await _prefs; + await prefs.setString('settings.country', country); + } catch (e) { + print("Failed to set country: $e"); + rethrow; + } + } + + Future readSettingsCountry() async { + try { + final prefs = await _prefs; + return prefs.getString('settings.country'); + } catch (e) { + print("Failed to get country: $e"); + rethrow; + } + } + + // Preferred Currency + Future writeSettingsPreferredCurrency(String currencyCode) async { + try { + final prefs = await _prefs; + await prefs.setString('settings.preferred_currency', currencyCode); + } catch (e) { + print("Failed to set preferred currency: $e"); + rethrow; + } + } + + Future readSettingsPreferredCurrency() async { + try { + final prefs = await _prefs; + return prefs.getString('settings.preferred_currency'); + } catch (e) { + print("Failed to get preferred currency: $e"); + rethrow; + } + } + + // Blockchain Explorer + Future writeSettingsBlockchainExplorer(String explorer) async { + try { + final prefs = await _prefs; + await prefs.setString('settings.blockchain_explorer', explorer); + } catch (e) { + print("Failed to set blockchain explorer: $e"); + rethrow; + } + } + + Future readSettingsBlockchainExplorer() async { + try { + final prefs = await _prefs; + return prefs.getString('settings.blockchain_explorer'); + } catch (e) { + print("Failed to get blockchain explorer: $e"); + rethrow; + } + } + + // Max Deviation from Market Price + Future writeSettingsMaxDeviationFromMarketPrice( + String deviation) async { + try { + final prefs = await _prefs; + await prefs.setString( + 'settings.max_deviation_from_market_price', deviation); + } catch (e) { + print("Failed to set max deviation from market price: $e"); + rethrow; + } + } + + Future readSettingsMaxDeviationFromMarketPrice() async { + try { + final prefs = await _prefs; + return prefs.getString('settings.max_deviation_from_market_price'); + } catch (e) { + print("Failed to get max deviation from market price: $e"); + rethrow; + } + } + + // Hide Non-Supported Payment Methods + Future writeSettingsHideNonSupportedPaymentMethods(bool value) async { + try { + final prefs = await _prefs; + await prefs.setBool('settings.hide_non_supported_payment_methods', value); + } catch (e) { + print("Failed to set hide non-supported payment methods: $e"); + rethrow; + } + } + + Future readSettingsHideNonSupportedPaymentMethods() async { + try { + final prefs = await _prefs; + return prefs.getBool('settings.hide_non_supported_payment_methods'); + } catch (e) { + print("Failed to get hide non-supported payment methods: $e"); + rethrow; + } + } + + // Sort Market Lists by Number of Offers/Trades + Future writeSettingsSortMarketListsByNumberOfOffersTrades( + bool value) async { + try { + final prefs = await _prefs; + await prefs.setBool( + 'settings.sort_market_lists_by_number_of_offers_trades', value); + } catch (e) { + print("Failed to set sort market lists by number of offers/trades: $e"); + rethrow; + } + } + + Future readSettingsSortMarketListsByNumberOfOffersTrades() async { + try { + final prefs = await _prefs; + return prefs + .getBool('settings.sort_market_lists_by_number_of_offers_trades'); + } catch (e) { + print("Failed to get sort market lists by number of offers/trades: $e"); + rethrow; + } + } + + // Use Dark Mode + Future writeSettingsUseDarkMode(bool value) async { + try { + final prefs = await _prefs; + await prefs.setBool('settings.use_dark_mode', value); + } catch (e) { + print("Failed to set use dark mode: $e"); + rethrow; + } + } + + Future readSettingsUseDarkMode() async { + try { + final prefs = await _prefs; + return prefs.getBool('settings.use_dark_mode'); + } catch (e) { + print("Failed to get use dark mode: $e"); + rethrow; + } + } + + // Auto Withdraw to New Stealth Address + Future writeSettingsAutoWithdrawToNewStealthAddress(bool value) async { + try { + final prefs = await _prefs; + await prefs.setBool( + 'settings.auto_withdraw_to_new_stealth_address', value); + } catch (e) { + print("Failed to set auto withdraw to new stealth address: $e"); + rethrow; + } + } + + Future readSettingsAutoWithdrawToNewStealthAddress() async { + try { + final prefs = await _prefs; + return prefs.getBool('settings.auto_withdraw_to_new_stealth_address'); + } catch (e) { + print("Failed to get auto withdraw to new stealth address: $e"); + rethrow; + } + } + + Future writeOnboardingStatus(bool completed) async { + try { + final prefs = await _prefs; + await prefs.setBool('onboarding_completed', completed); + } catch (e) { + print("Failed to set onboarding status: $e"); + rethrow; + } + } + + Future readOnboardingStatus() async { + try { + final prefs = await _prefs; + return prefs.getBool('onboarding_completed'); + } catch (e) { + print("Failed to get onboarding status: $e"); + rethrow; + } + } + + Future writePids(List pids) async { + try { + final prefs = await _prefs; + await prefs.setString('pids', jsonEncode(pids)); + } catch (e) { + print('Error writing pids: $e'); + } + } + + Future> readPids() async { + try { + final prefs = await _prefs; + final pidsString = prefs.getString('pids'); + if (pidsString != null) { + List pidsDynamic = jsonDecode(pidsString); + List pids = pidsDynamic.cast(); + return pids; + } + } catch (e) { + print('Error reading pids: $e'); + } + return []; + } + + // Desktop Only Methods + Future writeDesktopClientHiddenServiceConfig( + HSV3OnionConfig hiddenServiceConfig) async { + if (!_isDesktopPlatform()) { + print("This method is only available on desktop platforms."); + return false; + } + try { + final prefs = await _prefs; + await prefs.setString('desktop_client_hidden_service_config', + jsonEncode(hiddenServiceConfig.toJson())); + return true; + } catch (e) { + print("Failed to write desktop client hidden service config: $e"); + rethrow; + } + } + + Future readDesktopClientHiddenServiceConfig() async { + if (!_isDesktopPlatform()) { + final prefs = await _prefs; + final jsonString = + prefs.getString('desktop_client_hidden_service_config'); + if (jsonString == null) return null; + final jsonMap = jsonDecode(jsonString); + return HSV3OnionConfig.fromJson(jsonMap); + } + return null; + } + + // Desktop Only Methods + Future writeHavenoDaemonPassword(String password) async { + try { + final prefs = await _prefs; + await prefs.setString( + 'desktop_generated_haveno_daemon_password', password); + return password; + } catch (e) { + print("Failed to write daemon password: $e"); + rethrow; + } + } + + Future readHavenoDaemonPassword() async { + try { + final prefs = await _prefs; + var value = prefs.getString('desktop_generated_haveno_daemon_password'); + return (value != null && value.isNotEmpty) ? value : null; + } catch (e) { + print("Failed to read daemon password: $e"); + rethrow; + } + } + + // Mobile Only Methods + Future writeHavenoDaemonConfig(HavenoDaemonConfig config, + {String? identifier}) async { + try { + final prefs = await _prefs; + final key = identifier ?? 'default_haveno_daemon'; + await prefs.setString(key, jsonEncode(config.toJson())); + } catch (e) { + print("Failed to write daemon config: $e"); + rethrow; + } + } + + Future readHavenoDaemonConfig( + {String? identifier}) async { + try { + final prefs = await _prefs; + final key = identifier ?? 'default_haveno_daemon'; + final jsonString = prefs.getString(key); + if (jsonString == null) return null; + final jsonMap = jsonDecode(jsonString); + return HavenoDaemonConfig.fromJson(jsonMap); + } catch (e) { + print("Failed to read daemon config: $e"); + rethrow; + } + } + + Future deleteHavenoDaemonConfig({String? identifier}) async { + try { + final prefs = await _prefs; + final key = identifier ?? 'default_haveno_daemon'; + await prefs.remove(key); + } catch (e) { + print("Failed to delete daemon config: $e"); + rethrow; + } + } + + + Future> readHavenoSeedNodes() async { + final prefs = await SharedPreferences.getInstance(); + final serializedNodes = prefs.getStringList('seed_nodes'); + + if (serializedNodes == null) { + return []; + } + + return serializedNodes.map((node) { + final jsonData = jsonDecode(node); + return HavenoSeedNode.fromJson(jsonData); + }).toList(); + } + + Future writeHavenoSeedNodes(List nodes) async { + final prefs = await SharedPreferences.getInstance(); + final serializedNodes = nodes.map((node) => jsonEncode(node.toJson())).toList(); + await prefs.setStringList('seed_nodes', serializedNodes); + } + + Future> listDaemonOnionKeys() async { + try { + final prefs = await _prefs; + final allKeys = prefs.getKeys(); + return allKeys.where((key) => key.contains('daemon_onion')).toList(); + } catch (e) { + print("Failed to list daemon onion keys: $e"); + rethrow; + } + } + + Future deleteDaemonOnion({String? identifier}) async { + try { + final prefs = await _prefs; + final key = identifier ?? 'default_daemon_onion'; + await prefs.remove(key); + } catch (e) { + print("Failed to delete daemon onion: $e"); + rethrow; + } + } + + Future writeConnectionStatus({String? identifier}) async { + try { + final prefs = await _prefs; + final key = identifier ?? 'default_connection_status'; + await prefs.remove(key); + } catch (e) { + print("Failed to delete connection status: $e"); + rethrow; + } + } + + Future readConnectionStatus() async { + try { + final prefs = await _prefs; + const key = 'connection_status'; + prefs.getString(key); + } catch (e) { + print("Failed to read connection status: $e"); + rethrow; + } + } + + Future deleteAll() async { + try { + final prefs = await _prefs; + prefs.clear(); + } catch (e) { + print("Failed to destory all shared preferences: $e"); + } + } + + bool _isDesktopPlatform() { + return kIsWeb || + [TargetPlatform.windows, TargetPlatform.linux, TargetPlatform.macOS] + .contains(defaultTargetPlatform); + } +} diff --git a/lib/services/security.dart b/lib/services/security.dart new file mode 100644 index 0000000..cee549c --- /dev/null +++ b/lib/services/security.dart @@ -0,0 +1,143 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'dart:convert'; +import 'dart:math'; +import 'package:flutter/foundation.dart'; +import 'package:haveno_app/services/secure_storage_service.dart'; +import 'package:haveno_app/utils/database_helper.dart'; +import 'package:pointycastle/api.dart'; +import 'package:pointycastle/digests/sha256.dart'; +import 'package:pointycastle/key_derivators/api.dart'; +import 'package:pointycastle/key_derivators/pbkdf2.dart'; +import 'package:pointycastle/macs/hmac.dart'; + +class SecurityService { + final SecureStorageService _secureStorage = SecureStorageService(); + final DatabaseHelper _databaseHelper = DatabaseHelper.instance; + + Future setupUserPassword(String userPassword) async { + final salt = _generateSalt(); + final hashedPassword = _hashPassword(userPassword, salt); + final encryptedPassword = _encrypt('$salt:$hashedPassword', userPassword); + await _secureStorage.writeUserPassword(encryptedPassword); + } + + Future authenticateUserPassword(String inputPassword) async { + final encryptedPassword = await _secureStorage.readUserPassword(); + if (encryptedPassword == null) { + return false; + } + + final decryptedPassword = _decrypt(encryptedPassword, inputPassword); + if (decryptedPassword == null) { + return false; + } + + final parts = decryptedPassword.split(':'); + if (parts.length != 2) { + return false; + } + + final salt = parts[0]; + final storedHashedPassword = parts[1]; + final inputHashedPassword = _hashPassword(inputPassword, salt); + return storedHashedPassword == inputHashedPassword; + } + + String _generateSalt([int length = 16]) { + final random = Random.secure(); + final saltBytes = List.generate(length, (_) => random.nextInt(256)); + return base64Url.encode(saltBytes); + } + + String _hashPassword(String password, String salt) { + final saltBytes = base64Url.decode(salt); + final pbkdf2 = PBKDF2KeyDerivator(HMac(SHA256Digest(), 64)) + ..init(Pbkdf2Parameters(saltBytes, 10000, 32)); + final key = pbkdf2.process(utf8.encode(password)); + return base64Url.encode(key); + } + + // Encrypts a value using AES + String _encrypt(String value, String password) { + final key = _deriveKey(password); + final iv = _generateIV(); + final cipher = _initCipher(true, key, iv); + final input = utf8.encode(value); + final encrypted = cipher.process(input); + final encryptedData = base64Url.encode(encrypted); + final encodedIV = base64Url.encode(iv); + return '$encodedIV:$encryptedData'; + } + + // Decrypts a value using AES + String? _decrypt(String encryptedValue, String password) { + try { + final parts = encryptedValue.split(':'); + if (parts.length != 2) { + return null; + } + final iv = base64Url.decode(parts[0]); + final encryptedData = base64Url.decode(parts[1]); + final key = _deriveKey(password); + final cipher = _initCipher(false, key, iv); + final decrypted = cipher.process(encryptedData); + return utf8.decode(decrypted); + } catch (e) { + print('Decryption failed: $e'); + return null; + } + } + + // derives an AES key from the password using PBKDF2 + KeyParameter _deriveKey(String password, {int iterations = 10000, int keyLength = 32}) { + final salt = utf8.encode('my_salt'); // use fix salt no issue + final pbkdf2 = PBKDF2KeyDerivator(HMac(SHA256Digest(), 64)) + ..init(Pbkdf2Parameters(salt, iterations, keyLength)); + final key = pbkdf2.process(utf8.encode(password)); + return KeyParameter(key); + } + + // denerates an AES cipher for encryption or decryption + PaddedBlockCipher _initCipher(bool forEncryption, KeyParameter key, Uint8List iv) { + final params = PaddedBlockCipherParameters, Null>( + ParametersWithIV(key, iv), null); + final cipher = PaddedBlockCipher('AES/CBC/PKCS7'); + cipher.init(forEncryption, params); + return cipher; + } + + // Generates a random IV for AES encryption + Uint8List _generateIV([int length = 16]) { + final random = Random.secure(); + final iv = List.generate(length, (_) => random.nextInt(256)); + return Uint8List.fromList(iv); + } + + Future resetAppData() async { + // Wipe the secure storage + await _secureStorage.deleteAll(); + // Wipe the database + await _databaseHelper.destroyDatabase(); + } +} diff --git a/lib/services/tor/tor_log_service.dart b/lib/services/tor/tor_log_service.dart new file mode 100644 index 0000000..78eea03 --- /dev/null +++ b/lib/services/tor/tor_log_service.dart @@ -0,0 +1,85 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +/* import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:haveno_app/models/schema.dart'; + +class TorLogService { + static final TorLogService _instance = TorLogService._internal(); + + final List _stdOutLogs = []; + final List _stdErrLogs = []; + + final StreamController _stdOutStreamController = StreamController.broadcast(); + final StreamController _stdErrStreamController = StreamController.broadcast(); + + static const int _logLimit = 200; // Limit the number of logs kept + + factory TorLogService() { + return _instance; + } + + TorLogService._internal(); + + // Start listening for Tor log events + void startListening(EventDispatcher dispatcher) { + dispatcher.subscribe(BackgroundEventType.torStdOutLog, _onStdOutLogReceived); + dispatcher.subscribe(BackgroundEventType.torStdErrLog, _onStdErrLogReceived); + } + + void _onStdOutLogReceived(Map data) { + final log = data['details'] ?? 'No details available'; + _addLog(_stdOutLogs, log); + + // Debug print stderr logs + print("[Tor StdOut] $log"); + + _stdOutStreamController.add(null); + } + + void _onStdErrLogReceived(Map data) { + final log = data['details'] ?? 'No details available'; + _addLog(_stdErrLogs, log); + + // Debug print stderr logs + print("[Tor StdErr] $log"); + + _stdErrStreamController.add(null); + } + + void _addLog(List logList, String log) { + logList.add(log); + if (logList.length > _logLimit) { + logList.removeAt(0); // Remove the oldest log to stay within the limit + } + } + + // Expose logs and streams for external listeners + List get stdOutLogs => List.unmodifiable(_stdOutLogs); + List get stdErrLogs => List.unmodifiable(_stdErrLogs); + + Stream get stdOutStream => _stdOutStreamController.stream; + Stream get stdErrStream => _stdErrStreamController.stream; +} + */ \ No newline at end of file diff --git a/lib/services/tor/tor_status_service.dart b/lib/services/tor/tor_status_service.dart new file mode 100644 index 0000000..ae768d4 --- /dev/null +++ b/lib/services/tor/tor_status_service.dart @@ -0,0 +1,83 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +/* import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_socks_proxy/socks_proxy.dart'; +import 'package:haveno_app/models/schema.dart'; + +class TorStatusService { + static final TorStatusService _instance = TorStatusService._internal(); + + String _torStatus = 'Unknown'; + String _torDetails = 'No details available'; + int? _torPort; + + // Completer to track initialization + final Completer _torInitializationCompleter = Completer(); + + final StreamController _statusStreamController = StreamController.broadcast(); + + String get torStatus => _torStatus; + String get torDetails => _torDetails; + int? get torPort => _torPort; + + factory TorStatusService() { + return _instance; + } + + TorStatusService._internal(); + + // Start listening to events before the UI is loaded + void startListening(EventDispatcher dispatcher) { + dispatcher.subscribe(BackgroundEventType.updateTorStatus, _onTorStatusUpdate); + } + + void _onTorStatusUpdate(Map data) { + _torStatus = data['status'] ?? 'Unknown'; + _torDetails = data['details'] ?? 'No details available'; + _torPort = data['port']; + + if (_torStatus == 'started') { + print("Tor listening connected on port $_torPort"); + } else { + print("Tor Status: $_torStatus ($_torDetails)"); + } + + if (_torStatus == 'started' && _torPort != null && !_torInitializationCompleter.isCompleted) { + _torInitializationCompleter.complete(); // Mark initialization as completed + //SocksProxy.initProxy(proxy: 'SOCKS5 127.0.0.1:$_torPort'); + } + + _statusStreamController.add(null); + } + + // Expose a stream for external listeners to subscribe to status updates + Stream get torStatusStream => _statusStreamController.stream; + + // Method to await until Tor is fully initialized + Future waitForInitialization() { + return _torInitializationCompleter.future; + } +} + */ \ No newline at end of file diff --git a/lib/services/tor_interface.dart b/lib/services/tor_interface.dart new file mode 100644 index 0000000..4c4de33 --- /dev/null +++ b/lib/services/tor_interface.dart @@ -0,0 +1,334 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'package:flutter/foundation.dart'; +import 'package:cryptography/cryptography.dart'; +import 'package:haveno_app/models/tor/tor_daemon_config.dart'; +import 'package:haveno_app/models/tor/hsv3_onion_config.dart'; +// import 'package:haveno_app/services/tor/tor_status_service.dart'; +import 'package:http/io_client.dart'; +//import 'package:tor/socks_socket.dart'; + +class TorService { + final StreamController _statusController = StreamController.broadcast(); + final StreamController _controlPortStatusController = StreamController.broadcast(); + late final TorDaemonConfig _torDaemonConfig; + + Stream get statusStream => _statusController.stream; + Stream get controlPortStatusStream => _controlPortStatusController.stream; + + TorService(); + + static Future isTorConnected() async { + //final torStatusService = TorStatusService(); + int socksPort = 9066; + bool socks5Open = false; + bool i2pOpen = false; + //if (Platform.isAndroid || Platform.isIOS) { + // if (torStatusService.torPort == null) { + // return false; + // } else { + // socksPort = torStatusService.torPort!; + // } + //} + + try { + if (Platform.isMacOS || Platform.isLinux || Platform.isWindows) { + socksPort = 9066; // Example port, adjust as needed + socks5Open = await checkSocks5Proxy(testHost: '127.0.0.1', testPort: socksPort); + if (!socks5Open) { + print("SOCKS5 proxy port $socksPort is not open at 127.0.0.1 (It should be, start the daemon...)"); + return false; // If the SOCKS5 proxy port is closed, return false + } else { + print("The socks5 port is open at $socksPort"); + return await checkTorViaApiStandardBinary(); + } + } else if (Platform.isAndroid) { + socks5Open = await checkSocks5Proxy(testHost: '127.0.0.1', testPort: 9050); + i2pOpen = await checkI2pTunnel(); + if (!socks5Open && !i2pOpen) { + return false; + } else { + print("I2P: $i2pOpen TOR: $socks5Open"); + if (i2pOpen) { + return true; + } + } + } else if (Platform.isIOS) { + // Skip the SOCKS5 proxy check since we don't know the port, proceed directly to checking Tor connection via .onion + // We can find the port using OrbotKit implementation later + return await checkTorOnionLink(); + } + } catch (e) { + print("Error when Tor via both Onion and Clearnet links: $e"); + return false; + } + + // If the SOCKS5 port is open, proceed with the actual request + try { + return await checkTorViaApiStandardBinary(); + } catch (e) { + print("Error during the Tor connection check: $e"); + return false; + } + } + + + // Function to check Tor connection via .onion link on iOS + static Future checkTorOnionLink() async { + final httpClient = HttpClient(); + try { + final request = await httpClient.headUrl(Uri.parse('http://2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion/robots.txt')); + final response = await request.close(); + + return response.statusCode == 200; + } catch (e) { + print("Error during .onion connection check: $e"); + try { + print('Attempting to check Tor connection via non-onion URL'); + final request = await httpClient.headUrl(Uri.parse('https://check.torproject.org/api/ip')); + final response = await request.close(); + + return response.statusCode == 200; + } catch (e) { + throw Exception('No connection to tor what soever, even through clearnet link'); + } + } + } + + // Function to check Tor connection via Tor Project API + // Make a proxied HTTP request using IOClient + static Future checkTorViaApiStandardBinary() async { + // Wrap the HttpClient in an IOClient for modern HTTP API + final ioClient = IOClient(createTorHttpClient()); + + try { + // Perform the HTTP GET request + final response = await ioClient.get(Uri.parse('https://check.torproject.org/api/ip')); + + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + print("Response from Tor website: $data"); + return data['IsTor'] == true; // Return true if IsTor is true + } else { + return false; // Return false if the API didn't return 200 OK + } + } catch (e) { + print("Error during Tor Project API check: $e"); + return false; + } finally { + ioClient.close(); // Always close the client + } + } + + static Future checkI2pTunnel({String testHost = '127.0.0.1', int? testPort}) async { + try { + final socket = await Socket.connect(testHost, testPort ?? 3201, timeout: const Duration(seconds: 7)); + socket.destroy(); // Close the socket as soon as the connection is established + return true; // If connected, the SOCKS5 proxy is available + } catch (e) { + print("Error connecting to an I2P tunnel: $e"); + return false; // Connection failed, proxy is not available + } + } + +/* static Future checkTorViaApiWithRustSocks5() async { + final torStatusService = TorStatusService(); + if (torStatusService.torPort == null) { + print("Tor port is not available."); + return false; + } + + try { + // Step 1: Create and connect to SOCKSSocket with SSL enabled + final socksSocket = await SOCKSSocket.create( + proxyHost: InternetAddress.loopbackIPv4.address, + proxyPort: torStatusService.torPort!, + sslEnabled: true, // Enable SSL for secure communication + ); + + // Connect to the Tor Project API through the SOCKS5 proxy + await socksSocket.connect(); + await socksSocket.connectTo('check.torproject.org', 443); // HTTPS port + + // Step 2: Send the HTTPS request manually + const request = 'GET /api/ip HTTP/1.1\r\n' + 'Host: check.torproject.org\r\n' + 'Connection: close\r\n\r\n'; + socksSocket.write(request); + + // Step 3: Collect the entire response + StringBuffer responseBuffer = StringBuffer(); + await for (var data in socksSocket.responseController.stream) { + responseBuffer.write(utf8.decode(data)); + } + + // Step 4: Extract and debug the response + final responseString = responseBuffer.toString(); + print('Full response from Tor Project API: $responseString'); + + // Step 5: Split the response into headers and body + final responseParts = responseString.split('\r\n\r\n'); + if (responseParts.length < 2) { + print('Invalid response format'); + return false; + } + + final body = responseParts[1]; // Extract the body (JSON content) + print('Extracted body: $body'); + + // Step 6: Parse the JSON body and check the IsTor field + final data = jsonDecode(body); + print('Parsed JSON: $data'); + + // Step 7: Close the SOCKSSocket + if (data['IsTor'] == true) { + socksSocket.close(); + print("Tor is 100% connected."); + return true; + } else { + socksSocket.close(); + print("IsTor contained ${data['IsTor']} and failed!"); + return false; + } + } catch (e) { + print("Error during Tor Project API check: $e"); + return false; + } + } */ + + + Future createHiddenServiceV3KeyPair(String identifier) async { + try { + final algorithm = Ed25519(); + final keyPair = await algorithm.newKeyPair(); + final privateKeyBytes = await keyPair.extractPrivateKeyBytes(); + final publicKey = await keyPair.extractPublicKey(); + return HSV3OnionConfig( + privateKeyBytes: privateKeyBytes, + publicKey: publicKey, + internalPort: 3201, + externalPort: 45256); + } catch (e) { + print("Failed to create Hidden Service key pair: $e"); + return null; + } + } + + Future publishOnionViaControlPort(HSV3OnionConfig onionConfig) async { + try { + final socket = await Socket.connect(_torDaemonConfig.host, _torDaemonConfig.controlPort); + final controlPortStream = socket.transform(utf8.decoder as StreamTransformer).transform(const LineSplitter()); + + // Authenticate with the control port + final authenticateCommand = 'AUTHENTICATE "${_torDaemonConfig.hashedPassword}"\r\n'; + socket.write(authenticateCommand); + + await for (var line in controlPortStream) { + if (line.startsWith('250')) { + print('Authentication successful'); + + // Add the hidden service using the private key + final addOnionCommand = 'ADD_ONION ${onionConfig.privateKey} Port=${onionConfig.externalPort},127.0.0.1:${onionConfig.internalPort}\r\n'; + socket.write(addOnionCommand); + + await for (var response in controlPortStream) { + if (response.startsWith('250')) { + print('Hidden service created successfully'); + socket.destroy(); + return true; + } else if (response.startsWith('551')) { + print('Error creating hidden service: $response'); + socket.destroy(); + return false; + } + } + } else if (line.startsWith('515')) { + print('Authentication failed: $line'); + socket.destroy(); + return false; + } + } + } catch (e) { + print('Error: $e'); + return false; + } + return false; + } + + static Future checkControlPort({String? password}) async { + try { + final socket = await Socket.connect('127.0.0.1', 9051, timeout: Duration(seconds: 5)); + if (password != null) { + socket.write('AUTHENTICATE "$password"\r\n'); + await socket.flush(); + final response = await socket.transform(utf8.decoder as StreamTransformer).join(); + socket.destroy(); + return response.contains('250 OK'); + } else { + socket.destroy(); + return true; + } + } catch (e) { + return false; + } + } + + + static Future checkSocks5Proxy({String testHost = '127.0.0.1', int? testPort}) async { + try { + final socket = await Socket.connect(testHost, testPort ?? 9050, timeout: const Duration(seconds: 5)); + socket.destroy(); // Close the socket as soon as the connection is established + return true; // If connected, the SOCKS5 proxy is available + } catch (e) { + print("Error connecting to SOCKS5 proxy: $e"); + return false; // Connection failed, proxy is not available + } + } + + static HttpClient createTorHttpClient() { + final httpClient = HttpClient(); + String proxy; + + if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) { + proxy = "PROXY 127.0.0.1:8888"; // Set your proxy for desktop platforms + } else if (Platform.isAndroid) { + proxy = 'PROXY 127.0.0.1:8118'; // Orbot's default HTTP proxy for Android + } else if (Platform.isIOS) { + proxy = ''; // iOS-specific handling can be implemented here + } else { + proxy = 'PROXY 127.0.0.1:8118'; // Fallback + } + + if (proxy.isNotEmpty) { + httpClient.findProxy = (uri) { + return proxy; // This sets the proxy for HttpClient + }; + } + + return httpClient; + } + +} \ No newline at end of file diff --git a/lib/system_tray.dart b/lib/system_tray.dart new file mode 100644 index 0000000..5815b24 --- /dev/null +++ b/lib/system_tray.dart @@ -0,0 +1,49 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import 'dart:io'; +import 'package:haveno_app/utils/file_utils.dart'; +import 'package:tray_manager/tray_manager.dart'; + +Future intializeSystemTray() async { + final trayManager = TrayManager.instance; + await trayManager.setIcon( + (Platform.isWindows + ? await extractAssetToTemp('assets/icon/app_icon.ico') + : await extractAssetToTemp('assets/icon/app_icon.png')) + ); + Menu menu = Menu( + items: [ + MenuItem( + key: 'tor_status', + label: 'Tor Daemon', + sublabel: 'Running' + ), + MenuItem.separator(), + MenuItem( + key: 'haveno_daemon', + label: 'Haveno Daemon', + sublabel: 'Running' + ), + ], + ); + await trayManager.setContextMenu(menu); +} \ No newline at end of file diff --git a/lib/utils/arch_helper.dart b/lib/utils/arch_helper.dart new file mode 100644 index 0000000..579f151 --- /dev/null +++ b/lib/utils/arch_helper.dart @@ -0,0 +1,76 @@ +import 'dart:io'; + +// Enum to represent CPU architectures +enum Architecture { x86, x86_64, arm, arm64, unknown } + +void main() { + print('Operating System: ${Platform.operatingSystem}'); + print('Architecture: ${getArchitecture()}'); +} + +Architecture getArchitecture() { + if (Platform.isWindows) { + return _getWindowsArchitecture(); + } else if (Platform.isLinux) { + return _getLinuxArchitecture(); + } else if (Platform.isMacOS) { + return _getMacOSArchitecture(); + } + return Architecture.unknown; +} + +Architecture _getWindowsArchitecture() { + String arch = Platform.environment['PROCESSOR_ARCHITECTURE'] ?? ''; + if (arch.contains('AMD64') || arch.contains('x86_64')) { + return Architecture.x86_64; + } else if (arch.contains('x86')) { + return Architecture.x86; + } else if (arch.contains('ARM')) { + return Architecture.arm; + } + return Architecture.unknown; +} + +Architecture _getLinuxArchitecture() { + if (Platform.isLinux) { + try { + var result = Process.runSync('uname', ['-m']); + if (result.exitCode == 0) { + String output = result.stdout.toString().trim(); + if (output == 'x86_64') { + return Architecture.x86_64; + } else if (output == 'x86') { + return Architecture.x86; + } else if (output == 'armv7l' || output == 'arm') { + return Architecture.arm; + } else if (output == 'aarch64') { + return Architecture.arm64; + } + } + } catch (e) { + print('Error getting Linux architecture: $e'); + } + } + return Architecture.unknown; +} + +Architecture _getMacOSArchitecture() { + if (Platform.isMacOS) { + try { + var result = Process.runSync('uname', ['-m']); + if (result.exitCode == 0) { + String output = result.stdout.toString().trim(); + if (output == 'x86_64') { + return Architecture.x86_64; + } else if (output == 'x86') { + return Architecture.x86; + } else if (output == 'arm64') { + return Architecture.arm64; + } + } + } catch (e) { + print('Error getting macOS architecture: $e'); + } + } + return Architecture.unknown; +} diff --git a/lib/utils/connection_helper.dart b/lib/utils/connection_helper.dart new file mode 100644 index 0000000..3e2283c --- /dev/null +++ b/lib/utils/connection_helper.dart @@ -0,0 +1,20 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . diff --git a/lib/utils/custom_http_overrides.dart b/lib/utils/custom_http_overrides.dart new file mode 100644 index 0000000..811e9e8 --- /dev/null +++ b/lib/utils/custom_http_overrides.dart @@ -0,0 +1,60 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'dart:io'; + +class MyHttpOverrides extends HttpOverrides { + + MyHttpOverrides(); + + @override + HttpClient createHttpClient(SecurityContext? context) { + HttpClient client = super.createHttpClient(context); + client.findProxy = (Uri uri) { + // Add your local network IP ranges here + if (uri.host.startsWith('192.168.') || + uri.host.startsWith('10.') || + uri.host.startsWith('172.16.') || + uri.host.startsWith('172.17.') || + uri.host.startsWith('172.18.') || + uri.host.startsWith('172.19.') || + uri.host.startsWith('172.20.') || + uri.host.startsWith('172.21.') || + uri.host.startsWith('172.22.') || + uri.host.startsWith('172.23.') || + uri.host.startsWith('172.24.') || + uri.host.startsWith('172.25.') || + uri.host.startsWith('172.26.') || + uri.host.startsWith('172.27.') || + uri.host.startsWith('172.28.') || + uri.host.startsWith('172.29.') || + uri.host.startsWith('172.30.') || + uri.host.startsWith('172.31.')) { + return 'DIRECT'; + } + return "PROXY 127.0.0.1:8118"; + }; + client.badCertificateCallback = + (X509Certificate cert, String host, int port) => true; + return client; + } +} diff --git a/lib/utils/database_helper.dart b/lib/utils/database_helper.dart new file mode 100644 index 0000000..ca4fc8a --- /dev/null +++ b/lib/utils/database_helper.dart @@ -0,0 +1,783 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io' as io; +import 'package:haveno/grpc_models.dart'; +import 'package:haveno/profobuf_models.dart'; +import 'package:path/path.dart' as path; +import 'package:sqflite_common_ffi/sqflite_ffi.dart'; +import 'package:path_provider/path_provider.dart'; + +class DatabaseHelper { + // Private constructor + DatabaseHelper._privateConstructor(); + + // Single instance of DatabaseHelper + static final DatabaseHelper instance = DatabaseHelper._privateConstructor(); + + static Database? _database; + + static bool _isResetting = false; + + // Initialize the sqflite ffi loader + Future _initFfiLoader() async { + sqfliteFfiInit(); // Initialize FFI loader + } + + Future get database async { + if (_isResetting) { + return null; + } + + if (_database != null) return _database!; + _database = await _initDatabase(); + return _database!; + } + + // Initialize the database with FFI support + Future _initDatabase() async { + await _initFfiLoader(); // Initialize FFI + + // Get application directory for cross-platform support + final io.Directory appDocumentsDir = await getApplicationSupportDirectory(); + + // Define the database path + final String databasePath = + path.join(appDocumentsDir.path, 'databases', 'haveno.db'); + + // Use the FFI database factory + var databaseFactory = databaseFactoryFfi; + + return await databaseFactory.openDatabase(databasePath, + options: OpenDatabaseOptions( + version: 1, + onCreate: (db, version) async { + await db.execute('PRAGMA foreign_keys = ON'); + // Create trades table + await db.execute(''' + CREATE TABLE IF NOT EXISTS trades( + tradeId TEXT PRIMARY KEY, + data TEXT + ) + '''); + + // Create disputes table + await db.execute(''' + CREATE TABLE IF NOT EXISTS disputes( + disputeId TEXT PRIMARY KEY, + tradeId TEXT UNIQUE, + data TEXT, + FOREIGN KEY(tradeId) REFERENCES trades(tradeId) ON DELETE CASCADE + ) + '''); + + // Create trade_chat_messages table + await db.execute(''' + CREATE TABLE IF NOT EXISTS trade_chat_messages( + messageId TEXT PRIMARY KEY, + tradeId TEXT, + data TEXT, + FOREIGN KEY(tradeId) REFERENCES trades(tradeId) ON DELETE CASCADE + ) + '''); + + // Create dispute_chat_messages table + await db.execute(''' + CREATE TABLE IF NOT EXISTS dispute_chat_messages( + messageId TEXT PRIMARY KEY, + disputeId TEXT, + data TEXT, + FOREIGN KEY(disputeId) REFERENCES disputes(disputeId) ON DELETE CASCADE + ) + '''); + + // Create payment_methods table + await db.execute(''' + CREATE TABLE IF NOT EXISTS payment_methods( + paymentMethodId TEXT PRIMARY KEY, + data TEXT + ) + '''); + + // Create payment_accounts table + await db.execute(''' + CREATE TABLE IF NOT EXISTS payment_accounts( + paymentAccountId TEXT PRIMARY KEY, + paymentMethodId TEXT, + accountName TEXT, + creationDate INT, + data TEXT, + FOREIGN KEY(paymentMethodId) REFERENCES payment_methods(paymentMethodId) ON DELETE CASCADE + ) + '''); + + // Create payment_account_forms table + await db.execute(''' + CREATE TABLE IF NOT EXISTS payment_account_forms( + paymentAccountFormId TEXT PRIMARY KEY, + paymentMethodId TEXT UNIQUE, + data TEXT + ) + '''); + + // Create trade_statistics table + await db.execute(''' + CREATE TABLE IF NOT EXISTS trade_statistics( + hash BLOB PRIMARY KEY, + hashcode INTEGER, + amount INTEGER, + paymentMethodId TEXT, + date INTEGER, + arbitrator TEXT, + price INTEGER, + currency TEXT, + makerDepositTxnId TEXT, + takerDepositTxnId TEXT, + extraData TEXT, + data TEXT + ) + '''); + + await db.execute(''' + CREATE TABLE IF NOT EXISTS offers( + offerId TEXT PRIMARY KEY, + paymentMethodId TEXT, + direction TEXT, + isMyOffer INTEGER, + ownerNodeAddress TEXT, + baseCurrencyCode TEXT, + counterCurrencyCode TEXT, + date INTEGER, + data TEXT, + hashcode INTEGER + ) + '''); + print("It's run the db init script"); + // Create indexes for tradeId and disputeId + await db.execute( + 'CREATE INDEX IF NOT EXISTS idx_tradeId ON trade_chat_messages(tradeId)'); + await db.execute( + 'CREATE INDEX IF NOT EXISTS idx_disputeId ON dispute_chat_messages(disputeId)'); + }, + )); + } + + // Close the database + Future closeDatabase() async { + final db = await instance.database; + if (db!.isOpen) { + await db.close(); + } + } + + Future destroyDatabase({bool reinitialize = true}) async { + try { + final db = await instance.database; + + // Set the reset flag and initialize the completer if needed + _isResetting = true; + + // Close and delete the database + final databasePath = await getApplicationSupportDirectory(); + final databaseFilePath = path.join(databasePath.path, 'haveno.db'); + + if (db!.isOpen) { + print("Closing database..."); + await db.close(); + print("Database closed."); + } + + print("Deleting database file..."); + await deleteDatabase(databaseFilePath); + print("Database file deleted."); + + // Nullify the current instance of the database + _database = null; + + // Delay to ensure file deletion is completed + await Future.delayed(const Duration(milliseconds: 500)); + + // Optionally reinitialize the database + if (reinitialize) { + print("Reinitializing the database..."); + _database = await _initDatabase(); + print("Database reinitialized."); + } + } catch (e) { + print("Error during database destruction: $e"); + } finally { + // Mark the database as reset and complete the completer + _isResetting = false; + } + } + + // Insert a trade into the database + Future insertTrade(TradeInfo trade) async { + final db = await instance.database; + final tradeJson = jsonEncode(trade.toProto3Json()); + await db!.insert( + 'trades', + { + 'tradeId': trade.tradeId, + 'data': tradeJson, + }, + conflictAlgorithm: ConflictAlgorithm.replace, + ); + } + + Future insertTrades(List trades) async { + final db = await instance.database; + final batch = db!.batch(); + + for (var trade in trades) { + final tradeJson = jsonEncode(trade.toProto3Json()); + + batch.insert( + 'trades', + { + 'tradeId': trade.tradeId, // string + 'data': tradeJson, // json string of proto3 object + }, + conflictAlgorithm: ConflictAlgorithm.replace, + ); + } + + // Commit the batch + await batch.commit(noResult: true); + } + + // Insert a trade chat message into the database + Future insertTradeChatMessage( + ChatMessage message, String tradeId) async { + final db = await instance.database; + final messageJson = jsonEncode(message.toProto3Json()); + await db!.insert( + 'trade_chat_messages', + { + 'messageId': message.uid, + 'tradeId': tradeId, + 'data': messageJson, + }, + conflictAlgorithm: ConflictAlgorithm.replace, + ); + } + + // Insert a dispute into the database + Future insertDispute(Dispute dispute) async { + final db = await instance.database; + final disputeJson = jsonEncode(dispute.toProto3Json()); + await db!.insert( + 'disputes', + { + 'disputeId': dispute.id, + 'tradeId': dispute.tradeId, + 'data': disputeJson, + }, + conflictAlgorithm: ConflictAlgorithm.replace, + ); + } + + // Insert a dispute chat message into the database + Future insertDisputeChatMessage( + ChatMessage message, String disputeId) async { + final db = await instance.database; + final messageJson = jsonEncode(message.toProto3Json()); + await db!.insert( + 'dispute_chat_messages', + { + 'messageId': message.uid, + 'disputeId': disputeId, + 'data': messageJson, + }, + conflictAlgorithm: ConflictAlgorithm.replace, + ); + } + + // Insert a payment method into the database + Future insertPaymentMethod(PaymentMethod paymentMethod) async { + final db = await instance.database; + final paymentMethodJson = jsonEncode(paymentMethod.toProto3Json()); + await db!.insert( + 'payment_methods', + { + 'paymentMethodId': paymentMethod.id, + 'data': paymentMethodJson, + }, + conflictAlgorithm: ConflictAlgorithm.replace, + ); + } + + // Batch insert payment methods + Future insertPaymentMethods(List paymentMethods) async { + final db = await instance.database; + final batch = db!.batch(); + + for (var paymentMethod in paymentMethods) { + final paymentMethodJson = jsonEncode(paymentMethod.toProto3Json()); + + batch.insert( + 'payment_methods', + { + 'paymentMethodId': paymentMethod.id, // string + 'data': paymentMethodJson, // json string of proto3 object + }, + conflictAlgorithm: ConflictAlgorithm.replace, + ); + } + + // Commit the batch + await batch.commit(noResult: true); + } + + Future insertOffer(OfferInfo offer) async { + final db = await instance.database; + final offerJson = jsonEncode(offer.toProto3Json()); + await db!.insert( + 'offers', + { + 'offerId': offer.id, + 'paymentMethodId': offer.paymentMethodId, + 'direction': offer.direction, + 'ownerNodeAddress': offer.ownerNodeAddress, + 'baseCurrencyCode': offer.baseCurrencyCode, + 'counterCurrencyCode': offer.counterCurrencyCode, + 'isMyOffer': offer.isMyOffer ? 1 : 0, + 'date': offer.date.toInt(), + 'data': offerJson, + 'hashcode': offer + .hashCode // should store this canse then we know if the object changed or not + }, + conflictAlgorithm: ConflictAlgorithm.replace, + ); + } + + // Batch insert payment accounts + Future insertOffers(List offers) async { + final db = await instance.database; + final batch = db!.batch(); + + for (var offer in offers) { + final offerJson = jsonEncode(offer.toProto3Json()); + + batch.insert( + 'offers', + { + 'offerId': offer.id, + 'paymentMethodId': offer.paymentMethodId, + 'direction': offer.direction, + 'ownerNodeAddress': offer.ownerNodeAddress, + 'baseCurrencyCode': offer.baseCurrencyCode, + 'counterCurrencyCode': offer.counterCurrencyCode, + 'isMyOffer': offer.isMyOffer ? 1 : 0, + 'date': offer.date.toInt(), + 'data': offerJson, + 'hashcode': offer.hashCode + }, + conflictAlgorithm: ConflictAlgorithm.replace, + ); + } + + // Commit the batch + await batch.commit(noResult: true); + } + + Future insertPaymentAccount(PaymentAccount paymentAccount) async { + final db = await instance.database; + final paymentAccountJson = jsonEncode(paymentAccount.toProto3Json()); + await db!.insert( + 'payment_accounts', + { + 'paymentAccountId': paymentAccount.id, + 'paymentMethodId': paymentAccount.paymentMethod.id, //string + 'accountName': paymentAccount.accountName, + 'creationDate': + paymentAccount.creationDate.toInt(), //int64 but convtered to int + 'data': paymentAccountJson, //json string of proto3 object + }, + conflictAlgorithm: ConflictAlgorithm.replace, + ); + } + + // Batch insert payment accounts + Future insertPaymentAccounts( + List paymentAccounts) async { + final db = await instance.database; + final batch = db!.batch(); + + for (var paymentAccount in paymentAccounts) { + final paymentAccountJson = jsonEncode(paymentAccount.toProto3Json()); + + batch.insert( + 'payment_accounts', + { + 'paymentAccountId': paymentAccount.id, + 'paymentMethodId': paymentAccount.paymentMethod.id, // string + 'accountName': paymentAccount.accountName, + 'creationDate': + paymentAccount.creationDate.toInt(), // int64 but converted to int + 'data': paymentAccountJson, // json string of proto3 object + }, + conflictAlgorithm: ConflictAlgorithm.replace, + ); + } + + // Commit the batch + await batch.commit(noResult: true); + } + + // Insert trade statistic + Future insertTradeStatistic(TradeStatistics3 tradeStatistic) async { + final db = await instance.database; + final tradeStatisticJson = jsonEncode(tradeStatistic.toProto3Json()); + await db!.insert( + 'trade_statistics', + { + 'hashcode': tradeStatistic.hashCode, //int + 'hash': tradeStatistic.hash, //list + 'amount': tradeStatistic.amount.toInt(), //int64 but converted to int + 'paymentMethodId': tradeStatistic.paymentMethod, //string + 'date': tradeStatistic.date.toInt(), //int64 but convtered to int + 'arbitrator': tradeStatistic.arbitrator, //string + 'price': tradeStatistic.price.toInt(), //int64 but converted to int + 'currency': tradeStatistic.currency, //string + 'makerDepositTxnId': tradeStatistic.makerDepositTxId, //string + 'takerDepositTxnId': tradeStatistic.takerDepositTxId, //string + 'extraData': jsonEncode(tradeStatistic + .extraData), //Map but json encoded to stirng... + 'data': tradeStatisticJson, //json string of proto3 object + }, + conflictAlgorithm: ConflictAlgorithm.replace, + ); + } + + // Get trade statistic groups by a specific peroid + Future> getTradeStatistics(String? period) async { + final db = await instance.database; + final List> maps = await db!.query('trade_statistics'); + + // Convert the list of maps to a list of PaymentMethod objects + List tradeStatistics = maps.map((map) { + final String tradeStatisticsJson = map['data']; + return TradeStatistics3.create() + ..mergeFromProto3Json(jsonDecode(tradeStatisticsJson)); + }).toList(); + + //print("Found ${tradeStatistics.length} trade statistics entries in the database."); + + return tradeStatistics; + } + + // Batch delete offers by id or isMyOffer + Future deleteOffers(List? offers, {bool? isMyOffer}) async { + final db = await instance.database; + final batch = db!.batch(); + + if (offers != null && offers.isNotEmpty) { + // Delete by offer id for the provided offers list + for (var offer in offers) { + batch.delete( + 'offers', + where: 'offerId = ?', + whereArgs: [offer.id], + ); + } + } else if (isMyOffer != null) { + // Delete by isMyOffer flag if offers are not provided + batch.delete( + 'offers', + where: 'isMyOffer = ?', + whereArgs: [isMyOffer ? 1 : 0], // 1 for true, 0 for false + ); + } + + // Commit the batch + await batch.commit(noResult: true); + } + + // Get all payment methods + Future> getAllPaymentMethods() async { + final db = await instance.database; + final List> maps = await db!.query('payment_methods'); + + // Convert the list of maps to a list of PaymentMethod objects + List paymentMethods = maps.map((map) { + final String paymentMethodJson = map['data']; + return PaymentMethod.create() + ..mergeFromProto3Json(jsonDecode(paymentMethodJson)); + }).toList(); + + //print("Found ${paymentMethods.length} payment methods in the database."); + + return paymentMethods; + } + + // Get all trades + Future> getAllTrades() async { + final db = await instance.database; + final List> maps = await db!.query('trades'); + + // Convert the list of maps to a list of PaymentMethod objects + List trades = maps.map((map) { + final String tradesJson = map['data']; + return TradeInfo.create()..mergeFromProto3Json(jsonDecode(tradesJson)); + }).toList(); + + return trades; + } + + // Get disputes + Future> getAllDisputes() async { + final db = await instance.database; + final List> maps = await db!.query('disputes'); + + // Convert the list of maps to a list of PaymentMethod objects + List disputes = maps.map((map) { + final String disputesJson = map['data']; + return Dispute.create()..mergeFromProto3Json(jsonDecode(disputesJson)); + }).toList(); + + return disputes; + } + + Future> getOffers( + {String? paymentMethodId, String? direction, bool? isMyOffer}) async { + final db = await instance.database; + + // Build the where clause dynamically based on the provided criteria + final List whereClauses = []; + final List whereArgs = []; + + if (paymentMethodId != null) { + whereClauses.add('paymentMethodId = ?'); + whereArgs.add(paymentMethodId); + } + + if (direction != null) { + whereClauses.add('direction = ?'); + whereArgs.add(direction); + } + + if (isMyOffer != null) { + whereClauses.add('isMyOffer = ?'); + whereArgs.add(isMyOffer + ? 1 + : 0); // Assuming isMyOffer is stored as 1 for true, 0 for false + } + + // Combine the where clauses into a single string + final whereClause = + whereClauses.isNotEmpty ? whereClauses.join(' AND ') : null; + + // Query the database with the dynamic where clause + final List> maps = await db!.query( + 'offers', + where: whereClause, + whereArgs: whereArgs, + ); + + // Convert the list of maps to a list of OfferInfo objects + final List offers = maps.map((map) { + final String offerJson = + map['data']; // Assuming 'data' contains the serialized OfferInfo JSON + return OfferInfo.create()..mergeFromProto3Json(jsonDecode(offerJson)); + }).toList(); + + //print("Found ${offers.length} offers in the database."); + + return offers; + } + + // Get payment method by ID + Future getPaymentMethodById(String paymentMethodId) async { + final db = await instance.database; + final List> maps = await db!.query( + 'payment_methods', + where: 'paymentMethodId = ?', + whereArgs: [paymentMethodId], + limit: 1, + ); + + if (maps.isNotEmpty) { + // The data column contains the JSON string of the PaymentMethod + final String paymentMethodJson = maps.first['data']; + // Deserialize JSON back into a PaymentMethod object + final paymentMethod = PaymentMethod.create() + ..mergeFromProto3Json(jsonDecode(paymentMethodJson)); + return paymentMethod; + } + + throw Exception( + "Could not find payment method with ID $paymentMethodId in database, local state or fetched from remote!"); + } + + // Get trade by ID + Future getTradeById(String tradeId) async { + final db = await instance.database; + final List> maps = await db!.query( + 'trades', + where: 'tradeId = ?', + whereArgs: [tradeId], + limit: 1, + ); + + if (maps.isNotEmpty) { + // The data column contains the JSON string of the PaymentMethod + final String tradeJson = maps.first['data']; + // Deserialize JSON back into a PaymentMethod object + final trade = TradeInfo.create() + ..mergeFromProto3Json(jsonDecode(tradeJson)); + return trade; + } + + throw Exception("Could not find trade with ID $tradeId in database..."); + } + + // Get payment method by ID + Future getDisputeByTradeId(String paymentMethodId) async { + final db = await instance.database; + final List> maps = await db!.query( + 'payment_methods', + where: 'paymentMethodId = ?', + whereArgs: [paymentMethodId], + limit: 1, + ); + + if (maps.isNotEmpty) { + // The data column contains the JSON string of the PaymentMethod + final String paymentMethodJson = maps.first['data']; + // Deserialize JSON back into a PaymentMethod object + final paymentMethod = PaymentMethod.create() + ..mergeFromProto3Json(jsonDecode(paymentMethodJson)); + return paymentMethod; + } + + throw Exception( + "Could not find payment method with ID $paymentMethodId in database, local state or fetched from remote!"); + } + + // Get all payment accounts + Future> getAllPaymentAccounts() async { + final db = await instance.database; + final List> maps = await db!.query('payment_accounts'); + + // Convert the list of maps to a list of PaymentMethod objects + List paymentAccounts = maps.map((map) { + final String paymentAccountsJson = map['data']; + return PaymentAccount.create() + ..mergeFromProto3Json(jsonDecode(paymentAccountsJson)); + }).toList(); + + return paymentAccounts; + } + + // Get payment account form by payment method ID + Future getPaymentAccountFormByPaymentMethodId( + String paymentMethodId) async { + final db = await instance.database; + final List> maps = await db!.query( + 'payment_account_forms', + where: 'paymentMethodId = ?', + whereArgs: [paymentMethodId], + limit: 1, + ); + + if (maps.isNotEmpty) { + // The data column contains the JSON string of the PaymentMethod + final String paymentAccountFormJson = maps.first['data']; + // Deserialize JSON back into a PaymentMethod object + final paymentAccountForm = PaymentAccountForm.create() + ..mergeFromProto3Json(jsonDecode(paymentAccountFormJson)); + return paymentAccountForm; + } else { + return null; + } + } + + // Get all payment methods + Future?> getAllPaymentAccountForms() async { + final db = await instance.database; + final List> maps = + await db!.query('payment_account_forms'); + + // Convert the list of maps to a list of PaymentMethod objects + List? paymentAccountForms = maps.map((map) { + final String paymentAccountFormsJson = map['data']; + return PaymentAccountForm.create() + ..mergeFromProto3Json(jsonDecode(paymentAccountFormsJson)); + }).toList(); + + //print("Found ${paymentAccountForms.length} payment account forms in the database."); + + return paymentAccountForms; + } + + // Insert a payment account into the database + Future insertPaymentAccountForm( + String paymentMethodId, PaymentAccountForm paymentAccountForm) async { + final db = await instance.database; + final paymentAccountFormJson = + jsonEncode(paymentAccountForm.toProto3Json()); + await db!.insert( + 'payment_account_forms', + { + 'paymentAccountFormId': paymentAccountForm.id.name, + 'paymentMethodId': paymentMethodId, + 'data': paymentAccountFormJson, + }, + conflictAlgorithm: ConflictAlgorithm.replace, + ); + } + + // Check if a trade is new + Future isTradeNew(String tradeId) async { + final db = await instance.database; + final result = + await db!.query('trades', where: 'tradeId = ?', whereArgs: [tradeId]); + return result.isEmpty; + } + + // Check if a trade chat message is new + Future isTradeChatMessageNew(String messageId) async { + final db = await instance.database; + final result = await db!.query('trade_chat_messages', + where: 'messageId = ?', whereArgs: [messageId]); + return result.isEmpty; + } + + // Check if a dispute is new + Future isDisputeNew(String disputeId) async { + final db = await instance.database; + final result = await db! + .query('disputes', where: 'disputeId = ?', whereArgs: [disputeId]); + return result.isEmpty; + } + + // Check if a dispute chat message is new + Future isDisputeChatMessageNew(String messageId) async { + final db = await instance.database; + final result = await db!.query('dispute_chat_messages', + where: 'messageId = ?', whereArgs: [messageId]); + return result.isEmpty; + } +} diff --git a/lib/utils/dependancy_helper.dart b/lib/utils/dependancy_helper.dart new file mode 100644 index 0000000..f870f8f --- /dev/null +++ b/lib/utils/dependancy_helper.dart @@ -0,0 +1,385 @@ +import 'dart:io'; +import 'package:background_downloader/background_downloader.dart'; +import 'package:haveno_app/utils/arch_helper.dart'; +import 'package:haveno_app/utils/file_utils.dart'; +import 'package:haveno_app/versions.dart'; +import 'package:archive/archive.dart'; +import 'package:path/path.dart' as path; +import 'package:path_provider/path_provider.dart'; + + +Future checkShouldDownloadMonero(String downloadTo) async { + final applicationSupportDir = await getApplicationSupportDirectory(); + const url = 'https://downloads.getmonero.org/cli/linux64'; // Monero CLI URL + const requiredBinaries = ['monerod', 'monero-wallet-rpc']; + const fileName = 'monero-linux.tar.bz2'; + final downloadPath = path.join(applicationSupportDir.path, downloadTo, fileName); + + // Ensure the download directory exists + final binDir = Directory(path.join(applicationSupportDir.path, downloadTo)); + final versionFilePath = path.join(binDir.path, 'monero_version'); + if (!binDir.existsSync()) { + binDir.createSync(recursive: true); + } + + // Check the current version + String? currentVersion; + if (File(versionFilePath).existsSync()) { + currentVersion = File(versionFilePath).readAsStringSync().trim(); + print('Current version: $currentVersion'); + if (currentVersion == 'v${Versions().getVersion("monero")}') { + return; + } else { + // Could deleteSync here but they will get overritten anyway, it might be approprioate to force pkill, and delete anyway because of the wallet. + } + } + + // Define the download task + final task = DownloadTask( + url: url, + filename: fileName, + directory: downloadTo, + baseDirectory: BaseDirectory.applicationSupport, + updates: Updates.statusAndProgress, + requiresWiFi: false, + retries: 3, + allowPause: false, + ); + + // Start the download + print('Downloading Monero CLI...'); + final result = await FileDownloader().download( + task, + onProgress: (progress) => print('Download Progress: ${progress * 100}%'), + onStatus: (status) => print('Download Status: $status'), + ); + + if (result.status != TaskStatus.complete) { + print('Download failed or was not completed.'); + return; + } + + print('Download complete. Extracting...'); + + // Decompress the .bz2 file + final compressedData = File(downloadPath).readAsBytesSync(); + final decompressedData = BZip2Decoder().decodeBytes(compressedData); + + // Extract the .tar archive + final archive = TarDecoder().decodeBytes(decompressedData); + + // Identify the nested folder (e.g., monero-x86_64-linux-gnu-v0.18.3.4) + String? nestedFolder; + for (final file in archive.files) { + if (file.isFile) { + final parts = path.split(file.name); + if (parts.length > 1) { + nestedFolder = parts.first; + break; + } + } + } + + if (nestedFolder == null) { + print('Could not identify nested folder in the archive.'); + return; + } + + print('Nested folder identified: $nestedFolder'); + + // Extract required binaries from the nested folder + for (final file in archive.files) { + if (file.isFile && requiredBinaries.contains(path.basename(file.name))) { + final relativePath = path.relative(file.name, from: nestedFolder); + final outputPath = path.join(binDir.path, relativePath); + final outputFile = File(outputPath); + outputFile.createSync(recursive: true); + outputFile.writeAsBytesSync(file.content as List); + print('Extracted binary: $outputPath'); + } + } + + // Save the version extracted from the folder name + final latestVersion = nestedFolder.split('-').last; // Extract version from folder name + File(versionFilePath).writeAsStringSync(latestVersion); + print('Updated monero_version to $latestVersion.'); + + // Clean up the downloaded .tar.bz2 file + File(downloadPath).deleteSync(); + print('Cleaned up temporary files.'); + + // Set executable permissions + setExecutablePermissions(path.join(applicationSupportDir.path, downloadTo, 'monerod')); + setExecutablePermissions(path.join(applicationSupportDir.path, downloadTo, 'monero-wallet-rpc')); +} + + +Future checkShouldDownloadHavenoDaemon(String downloadTo) async { + var applicationSupportDir = await getApplicationSupportDirectory(); + const url = 'https://github.com/KewbitXMR/haveno-app/releases/download/0.1.0%2B4/daemon-all.jar'; + const fileName = 'daemon-all.jar'; + final downloadPath = path.join(applicationSupportDir.path, downloadTo, fileName); + + // Ensure the download directory exists + final downloadDir = Directory(downloadTo); + if (!downloadDir.existsSync()) { + downloadDir.createSync(recursive: true); + } + + // Check if the file already exists + if (File(downloadPath).existsSync()) { + print('Haveno Daemon JAR already exists at $downloadPath. No download needed.'); + return; + } + + // Define the download task + final task = DownloadTask( + url: url, + filename: fileName, + directory: downloadTo, + baseDirectory: BaseDirectory.applicationSupport, + updates: Updates.statusAndProgress, // Show progress updates + retries: 3, + allowPause: false, + requiresWiFi: false, + ); + + // Start the download + print('Downloading Haveno Daemon JAR...'); + final result = await FileDownloader().download( + task, + onProgress: (progress) => print('Haveno Daemon Download Progress: ${progress * 100}%'), + onStatus: (status) => print('Haveno Daemon Download Status: $status'), + ); + + // Handle the download result + switch (result.status) { + case TaskStatus.complete: + print('Haveno Daemon JAR downloaded successfully to $downloadPath.'); + break; + case TaskStatus.canceled: + print('Haveno Daemon download was canceled.'); + break; + case TaskStatus.paused: + print('Haveno Daemon download was paused.'); + break; + default: + print('Failed to download Haveno Daemon JAR.'); + break; + } +} + +Future checkShouldDownloadTor(String downloadTo) async { + + // Get the application support directory + final applicationSupportDir = await getApplicationSupportDirectory(); + final downloadFilename = 'tor-expert-bundle.tar.gz'; + final downloadPath = path.join(applicationSupportDir.path, downloadFilename); + + // Get default tor version and make sure it exists + + final version = Versions().getVersion('tor'); + + final url = 'https://dist.torproject.org/torbrowser/$version/tor-expert-bundle-linux-x86_64-$version.tar.gz'; + final torDir = path.join(applicationSupportDir.path, downloadTo, version); + + // Ensure the Tor directory exists + final targetDir = Directory(torDir); + final targetBin = File(path.join(torDir, 'tor')); + if (!targetBin.existsSync()) { + targetDir.createSync(recursive: true); + } else { + print('Tor $version is already installed.'); + return; + } + + // Define the download task + final task = DownloadTask( + url: url, + filename: downloadFilename, + directory: path.join(downloadTo, version), + baseDirectory: BaseDirectory.applicationSupport, + updates: Updates.statusAndProgress, + requiresWiFi: false, + retries: 3, + allowPause: false, + ); + + // Start the download + print('Downloading Tor Expert Bundle...'); + final result = await FileDownloader().download( + task, + onProgress: (progress) => print('Download Progress: ${progress * 100}%'), + onStatus: (status) => print('Download Status: $status'), + ); + + if (result.status != TaskStatus.complete) { + print('Download failed or was not completed.'); + return; + } + + print('Download complete. Extracting from {$torDir}/$downloadFilename...'); + + // Read and decompress the .tar.gz file + final compressedData = File(path.join(torDir, downloadFilename)).readAsBytesSync(); + final tarGzDecoder = GZipDecoder(); + final tarData = tarGzDecoder.decodeBytes(compressedData); + + // Extract the .tar archive + final archive = TarDecoder().decodeBytes(tarData); + for (final file in archive.files) { + final filePath = path.join(torDir, file.name.replaceFirst('tor/', '')); + final fileName = file.name.replaceFirst('tor/', ''); + if (file.isFile) { + if (file.name.startsWith('tor/')) { + final outputFile = File(path.join(torDir, fileName)); + outputFile.createSync(recursive: true); + outputFile.writeAsBytesSync(file.content as List); + print('Extracted file to: ${path.join(torDir, fileName)}}'); + } else { + final outputFile = File(filePath); + outputFile.createSync(recursive: true); + outputFile.writeAsBytesSync(file.content as List); + print('Extracted file to: ${path.join(torDir, fileName)}}'); + } + } else { + Directory(filePath).createSync(recursive: true); + print('Created directory: $filePath'); + } + } + + // Clean up the downloaded .tar.gz file + File(path.join(torDir, downloadFilename)).deleteSync(); + print('Cleaned up temporary files.'); + + // Put the config + if (!File(path.join(torDir, 'torrc')).existsSync()) { + await extractAssetToFile('assets/config/default/torrc', 'Tor/torrc'); + } + + // Set permissions + setExecutablePermissions(path.join(torDir)); + + return; + +} + +Future checkShouldDownloadJava(downloadTo) async { + + // First check if java bin file exists for the version specified + // Get the application support directory + final applicationSupportDir = await getApplicationSupportDirectory(); + final javaDir = path.join(applicationSupportDir.path, 'Java', '21.0.4+7'); + const fileName = 'java.tar.gz'; + final downloadPath = path.join(javaDir, fileName); + File javaExecutableFile = File(path.join(javaDir, 'bin', 'java')); + + if (javaExecutableFile.existsSync()) { + print('The correct version of java is already installed...'); + return; + } else { + // Since we will now continue to install a different version of Java that will be used we'll clear the Java folder completely + Directory(javaDir).deleteSync(); + print("Deleted old Java version as there is a new version required"); + } + + Architecture arch = getArchitecture(); + String? url; + if (arch == Architecture.x86_64) { + url = 'https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.4%2B7/OpenJDK21U-jre_x64_linux_hotspot_21.0.4_7.tar.gz'; + } else if (arch == Architecture.arm64) { + url = 'https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.4%2B7/OpenJDK21U-jre_aarch64_linux_hotspot_21.0.4_7.tar.gz'; + } else { + url = null; + throw Exception("Unsupported operating system architecture"); + } + + // Ensure the Java directory exists + final targetDir = Directory(javaDir); + if (!targetDir.existsSync()) { + targetDir.createSync(recursive: true); + } + + var finalExtractionPath = path.join(downloadTo, '21.0.4+7'); + + // Define the download task + final task = DownloadTask( + url: url, + filename: fileName, + directory: finalExtractionPath, + baseDirectory: BaseDirectory.applicationSupport, + updates: Updates.statusAndProgress, + requiresWiFi: false, + retries: 3, + allowPause: false, + ); + + // Start the download + print('Downloading Java...'); + final result = await FileDownloader().download( + task, + onProgress: (progress) => print('Download Progress: ${progress * 100}%'), + onStatus: (status) => print('Download Status: $status'), + ); + + if (result.status != TaskStatus.complete) { + print('Download failed or was not completed.'); + return; + } + + print('Download complete. Extracting from {$javaDir}/$fileName...'); + + // Read and decompress the .tar.gz file + final compressedData = File(downloadPath).readAsBytesSync(); + final tarGzDecoder = GZipDecoder(); + final tarData = tarGzDecoder.decodeBytes(compressedData); + + // Extract the .tar archive + final archive = TarDecoder().decodeBytes(tarData); + + // Identify the first nested directory if it exists + String? firstNestedDir; + for (final file in archive.files) { + if (file.isFile) { + // Check for the first directory in the archive and store its path + if (firstNestedDir == null) { + final parts = path.split(file.name); + if (parts.length > 1) { + firstNestedDir = parts.first; + } + } + } + } + + // Extract files, skipping the first nested directory + for (final file in archive.files) { + String filePath; + if (firstNestedDir != null && file.name.startsWith(firstNestedDir)) { + // Remove the first nested directory from the file's path + final relativePath = file.name.substring(firstNestedDir.length + 1); // Skip the first nested directory + filePath = path.join(javaDir, relativePath); // Extract directly into javaDir + } else { + filePath = path.join(javaDir, file.name); + } + + if (file.isFile) { + final outputFile = File(filePath); + outputFile.createSync(recursive: true); + outputFile.writeAsBytesSync(file.content as List); + print('Extracted file to: $filePath'); + } else { + Directory(filePath).createSync(recursive: true); + print('Created directory: $filePath'); + } + } + + // Clean up the downloaded .tar.gz file + File(path.join(javaDir, fileName)).deleteSync(); + + print('Cleaned up temporary files.'); + + // Optionally set executable permissions if needed (for example, for binaries) + setExecutablePermissions(path.join(applicationSupportDir.path, finalExtractionPath, 'bin')); + + return; +} diff --git a/lib/utils/file_utils.dart b/lib/utils/file_utils.dart new file mode 100644 index 0000000..62dc49b --- /dev/null +++ b/lib/utils/file_utils.dart @@ -0,0 +1,91 @@ +import 'dart:io'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart' show rootBundle; +import 'package:path_provider/path_provider.dart'; + +Future extractAssetToTemp(String assetPath) async { + // Load the asset using rootBundle + final byteData = await rootBundle.load(assetPath); + + // Get the temporary directory + final tempDir = await getTemporaryDirectory(); + + // Create a file in the temporary directory + final file = File('${tempDir.path}/temp_asset_file'); + + // Write the asset's byte data to the file + await file.writeAsBytes(byteData.buffer.asUint8List()); + + // Return the file path + return file.path; +} + +Future extractAssetToFile(String assetPath, String filename) async { + // Step 1: Load the asset as a byte array + ByteData data = await rootBundle.load(assetPath); + + // Step 2: Convert the byte data to a list of bytes + List bytes = data.buffer.asUint8List(); + + // Step 3: Get the application's document directory or specific directory + Directory appDocDir = await getApplicationSupportDirectory(); + String filePath = '${appDocDir.path}/$filename'; + + // Step 4: Create a file in the desired location + File file = File(filePath); + + // Step 5: Write the bytes to the file + await file.writeAsBytes(bytes); + + print("Asset saved to $filePath"); +} + + +bool hasExecutablePermissions(String filePath) { + try { + File file = File(filePath); + FileStat stats = file.statSync(); + + return stats.mode & 0x1 == 0x1; + } catch (e) { + throw Exception('Error checking permissions: $e'); + } +} + +void setExecutablePermissions(String filePath) { + if (Platform.isLinux || Platform.isMacOS) { + try { + final file = File(filePath); + final directory = Directory(filePath); + + if (file.existsSync()) { + // If it's a file, set executable permission for the file itself + var result = Process.runSync('chmod', ['+x', filePath]); + if (result.exitCode == 0) { + print('Executable permission set for file: $filePath'); + } else { + print('Failed to set executable permission for file: ${result.stderr}'); + } + } else if (directory.existsSync()) { + // If it's a directory, set executable permission for all files inside the directory + directory.listSync(recursive: true).forEach((entity) { + if (entity is File) { + // Set executable permissions for each file inside the directory + var result = Process.runSync('chmod', ['+x', entity.path]); + if (result.exitCode == 0) { + print('Executable permission set for file: ${entity.path}'); + } else { + print('Failed to set executable permission for file: ${result.stderr}'); + } + } + }); + } else { + print('File or directory does not exist: $filePath'); + } + } catch (e) { + print('Error setting permissions: $e'); + } + } else { + print('Setting executable permissions is not supported on this platform.'); + } +} diff --git a/lib/utils/human_readable_helpers.dart b/lib/utils/human_readable_helpers.dart new file mode 100644 index 0000000..c16accd --- /dev/null +++ b/lib/utils/human_readable_helpers.dart @@ -0,0 +1,167 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import 'package:haveno/profobuf_models.dart'; +import 'package:haveno_app/utils/string_utils.dart'; + +String humanReadableDisputeStateAs(String disputeState, bool isBuyer, bool directedAtUser) { + final disputeStateMap = { + "PB_ERROR_DISPUTE_STATE": directedAtUser ? "You encountered an error in dispute state" : "Error in Dispute State", + "NO_DISPUTE": "No Dispute", + "DISPUTE_REQUESTED": directedAtUser ? "You requested a dispute" : isBuyer ? "Buyer requested a dispute" : "Seller requested a dispute", + "DISPUTE_OPENED": directedAtUser ? "You opened a dispute" : isBuyer ? "Buyer opened a dispute" : "Seller opened a dispute", + "ARBITRATOR_SENT_DISPUTE_CLOSED_MSG": directedAtUser ? "Arbitrator closed the dispute" : "Dispute closed by Arbitrator", + "ARBITRATOR_SEND_FAILED_DISPUTE_CLOSED_MSG": directedAtUser ? "Failed to receive dispute closed message from arbitrator" : "Failed to close dispute by Arbitrator", + "ARBITRATOR_STORED_IN_MAILBOX_DISPUTE_CLOSED_MSG": directedAtUser ? "Arbitrator stored dispute closed message in your mailbox" : "Dispute closed message stored in mailbox", + "ARBITRATOR_SAW_ARRIVED_DISPUTE_CLOSED_MSG": directedAtUser ? "Arbitrator saw your dispute closed message" : "Arbitrator saw arrived dispute closed message", + "DISPUTE_CLOSED": directedAtUser ? "You closed the dispute" : "Dispute Closed", + "MEDIATION_REQUESTED": directedAtUser ? "You requested mediation" : isBuyer ? "Buyer requested mediation" : "Seller requested mediation", + "MEDIATION_STARTED_BY_PEER": directedAtUser ? "The other party started mediation" : "Mediation started by peer", + "MEDIATION_CLOSED": directedAtUser ? "You closed the mediation" : "Mediation Closed", + "REFUND_REQUESTED": directedAtUser ? "You requested a refund" : isBuyer ? "Buyer requested a refund" : "Seller requested a refund", + "REFUND_REQUEST_STARTED_BY_PEER": directedAtUser ? "The other party started a refund request" : "Refund request started by peer", + "REFUND_REQUEST_CLOSED": directedAtUser ? "You closed the refund request" : "Refund request closed", + }; + + // Return the human-readable string or a fallback if the state is not found + return disputeStateMap[disputeState] ?? "Unknown Dispute State"; +} + +String humanReadablePhaseAs(String phase, bool isBuyer, bool isDirectedAtBuyer) { + final phaseMap = { + "PB_ERROR_PHASE": isDirectedAtBuyer + ? isBuyer + ? "You encountered an error in the trade phase" + : "The buyer encountered an error in the trade phase" + : isBuyer + ? "Buyer encountered an error in the trade phase" + : "Seller encountered an error in the trade phase", + "INIT": isDirectedAtBuyer + ? isBuyer + ? "You initialized the trade" + : "The seller initialized the trade" + : isBuyer + ? "Buyer initialized the trade" + : "Seller initialized the trade", + "DEPOSIT_REQUESTED": isDirectedAtBuyer + ? isBuyer + ? "You requested a deposit" + : "The seller requested a deposit" + : isBuyer + ? "Buyer requested a deposit" + : "Seller requested a deposit", + "DEPOSITS_PUBLISHED": isDirectedAtBuyer + ? isBuyer + ? "Your deposits were published" + : "The seller's deposits were published" + : isBuyer + ? "Buyer published the deposits" + : "Seller published the deposits", + "DEPOSITS_CONFIRMED": isDirectedAtBuyer + ? isBuyer + ? "Your deposits are confirmed" + : "The seller's deposits are confirmed" + : isBuyer + ? "Buyer's deposits are confirmed" + : "Seller's deposits are confirmed", + "DEPOSITS_UNLOCKED": isDirectedAtBuyer + ? isBuyer + ? "You must now send payment" + : "The seller must now send payment" + : isBuyer + ? "Buyer's deposits are unlocked" + : "Waiting for peer to pay", + "PAYMENT_SENT": isDirectedAtBuyer + ? isBuyer + ? "You marked payment as sent" + : "The seller marked payment as sent" + : isBuyer + ? "Buyer marked payment as sent" + : "Seller confirming payment", + "PAYMENT_RECEIVED": isDirectedAtBuyer + ? isBuyer + ? "You received the payment" + : "The seller received your payment" + : isBuyer + ? "Seller received the payment" + : "Buyer received the payment", + }; + + // Return the human-readable string or a fallback if the phase is not found + return phaseMap[phase] ?? "Unknown Trade Phase"; +} + + +String humanReadablePayoutStateAs(String status, bool isBuyer, bool isDirectedAtBuyer) { + final stateMap = { + "PAYOUT_UNPUBLISHED": isDirectedAtBuyer + ? isBuyer + ? "Your payout is being published" + : "Seller's payout is published" + : isBuyer + ? "Buyer's payout is unpublished" + : "Your payout is unpublished", + "PAYOUT_PUBLISHED": isDirectedAtBuyer + ? isBuyer + ? "Your payout has been published" + : "Seller's payout has been published" + : isBuyer + ? "Buyer's payout has been published" + : "Your payout has been published", + "PAYOUT_CONFIRMED": isDirectedAtBuyer + ? isBuyer + ? "Your payout is confirmed" + : "Seller's payout is confirmed" + : isBuyer + ? "Buyer's payout is confirmed" + : "Your payout is confirmed", + "PAYOUT_UNLOCKED": isDirectedAtBuyer + ? isBuyer + ? "Completed" + : "Completed" + : isBuyer + ? "Completed" + : "Completed", + }; + + // Return the human-readable string or a fallback if the status is not found + return stateMap[status] ?? "Unknown Payout State"; +} + + +/// A utility function to get a human-readable label for a given form field. +String getHumanReadablePaymentMethodFormFieldLabel( + MapEntry entry, + List fields, +) { + var matchId = convertCamelCaseToSnakeCase(entry.key); + + var matchingField = fields.firstWhere( + (field) => field != null && field.id.toString() == matchId, + orElse: () { + throw Exception("Couldn't map ${entry.toString()} to a payload for form for any of the fields: ${fields.join(", ")}."); + }, + ); + + return matchingField != null && matchingField.label.isNotEmpty + ? matchingField.label + : entry.key; +} diff --git a/lib/utils/ios_orbot_interface.dart b/lib/utils/ios_orbot_interface.dart new file mode 100644 index 0000000..148f425 --- /dev/null +++ b/lib/utils/ios_orbot_interface.dart @@ -0,0 +1,119 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +/* import 'package:flutter/material.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class OrbotApi { + // Base URLs + static const String _baseHttpsUrl = 'https://orbot.app/rc'; + static const String _baseSchemeUrl = 'orbot'; + + // Launch a URL using the orbot scheme + Future _launchOrbotUri(String path, {String? query}) async { + final uri = Uri( + scheme: _baseSchemeUrl, + path: path, + query: query, + ); + + if (await canLaunchUrl(uri)) { + await launchUrl(uri); + } else { + print("Could not launch Orbot URI: $uri"); + } + } + + // Launch a URL using HTTPS as a fallback + Future _launchOrbotHttps(String path, {String? query}) async { + final uri = Uri( + scheme: 'https', + host: 'orbot.app', + path: '/rc/$path', + query: query, + ); + + if (await canLaunchUrl(uri)) { + await launchUrl(uri); + } else { + print("Could not launch Orbot HTTPS URL: $uri"); + } + } + + // Shows the Orbot app + Future showOrbot() async { + await _launchOrbotUri('show'); + } + + // Starts the Tor Network Extension via Orbot + Future startOrbot() async { + await _launchOrbotUri('start'); + } + + // Shows the Orbot settings + Future showSettings() async { + await _launchOrbotUri('show/settings'); + } + + // Shows the bridge configuration screen in Orbot + Future showBridges() async { + await _launchOrbotUri('show/bridges'); + } + + // Shows the v3 Onion service authentication tokens in Orbot + Future showAuth() async { + await _launchOrbotUri('show/auth'); + } + + // Adds a v3 Onion service authentication token in Orbot + Future addAuth({required String url, required String key}) async { + final query = 'url=$url&key=$key'; + await _launchOrbotUri('add/auth', query: query); + } + + // A fallback method that attempts to use HTTPS URLs instead of the Orbot scheme + Future showOrbotFallback() async { + await _launchOrbotHttps('show'); + } + + Future startOrbotFallback() async { + await _launchOrbotHttps('start'); + } + + Future showSettingsFallback() async { + await _launchOrbotHttps('show/settings'); + } + + Future showBridgesFallback() async { + await _launchOrbotHttps('show/bridges'); + } + + Future showAuthFallback() async { + await _launchOrbotHttps('show/auth'); + } + + Future addAuthFallback({required String url, required String key}) async { + final query = 'url=$url&key=$key'; + await _launchOrbotHttps('add/auth', query: query); + } +} + */ \ No newline at end of file diff --git a/lib/utils/kill.dart b/lib/utils/kill.dart new file mode 100644 index 0000000..fa900f5 --- /dev/null +++ b/lib/utils/kill.dart @@ -0,0 +1,34 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'dart:io'; + +void killPids(List pids) { + for (int pid in pids) { + bool success = Process.killPid(pid); + if (success) { + print('Successfully killed process with PID: $pid'); + } else { + print('Failed to kill process with PID: $pid'); + } + } +} \ No newline at end of file diff --git a/lib/utils/launchctl_manager.dart b/lib/utils/launchctl_manager.dart new file mode 100644 index 0000000..07637cb --- /dev/null +++ b/lib/utils/launchctl_manager.dart @@ -0,0 +1,119 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'dart:io'; + +class LaunchctlManager { + // Method to execute a launchctl command + Future _runLaunchctl(List arguments) async { + String printArgs = arguments.join(' '); + print("Running command: launchctl $printArgs"); + return await Process.run('launchctl', arguments); + } + + // Load a LaunchAgent or LaunchDaemon + Future load(String plistPath) async { + try { + await _runLaunchctl(['unload', '-w', plistPath]); + } catch (e) { + // no worries if there was an error its probably already unloaded + } + final result = await _runLaunchctl(['load', '-w', plistPath]); + print("${result.stdout} : ${result.stderr}"); + if (result.exitCode == 0) { + print('Successfully loaded: $plistPath'); + } else { + print('Failed to load: $plistPath\n${result.stderr}'); + } + } + + // Unload a LaunchAgent or LaunchDaemon + Future unload(String plistPath) async { + final result = await _runLaunchctl(['unload', plistPath]); + if (result.exitCode == 0) { + print('Successfully unloaded: $plistPath'); + } else { + print('Failed to unload: $plistPath\n${result.stderr}'); + } + } + + // List all loaded LaunchAgents and LaunchDaemons + Future list() async { + final result = await _runLaunchctl(['list']); + if (result.exitCode == 0) { + print(result.stdout); + } else { + print('Failed to list services\n${result.stderr}'); + } + } + + // Bootstrap a LaunchAgent or LaunchDaemon + Future bootstrap(String domain, String plistPath) async { + final result = await _runLaunchctl(['bootstrap', domain, plistPath]); + if (result.exitCode == 0) { + print('Successfully bootstrapped: $plistPath in $domain'); + } else { + print('Failed to bootstrap: $plistPath in $domain\n${result.stderr}'); + } + } + + // Debug a service + Future debug(String serviceTarget, {String? stdoutPath, String? stderrPath, List? environment}) async { + List arguments = ['debug', serviceTarget]; + if (stdoutPath != null) { + arguments.addAll(['--stdout', stdoutPath]); + } + if (stderrPath != null) { + arguments.addAll(['--stderr', stderrPath]); + } + if (environment != null) { + arguments.add('--environment'); + arguments.addAll(environment); + } + final result = await _runLaunchctl(arguments); + if (result.exitCode == 0) { + print('Successfully started debugging: $serviceTarget'); + } else { + print('Failed to start debugging: $serviceTarget\n${result.stderr}'); + } + } + + // Set environment variables + Future setenv(String key, String value) async { + final result = await _runLaunchctl(['setenv', key, value]); + if (result.exitCode == 0) { + print('Successfully set environment variable: $key=$value'); + } else { + print('Failed to set environment variable: $key\n${result.stderr}'); + } + } + + // Unset environment variables + Future unsetenv(String key) async { + final result = await _runLaunchctl(['unsetenv', key]); + if (result.exitCode == 0) { + print('Successfully unset environment variable: $key'); + } else { + print('Failed to unset environment variable: $key\n${result.stderr}'); + } + } +} diff --git a/lib/utils/monero_utils.dart b/lib/utils/monero_utils.dart new file mode 100644 index 0000000..925b688 --- /dev/null +++ b/lib/utils/monero_utils.dart @@ -0,0 +1,63 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'dart:convert'; +import 'dart:math'; +import '../services/http_service.dart'; + +class MoneroService { + final List nodes = [ + 'http://xmr-node-uk.cakewallet.com:18081', + 'http://xmr-node.cakewallet.com:18081' + ]; + + final Random _random = Random(); + final HttpService _httpService; + + MoneroService({String proxyHost = '127.0.0.1', int proxyPort = 9050}) + : _httpService = HttpService(proxyHost: proxyHost, proxyPort: proxyPort); + + String _getRandomNode() { + return nodes[_random.nextInt(nodes.length)]; + } + + Future> getInfo() async { + final node = _getRandomNode(); + final response = await _httpService.request( + 'GET', + '$node/getinfo', + headers: null, + body: null, + ); + + if (response.statusCode == 200) { + final responseBody = await response.transform(utf8.decoder).join(); + return jsonDecode(responseBody); + } else { + throw Exception('Failed to communicate with node'); + } + } + + void close() { + _httpService.close(); + } +} diff --git a/lib/utils/nssm_manager.dart b/lib/utils/nssm_manager.dart new file mode 100644 index 0000000..faff6a8 --- /dev/null +++ b/lib/utils/nssm_manager.dart @@ -0,0 +1,76 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import 'dart:io'; + +class NSSMServiceManager { + final String nssmPath; + + NSSMServiceManager(this.nssmPath); + + String get quotedNssmPath => '"$nssmPath"'; + + Future setServiceParameters( + String serviceName, List args) async { + var arguments = [ + 'set', + serviceName, + 'AppParameters', + args.join(' '), + ]; + var result = await Process.run(quotedNssmPath, arguments); + + print("Ran command: $quotedNssmPath ${arguments.join(" ")}"); + print(result.stdout); + print(result.stderr); + } + + Future serviceStart(String serviceName) async { + print("Path: $quotedNssmPath"); + final ProcessResult result = + await Process.run(quotedNssmPath, ['start', serviceName]); + + print(result.stdout); + print(result.stderr); + } + + Future serviceStop(String serviceName) async { + final ProcessResult result = + await Process.run(quotedNssmPath, ['stop', serviceName]); + + print(result.stdout); + print(result.stderr); + } + + Future serviceRestart(String serviceName) async { + final ProcessResult result = + await Process.run(quotedNssmPath, ['restart', serviceName]); + print(result.stdout); + print(result.stderr); + } + + Future serviceStatus(String serviceName) async { + final ProcessResult result = + await Process.run(quotedNssmPath, ['status', serviceName]); + print(result.stdout); + print(result.stderr); + } +} diff --git a/lib/utils/payment_utils.dart b/lib/utils/payment_utils.dart new file mode 100644 index 0000000..8d6e2f2 --- /dev/null +++ b/lib/utils/payment_utils.dart @@ -0,0 +1,183 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'package:fixnum/fixnum.dart'; + +bool isFiatCurrency(String currencyCode) { + return fiatCurrencies.contains(currencyCode); +} + +bool isCryptoCurrency(String currencyCode) { + return cryptoCurrencies.contains(currencyCode); +} + +Set getAllFiatCurrencies() { + return fiatCurrencies; +} + +Set getAllCryptoCurrencies() { + return cryptoCurrencies; +} + +// Define sets for fiat and crypto currencies +const Set fiatCurrencies = { + 'AED', 'AFN', 'ALL', 'AMD', 'ANG', 'AOA', 'ARS', 'AUD', 'AWG', 'AZN', + 'BAM', 'BBD', 'BDT', 'BGN', 'BHD', 'BIF', 'BMD', 'BND', 'BOB', 'BRL', + 'BSD', 'BTN', 'BWP', 'BYN', 'BZD', 'CAD', 'CDF', 'CHF', 'CLP', 'CNY', + 'COP', 'CRC', 'CUP', 'CVE', 'CZK', 'DJF', 'DKK', 'DOP', 'DZD', 'EGP', + 'ERN', 'ETB', 'EUR', 'FJD', 'FKP', 'FOK', 'GBP', 'GEL', 'GGP', 'GHS', + 'GIP', 'GMD', 'GNF', 'GTQ', 'GYD', 'HKD', 'HNL', 'HRK', 'HTG', 'HUF', + 'IDR', 'ILS', 'IMP', 'INR', 'IQD', 'IRR', 'ISK', 'JMD', 'JOD', 'JPY', + 'KES', 'KGS', 'KHR', 'KMF', 'KRW', 'KWD', 'KYD', 'KZT', 'LAK', 'LBP', + 'LKR', 'LRD', 'LSL', 'LYD', 'MAD', 'MDL', 'MGA', 'MKD', 'MMK', 'MNT', + 'MOP', 'MRU', 'MUR', 'MVR', 'MWK', 'MXN', 'MYR', 'MZN', 'NAD', 'NGN', + 'NIO', 'NOK', 'NPR', 'NZD', 'OMR', 'PAB', 'PEN', 'PGK', 'PHP', 'PKR', + 'PLN', 'PYG', 'QAR', 'RON', 'RSD', 'RUB', 'RWF', 'SAR', 'SBD', 'SCR', + 'SDG', 'SEK', 'SGD', 'SHP', 'SLE', 'SOS', 'SRD', 'SSP', 'STN', 'SYP', + 'SZL', 'THB', 'TJS', 'TMT', 'TND', 'TOP', 'TRY', 'TTD', 'TVD', 'TWD', + 'TZS', 'UAH', 'UGX', 'USD', 'UYU', 'UZS', 'VES', 'VND', 'VUV', 'WST', + 'XAF', 'XCD', 'XOF', 'XPF', 'YER', 'ZAR', 'ZMW', 'ZWL' +}; + +const Set cryptoCurrencies = { + 'BTC', // Bitcoin + 'BCH', // Bitcoin Cash + 'LTC', // Litecoin + 'ETH', // Ethereum + 'XMR' // Monero +}; + +enum PaymentMethodType { + CRYPTO, + FIAT, + UNKNOWN, +} + +// Payment Method Mappings +const Map fiatPaymentMethodLabels = { + 'AUSTRALIA_PAYID': 'Australia PayID', + 'CASH_APP': 'Cash App', + 'CASH_AT_ATM': 'Cash at ATM', + 'F2F': 'Face to Face', + 'FASTER_PAYMENTS': 'Faster Payments', + 'MONEY_GRAM': 'MoneyGram', + 'PAXUM': 'Paxum', + 'PAYPAL': 'PayPal', + 'PAY_BY_MAIL': 'Pay by Mail', + 'REVOLUT': 'Revolut', + 'SEPA': 'SEPA', + 'SEPA_INSTANT': 'SEPA Instant', + 'STRIKE': 'Strike', + 'SWIFT': 'SWIFT', + 'TRANSFERWISE': 'TransferWise', + 'UPHOLD': 'Uphold', + 'VENMO': 'Venmo', + 'ZELLE': 'Zelle', +}; + +const Map cryptoPaymentMethodLabels = { + 'BLOCK_CHAINS': 'Blockchains' +}; + +// Combine both maps for easy lookup +const Map paymentMethodLabels = { + ...fiatPaymentMethodLabels, + ...cryptoPaymentMethodLabels, +}; + +String getPaymentMethodLabel(String id) { + return paymentMethodLabels[id] ?? 'Unknown Payment Method'; +} + +PaymentMethodType getPaymentMethodType(String paymentMethodId) { + if (cryptoPaymentMethodLabels.containsKey(paymentMethodId)) { + return PaymentMethodType.CRYPTO; + } else if (fiatPaymentMethodLabels.containsKey(paymentMethodId)) { + return PaymentMethodType.FIAT; + } else { + return PaymentMethodType.UNKNOWN; + } +} + +dynamic formatXmr(Int64? atomicUnits, {bool returnString = true}) { + if (atomicUnits == null) { + return returnString ? 'N/A' : null; + } + double value = atomicUnits.toInt() / 1e12; + return returnString ? value.toStringAsFixed(5) : value; +} + + +String formatFiat(double amount) { + return amount.toStringAsFixed(2); +} + +String autoFormatCurrency(dynamic amount, String currencyCode, {bool includeCurrencyCode = true}) { + // Check if the currency is fiat + if (isFiatCurrency(currencyCode)) { + double fiatAmount; + + // Convert amount to double if it isn't already + if (amount is Int64) { + fiatAmount = amount.toDouble(); + } else if (amount is String) { + fiatAmount = double.tryParse(amount) ?? 0.0; + } else if (amount is double) { + fiatAmount = amount; + } else { + return 'N/A'; // Return 'N/A' if the type is unexpected + } + + // Format for fiat: 2 decimal places + String formattedAmount = formatFiat(fiatAmount); + + // Append currency code if required + return includeCurrencyCode ? '$formattedAmount $currencyCode' : formattedAmount; + } + + // Check if the currency is crypto + else if (isCryptoCurrency(currencyCode)) { + double cryptoAmount; + + // Convert amount to double if it isn't already + if (amount is Int64) { + cryptoAmount = formatXmr(amount, returnString: false) as double; + } else if (amount is String) { + cryptoAmount = double.tryParse(amount) ?? 0.0; + } else if (amount is double) { + cryptoAmount = amount; + } else { + return 'N/A'; // Return 'N/A' if the type is unexpected + } + + // Format for crypto: 5 decimal places, but strip unnecessary zeros + String formattedAmount = cryptoAmount.toStringAsFixed(5); + formattedAmount = formattedAmount.replaceAll(RegExp(r'0+$'), '').replaceAll(RegExp(r'\.$'), ''); + + return includeCurrencyCode ? '$formattedAmount $currencyCode' : formattedAmount; + } + + // If the currency is neither fiat nor crypto, return unknown + else { + return 'Unknown Currency'; + } +} diff --git a/lib/utils/salt.dart b/lib/utils/salt.dart new file mode 100644 index 0000000..2f54d3c --- /dev/null +++ b/lib/utils/salt.dart @@ -0,0 +1,38 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'dart:math'; + +String generateHexSalt([int length = 32]) { + final secureRandom = Random.secure(); + final saltBytes = + List.generate(length, (i) => secureRandom.nextInt(256)); + return bytesToHex(saltBytes); +} + +String bytesToHex(List bytes) { + final buffer = StringBuffer(); + for (var byte in bytes) { + buffer.write(byte.toRadixString(16).padLeft(2, '0')); + } + return buffer.toString(); +} diff --git a/lib/utils/string_utils.dart b/lib/utils/string_utils.dart new file mode 100644 index 0000000..5bc800d --- /dev/null +++ b/lib/utils/string_utils.dart @@ -0,0 +1,89 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +String convertCamelCaseToSnakeCase(String input) { + // Use a regular expression to find capital letters + final RegExp exp = RegExp(r'(? '_${match.group(0)}'); + + // Convert the final string to uppercase + return snakeCase.toUpperCase(); +} + + +Map parseNodeUrl(String url) { + String? host; + String? port; + bool hasPort = false; + + // Check if the URL contains an .onion address + if (url.contains('.onion')) { + // If it's an .onion link, handle manually since Uri.parse won't work for .onion + if (url.contains('https://') || url.contains('http://')) { + // Strip the protocol + var hostnameAndBeyond = url.split('://').last; + hasPort = hostnameAndBeyond.contains(':'); // Check if there is a port + + if (hasPort) { + // Separate host and port + host = hostnameAndBeyond.split(':').first; + port = hostnameAndBeyond.split(':').last; + } else { + host = hostnameAndBeyond; + port = null; + } + } else if (url.endsWith('.onion')) { + // If the onion address does not have a protocol, treat the whole thing as the host + host = url; + port = null; // Default port can be set later if needed + } + } else { + // For regular domains and IPs, we can rely on Uri.parse + try { + final uri = Uri.parse(url); + host = uri.host.isNotEmpty ? uri.host : null; + + if (uri.hasPort) { + port = uri.port.toString(); + } else { + port = null; + } + } catch (e) { + // In case of any parsing errors, log or handle accordingly + print('Invalid URL format: $url'); + return { + 'host': null, + 'port': null, + }; + } + } + + // Set default port if not provided + //port ??= '18081'; // Default port for Monero nodes + + return { + 'host': host, + 'port': port, + }; +} diff --git a/lib/utils/systemd_manager.dart b/lib/utils/systemd_manager.dart new file mode 100644 index 0000000..7ed7c5d --- /dev/null +++ b/lib/utils/systemd_manager.dart @@ -0,0 +1,64 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'dart:io'; + +class SystemdManager { + final String serviceName; + + SystemdManager({required this.serviceName}); + + /// Starts the service with optional environment variables. + Future startService({Map? environment}) async { + await _runSystemctlCommand('start', environment: environment); + } + + /// Stops the service. + Future stopService() async { + await _runSystemctlCommand('stop'); + } + + /// Restarts the service with optional environment variables. + Future restartService({Map? environment}) async { + await _runSystemctlCommand('restart', environment: environment); + } + + /// Checks the status of the service. + Future statusService() async { + await _runSystemctlCommand('status'); + } + + /// Runs a systemctl command for the service with optional environment variables. + Future _runSystemctlCommand(String command, {Map? environment}) async { + final result = await Process.run( + 'systemctl', + ['--user', command, serviceName], + environment: environment, + ); + + if (result.exitCode == 0) { + print('Service $serviceName $command successfully.'); + } else { + print('Failed to $command $serviceName: ${result.stderr}'); + } + } +} \ No newline at end of file diff --git a/lib/utils/time_utils.dart b/lib/utils/time_utils.dart new file mode 100644 index 0000000..58d2184 --- /dev/null +++ b/lib/utils/time_utils.dart @@ -0,0 +1,52 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'package:fixnum/fixnum.dart'; + +String calculateFormattedTimeSince(dynamic creationDate) { + DateTime creationDateTime; + + if (creationDate is Int64) { + creationDateTime = DateTime.fromMillisecondsSinceEpoch(creationDate.toInt()); + } else if (creationDate is DateTime) { + creationDateTime = creationDate; + } else { + throw ArgumentError('Invalid argument type. Must be Int64 or DateTime.'); + } + + final now = DateTime.now(); + final difference = now.difference(creationDateTime); + + if (difference.inMinutes < 60) { + return '${difference.inMinutes} minutes'; + } else if (difference.inHours < 24) { + return '${difference.inHours} hours'; + } else if (difference.inDays < 30) { + return '${difference.inDays} days'; + } else if (difference.inDays < 365) { + final months = (difference.inDays / 30).floor(); + return '$months months'; + } else { + final years = (difference.inDays / 365).floor(); + return '$years years'; + } +} \ No newline at end of file diff --git a/lib/versions.dart b/lib/versions.dart new file mode 100644 index 0000000..a220362 --- /dev/null +++ b/lib/versions.dart @@ -0,0 +1,37 @@ +import 'dart:convert'; +import 'package:flutter/services.dart'; // For rootBundle + +class Versions { + // Private static instance variable + static final Versions _instance = Versions._internal(); + + // Internal map to store version data + late Map> _data; + + // Private constructor + Versions._internal(); + + // Public factory constructor to provide the same instance + factory Versions() { + return _instance; + } + + // Load JSON data + Future load() async { + try { + // Load the JSON file + final String jsonString = await rootBundle.loadString('assets/versions.json'); + final Map jsonResponse = json.decode(jsonString); + + // Convert the loaded JSON into a Map + _data = jsonResponse.map((key, value) => MapEntry(key, Map.from(value))); + } catch (e) { + print("Error loading versions: $e"); + } + } + + // Get version for a given component + String? getVersion(String component) { + return _data[component]?['default']; + } +} diff --git a/lib/views/desktop_lifecycle.dart b/lib/views/desktop_lifecycle.dart new file mode 100644 index 0000000..c176005 --- /dev/null +++ b/lib/views/desktop_lifecycle.dart @@ -0,0 +1,200 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import 'package:flutter/material.dart'; +import 'package:haveno/enums.dart'; +import 'package:haveno/haveno_client.dart'; +import 'package:haveno/haveno_service.dart'; +import 'package:haveno_app/models/schema.dart'; +import 'package:haveno_app/providers/haveno_client_providers/disputes_provider.dart'; +import 'package:haveno_app/providers/haveno_client_providers/offers_provider.dart'; +import 'package:haveno_app/providers/haveno_client_providers/price_provider.dart'; +import 'package:haveno_app/providers/haveno_client_providers/trades_provider.dart'; +import 'package:haveno_app/providers/haveno_client_providers/wallets_provider.dart'; +import 'package:haveno_app/providers/haveno_client_providers/xmr_connections_provider.dart'; +import 'package:haveno_app/services/desktop_manager_service.dart'; +import 'package:haveno_app/services/local_notification_service.dart'; +import 'package:haveno_app/services/platform_system_service/factory.dart'; +import 'package:haveno_app/services/platform_system_service/schema.dart'; +import 'package:haveno_app/services/secure_storage_service.dart'; +import 'package:haveno_app/utils/payment_utils.dart'; +import 'package:haveno_app/utils/salt.dart'; +import 'package:haveno_app/views/screens/seednode_setup_screen.dart'; +import 'package:provider/provider.dart'; +import 'package:tray_manager/tray_manager.dart'; + +class DesktopLifecycleWidget extends PlatformLifecycleWidget { + const DesktopLifecycleWidget({ + super.key, + required super.child, + required super.builder, + }); + + @override + _DesktopLifecycleWidgetState createState() => _DesktopLifecycleWidgetState(); +} + +class _DesktopLifecycleWidgetState extends PlatformLifecycleState with TrayListener { + late PlatformService platformService; + late SyncManager syncManager; + late NotificationsService notificationsService; + late String? _daemonPassword; + + @override + Future initPlatform() async { + final desktopManagerService = DesktopManagerService(); + + print("Intializing tray mananger and adding lifecycle widget as listener"); + //intializeSystemTray(); + trayManager.addListener(this); + print("Initialized desktop platform"); + HavenoChannel havenoChannel = HavenoChannel(); + SecureStorageService secureStorageService = SecureStorageService(); + platformService = await getPlatformService(); + + print("Setting up Tor daemon..."); + await platformService.setupTorDaemon(); + + + // Ensure seed node is configured + var seedNodeConfigured = await desktopManagerService.isSeednodeConfigured(); + while (seedNodeConfigured == null || seedNodeConfigured == false) { + // Navigate to SeedNodeSetupScreen and wait for the result + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => SeedNodeSetupScreen(), + ), + ); + // Recheck seed node configuration + seedNodeConfigured = await desktopManagerService.isSeednodeConfigured(); + } + + _daemonPassword = await secureStorageService.readHavenoDaemonPassword(); + + if (_daemonPassword == null) { + _daemonPassword = generateHexSalt(16); + await secureStorageService + .writeHavenoDaemonPassword(_daemonPassword as String); + print("Generated and saved new daemon password."); + await platformService.setupHavenoDaemon(_daemonPassword); + await havenoChannel.connect('127.0.0.1', 3201, _daemonPassword as String); + } else { + print("Loaded existing daemon password."); + await platformService.setupHavenoDaemon(_daemonPassword); + await havenoChannel.connect('127.0.0.1', 3201, _daemonPassword as String); + + print("Haveno Daemon connected."); + setState(() {}); + + WidgetsBinding.instance.addPostFrameCallback((_) async { + print( + "Setting up and starting sync manager and notification listeners"); + await havenoChannel.onConnected; + await _createSyncManagerWithTasks(); + await _createNotificationListeners(); + }); + } + } + + Future _createSyncManagerWithTasks() async { + syncManager = SyncManager(checkInterval: const Duration(seconds: 1)); + + var offersProvider = Provider.of(context, listen: false); + var pricesProvider = Provider.of(context, listen: false); + var walletsProvider = Provider.of(context, listen: false); + var tradesProvider = Provider.of(context, listen: false); + var xmrConnectionsProvider = Provider.of(context, listen: false); + + var fetchOffersTask = SyncTask(taskFunction: offersProvider.getOffers, cooldown: const Duration(minutes: 1)); + var fetchMyOffersTask = SyncTask(taskFunction: offersProvider.getMyOffers, cooldown: const Duration(minutes: 2)); + var fetchPricesTask = SyncTask(taskFunction: pricesProvider.getXmrMarketPrices, cooldown: const Duration(seconds: 5)); + var fetchBalancesTask = SyncTask(taskFunction: walletsProvider.getBalances, cooldown: const Duration(minutes: 2)); + var fetchTransactionsTask = SyncTask(taskFunction: walletsProvider.getXmrTxs, cooldown: const Duration(minutes: 2)); + var fetchTradesTask = SyncTask(taskFunction: tradesProvider.getTrades, cooldown: const Duration(minutes: 1)); + var fetchXmrConnections = SyncTask(taskFunction: xmrConnectionsProvider.checkConnections, cooldown: const Duration(minutes: 1)); + + + syncManager.addTask(fetchOffersTask); + syncManager.addTask(fetchMyOffersTask); + syncManager.addTask(fetchPricesTask); + syncManager.addTask(fetchBalancesTask); + syncManager.addTask(fetchTransactionsTask); + syncManager.addTask(fetchTradesTask); + syncManager.addTask(fetchXmrConnections); + + syncManager.start(); + } + + Future _createNotificationListeners() async { + var tradesProvider = Provider.of(context, listen: false); + var disputesProvider = + Provider.of(context, listen: false); + notificationsService = NotificationsService(); + var localNotificationsService = LocalNotificationsService(); + + notificationsService.addListener( + NotificationMessage_NotificationType.CHAT_MESSAGE, + (notification) { + if (notification.chatMessage.type == SupportType.TRADE) { + tradesProvider.addChatMessage(notification.chatMessage); + localNotificationsService.showNotification( + id: notification.chatMessage.hashCode, + title: 'New Message', + body: notification.chatMessage.message); + } else { + disputesProvider.addChatMessage(notification.chatMessage); + tradesProvider.addChatMessage(notification.chatMessage); + localNotificationsService.showNotification( + id: notification.chatMessage.hashCode, + title: 'New Support Message', + body: notification.chatMessage.message); + } + }, + ); + + notificationsService.addListener( + NotificationMessage_NotificationType.TRADE_UPDATE, + (notification) { + tradesProvider.createOrUpdateTrade(notification.trade); + var direction = + notification.trade.role.contains('buyer') ? 'buying' : 'selling'; + var total = formatXmr(notification.trade.amount); + var paymentMethod = notification.trade.offer.paymentMethodShortName; + localNotificationsService.showNotification( + id: notification.chatMessage.hashCode, + title: 'New Trade Opened', + body: + 'You\'re $direction a total of $total XMR via $paymentMethod'); + }, + ); + + notificationsService.listen(); + } + + @override + void dispose() { + notificationsService.stop(); // Stop listening to notifications + syncManager.stop(); // Stop sync manager + trayManager.removeListener(this); + super.dispose(); + } +} diff --git a/lib/views/drawer/link_to_mobile_screen.dart b/lib/views/drawer/link_to_mobile_screen.dart new file mode 100644 index 0000000..c4ce4cc --- /dev/null +++ b/lib/views/drawer/link_to_mobile_screen.dart @@ -0,0 +1,105 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:haveno_app/services/desktop_manager_service.dart'; +import 'package:flutter/services.dart'; +import 'package:qr_flutter/qr_flutter.dart'; + +class LinkToMobileScreen extends StatelessWidget { + const LinkToMobileScreen({super.key}); + + @override + Widget build(BuildContext context) { + final desktopManager = DesktopManagerService(); + + return Scaffold( + appBar: AppBar( + title: const Text('Link Your Mobile'), + ), + body: FutureBuilder( + future: desktopManager.getDesktopDaemonNodeUri(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } else if (snapshot.hasError) { + return Center(child: Text('Error: ${snapshot.error}')); + } else if (!snapshot.hasData || snapshot.data == null) { + return const Center(child: Text('No URL available')); + } else { + final url = snapshot.data!.toString(); + final base64Url = base64.encode(utf8.encode("http://$url")); + + return Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + 'Download the app and scan the QR code to link your mobile to your desktop:', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 18), + ), + const SizedBox(height: 20), + QrImageView( + data: 'http://$url', + version: QrVersions.auto, + size: 200.0, + eyeStyle: const QrEyeStyle(color: Colors.white, eyeShape: QrEyeShape.square), + dataModuleStyle: const QrDataModuleStyle(color: Colors.white, dataModuleShape: QrDataModuleShape.square), + ), + const SizedBox(height: 30), + const Text( + 'Alternatively, you can use the Linkage Key:', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 18), + ), + const SizedBox(height: 10), + TextField( + controller: TextEditingController(text: base64Url), + readOnly: true, + decoration: InputDecoration( + border: OutlineInputBorder(), + labelText: 'Linkage Key', + suffixIcon: IconButton( + icon: const Icon(Icons.copy), + onPressed: () { + Clipboard.setData(ClipboardData(text: base64Url)); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Linkage Key copied to clipboard')), + ); + }, + ), + ), + ), + ], + ), + ), + ); + } + }, + ), + ); + } +} diff --git a/lib/views/drawer/node_manager_screen.dart b/lib/views/drawer/node_manager_screen.dart new file mode 100644 index 0000000..6d09f80 --- /dev/null +++ b/lib/views/drawer/node_manager_screen.dart @@ -0,0 +1,226 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'package:flutter/material.dart'; +import 'package:haveno/enums.dart'; +import 'package:haveno_app/providers/haveno_client_providers/xmr_connections_provider.dart'; +import 'package:provider/provider.dart'; + +class NodeManagerScreen extends StatefulWidget { + const NodeManagerScreen({super.key}); + + @override + _NodeManagerScreenState createState() => _NodeManagerScreenState(); +} + +class _NodeManagerScreenState extends State { + bool _isAutoSwitchEnabled = false; // This tracks the state of the toggle switch + final Map _isDeleting = {}; // Tracks the deletion status for each node + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + final provider = Provider.of(context, listen: false); + + // Fetch all node connections + provider.getXmrConnectionSettings(); + + // Fetch the current active node + provider.getActiveConnection(); + + // Fetch if auto switch is enabled and update the state + provider.getAutoSwitchBestConnection().then((autoSwitchEnabled) { + print("Is Auto Switch Enabled On Daemon: $autoSwitchEnabled"); + setState(() { + _isAutoSwitchEnabled = autoSwitchEnabled; + }); + }); + }); + } + + // Toggle switch handler + Future _handleAutoSwitchToggle(bool value, XmrConnectionsProvider provider) async { + setState(() { + _isAutoSwitchEnabled = value; // Optimistically update the UI + }); + print("You turned $value xmr connection autosswitch"); + // Call the provider's autoSwitchBestConnection method + final success = await provider.setAutoSwitchBestConnection(value); + + if (!success) { + // If the operation failed, revert the switch and show an error snackbar + setState(() { + _isAutoSwitchEnabled = !value; // Revert to previous state + }); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Failed to enable auto-switch to best node.'), + backgroundColor: Colors.red, + ), + ); + } else { + // Show success snackbar + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text('Auto-switch to best node enabled successfully.'), + backgroundColor: Colors.green, + ), + ); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Node Manager'), + ), + body: Consumer( + builder: (context, provider, child) { + final nodes = provider.xmrNodeConnections; + final activeNode = provider.xmrActiveConnection; + + if (nodes.isEmpty) { + return const Center(child: CircularProgressIndicator()); + } + + return Column( + children: [ + // Toggle switch widget + Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('Automatically connect to the best node'), + Switch( + value: _isAutoSwitchEnabled, + onChanged: (value) => _handleAutoSwitchToggle(value, provider), + ), + ], + ), + ), + Expanded( + child: ListView.builder( + itemCount: nodes.length, + itemBuilder: (context, index) { + final node = nodes[index]; + final isActiveNode = activeNode != null && activeNode.url == node.url; + final isOnline = node.onlineStatus == UrlConnection_OnlineStatus.ONLINE; + final dotColor = isOnline ? Colors.green : Colors.red; + final isDeleting = _isDeleting[node.url] ?? false; // Check if the node is being deleted + + return GestureDetector( + onTap: () async { + await provider.setConnection(node); // Set the new active node + }, + child: Card( + margin: const EdgeInsets.fromLTRB(8, 8, 8, 2), // Consistent margins with the PaymentAccountsScreen + color: Theme.of(context).cardTheme.color, + elevation: isActiveNode ? 4.0 : 2.0, // Slightly more elevated when active + shadowColor: isActiveNode ? const Color.fromARGB(255, 255, 103, 2).withOpacity(0.5) : Colors.black12, // Glow effect when active + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + side: isActiveNode + ? BorderSide( + color: const Color.fromARGB(255, 255, 103, 2).withOpacity(0.5), // Orange border for the active node + width: 2, + ) + : BorderSide.none, + ), + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 8, 8), // Consistent padding + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, // Vertically center the row content + children: [ + Icon(Icons.circle, color: dotColor, size: 16), // Status icon + const SizedBox(width: 16), // Spacing between the icon and the text + Expanded( + child: Text( + node.url, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + isDeleting + ? const SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator( + strokeWidth: 2.0, + ), + ) + : IconButton( + icon: Icon(Icons.delete, color: Theme.of(context).colorScheme.secondary.withOpacity(0.23)), + onPressed: () async { + setState(() { + _isDeleting[node.url] = true; + }); + + // Call the removeConnection function in the provider + final success = await provider.removeConnection(node.url); + + // After the response, remove the loading indicator + setState(() { + _isDeleting.remove(node.url); + }); + + if (success) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Node removed: ${node.url}'), + ), + ); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Failed to remove node: ${node.url}'), + ), + ); + } + }, + ), + ], + ), + ), + ), + ); + }, + ), + ), + ], + ); + }, + ), + floatingActionButton: FloatingActionButton( + onPressed: () { + // Add your logic for adding a new node here. + }, + child: const Icon(Icons.add), + ), + ); + } +} diff --git a/lib/views/drawer/payment_accounts_screen.dart b/lib/views/drawer/payment_accounts_screen.dart new file mode 100644 index 0000000..db8a92a --- /dev/null +++ b/lib/views/drawer/payment_accounts_screen.dart @@ -0,0 +1,217 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'package:flutter/material.dart'; +import 'package:haveno/profobuf_models.dart'; +import 'package:haveno_app/views/screens/payment_account_detail_screen.dart'; +import 'package:haveno_app/utils/payment_utils.dart'; +import 'package:haveno_app/views/widgets/add_payment_method_form.dart'; +import 'package:provider/provider.dart'; +import 'package:haveno_app/providers/haveno_client_providers/payment_accounts_provider.dart'; + +class PaymentAccountsScreen extends StatefulWidget { + const PaymentAccountsScreen({super.key}); + + @override + _PaymentAccountsScreenState createState() => _PaymentAccountsScreenState(); +} + +class _PaymentAccountsScreenState extends State with SingleTickerProviderStateMixin { + late TabController _tabController; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 2, vsync: this); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + appBar: AppBar( + title: const Text('Payment Accounts'), + bottom: TabBar( + controller: _tabController, + tabs: const [ + Tab(text: 'Fiat Accounts'), + Tab(text: 'Crypto Accounts'), + ], + ), + ), + body: FutureBuilder( + future: fetchData(context), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } else if (snapshot.hasError) { + return Center( + child: Text('Error: ${snapshot.error}'), + ); + } else { + return TabBarView( + controller: _tabController, + children: [ + _buildAccountList(context, PaymentMethodType.FIAT), + _buildAccountList(context, PaymentMethodType.CRYPTO), + ], + ); + } + }, + ), + floatingActionButton: FloatingActionButton( + onPressed: () => _showCreateAccountForm(context), + backgroundColor: Theme.of(context).colorScheme.primary, + child: const Icon(Icons.add), + ), + ); + } + + Future fetchData(BuildContext context) async { + final paymentAccountsProvider = Provider.of(context, listen: false); + if (paymentAccountsProvider.paymentAccounts.isEmpty) { + await paymentAccountsProvider.getPaymentAccounts(); + } + if (paymentAccountsProvider.paymentMethods.isEmpty) { + await paymentAccountsProvider.getPaymentMethods(); + } + if (paymentAccountsProvider.cryptoCurrencyPaymentMethods.isEmpty) { + await paymentAccountsProvider.getCryptoCurrencyPaymentMethods(); + } + } + +Widget _buildAccountList(BuildContext context, PaymentMethodType accountType) { + final provider = Provider.of(context, listen: false); + + if (provider.paymentAccounts.isEmpty) { + return const Center(child: Text('No accounts available')); + } else { + final accounts = provider.paymentAccounts.where((account) { + final methodType = getPaymentMethodType(account.paymentMethod.id); + return methodType == accountType; + }).toList(); + + if (accounts.isEmpty) { + return const Center( + child: Text( + 'You do not currently have any accounts', + style: TextStyle(color: Colors.white70, fontSize: 18), + ), + ); + } else { + return ListView.builder( + itemCount: accounts.length, + itemBuilder: (context, index) { + final account = accounts[index]; + var paymentMethodLabel = accountType.name == 'CRYPTO' + ? account.selectedTradeCurrency.name + : getPaymentMethodLabel(account.paymentMethod.id); + print(accountType.name); + return GestureDetector( + onTap: () => _viewAccountDetails(account), + child: Card( + margin: const EdgeInsets.fromLTRB(8, 8, 8, 2), + color: Theme.of(context).cardTheme.color, + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 8, 8), // Updated padding + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, // Vertically center the row content + children: [ + Expanded( + child: Row( + children: [ + Text( + '$paymentMethodLabel (${account.accountName})', + style: const TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + IconButton( + icon: Icon(Icons.delete, color: Theme.of(context).colorScheme.secondary.withOpacity(0.23)), + onPressed: () => _showDeleteConfirmationDialog(context, account), + ), + ], + ), + ), + ), + ); + }, + ); + } + } +} + + + void _showCreateAccountForm(BuildContext context) { + final isFiat = _tabController.index == 0; + + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (BuildContext context) { + return PaymentMethodSelectionForm(accountType: isFiat ? 'FIAT' : 'CRYPTO'); + }, + ); + } + + void _showDeleteConfirmationDialog(BuildContext context, PaymentAccount account) { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Delete Account'), + content: const Text('Are you sure you want to delete this account?'), + actions: [ + TextButton( + child: const Text('Cancel'), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + TextButton( + child: const Text('Delete'), + onPressed: () { + // Call the provider to delete the account + //Provider.of(context, listen: false).deletePaymentAccount(account.id); + Navigator.of(context).pop(); // Close the dialog + }, + ), + ], + ); + }, + ); + } + + void _viewAccountDetails(PaymentAccount account) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => PaymentAccountDetailScreen(paymentAccount: account), + ), + ); + } +} diff --git a/lib/views/drawer/seednode_manager_screen.dart b/lib/views/drawer/seednode_manager_screen.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/views/drawer/settings_screen.dart b/lib/views/drawer/settings_screen.dart new file mode 100644 index 0000000..15dde87 --- /dev/null +++ b/lib/views/drawer/settings_screen.dart @@ -0,0 +1,225 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:haveno_app/providers/haveno_providers/settings_provider.dart'; + +class SettingsScreen extends StatelessWidget { + const SettingsScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + appBar: AppBar( + title: const Text('Settings'), + ), + body: Padding( + padding: const EdgeInsets.all(8.0), + child: Consumer( + builder: (context, settingsProvider, child) { + return ListView( + children: [ + Card( + color: Theme.of(context).cardTheme.color, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Preferences', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + const SizedBox(height: 16), + DropdownButtonFormField( + decoration: const InputDecoration( + labelText: 'Language', + border: OutlineInputBorder(), + labelStyle: TextStyle(color: Colors.white), + ), + value: settingsProvider.preferredLanguage, + items: ['English', 'Spanish', 'French'] + .map((language) => DropdownMenuItem( + value: language, + child: Text(language), + )) + .toList(), + onChanged: (value) { + if (value != null) { + settingsProvider.setPreferredLanguage(value); + } + }, + ), + const SizedBox(height: 16), + DropdownButtonFormField( + decoration: const InputDecoration( + labelText: 'Country', + border: OutlineInputBorder(), + labelStyle: TextStyle(color: Colors.white), + ), + value: settingsProvider.country, + items: ['USA', 'Canada', 'UK'] + .map((country) => DropdownMenuItem( + value: country, + child: Text(country), + )) + .toList(), + onChanged: (value) { + if (value != null) { + settingsProvider.setCountry(value); + } + }, + ), + const SizedBox(height: 16), + DropdownButtonFormField( + decoration: const InputDecoration( + labelText: 'Preferred Currency', + border: OutlineInputBorder(), + labelStyle: TextStyle(color: Colors.white), + ), + value: settingsProvider.preferredCurrency, + items: (settingsProvider.supportedCurrencies..sort()) + .map((currency) => DropdownMenuItem( + value: currency, + child: Text(currency), + )) + .toList(), + onChanged: (value) { + if (value != null) { + settingsProvider.setPreferredCurrency(value); + } + }, + ), + const SizedBox(height: 16), + DropdownButtonFormField( + decoration: const InputDecoration( + labelText: 'Blockchain Explorer', + border: OutlineInputBorder(), + labelStyle: TextStyle(color: Colors.white), + ), + value: settingsProvider.blockchainExplorer, + items: ['Haveno.com', 'MoneroExplorer.com'] + .map((explorer) => DropdownMenuItem( + value: explorer, + child: Text(explorer), + )) + .toList(), + onChanged: (value) { + if (value != null) { + settingsProvider.setBlockchainExplorer(value); + } + }, + ), + const SizedBox(height: 16), + TextField( + decoration: const InputDecoration( + labelText: 'Max Deviation from Market Price', + border: OutlineInputBorder(), + labelStyle: TextStyle(color: Colors.white), + suffixText: '%', + ), + keyboardType: TextInputType.number, + onChanged: (value) { + settingsProvider.setMaxDeviationFromMarketPrice(value); + }, + ), + const SizedBox(height: 16), + SwitchListTile( + title: const Text( + 'Trade Payout Automatically Withdraws to New Stealth Address', + style: TextStyle(color: Colors.white), + ), + value: settingsProvider.autoWithdrawToNewStealthAddress, + onChanged: (value) { + settingsProvider.setAutoWithdrawToNewStealthAddress(value); + }, + ), + ], + ), + ), + ), + const SizedBox(height: 4), + Card( + color: Theme.of(context).cardTheme.color, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Display Options', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.white), + ), + const SizedBox(height: 16), + SwitchListTile( + title: const Text( + 'Hide Non-Supported Payment Methods', + style: TextStyle(color: Colors.white), + ), + value: settingsProvider.hideNonSupportedPaymentMethods, + onChanged: (value) { + settingsProvider.setHideNonSupportedPaymentMethods(value); + }, + ), + const SizedBox(height: 16), + SwitchListTile( + title: const Text( + 'Sort Market Lists by Number of Offers/Trades', + style: TextStyle(color: Colors.white), + ), + value: settingsProvider.sortMarketListsByNumberOfOffersTrades, + onChanged: (value) { + settingsProvider.setSortMarketListsByNumberOfOffersTrades(value); + }, + ), + const SizedBox(height: 16), + SwitchListTile( + title: const Text( + 'Use Dark Mode', + style: TextStyle(color: Colors.white), + ), + value: settingsProvider.useDarkMode, + onChanged: (value) { + settingsProvider.setUseDarkMode(value); + }, + ), + ], + ), + ), + ), + ], + ); + }, + ), + ), + ); + } +} diff --git a/lib/views/drawer/wallet_screen.dart b/lib/views/drawer/wallet_screen.dart new file mode 100644 index 0000000..662f7f6 --- /dev/null +++ b/lib/views/drawer/wallet_screen.dart @@ -0,0 +1,281 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:haveno/grpc_models.dart'; +import 'package:provider/provider.dart'; +import 'package:haveno_app/providers/haveno_client_providers/wallets_provider.dart'; +import 'package:fixnum/fixnum.dart'; +import 'package:intl/intl.dart'; + +class WalletScreen extends StatefulWidget { + const WalletScreen({super.key}); + + @override + _WalletsScreenState createState() => _WalletsScreenState(); +} + +class _WalletsScreenState extends State { + + @override + void initState() { + super.initState(); + final walletsProvider = context.read(); + walletsProvider.getBalances(); + walletsProvider.getXmrPrimaryAddress(); + walletsProvider.getXmrTxs(); + } + + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Wallet'), + ), + body: Center( + child: Consumer( + builder: (context, walletsProvider, child) { + if (walletsProvider.balances == null) { + return const CircularProgressIndicator(); + } else { + final balances = walletsProvider.balances!; + return ListView( + padding: const EdgeInsets.all(8.0), + children: [ + if (balances.hasXmr()) _buildXmrBalanceCard('XMR', balances.xmr), + const SizedBox(height: 4.0), + _buildXmrAddressCard(walletsProvider.xmrPrimaryAddress), + const SizedBox(height: 4.0), + _buildXmrTransactionsList(walletsProvider.xmrTxs), + ], + ); + } + }, + ), + ), + ); + } + + Widget _buildXmrBalanceCard(String coin, XmrBalanceInfo balance) { + return Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Balances', + style: TextStyle(fontSize: 20.0, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 10.0), + Text('Available Balance: ${_formatXmr(balance.availableBalance)} XMR'), + Text('Pending Balance: ${_formatXmr(balance.pendingBalance)} XMR'), + const SizedBox(height: 10.0), + Text('Reserved Offer Balance: ${_formatXmr(balance.reservedOfferBalance)} XMR'), + Text('Reserved Trade Balance: ${_formatXmr(balance.reservedTradeBalance)} XMR'), + const SizedBox(height: 16.0), + Row( + children: [ + Expanded( + child: ElevatedButton( + onPressed: () { + // handle deposit balance logic here + }, + child: const Text('Send'), + ), + ), + const SizedBox(width: 16.0), + Expanded( + child: ElevatedButton( + onPressed: () { + // handle withdraw balance logic here + }, + child: const Text('Receive'), + ), + ), + ], + ), + ], + ), + ), + ); + } + + Widget _buildXmrAddressCard(String? xmrAddress) { + return Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: xmrAddress != null + ? Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Addresses', + style: TextStyle(fontSize: 20.0, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 10.0), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Text( + xmrAddress, + style: const TextStyle(fontSize: 16.0), + ), + ), + IconButton( + icon: const Icon(Icons.copy), + onPressed: () { + Clipboard.setData(ClipboardData(text: xmrAddress)); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Address copied to clipboard')), + ); + }, + ), + ], + ), + ], + ) + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'You currently don\'t have an XMR address', + style: TextStyle(fontSize: 16.0), + ), + const SizedBox(height: 10.0), + ElevatedButton( + onPressed: () { + // request a new XMR address here + }, + child: const Text('Request a new address'), + ), + ], + ), + ), + ); + } + + Widget _buildXmrTransactionsList(List? transactions) { + if (transactions != null) { + transactions.sort((a, b) => b.timestamp.compareTo(a.timestamp)); + } + + return Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Recent Transactions', + style: TextStyle(fontSize: 20.0, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 10.0), + transactions == null || transactions.isEmpty + ? const Text('No transactions available') + : ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: transactions.length, + itemBuilder: (context, index) { + final tx = transactions[index]; + final amounts = _getTransactionAmounts(tx); + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + _buildTransactionInfo(tx, amounts), + style: const TextStyle(fontSize: 16.0), + ), + Tooltip( + message: _formatTimestamp(tx.timestamp.toInt()), + child: Text( + _formatDate(tx.timestamp.toInt()), + style: const TextStyle(color: Colors.grey, fontSize: 14.0), + ), + ), + ], + ), + ), + IconButton( + icon: const Icon(Icons.copy), + onPressed: () { + Clipboard.setData(ClipboardData(text: tx.hash)); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Transaction ID copied to clipboard')), + ); + }, + ), + ], + ), + ); + }, + ), + ], + ), + ), + ); + } + + List _getTransactionAmounts(XmrTx tx) { + final List amounts = []; + if (tx.hasOutgoingTransfer()) { + amounts.add(Int64.parseInt(tx.outgoingTransfer.amount)); + } + amounts.addAll(tx.incomingTransfers + .map((transfer) => Int64.parseInt(transfer.amount))); + return amounts; + } + + String _buildTransactionInfo(XmrTx tx, List amounts) { + final amountString = + amounts.map((amount) => '${_formatXmr(amount)} XMR').join(', '); + final type = tx.hasOutgoingTransfer() ? 'Sent' : 'Received'; + return '$type $amountString'; + } + + String _formatXmr(Int64? atomicUnits) { + if (atomicUnits == null) { + return 'N/A'; + } + return (atomicUnits.toInt() / 1e12).toStringAsFixed(5); + } + + String _formatTimestamp(int timestamp) { + final date = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000); + return DateFormat('dd/MM/yyyy HH:mm:ss').format(date); + } + + String _formatDate(int timestamp) { + final date = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000); + return DateFormat('dd/MM/yyyy').format(date); + } +} diff --git a/lib/views/mobile_lifecycle.dart b/lib/views/mobile_lifecycle.dart new file mode 100644 index 0000000..094f542 --- /dev/null +++ b/lib/views/mobile_lifecycle.dart @@ -0,0 +1,126 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import 'package:flutter/material.dart'; +import 'package:haveno/haveno_client.dart'; +import 'package:haveno_app/models/haveno_daemon_config.dart'; +import 'package:haveno_app/models/schema.dart'; +import 'package:haveno_app/providers/haveno_client_providers/offers_provider.dart'; +import 'package:haveno_app/providers/haveno_client_providers/price_provider.dart'; +import 'package:haveno_app/providers/haveno_client_providers/trades_provider.dart'; +import 'package:haveno_app/providers/haveno_client_providers/wallets_provider.dart'; +import 'package:haveno_app/services/connection_checker_service.dart'; +import 'package:haveno_app/services/mobile_manager_service.dart'; +import 'package:haveno_app/services/platform_system_service/schema.dart'; +import 'package:provider/provider.dart'; + +class MobileLifecycleWidget extends PlatformLifecycleWidget { + const MobileLifecycleWidget({ + super.key, + required super.child, + required super.builder, + }); + + @override + _MobileLifecycleWidgetState createState() => _MobileLifecycleWidgetState(); +} + +class _MobileLifecycleWidgetState extends PlatformLifecycleState { + late PlatformService platformService; + late String daemonPassword; + HavenoDaemonConfig? _remoteHavenoDaemonNodeConfig; + bool _hasHavenoDaemonNodeConfig = false; + + get tradesProvider => null; + + @override + Future initPlatform() async { + + //final torStatusService = TorStatusService(); + HavenoChannel havenoChannel = HavenoChannel(); + final mobileManagerService = MobileManagerService(); + + _remoteHavenoDaemonNodeConfig = await mobileManagerService.getRemoteHavenoDaemonNode(); + + if (_remoteHavenoDaemonNodeConfig != null) { + // Paired with desktop + try { + //await torStatusService.waitForInitialization(); + await ConnectionCheckerService().isTorConnected(); + print("About to try to connect to daemon"); + await havenoChannel.connect( + _remoteHavenoDaemonNodeConfig!.host, + _remoteHavenoDaemonNodeConfig!.port, + _remoteHavenoDaemonNodeConfig!.clientAuthPassword, + ); + print("Should be connected to Daemon!"); + _hasHavenoDaemonNodeConfig = true; + } catch (e) { + print("Failed to connect to HavenoService: $e"); + } + } else { + // Not yet paired with desktop + _hasHavenoDaemonNodeConfig = false; + } + + // Delay the initialization of providers until the widget is fully initialized + WidgetsBinding.instance.addPostFrameCallback((_) async { + await havenoChannel.onConnected; + print("Haveno Daemon Connected!"); + await createSyncManagerWithTasks(); + print("Created sync manager with tasks!"); + }); + + } + + Future createSyncManagerWithTasks() async { + + var syncManager = SyncManager(checkInterval: const Duration(seconds: 1)); + + // Access providers using context now that the widget is fully initialized + var offersProvider = Provider.of(context, listen: false); + var pricesProvider = Provider.of(context, listen: false); + var walletsProvider = Provider.of(context, listen: false); + var tradesProvider = Provider.of(context, listen: false); + + var fetchOffersTask = SyncTask(taskFunction: offersProvider.getAllOffers, cooldown: const Duration(minutes: 3)); + var fetchPricesTask = SyncTask(taskFunction: pricesProvider.getXmrMarketPrices, cooldown: const Duration(seconds: 5)); + var fetchBalancesTask = SyncTask(taskFunction: walletsProvider.getBalances, cooldown: const Duration(minutes: 2)); + var fetchTransactionsTask = SyncTask(taskFunction: walletsProvider.getXmrTxs, cooldown: const Duration(minutes: 2)); + //var fetchTrades = SyncTask(taskFunction: tradesProvider.getTrades, cooldown: const Duration(minutes: 1)); + + syncManager.addTask(fetchOffersTask); + syncManager.addTask(fetchPricesTask); + syncManager.addTask(fetchBalancesTask); + //syncManager.addTask(fetchTrades); + syncManager.addTask(fetchTransactionsTask); + + // Start the sync manager (if it needs to run immediately) + + syncManager.start(); + } + + @override + void dispose() { + // Clean up any resources if needed + super.dispose(); + } +} \ No newline at end of file diff --git a/lib/views/screens/active_buyer_trade_timeline_screen.dart b/lib/views/screens/active_buyer_trade_timeline_screen.dart new file mode 100644 index 0000000..965349c --- /dev/null +++ b/lib/views/screens/active_buyer_trade_timeline_screen.dart @@ -0,0 +1,244 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'dart:async'; +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:haveno/grpc_models.dart'; +import 'package:haveno_app/views/screens/trade_timeline/buyer_phases/phase_deposits_confirmed_buyer.dart'; +import 'package:haveno_app/views/screens/trade_timeline/buyer_phases/phase_deposits_published_buyer.dart'; +import 'package:haveno_app/views/screens/trade_timeline/buyer_phases/phase_deposits_unlocked_buyer.dart'; +import 'package:haveno_app/views/screens/trade_timeline/buyer_phases/phase_init_buyer.dart'; +import 'package:haveno_app/views/screens/trade_timeline/buyer_phases/phase_payment_received_buyer.dart'; +import 'package:haveno_app/views/screens/trade_timeline/buyer_phases/phase_payment_sent_buyer.dart'; +import 'package:haveno_app/utils/payment_utils.dart'; +import 'package:provider/provider.dart'; +import 'package:haveno_app/providers/haveno_client_providers/trades_provider.dart'; + +class ActiveBuyerTradeTimelineScreen extends StatefulWidget { + final TradeInfo trade; + + const ActiveBuyerTradeTimelineScreen({super.key, required this.trade}); + + @override + _ActiveBuyerTradeTimelineScreenState createState() => + _ActiveBuyerTradeTimelineScreenState(); +} + +class _ActiveBuyerTradeTimelineScreenState + extends State { + PageController _pageController = PageController(); + int _currentPage = 0; + Timer? _countdownTimer; + late TradesProvider tradesProvider; + + late final tradesProviderListener; + + @override + void initState() { + super.initState(); + tradesProvider = Provider.of(context, listen: false); + _initializePage(); + _listenToPhaseUpdates(); + } + + void _initializePage() { + if (mounted) { + setState(() { + _currentPage = _getPhaseIndex(widget.trade.phase); + _pageController = PageController(initialPage: _currentPage); + }); + } + } + + void _listenToPhaseUpdates() { + tradesProviderListener = () { + final updatedTrade = tradesProvider.trades + .firstWhere((trade) => trade.tradeId == widget.trade.tradeId); + if (updatedTrade.phase != widget.trade.phase) { + if (mounted) { + setState(() { + widget.trade.phase = updatedTrade.phase; + _currentPage = _getPhaseIndex(updatedTrade.phase); + _pageController.animateToPage( + _currentPage, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + }); + } + } + }; + tradesProvider.addListener(tradesProviderListener); + } + + void _listenToDisputeStateUpdates() { + tradesProviderListener = () { + final updatedTrade = tradesProvider.trades + .firstWhere((trade) => trade.tradeId == widget.trade.tradeId); + if (updatedTrade.phase != widget.trade.phase) { + if (mounted) { + setState(() { + widget.trade.phase = updatedTrade.phase; + _currentPage = _getPhaseIndex(updatedTrade.phase); + _pageController.animateToPage( + _currentPage, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + }); + } + } + }; + tradesProvider.addListener(tradesProviderListener); + } + + int _getPhaseIndex(String phase) { + switch (phase) { + case 'INIT': + return 0; + case 'DEPOSITS_PUBLISHED': + return 1; + case 'DEPOSITS_CONFIRMED': + return 2; + case 'DEPOSITS_UNLOCKED': + return 3; + case 'PAYMENT_SENT': + return 4; + case 'PAYMENT_RECEIVED': + return 5; + default: + return 0; + } + } + + Widget _getPhaseWidget(int index) { + switch (index) { + case 0: + return const PhaseInitBuyer(); + case 1: + return const PhaseDepositsPublishedBuyer(); + case 2: + return const PhaseDepositsConfirmedBuyer(); + case 3: + return PhaseDepositsUnlockedBuyer( + takerPaymentAccountPayload: _extractAccountPayload( + jsonDecode(jsonEncode(widget.trade.contract.takerPaymentAccountPayload.toProto3Json()))), + makerPaymentAccountPayload: _extractAccountPayload( + jsonDecode(jsonEncode(widget.trade.contract.makerPaymentAccountPayload.toProto3Json()))), + trade: widget.trade, + onPaidInFull: _onPaidInFull, + ); + case 4: + return PhasePaymentSentBuyer(trade: widget.trade); + case 5: + return const PhasePaymentReceivedBuyer(); + default: + return const PhaseInitBuyer(); // Fallback to an initial phase + } + } + + Map _extractAccountPayload(Map json) { + return json.entries + .firstWhere((entry) => entry.key.contains('AccountPayload')) + .value as Map; + } + + String formatFiat(double amount, String currencyCode) { + return isFiatCurrency(currencyCode) + ? '${amount.toStringAsFixed(2)} $currencyCode' + : amount.toString(); + } + + Future _onPaidInFull() async { + try { + // Confirm the payment + await tradesProvider.confirmPaymentSent(widget.trade.tradeId); + + // Move to the next phase screen, which is index 4 (PAYMENT_SENT phase) + setState(() { + _currentPage = 4; + }); + + _pageController.animateToPage( + _currentPage, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + } catch (error) { + // Handle error and notify the user + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Failed to confirm payment, please try again in a moment.')), + ); + } + } + + @override + Widget build(BuildContext context) { + const int totalPages = 6; // Number of phases + + return Scaffold( + appBar: AppBar( + title: const Text('Buying XMR'), + ), + body: Column( + children: [ + Expanded( + child: PageView.builder( + controller: _pageController, + physics: const NeverScrollableScrollPhysics(), + itemCount: totalPages, + itemBuilder: (context, index) { + return _getPhaseWidget(index); + }, + ), + ), + Padding( + padding: const EdgeInsets.only(bottom: 16.0, top: 4.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate(totalPages, (index) { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 4.0), + width: 12.0, + height: 12.0, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: index <= _currentPage ? Colors.green : Colors.grey, + ), + ); + }), + ), + ), + ], + ), + ); + } + + @override + void dispose() { + _countdownTimer?.cancel(); + _pageController.dispose(); + tradesProvider.removeListener(tradesProviderListener); + super.dispose(); + } +} diff --git a/lib/views/screens/active_seller_trade_timeline_screen.dart b/lib/views/screens/active_seller_trade_timeline_screen.dart new file mode 100644 index 0000000..8348006 --- /dev/null +++ b/lib/views/screens/active_seller_trade_timeline_screen.dart @@ -0,0 +1,179 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:haveno/grpc_models.dart'; +import 'package:haveno_app/views/screens/trade_timeline/seller_phases/phase_deposits_confirmed_seller.dart'; +import 'package:haveno_app/views/screens/trade_timeline/seller_phases/phase_deposits_published_seller.dart'; +import 'package:haveno_app/views/screens/trade_timeline/seller_phases/phase_deposits_unlocked_seller.dart'; +import 'package:haveno_app/views/screens/trade_timeline/seller_phases/phase_init_seller.dart'; +import 'package:haveno_app/views/screens/trade_timeline/seller_phases/phase_payment_received_seller.dart'; +import 'package:haveno_app/views/screens/trade_timeline/seller_phases/phase_payment_sent_seller.dart'; +import 'package:provider/provider.dart'; +import 'package:haveno_app/providers/haveno_client_providers/trades_provider.dart'; + +class ActiveSellerTradeTimelineScreen extends StatefulWidget { + final TradeInfo trade; + + const ActiveSellerTradeTimelineScreen({super.key, required this.trade}); + + @override + _ActiveSellerTradeTimelineScreenState createState() => + _ActiveSellerTradeTimelineScreenState(); +} + +class _ActiveSellerTradeTimelineScreenState + extends State { + PageController _pageController = PageController(); + int _currentPage = 0; + Timer? _countdownTimer; + late TradesProvider tradesProvider; + + late final tradesProviderListener; + + @override + void initState() { + super.initState(); + tradesProvider = Provider.of(context, listen: false); + _initializePage(); + _listenToPhaseUpdates(); + } + + void _initializePage() { + if (mounted) { + setState(() { + _currentPage = _getPhaseIndex(widget.trade.phase); + _pageController = PageController(initialPage: _currentPage); + }); + } + } + + void _listenToPhaseUpdates() { + tradesProviderListener = () { + final updatedTrade = tradesProvider.trades + .firstWhere((trade) => trade.tradeId == widget.trade.tradeId); + if (updatedTrade.phase != widget.trade.phase) { + if (mounted) { + setState(() { + widget.trade.phase = updatedTrade.phase; + _currentPage = _getPhaseIndex(updatedTrade.phase); + _pageController.animateToPage( + _currentPage, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + }); + } + } + }; + tradesProvider.addListener(tradesProviderListener); + } + + int _getPhaseIndex(String phase) { + switch (phase) { + case 'INIT': + return 0; + case 'DEPOSITS_PUBLISHED': + return 1; + case 'DEPOSITS_CONFIRMED': + return 2; + case 'DEPOSITS_UNLOCKED': + return 3; + case 'PAYMENT_SENT': + return 4; + case 'PAYMENT_RECEIVED': + return 5; + default: + return 0; + } + } + + Widget _getPhaseWidget(int index) { + switch (index) { + case 0: + return const PhaseInitSeller(); + case 1: + return const PhaseDepositsPublishedSeller(); + case 2: + return const PhaseDepositsConfirmedSeller(); + case 3: + return PhaseDepositsUnlockedSeller(trade: widget.trade); + case 4: + return PhasePaymentSentSeller(trade: widget.trade); + case 5: + return const PhasePaymentReceivedSeller(); + default: + return const PhaseInitSeller(); // Fallback to an initial phase + } + } + + @override + Widget build(BuildContext context) { + const int totalPages = 6; // Number of phases + + return Scaffold( + appBar: AppBar( + title: const Text('Selling XMR'), + ), + body: Column( + children: [ + Expanded( + child: PageView.builder( + controller: _pageController, + physics: const NeverScrollableScrollPhysics(), + itemCount: totalPages, + itemBuilder: (context, index) { + return _getPhaseWidget(index); + }, + ), + ), + Padding( + padding: const EdgeInsets.only(bottom: 16.0, top: 4.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate(totalPages, (index) { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 4.0), + width: 12.0, + height: 12.0, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: index <= _currentPage ? Colors.green : Colors.grey, + ), + ); + }), + ), + ), + ], + ), + ); + } + + @override + void dispose() { + _countdownTimer?.cancel(); + _pageController.dispose(); + tradesProvider.removeListener(tradesProviderListener); + super.dispose(); + } +} diff --git a/lib/views/screens/base_chat_screen.dart b/lib/views/screens/base_chat_screen.dart new file mode 100644 index 0000000..6e1f0db --- /dev/null +++ b/lib/views/screens/base_chat_screen.dart @@ -0,0 +1,183 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'package:flutter/material.dart'; +import 'package:chatview/chatview.dart'; +import 'package:haveno/profobuf_models.dart'; +import 'package:uuid/uuid.dart'; + +abstract class BaseChatScreen extends StatefulWidget { + final String chatId; + final String chatTitle; + + const BaseChatScreen({super.key, required this.chatId, required this.chatTitle}); + + @override + BaseChatScreenState createState(); +} + +abstract class BaseChatScreenState extends State { + ChatController? _chatController; + late final ChatUser _systemUser = ChatUser(id: 'me', name: 'Me'); + late final ChatUser _myUser = ChatUser(id: 'me', name: 'Me'); + List otherUsers = []; + + @override + void initState() { + super.initState(); + _initializeChatController(); + } + + @override + void dispose() { + _chatController?.scrollController.dispose(); + super.dispose(); + } + + Future _initializeChatController() async { + final chatMessages = await loadChatMessages(); + + // Map ChatMessage to the Message type expected by the ChatController + final messageList = chatMessages.map(_mapChatMessageToMessage).toList(); + + _chatController = ChatController( + initialMessageList: messageList, + scrollController: ScrollController(), + currentUser: _myUser, + otherUsers: otherUsers, + ); + } + + Message _mapChatMessageToMessage(ChatMessage chatMessage) { + final sentBy = _determineSender(chatMessage); + return Message( + id: chatMessage.uid, + message: chatMessage.message, + createdAt: DateTime.fromMillisecondsSinceEpoch(chatMessage.date.toInt()), + sentBy: sentBy, + status: chatMessage.acknowledged ? MessageStatus.read : MessageStatus.delivered, + ); + } + + void _addSystemMessage(String text) { + final systemMessage = Message( + id: const Uuid().v1(), + message: text, + createdAt: DateTime.now(), + sentBy: _systemUser.name, + ); + + _chatController?.addMessage(systemMessage); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: FutureBuilder( + future: _initializeChatController(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } + return ChatView( + chatController: _chatController!, + onSendTap: _handleSendPressed, + chatViewState: _chatController!.initialMessageList.isEmpty ? ChatViewState.noData : ChatViewState.hasMessages, + appBar: ChatViewAppBar( + backGroundColor: Theme.of(context).scaffoldBackgroundColor, + profilePicture: 'X', + chatTitle: widget.chatTitle, + userStatus: 'Active', + actions: buildAppBarActions(), + ), + featureActiveConfig: const FeatureActiveConfig( + enableSwipeToReply: true, + enableSwipeToSeeTime: true, + ), + chatBackgroundConfig: ChatBackgroundConfiguration( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + messageSorter: (message1, message2) { + return message1.createdAt.compareTo(message2.createdAt); + } + ), + sendMessageConfig: const SendMessageConfiguration( + replyMessageColor: Colors.white, + replyDialogColor: Colors.blue, + replyTitleColor: Colors.black, + closeIconColor: Colors.black, + textFieldBackgroundColor: Color(0xFF424242), + textFieldConfig: TextFieldConfiguration() + ), + chatBubbleConfig: ChatBubbleConfiguration( + outgoingChatBubbleConfig: const ChatBubble( + color: Colors.blue, + borderRadius: BorderRadius.only( + topRight: Radius.circular(12), + topLeft: Radius.circular(12), + bottomLeft: Radius.circular(12), + ), + ), + inComingChatBubbleConfig: ChatBubble( + color: Colors.black.withOpacity(0.2), + textStyle: const TextStyle(color: Colors.white), + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(12), + topRight: Radius.circular(12), + bottomRight: Radius.circular(12), + ), + ), + ), + ); + }, + ), + ); + } + + void _handleSendPressed(String messageText, ReplyMessage replyMessage, MessageType messageType) async { + final newMessage = Message( + id: Uuid().v4(), + message: messageText, + createdAt: DateTime.now(), + sentBy: 'me', + messageType: messageType, + replyMessage: replyMessage, + ); + + _chatController?.addMessage(newMessage); + + try { + await sendMessage(messageText); + newMessage.setStatus = MessageStatus.delivered; + } catch (e) { + newMessage.setStatus = MessageStatus.undelivered; + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Message sending failed!')), + ); + } + } + + // These methods are to be implemented by subclasses + Future> loadChatMessages(); + String _determineSender(ChatMessage chatMessage); + Future sendMessage(String messageText); + List buildAppBarActions(); +} diff --git a/lib/views/screens/dispute_chat_screen.dart b/lib/views/screens/dispute_chat_screen.dart new file mode 100644 index 0000000..6f05195 --- /dev/null +++ b/lib/views/screens/dispute_chat_screen.dart @@ -0,0 +1,289 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:haveno/grpc_models.dart'; +import 'package:haveno/profobuf_models.dart'; +import 'package:haveno_app/providers/haveno_client_providers/disputes_provider.dart'; +import 'package:provider/provider.dart'; +import 'package:uuid/uuid.dart'; +import 'package:haveno_app/providers/haveno_client_providers/trades_provider.dart'; +import 'package:chatview/chatview.dart'; + +class DisputeChatScreen extends StatefulWidget { + final String tradeId; + + const DisputeChatScreen({super.key, required this.tradeId}); + + @override + _DisputeChatScreenState createState() => _DisputeChatScreenState(); +} + +class _DisputeChatScreenState extends State { + ChatController? _chatController; + late final ChatUser _systemUser; + late final ChatUser _myUser; + late String _tradePeerId; + late ChatUser _tradePeerUser; + late String _arbitratorId; + late ChatUser _arbitratorUser; + Dispute? _dispute; + TradeInfo? _trade; + Timer? _timer; + + @override + void initState() { + super.initState(); + _systemUser = ChatUser(id: 'system', name: 'System'); + _myUser = ChatUser(id: 'me', name: 'Me'); + } + + @override + void dispose() { + _timer?.cancel(); + super.dispose(); + } + + Future _initializeDisputeData() async { + _trade = await _getTrade(widget.tradeId); + if (_trade == null) { + Navigator.of(context).pop(); // Exit if trade is not found + return; + } + _dispute = Provider.of(context, listen: false).getDisputeByTradeId(widget.tradeId); + _setUserRoles(); + await _initializeChatController(); + } + + Future _getTrade(String tradeId) async { + final tradesProvider = Provider.of(context, listen: false); + await tradesProvider.getTrade(tradeId); + var trade = tradesProvider.trades.firstWhere((trade) => trade.tradeId == tradeId); + return trade; + } + + void _setUserRoles() { + if (_trade == null) return; + + _tradePeerId = _trade!.tradePeerNodeAddress.split('.').first; + _arbitratorId = _trade!.arbitratorNodeAddress.split('.').first; + + _tradePeerUser = ChatUser(id: _tradePeerId, name: 'Peer'); + _arbitratorUser = ChatUser(id: _arbitratorId, name: 'Arbitrator'); + + print(_myUser.id); + print(_tradePeerUser.id); + } + + Future _initializeChatController() async { + if (_chatController != null || _trade == null || _dispute == null) return; + + final disputesProvider = Provider.of(context, listen: false); + List chatMessages = []; + + if (_trade!.disputeState != 'NO_DISPUTE') { + try { + await disputesProvider.loadInitialMessages(_dispute!.id); + List? disputeChatMessages = disputesProvider.getInitialChatMessages(_dispute!.id); + chatMessages.addAll(disputeChatMessages); + + disputesProvider.chatMessagesStream(_dispute!.id).listen((newMessages) { + _updateChatControllerWithNewMessages(newMessages); + }); + } catch (e) { + print("Dispute state is set but provider returned no dispute"); + } + } + + chatMessages.sort((a, b) => a.date.compareTo(b.date)); + + final messageList = chatMessages.map(_mapChatMessageToMessage).toList(); + + _chatController = ChatController( + initialMessageList: messageList, + scrollController: ScrollController(), + currentUser: _myUser, + otherUsers: [_tradePeerUser, _arbitratorUser, _systemUser], + ); + } + + void _updateChatControllerWithNewMessages(List newMessages) { + final newMessageList = newMessages.map(_mapChatMessageToMessage).toList(); + for (var message in newMessageList) { + _chatController?.addMessage(message); + } + } + + Message _mapChatMessageToMessage(ChatMessage chatMessage) { + final senderNodeAddress = chatMessage.senderNodeAddress.hostName.split('.').first; + + final sentBy = senderNodeAddress == _tradePeerId + ? _tradePeerId + : senderNodeAddress == _arbitratorId + ? _arbitratorId + : chatMessage.isSystemMessage + ? _systemUser.name + : _myUser.name; + + return Message( + id: chatMessage.uid, + message: chatMessage.message, + createdAt: DateTime.fromMillisecondsSinceEpoch(chatMessage.date.toInt()), + sentBy: sentBy, + status: chatMessage.acknowledged ? MessageStatus.read : MessageStatus.delivered, + ); + } + + void _handlePaymentSentPressed() { + final tradesProvider = Provider.of(context, listen: false); + tradesProvider.confirmPaymentSent(_trade!.tradeId).then((_) { + _addSystemMessage('Payment marked as sent.'); + }).catchError((error) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to confirm payment: $error')), + ); + }); + } + + void _handleDisputePressed() { + final disputesProvider = Provider.of(context, listen: false); + disputesProvider.openDispute(_trade!.tradeId).then((_) { + print("Simulating navigation to dispute chat screen"); + }).catchError((error) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to dispute trade: $error')), + ); + }); + } + + void _handleSendPressed(String messageText, ReplyMessage replyMessage, MessageType messageType) async { + final newMessage = Message( + id: const Uuid().v4(), + message: messageText, + createdAt: DateTime.now(), + sentBy: _myUser.name, + messageType: messageType, + replyMessage: replyMessage, + ); + + _chatController?.addMessage(newMessage); + + final disputesProvider = Provider.of(context, listen: false); + try { + await disputesProvider.sendDisputeChatMessage(_trade!.tradeId, messageText, []); + newMessage.setStatus = MessageStatus.delivered; + } catch (e) { + newMessage.setStatus = MessageStatus.undelivered; + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('You can only send 1 message per minute!')), + ); + } + } + + void _addSystemMessage(String text) { + final systemMessage = Message( + id: const Uuid().v1(), + message: text, + createdAt: DateTime.now(), + sentBy: _systemUser.name, + ); + + _chatController?.addMessage(systemMessage); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: FutureBuilder( + future: _initializeDisputeData(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } + + if (_trade == null || _dispute == null) { + return const Center(child: Text("Trade or Dispute not found")); + } + + return ChatView( + chatController: _chatController!, + onSendTap: _handleSendPressed, + chatViewState: _chatController!.initialMessageList.isEmpty ? ChatViewState.noData : ChatViewState.hasMessages, + appBar: ChatViewAppBar( + backGroundColor: Theme.of(context).scaffoldBackgroundColor, + chatTitle: 'Support Chat for Trade #${_trade!.shortId}', + userStatus: 'Active', + actions: [ + if (_trade!.isPaymentSent) + IconButton( + icon: const Icon(Icons.check), + onPressed: _handlePaymentSentPressed, + tooltip: 'Confirm Transfer of Funds', + ), + ], + ), + featureActiveConfig: const FeatureActiveConfig( + enableSwipeToReply: true, + enableSwipeToSeeTime: true, + ), + chatBackgroundConfig: ChatBackgroundConfiguration( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + messageSorter: (message1, message2) { + return message1.createdAt.compareTo(message2.createdAt); + }, + ), + sendMessageConfig: const SendMessageConfiguration( + replyMessageColor: Colors.white, + replyDialogColor: Colors.blue, + replyTitleColor: Colors.black, + closeIconColor: Colors.black, + textFieldBackgroundColor: Color(0xFF424242), + textFieldConfig: TextFieldConfiguration(), + enableCameraImagePicker: true, + enableGalleryImagePicker: true, + ), + chatBubbleConfig: ChatBubbleConfiguration( + outgoingChatBubbleConfig: const ChatBubble( + color: Colors.blue, + borderRadius: BorderRadius.only( + topRight: Radius.circular(12), + topLeft: Radius.circular(12), + bottomLeft: Radius.circular(12), + ), + ), + inComingChatBubbleConfig: ChatBubble( + color: Colors.black.withOpacity(0.2), + textStyle: const TextStyle(color: Colors.white), + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(12), + topRight: Radius.circular(12), + bottomRight: Radius.circular(12), + ), + ), + ), + ); + }, + ), + ); + } +} diff --git a/lib/views/screens/establish_connection_screen.dart b/lib/views/screens/establish_connection_screen.dart new file mode 100644 index 0000000..b17bada --- /dev/null +++ b/lib/views/screens/establish_connection_screen.dart @@ -0,0 +1,286 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'dart:async'; +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:grpc/grpc.dart'; +import 'package:haveno_app/providers/haveno_client_providers/disputes_provider.dart'; +import 'package:haveno_app/providers/haveno_client_providers/offers_provider.dart'; +import 'package:haveno_app/providers/haveno_client_providers/payment_accounts_provider.dart'; +import 'package:haveno_app/providers/haveno_client_providers/price_provider.dart'; +import 'package:haveno_app/providers/haveno_client_providers/trades_provider.dart'; +import 'package:haveno_app/providers/haveno_client_providers/wallets_provider.dart'; +import 'package:haveno_app/services/connection_checker_service.dart'; +import 'package:haveno_app/views/screens/home_screen.dart'; +import 'package:provider/provider.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class EstablishConnectionScreen extends StatefulWidget { + const EstablishConnectionScreen({super.key}); + + @override + _EstablishConnectionScreenState createState() => + _EstablishConnectionScreenState(); +} + +class _EstablishConnectionScreenState + extends State { + bool orbotMessageShown = false; + String message = "Connecting to Tor..."; + double progress = 0.0; // Progress value + bool _isTorConnected = false; // Track whether Tor is connected + int failedAttempts = 0; + Timer? connectionTimer; + bool isCheckingConnection = false; // To ensure only one connection check at a time + final _connectionCheckerService = ConnectionCheckerService(); + + @override + void initState() { + super.initState(); + _checkTorConnection(); + } + + + + Future _checkTorConnection() async { + connectionTimer = Timer.periodic(const Duration(seconds: 5), (timer) async { + if (isCheckingConnection) return; // Skip if a check is already in progress + + isCheckingConnection = true; // Mark as checking connection + try { + _isTorConnected = await _connectionCheckerService.isTorConnected(); + if (_isTorConnected) { + setState(() { + message = "Connecting to Daemon..."; + _isTorConnected = true; // Mark Tor as connected + }); + timer.cancel(); // Stop further attempts + await _initializeProvidersSequentially(); + } else { + failedAttempts++; + if (failedAttempts >= 10 && !orbotMessageShown) { + _showOrbotDownloadMessage(); + orbotMessageShown = true; + } + } + } catch (e) { + print("Error checking Tor connection: $e"); + } finally { + isCheckingConnection = false; // Reset the flag after check + } + }); + } + + Future _initializeProvidersSequentially() async { + if (_isTorConnected) { + while (!await _connectionCheckerService.isHavenoDaemonConnected()) { + await Future.delayed(const Duration(seconds: 1)); + } + + await Future.delayed(const Duration(seconds: 10)); + + // Total steps + const int totalSteps = 9; + int currentStep = 0; + + // Initialize each provider sequentially with retry logic + await _initializeProviderWithRetry(_initializePaymentMethods, "Fetching Payment Methods...", totalSteps, ++currentStep); + await _initializeProviderWithRetry(_initializeOffers, "Fetching Offers...", totalSteps, ++currentStep); + await _initializeProviderWithRetry(_initializeDisputes, "Fetching Integrity Profile...", totalSteps, ++currentStep); + await _initializeProviderWithRetry(_initializeTrades, "Fetching Trades...", totalSteps, ++currentStep); + _initializeProviderWithRetry(_initializePrices, "Fetching Market Prices...", totalSteps, ++currentStep); + await _initializeProviderWithRetry(_initializePaymentAccounts, "Fetching Payment Accounts...", totalSteps, ++currentStep); + await _initializeProviderWithRetry(_initializePaymentAccountForms, "Fetching Payment Accounts...", totalSteps, ++currentStep); + await _initializeProviderWithRetry(_initializeWallets, "Wallet Initializing...", totalSteps, ++currentStep); + await _initializeProviderWithRetry(_initializePrimaryWalletAddress, 'Retrieving Primary Wallet Address...', totalSteps, ++currentStep); + + _navigateToHomeScreen(); + + } else { + setState(() { + message = "Tor connection failed."; + }); + print("Tor connection failed."); + } + } + + Future _initializeProviderWithRetry( + Future Function() initializeFunction, String initializationMessage, int totalSteps, int currentStep) async { + bool success = false; + while (!success) { + try { + setState(() { + message = initializationMessage; + progress = currentStep / totalSteps; // Update progress + }); + + // Add a delay before each initialization + await Future.delayed(const Duration(seconds: 2)); + + await initializeFunction(); + success = true; // If successful, exit the loop + } on GrpcError catch (e) { + final errorString = e.toString(); + if (errorString.contains("wallet and network is not yet") || errorString.contains("wallet is not yet")) { + setState(() { + message = "Wallet Initializing..."; + }); + print("Wallet not initialized. Retrying..."); + } else if (errorString.contains("Connection refused") || errorString.contains("the maximum allowed number")) { + setState(() { + message = "Connecting to Daemon..."; + }); + print("Connection refused. Retrying..."); + } else { + setState(() { + message = "Error: ${e.toString()}"; + }); + print("${e.message}"); + } + await Future.delayed(const Duration(seconds: 5)); + } catch (e) { + setState(() { + message = "Unexpected Error: ${e.toString()}"; + }); + print(e.toString()); + await Future.delayed(const Duration(seconds: 5)); + } + } + } + + + Future _initializeWallets() async { + await Provider.of(context, listen: false).getBalances(); + } + + Future _initializePrimaryWalletAddress() async { + await Provider.of(context, listen: false).getXmrPrimaryAddress(); + } + + Future _initializeOffers() async { + await Provider.of(context, listen: false).getAllOffers(); + } + + Future _initializeDisputes() async { + await Provider.of(context, listen: false).getDisputes(); + } + + Future _initializeTrades() async { + await Provider.of(context, listen: false).getTrades(); + } + + Future _initializePrices() async { + await Provider.of(context, listen: false).getXmrMarketPrices(); + } + + Future _initializePaymentMethods() async { + await Provider.of(context, listen: false).getPaymentMethods(); + } + + Future _initializePaymentAccounts() async { + await Provider.of(context, listen: false).getPaymentAccounts(); + } + + Future _initializePaymentAccountForms() async { + await Provider.of(context, listen: false).getAllPaymentAccountForms(); + } + + void _showOrbotDownloadMessage() { + setState(() { + if (Platform.isAndroid) { + message = + "You must download the Orbot app to use Haveno Mobile. This ensures there are no leaks from your connection. You can click here to download it from the Play Store, otherwise make sure it's switched on in the app."; + } else if (Platform.isIOS) { + message = + "You must download the Orbot app to use Haveno Mobile. This ensures there are no leaks from your connection. You can click here to download it from the App Store, otherwise make sure it's switched on in the app."; + } + }); + } + + void _launchStore() async { + String url = ''; + if (Platform.isAndroid) { + url = + 'https://play.google.com/store/apps/details?id=org.torproject.android'; + } else if (Platform.isIOS) { + url = 'https://apps.apple.com/us/app/orbot/id1456634573'; + } + + if (await canLaunchUrl(Uri.parse(url))) { + await launchUrl(Uri.parse(url)); + } else { + throw 'Could not launch $url'; + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: _buildLoadingScreen(), + ); + } + + Widget _buildLoadingScreen() { + return Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + _isTorConnected ? 'assets/haveno-logo.png' : 'assets/tor-logo.png', + height: 100, + ), // Tor logo until connected, then Haveno logo + const SizedBox(height: 16), + LinearProgressIndicator(value: progress), + const SizedBox(height: 16), + Text( + message, + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 18), + ), + if (message.contains("download it from the")) + TextButton( + onPressed: _launchStore, + child: Text( + "Download from the ${Platform.isAndroid ? 'Play Store' : 'App Store'}", + style: const TextStyle(color: Colors.blue), + ), + ), + ], + ), + ), + ); + } + + void _navigateToHomeScreen() { + Future.delayed(const Duration(seconds: 2), () { + if (mounted) { + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (context) => HomeScreen()), + ); + } + }); + } +} diff --git a/lib/views/screens/home_screen.dart b/lib/views/screens/home_screen.dart new file mode 100644 index 0000000..c49f7fd --- /dev/null +++ b/lib/views/screens/home_screen.dart @@ -0,0 +1,110 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'package:flutter/material.dart'; +import 'package:haveno_app/views/tabs/buy_tab.dart'; +import 'package:haveno_app/views/tabs/market_statistics.dart'; +import 'package:haveno_app/views/tabs/sell_tab.dart'; +import 'package:haveno_app/views/tabs/trades_tab.dart'; +import 'package:haveno_app/views/widgets/balance.dart'; +import 'package:haveno_app/views/widgets/main_drawer.dart'; + +class HomeScreen extends StatefulWidget { + final int initialIndex; + + const HomeScreen({super.key, this.initialIndex = 0}); + + @override + _HomeScreenState createState() => _HomeScreenState(); +} + +class _HomeScreenState extends State { + late int _selectedIndex; + + static final List _widgetOptions = [ + MarketStatistics(), + BuyTab(), + SellTab(), + TradesTab(), + ]; + + @override + void initState() { + super.initState(); + _selectedIndex = widget.initialIndex; + } + + void _onItemTapped(int index) { + setState(() { + _selectedIndex = index; + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + appBar: AppBar( + leading: Builder( + builder: (context) => IconButton( + icon: Icon(Icons.menu), + onPressed: () { + Scaffold.of(context).openDrawer(); + }, + ), + ), + title: Row( + mainAxisAlignment: MainAxisAlignment.end, // Aligns the widget to the right + children: [ + MoneroBalanceWidget(), + ], + ), + ), + drawer: MainDrawer(), + body: Center( + child: _widgetOptions.elementAt(_selectedIndex), + ), + bottomNavigationBar: NavigationBar( + selectedIndex: _selectedIndex, + onDestinationSelected: _onItemTapped, + destinations: const [ + NavigationDestination( + icon: Icon(Icons.query_stats), + label: 'Market Stats', + ), + NavigationDestination( + icon: Icon(Icons.shopping_cart), + label: 'Buy', + ), + NavigationDestination( + icon: Icon(Icons.sell), + label: 'Sell', + ), + NavigationDestination( + icon: Icon(Icons.swap_vert), + label: 'Trades', + ), + ], + ), + ); + } +} diff --git a/lib/views/screens/link_to_desktop_screen.dart b/lib/views/screens/link_to_desktop_screen.dart new file mode 100644 index 0000000..9a1646e --- /dev/null +++ b/lib/views/screens/link_to_desktop_screen.dart @@ -0,0 +1,381 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'dart:async'; +import 'dart:convert'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:haveno_app/services/mobile_manager_service.dart'; +import 'package:haveno_app/services/secure_storage_service.dart'; +import 'package:mobile_scanner/mobile_scanner.dart'; +import 'establish_connection_screen.dart'; + +class LinkToDesktopScreen extends StatefulWidget { + const LinkToDesktopScreen({super.key}); + + @override + State createState() => _LinkToDesktopScreenState(); +} + +class _LinkToDesktopScreenState extends State { + final MobileScannerController controller = MobileScannerController(); + DateTime? _lastScanTime; // Track the last scan time + final TextEditingController pasteController = TextEditingController(); + bool isProcessing = false; + SecureStorageService secureStorageService = SecureStorageService(); + + @override + void initState() { + super.initState(); + pasteController.addListener(_handlePaste); + } + + @override + void dispose() { + controller.dispose(); + pasteController.dispose(); + super.dispose(); + } + + /// 1. Handle barcode scanning separately + void _handleBarcodeScan(String barcode) { + if (isProcessing) return; // Prevent multiple simultaneous scans + isProcessing = true; + + final now = DateTime.now(); + if (_lastScanTime != null && now.difference(_lastScanTime!) < const Duration(seconds: 1)) { + // Ignore barcode scans if less than 1 second has passed + isProcessing = false; + return; + } + + _lastScanTime = now; // Update the last scan time + + try { + _processUri(barcode); // Process the barcode URI + } catch (e) { + print('Error handling barcode: $e'); + } finally { + isProcessing = false; + } + } + + /// 2. Handle linkage key pasting separately + void _handlePaste() { + Timer(const Duration(milliseconds: 500), () { + final text = pasteController.text.trim(); // Trim whitespace + if (text.isEmpty || isProcessing) return; + isProcessing = true; + + try { + final decoded = utf8.decode(base64.decode(text)).trim(); + _processUri(decoded); // Process the pasted linkage key + } catch (e) { + _showInvalidUriAlert(); // Show error if URI is invalid + } finally { + isProcessing = false; + } + }); + } + + /// Process the URI from either barcode or pasted linkage key + void _processUri(String uriString) async { + try { + final Uri onionUri = Uri.parse(uriString); + print("Processed URI: $onionUri"); + + final mobileManagerService = MobileManagerService(); + mobileManagerService.setHavenoDaemonNodeConfig(onionUri).then((daemonConfig) { + if (daemonConfig != null) { + print("Valid daemon config received"); + secureStorageService.writeOnboardingStatus(true).then((_) { + if (mounted) { + Navigator.of(context).pushReplacement( + MaterialPageRoute(builder: (context) => EstablishConnectionScreen()), + ); + } + }); + } + }).catchError((e) { + print('Error setting daemon config: $e'); + }); + } catch (e) { + print('Error processing URI: $e'); + _showInvalidUriAlert(); + } + } + + /// Show alert dialog for invalid linkage key + void _showInvalidUriAlert() { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Invalid Linkage Key'), + content: const Text('The pasted key is not a valid URI.'), + actions: [ + TextButton( + child: const Text('OK'), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ], + ); + }, + ); + } + + /// Build barcode overlay for scanner + Widget _buildBarcodeOverlay() { + return ValueListenableBuilder( + valueListenable: controller, + builder: (context, value, child) { + if (!value.isInitialized || !value.isRunning || value.error != null) { + return const SizedBox(); + } + + return StreamBuilder( + stream: controller.barcodes, + builder: (context, snapshot) { + final BarcodeCapture? barcodeCapture = snapshot.data; + + if (barcodeCapture == null || barcodeCapture.barcodes.isEmpty) { + return const SizedBox(); + } + + final scannedBarcode = barcodeCapture.barcodes.first; + if (scannedBarcode.rawValue != null) { + _handleBarcodeScan(scannedBarcode.rawValue!); + } + + if (scannedBarcode.corners.isEmpty || + value.size.isEmpty || + barcodeCapture.size.isEmpty) { + return const SizedBox(); + } + + return CustomPaint( + painter: BarcodeOverlay( + barcodeCorners: scannedBarcode.corners, + barcodeSize: barcodeCapture.size, + boxFit: BoxFit.contain, + cameraPreviewSize: value.size, + ), + ); + }, + ); + }, + ); + } + + Widget _buildScanWindow(Rect scanWindowRect) { + return ValueListenableBuilder( + valueListenable: controller, + builder: (context, value, child) { + if (!value.isInitialized || + !value.isRunning || + value.error != null || + value.size.isEmpty) { + return const SizedBox(); + } + + return CustomPaint( + painter: ScannerOverlay(scanWindowRect), + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + final scanWindow = Rect.fromCenter( + center: MediaQuery.sizeOf(context).center(Offset.zero), + width: 200, + height: 200, + ); + + return Scaffold( + appBar: AppBar(title: const Text('Link to Desktop')), + backgroundColor: Colors.black, + body: Stack( + fit: StackFit.expand, + children: [ + MobileScanner( + fit: BoxFit.contain, + scanWindow: scanWindow, + controller: controller, + ), + _buildBarcodeOverlay(), + _buildScanWindow(scanWindow), + Positioned( + bottom: 20, + left: 20, + right: 20, + child: Column( + children: [ + const Text( + 'Alternatively, paste your linkage key below:', + style: TextStyle(color: Colors.white), + ), + const SizedBox(height: 10), + Row( + children: [ + Expanded( + child: TextField( + controller: pasteController, + readOnly: true, + decoration: InputDecoration( + border: OutlineInputBorder(), + labelText: 'Linkage Key', + labelStyle: const TextStyle(color: Colors.white), + enabledBorder: const OutlineInputBorder( + borderSide: BorderSide(color: Colors.white), + ), + focusedBorder: const OutlineInputBorder( + borderSide: BorderSide(color: Colors.blue), + ), + ), + ), + ), + IconButton( + icon: const Icon(Icons.paste, color: Colors.white), + onPressed: () async { + ClipboardData? data = await Clipboard.getData('text/plain'); + if (data != null) { + pasteController.text = data.text!; + } + }, + ), + ], + ), + ], + ), + ), + ], + ), + ); + } +} + + +class ScannerOverlay extends CustomPainter { + ScannerOverlay(this.scanWindow); + + final Rect scanWindow; + + @override + void paint(Canvas canvas, Size size) { + final backgroundPath = Path()..addRect(Rect.largest); + final cutoutPath = Path()..addRect(scanWindow); + + final backgroundPaint = Paint() + ..color = Colors.black.withOpacity(0.5) + ..style = PaintingStyle.fill + ..blendMode = BlendMode.dstOut; + + final backgroundWithCutout = Path.combine( + PathOperation.difference, + backgroundPath, + cutoutPath, + ); + canvas.drawPath(backgroundWithCutout, backgroundPaint); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) { + return false; + } +} + +class BarcodeOverlay extends CustomPainter { + BarcodeOverlay({ + required this.barcodeCorners, + required this.barcodeSize, + required this.boxFit, + required this.cameraPreviewSize, + }); + + final List barcodeCorners; + final Size barcodeSize; + final BoxFit boxFit; + final Size cameraPreviewSize; + + @override + void paint(Canvas canvas, Size size) { + if (barcodeCorners.isEmpty || + barcodeSize.isEmpty || + cameraPreviewSize.isEmpty) { + return; + } + + final adjustedSize = applyBoxFit(boxFit, cameraPreviewSize, size); + + double verticalPadding = size.height - adjustedSize.destination.height; + double horizontalPadding = size.width - adjustedSize.destination.width; + if (verticalPadding > 0) { + verticalPadding = verticalPadding / 2; + } else { + verticalPadding = 0; + } + + if (horizontalPadding > 0) { + horizontalPadding = horizontalPadding / 2; + } else { + horizontalPadding = 0; + } + + final double ratioWidth; + final double ratioHeight; + + if (!kIsWeb && defaultTargetPlatform == TargetPlatform.iOS) { + ratioWidth = barcodeSize.width / adjustedSize.destination.width; + ratioHeight = barcodeSize.height / adjustedSize.destination.height; + } else { + ratioWidth = cameraPreviewSize.width / adjustedSize.destination.width; + ratioHeight = cameraPreviewSize.height / adjustedSize.destination.height; + } + + final List adjustedOffset = [ + for (final offset in barcodeCorners) + Offset( + offset.dx / ratioWidth + horizontalPadding, + offset.dy / ratioHeight + verticalPadding, + ), + ]; + + final cutoutPath = Path()..addPolygon(adjustedOffset, true); + + final backgroundPaint = Paint() + ..color = Colors.red.withOpacity(0.3) + ..style = PaintingStyle.fill + ..blendMode = BlendMode.dstOut; + + canvas.drawPath(cutoutPath, backgroundPaint); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) { + return false; + } +} diff --git a/lib/views/screens/local_node_configuration_screen.dart b/lib/views/screens/local_node_configuration_screen.dart new file mode 100644 index 0000000..28d3cae --- /dev/null +++ b/lib/views/screens/local_node_configuration_screen.dart @@ -0,0 +1,351 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'package:flutter/material.dart'; + +class LocalNodeConfigurationScreen extends StatefulWidget { + const LocalNodeConfigurationScreen({super.key}); + + @override + _LocalNodeConfigurationScreenState createState() => _LocalNodeConfigurationScreenState(); +} + +class _LocalNodeConfigurationScreenState extends State { + // Controllers + final TextEditingController _daemonAddressController = TextEditingController(); + final TextEditingController _daemonPortController = TextEditingController(text: '18081'); + final TextEditingController _sslPrivateKeyController = TextEditingController(); + final TextEditingController _sslCertificateController = TextEditingController(); + final TextEditingController _daemonUsernameController = TextEditingController(); + final TextEditingController _daemonPasswordController = TextEditingController(); + final TextEditingController _proxyIpController = TextEditingController(); + final TextEditingController _proxyPortController = TextEditingController(); + final TextEditingController _logFileController = TextEditingController(); + + // State Variables + String _networkType = 'Mainnet'; + bool _useSSL = false; + bool _daemonAuthentication = false; + bool _trustedDaemon = false; + bool _useProxy = false; + bool _offlineMode = false; + bool _allowMismatchedDaemonVersion = false; + String _logLevel = '0'; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Configure Local Node'), + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Network Selection + _buildSectionTitle('Network Selection'), + _buildDropdown( + label: 'Network Type', + value: _networkType, + items: ['Mainnet', 'Testnet', 'Stagenet'], + onChanged: (newValue) { + setState(() { + _networkType = newValue!; + }); + }, + ), + const SizedBox(height: 24), + // Daemon Connection Settings + _buildSectionTitle('Daemon Connection Settings'), + _buildTextInputField( + controller: _daemonAddressController, + label: 'Daemon Address', + hint: 'e.g., 127.0.0.1', + ), + const SizedBox(height: 16), + _buildTextInputField( + controller: _daemonPortController, + label: 'Daemon Port', + hint: 'e.g., 18081', + keyboardType: TextInputType.number, + ), + const SizedBox(height: 16), + _buildToggleSwitch( + label: 'Use SSL', + value: _useSSL, + onChanged: (newValue) { + setState(() { + _useSSL = newValue; + }); + }, + ), + if (_useSSL) ...[ + const SizedBox(height: 16), + _buildTextInputField( + controller: _sslPrivateKeyController, + label: 'SSL Private Key Path', + hint: 'Path to PEM format private key', + ), + const SizedBox(height: 16), + _buildTextInputField( + controller: _sslCertificateController, + label: 'SSL Certificate Path', + hint: 'Path to PEM format certificate', + ), + ], + const SizedBox(height: 24), + + // Authentication Settings + _buildSectionTitle('Authentication Settings'), + _buildToggleSwitch( + label: 'Requires Authentication', + value: _daemonAuthentication, + onChanged: (newValue) { + setState(() { + _daemonAuthentication = newValue; + }); + }, + ), + if (_daemonAuthentication) ...[ + const SizedBox(height: 16), + _buildTextInputField( + controller: _daemonUsernameController, + label: 'Username', + ), + const SizedBox(height: 16), + _buildTextInputField( + controller: _daemonPasswordController, + label: 'Password', + obscureText: true, + ), + ], + const SizedBox(height: 24), + + // Advanced Options + _buildSectionTitle('Advanced Options'), + _buildToggleSwitch( + label: 'Trusted Daemon', + value: _trustedDaemon, + onChanged: (newValue) { + setState(() { + _trustedDaemon = newValue; + }); + }, + ), + const SizedBox(height: 16), + _buildToggleSwitch( + label: 'Use Proxy', + value: _useProxy, + onChanged: (newValue) { + setState(() { + _useProxy = newValue; + }); + }, + ), + if (_useProxy) ...[ + const SizedBox(height: 16), + _buildTextInputField( + controller: _proxyIpController, + label: 'Proxy IP', + hint: 'e.g., 127.0.0.1', + ), + const SizedBox(height: 16), + _buildTextInputField( + controller: _proxyPortController, + label: 'Proxy Port', + hint: 'e.g., 9050', + keyboardType: TextInputType.number, + ), + ], + const SizedBox(height: 24), + + // Logging Options + _buildSectionTitle('Logging Options'), + _buildDropdown( + label: 'Log Level', + value: _logLevel, + items: ['0', '1', '2', '3', '4'], + onChanged: (newValue) { + setState(() { + _logLevel = newValue!; + }); + }, + ), + const SizedBox(height: 16), + _buildTextInputField( + controller: _logFileController, + label: 'Log File Path', + hint: 'Specify log file path', + ), + const SizedBox(height: 24), + + // Miscellaneous Options + _buildSectionTitle('Miscellaneous Options'), + _buildToggleSwitch( + label: 'Offline Mode', + value: _offlineMode, + onChanged: (newValue) { + setState(() { + _offlineMode = newValue; + }); + }, + ), + const SizedBox(height: 16), + _buildToggleSwitch( + label: 'Allow Mismatched Daemon Version', + value: _allowMismatchedDaemonVersion, + onChanged: (newValue) { + setState(() { + _allowMismatchedDaemonVersion = newValue; + }); + }, + ), + const SizedBox(height: 32), + + // Save Button + Center( + child: ElevatedButton.icon( + onPressed: _saveConfiguration, + icon: const Icon(Icons.save), + label: const Text('Save Configuration'), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + textStyle: const TextStyle(fontSize: 16), + ), + ), + ), + const SizedBox(height: 16), + ], + ), + ), + ); + } + + // Helper methods + + Widget _buildTextInputField({ + required TextEditingController controller, + required String label, + String? hint, + TextInputType keyboardType = TextInputType.text, + bool obscureText = false, + }) { + return TextField( + controller: controller, + keyboardType: keyboardType, + obscureText: obscureText, + decoration: InputDecoration( + labelText: label, + hintText: hint, + border: OutlineInputBorder(), + contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 16), + ), + ); + } + + Widget _buildDropdown({ + required String label, + required String value, + required List items, + required ValueChanged onChanged, + }) { + return InputDecorator( + decoration: InputDecoration( + labelText: label, + border: OutlineInputBorder(), + contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 4), + ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + value: value, + isExpanded: true, + items: items.map((item) { + return DropdownMenuItem( + value: item, + child: Text(item), + ); + }).toList(), + onChanged: onChanged, + ), + ), + ); + } + + Widget _buildToggleSwitch({ + required String label, + required bool value, + required ValueChanged onChanged, + }) { + return SwitchListTile( + title: Text(label), + value: value, + onChanged: onChanged, + contentPadding: EdgeInsets.zero, + ); + } + + Widget _buildSectionTitle(String title) { + return Padding( + padding: EdgeInsets.only(bottom: 8), + child: Text( + title, + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + ); + } + + void _saveConfiguration() { + // Collect all the configurations + Map config = { + 'networkType': _networkType, + 'daemonAddress': _daemonAddressController.text, + 'daemonPort': _daemonPortController.text, + 'useSSL': _useSSL, + 'sslPrivateKey': _sslPrivateKeyController.text, + 'sslCertificate': _sslCertificateController.text, + 'daemonAuthentication': _daemonAuthentication, + 'daemonUsername': _daemonUsernameController.text, + 'daemonPassword': _daemonPasswordController.text, + 'trustedDaemon': _trustedDaemon, + 'useProxy': _useProxy, + 'proxyIp': _proxyIpController.text, + 'proxyPort': _proxyPortController.text, + 'logLevel': _logLevel, + 'logFile': _logFileController.text, + 'offlineMode': _offlineMode, + 'allowMismatchedDaemonVersion': _allowMismatchedDaemonVersion, + }; + + // TODO: Save the configuration to the appropriate place + // For now, just print it to the console + print('Configuration Saved: $config'); + + // Provide feedback to the user + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Configuration saved successfully')), + ); + + // Navigate back + Navigator.pop(context); + } +} diff --git a/lib/views/screens/login_screen.dart b/lib/views/screens/login_screen.dart new file mode 100644 index 0000000..3e2283c --- /dev/null +++ b/lib/views/screens/login_screen.dart @@ -0,0 +1,20 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . diff --git a/lib/views/screens/onboarding_screen.dart b/lib/views/screens/onboarding_screen.dart new file mode 100644 index 0000000..5f90e71 --- /dev/null +++ b/lib/views/screens/onboarding_screen.dart @@ -0,0 +1,245 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:haveno_app/services/secure_storage_service.dart'; +import 'package:haveno_app/views/screens/password_screen.dart'; + +class OnboardingScreen extends StatefulWidget { + const OnboardingScreen({super.key}); + + @override + _OnboardingScreenState createState() => _OnboardingScreenState(); +} + +class _OnboardingScreenState extends State { + final PageController _pageController = PageController(); + int _currentIndex = 0; + + late final List _onboardingPages; + + @override + void initState() { + super.initState(); + + String description; + + if (Platform.isLinux || Platform.isMacOS || Platform.isWindows) { + description = "We'll get through some basic steps to getr your account setup and connected to the network."; + } else { + description = "If you would like to use Haveno on your mobile you can do so by first downloading the client from Haveno.com/dekstop, once you have done that, come back here to scan the QR that will be displayed on your computer screen with your phone to make the connection."; + } + + _onboardingPages = [ + const OnboardingPage( + title: "Haveno", + description: "A P2P decentralized trading platform for Monero.", + imagePath: "assets/haveno-logo.png", + ), + const OnboardingPage( + title: "Privacy by Tor", + description: "Track your task progress with ease.", + imagePath: "assets/tor-logo.png", + ), + const OnboardingPage( + title: "Arbitration", + description: "A distributed network of arbitrators will provide security in your transactions.", + imagePath: "assets/arbitration-logo.png", + ), + OnboardingPage( + title: "Get Started", + description: description, + imagePath: "assets/getting-started-logo.png", + isLastPage: true, + onFinish: _onFinish, + ), + ]; + } + + void _onPageChanged(int index) { + setState(() { + _currentIndex = index; + }); + } + + void _onFinish() async { + SecureStorageService secureStorageService = SecureStorageService(); + if (Platform.isWindows || Platform.isMacOS || Platform.isLinux) { + secureStorageService.writeOnboardingStatus(true); + } + Navigator.of(context).pushReplacement( + MaterialPageRoute( + builder: (context) => PasswordScreen(), + ), + ); + } + + void _nextPage() { + if (_currentIndex < _onboardingPages.length - 1) { + _pageController.nextPage( + duration: const Duration(milliseconds: 300), + curve: Curves.easeIn, + ); + } + } + + void _previousPage() { + if (_currentIndex > 0) { + _pageController.previousPage( + duration: const Duration(milliseconds: 300), + curve: Curves.easeIn, + ); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Stack( + children: [ + PageView.builder( + controller: _pageController, + onPageChanged: _onPageChanged, + itemCount: _onboardingPages.length, + itemBuilder: (context, index) { + return _onboardingPages[index]; + }, + ), + Positioned( + top: 40, + left: 16, + child: Visibility( + visible: _currentIndex > 0, + child: TextButton( + onPressed: _previousPage, + child: const Row( + children: [ + Icon(Icons.chevron_left, size: 32), + SizedBox(width: 4), + Text("Previous"), + ], + ), + ), + ), + ), + Positioned( + top: 40, + right: 16, + child: Visibility( + visible: _currentIndex < _onboardingPages.length - 1, + child: TextButton( + onPressed: _nextPage, + child: Row( + children: [ + const Text("Next"), + const SizedBox(width: 4), + const Icon(Icons.chevron_right, size: 32), + ], + ), + ), + ), + ), + Positioned( + bottom: 40, + left: 0, + right: 0, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate( + _onboardingPages.length, + (index) => _buildDot(index, context), + ), + ), + ), + ], + ), + ); + } + + Widget _buildDot(int index, BuildContext context) { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 4.0), + height: 8, + width: _currentIndex == index ? 24 : 8, + decoration: BoxDecoration( + color: _currentIndex == index + ? Theme.of(context).primaryColor.withOpacity(0.23) + : Colors.grey, + borderRadius: BorderRadius.circular(12), + ), + ); + } +} + +class OnboardingPage extends StatelessWidget { + final String title; + final String description; + final String imagePath; + final bool isLastPage; + final VoidCallback? onFinish; + + const OnboardingPage({super.key, + required this.title, + required this.description, + required this.imagePath, + this.isLastPage = false, + this.onFinish, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset(imagePath, height: 150), + const SizedBox(height: 40), + Text( + title, + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 20), + Text( + description, + style: const TextStyle( + fontSize: 16, + color: Colors.grey, + ), + textAlign: TextAlign.center, + ), + if (isLastPage) ...[ + const SizedBox(height: 40), + ElevatedButton( + onPressed: onFinish, + child: const Text("Begin Setup"), + ), + ], + ], + ), + ); + } +} diff --git a/lib/views/screens/password_screen.dart b/lib/views/screens/password_screen.dart new file mode 100644 index 0000000..d7dbd2c --- /dev/null +++ b/lib/views/screens/password_screen.dart @@ -0,0 +1,402 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:haveno_app/services/secure_storage_service.dart'; +import 'package:haveno_app/services/security.dart'; +import 'package:haveno_app/views/screens/establish_connection_screen.dart'; +import 'package:haveno_app/views/screens/link_to_desktop_screen.dart'; +import 'package:haveno_app/views/screens/onboarding_screen.dart'; + +class PasswordScreen extends StatefulWidget { + const PasswordScreen({super.key}); + + @override + _PasswordScreenState createState() => _PasswordScreenState(); +} + +class _PasswordScreenState extends State + with SingleTickerProviderStateMixin { + final SecureStorageService _secureStorage = SecureStorageService(); + final TextEditingController _passwordController = TextEditingController(); + final SecurityService _securityService = SecurityService(); + final _formKey = GlobalKey(); + bool _isPasswordSet = false; + bool _isSettingPassword = false; + bool _isObscured = true; + bool _isHovered = false; + bool _isUnlocked = false; + Color _lockColor = Colors.white; + IconData _lockIcon = Icons.lock; + + // Animation controller for shaking the padlock + late AnimationController _shakeController; + late Animation _shakeAnimation; + + @override + void initState() { + super.initState(); + _checkPasswordStatus(); + + // Initialize shake animation + _shakeController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 500), + ); + + _shakeAnimation = Tween(begin: 0, end: 10).chain( + CurveTween(curve: Curves.elasticIn), + ).animate(_shakeController) + ..addStatusListener((status) { + if (status == AnimationStatus.completed) { + _shakeController.reverse(); + } + }); + } + + @override + void dispose() { + _shakeController.dispose(); + super.dispose(); + } + + Future _checkPasswordStatus() async { + String? storedPassword = await _secureStorage.readUserPassword(); + setState(() { + _isPasswordSet = storedPassword != null; + _isSettingPassword = storedPassword == null; + }); + } + + Future _setPassword(String password) async { + await _securityService.setupUserPassword(password); + setState(() { + _isPasswordSet = true; + _isSettingPassword = false; + }); + } + + Future _verifyPassword(String password) async { + if (await _securityService.authenticateUserPassword(password)) { + setState(() { + _lockColor = Colors.green; + _lockIcon = Icons.lock_open; + _isUnlocked = true; + }); + + await Future.delayed(const Duration(seconds: 2)); + + _decideNextScreen(); + } else { + _shakeController.forward(from: 0); // Start the shake animation + setState(() { + _lockColor = Colors.red; + _lockIcon = Icons.lock; + }); + + await Future.delayed(const Duration(seconds: 2)); + + setState(() { + _lockColor = Colors.white; + }); + } + } + + void _toggleObscured() { + setState(() { + _isObscured = !_isObscured; + }); + } + + void _decideNextScreen() { + // Check if the user is new and still part of the onboarding process and send him to monero screen + print("Navigating to the next screen..."); + if (Platform.isIOS || Platform.isAndroid) { + _secureStorage.readHavenoDaemonConfig().then((config) => { + if (config == null) { + Navigator.of(context).pushReplacement( + MaterialPageRoute( + builder: (context) => LinkToDesktopScreen(), + ), + ) + } else { + Navigator.of(context).pushReplacement( + MaterialPageRoute( + builder: (context) => EstablishConnectionScreen(), + ), + ) + } + }); + } else { + Navigator.of(context).pushReplacement( + MaterialPageRoute( + builder: (context) => EstablishConnectionScreen(), + ), + ); + } + } + + void _showResetConfirmation() { + showDialog( + context: context, + builder: (BuildContext context) { + return StatefulBuilder( + builder: (context, setState) { + bool isResetting = false; + bool resetComplete = false; + String? errorMessage; + + Future startReset() async { + setState(() { + isResetting = true; + }); + + try { + await _securityService.resetAppData(); + + setState(() { + resetComplete = true; + }); + + await Future.delayed(const Duration(seconds: 1)); // Wait for the tick to show + + Navigator.of(context).pop(); // Close the dialog box + + Navigator.pushReplacement( + context, + PageRouteBuilder( + pageBuilder: (context, animation1, animation2) => OnboardingScreen(), + transitionsBuilder: (context, animation1, animation2, child) { + return SlideTransition( + position: Tween( + begin: const Offset(-1, 0), // Start from left + end: Offset.zero, + ).animate(animation1), + child: child, + ); + }, + ), + ); + } catch (e) { + setState(() { + isResetting = false; + errorMessage = "Failed to reset app data. Please try again."; + }); + print("Reset error: $e"); + } + } + + return AlertDialog( + title: isResetting + ? null + : const Text('Confirm Reset'), + content: isResetting + ? resetComplete + ? const Icon(Icons.check_circle, color: Colors.green, size: 64) + : const SizedBox( + height: 64, + width: 64, + child: CircularProgressIndicator(), + ) + : errorMessage != null + ? Text(errorMessage!) + : const Text('Are you sure you want to reset the device? This action cannot be undone.'), + actions: isResetting || resetComplete + ? null + : [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + startReset(); + }, + child: const Text('Reset'), + ), + ], + ); + }, + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Stack( + children: [ + Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 32.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Column( + children: [ + Image.asset( + 'assets/haveno-logo.png', + height: 100, + ), + const SizedBox(height: 16), + AnimatedBuilder( + animation: _shakeAnimation, + builder: (context, child) { + return Transform.translate( + offset: Offset(_shakeAnimation.value, 0), + child: Icon( + _lockIcon, + size: 48, + color: _lockColor, + ), + ); + }, + ), + const SizedBox(height: 16), + ], + ), + Text( + _isSettingPassword ? 'Set Your Password' : 'Enter Your Password', + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 24), + Form( + key: _formKey, + child: Column( + children: [ + TextFormField( + controller: _passwordController, + obscureText: _isObscured, + decoration: InputDecoration( + labelText: 'Password', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8.0), + ), + suffixIcon: IconButton( + icon: Icon( + _isObscured ? Icons.visibility : Icons.visibility_off, + ), + onPressed: _toggleObscured, + ), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a password'; + } + if (_isSettingPassword && value.length < 8) { + return 'Password must be at least 8 characters'; + } + return null; + }, + ), + const SizedBox(height: 24), + ElevatedButton( + onPressed: () { + if (_formKey.currentState!.validate()) { + if (_isSettingPassword) { + _setPassword(_passwordController.text); + } else { + _verifyPassword(_passwordController.text); + } + } + }, + child: Text( + _isSettingPassword ? 'Set Password' : 'Verify Password', + ), + ), + ], + ), + ), + ], + ), + ), + ), + Positioned( + bottom: 16, + right: 16, + child: MouseRegion( + cursor: SystemMouseCursors.click, + onEnter: (_) { + setState(() { + _isHovered = true; + }); + }, + onExit: (_) { + setState(() { + _isHovered = false; + }); + }, + child: GestureDetector( + onTap: _showResetConfirmation, + child: Tooltip( + message: 'This will reset the device to its factory settings.', + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.info_outline, + color: _isHovered ? Colors.orange : Colors.grey.withOpacity(0.46), + ), + const SizedBox(width: 4), + SizedBox( + height: 20, + child: Stack( + children: [ + Text( + 'Reset Device', + style: TextStyle( + color: _isHovered ? Colors.white : Colors.grey.withOpacity(0.46), + fontSize: 14, + decoration: TextDecoration.none, + ), + ), + Positioned( + bottom: -2, + left: 0, + right: 0, + child: Container( + height: 2, + color: _isHovered ? Colors.orange : Colors.transparent, + ), + ), + ], + ), + ), + ], + ), + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/views/screens/payment_account_detail_screen.dart b/lib/views/screens/payment_account_detail_screen.dart new file mode 100644 index 0000000..c89a8c2 --- /dev/null +++ b/lib/views/screens/payment_account_detail_screen.dart @@ -0,0 +1,464 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'dart:convert'; +import 'package:fixnum/fixnum.dart'; +import 'package:flutter/material.dart'; +import 'package:haveno/profobuf_models.dart'; +import 'package:haveno_app/providers/haveno_client_providers/payment_accounts_provider.dart'; +import 'package:haveno_app/utils/human_readable_helpers.dart'; +import 'package:haveno_app/utils/payment_utils.dart'; +import 'package:haveno_app/utils/time_utils.dart'; +import 'package:provider/provider.dart'; + +class PaymentAccountDetailScreen extends StatefulWidget { + final PaymentAccount paymentAccount; + + const PaymentAccountDetailScreen({super.key, required this.paymentAccount}); + + @override + _PaymentAccountDetailScreenState createState() => + _PaymentAccountDetailScreenState(); +} + +class _PaymentAccountDetailScreenState extends State with SingleTickerProviderStateMixin { + late Future> _futurePaymentAccountFormFields; + late TextEditingController _accountNameController; + bool _isNameChanged = false; + bool _isSaving = false; + bool _showCheckmark = false; + bool _showError = false; + + late AnimationController _animationController; + + @override + void initState() { + super.initState(); + _accountNameController = TextEditingController(text: widget.paymentAccount.accountName); + _accountNameController.addListener(() { + setState(() { + _isNameChanged = _accountNameController.text != widget.paymentAccount.accountName; + }); + }); + _futurePaymentAccountFormFields = fetchData(); + + // Initialize the animation controller + _animationController = AnimationController( + duration: const Duration(milliseconds: 1500), + vsync: this, + ); + } + + @override + void dispose() { + _accountNameController.dispose(); + _animationController.dispose(); // Dispose the animation controller + super.dispose(); + } + + Future> fetchData() async { + final paymentAccountsProvider = Provider.of(context, listen: false); + await paymentAccountsProvider.getPaymentMethods(); + var paymentAccountForm = await paymentAccountsProvider.getPaymentAccountForm(widget.paymentAccount.paymentAccountPayload.paymentMethodId); + return paymentAccountForm!.fields; + } + + Future _saveAccountName() async { + setState(() { + _isSaving = true; + _showError = false; + _showCheckmark = false; + }); + + // Simulate a save operation + await Future.delayed(const Duration(seconds: 2)); + + final isSuccess = true; // Simulate success or failure + setState(() { + _isSaving = false; + if (isSuccess) { + _showCheckmark = true; + _animationController.forward(from: 0.0); // Start the fade-out animation + } else { + _showError = true; + _animationController.forward(from: 0.0); // Start the fade-out animation + } + }); + + // Show the checkmark or error for 3 seconds + await Future.delayed(const Duration(seconds: 3)); + + setState(() { + _showCheckmark = false; + _showError = false; + _isNameChanged = false; + }); + } + + Animation _fadeOutAnimation() { + return Tween(begin: 1.0, end: 0.0).animate( + CurvedAnimation( + parent: _animationController, + curve: Curves.easeOut, + ), + ); + } + + @override + Widget build(BuildContext context) { + final paymentAccountPayloadJson = widget.paymentAccount.paymentAccountPayload.toProto3Json(); + final paymentAccountPayload = _extractAccountPayload(jsonDecode(jsonEncode(paymentAccountPayloadJson))); + + return Scaffold( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + appBar: AppBar( + title: const Text('Manage Payment Account'), + ), + body: Stack( + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 80.0), + child: FutureBuilder>( + future: _futurePaymentAccountFormFields, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } else if (snapshot.hasError) { + return Center( + child: Text('Error: ${snapshot.error}'), + ); + } else if (snapshot.hasData) { + return SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _buildAccountInfoCard( + context, + widget.paymentAccount, + ), + const SizedBox(height: 8), + _buildPaymentDetails( + context, + 'Payment Details', + paymentAccountPayload, + snapshot.requireData, + widget.paymentAccount.tradeCurrencies, + ), + //const SizedBox(height: 8), + ], + ), + ); + } else { + return const Center(child: Text('No details available')); + } + }, + ), + ), + Align( + alignment: Alignment.bottomCenter, + child: Container( + padding: const EdgeInsets.all(16.0), + color: Theme.of(context).scaffoldBackgroundColor, + child: Row( + children: [ + Expanded( + child: ElevatedButton( + onPressed: () { + // Logic for exporting the account + }, + child: const Text('Export Account'), + ), + ), + const SizedBox(width: 16), + Expanded( + child: ElevatedButton( + onPressed: () { + // Logic for deleting the account + }, + style: ElevatedButton.styleFrom(backgroundColor: Colors.red), + child: const Text('Delete Account'), + ), + ), + ], + ), + ), + ), + ], + ), + ); + } + + Map _extractAccountPayload(Map json) { + return json.entries + .firstWhere((entry) => entry.key.contains('AccountPayload')) + .value as Map; + } + + Widget _buildAccountInfoCard(BuildContext context, PaymentAccount account) { + return Card( + color: Theme.of(context).cardTheme.color, + margin: const EdgeInsets.fromLTRB(8, 8, 8, 0), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Settings', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + color: Colors.white, + ), + ), + const SizedBox(height: 10), + // Editable Account Name Field with Save Suffix + TextFormField( + controller: _accountNameController, + decoration: InputDecoration( + labelText: 'Label', + border: const OutlineInputBorder(), + suffixIcon: _isNameChanged + ? MouseRegion( + onEnter: (_) => setState(() {}), + onExit: (_) => setState(() {}), + child: GestureDetector( + onTap: _isSaving ? null : _saveAccountName, + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + transitionBuilder: (child, animation) { + return FadeTransition(opacity: animation, child: child); + }, + child: _isSaving + ? SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + color: Theme.of(context) + .colorScheme + .primary + .withOpacity(0.7), + strokeWidth: 2.0, + ), + ) + : _showCheckmark + ? FadeTransition( + opacity: _fadeOutAnimation(), + child: const Icon( + Icons.check, + key: ValueKey('checkmark'), + color: Colors.green, + ), + ) + : _showError + ? FadeTransition( + opacity: _fadeOutAnimation(), + child: const Icon( + Icons.close, + key: ValueKey('error'), + color: Colors.red, + ), + ) + : Icon( + Icons.save, + key: const ValueKey('save'), + color: _isNameChanged + ? Theme.of(context) + .colorScheme + .primary + : Theme.of(context) + .colorScheme + .secondary + .withOpacity(0.23), + ), + ), + ), + ) + : null, + ), + style: const TextStyle(color: Colors.white), + ), + const SizedBox(height: 10), + const Text( + 'Information', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + color: Colors.white, + ), + ), + const SizedBox(height: 10), + // Account Info Grid + GridView( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + mainAxisSpacing: 8.0, + crossAxisSpacing: 8.0, + childAspectRatio: 4, // Adjust as needed + ), + children: [ + _buildGridTile('Account Type', account.paymentMethod.id), + _buildGridTile('Account Age', '${calculateFormattedTimeSince(account.creationDate)} old'), + _buildGridTile('Max Sell Limit', "${formatXmr(account.paymentMethod.maxTradeLimit)} XMR"), + _buildGridTile('Max Buy Limit', "${formatXmr(account.paymentMethod.maxTradeLimit)} XMR"), + _buildGridTile('Max Trade Period', _formatTradePeriod(account.paymentMethod.maxTradePeriod)), + _buildGridTile('Status', 'Not Signed'), + _buildGridTile('Total Trades', '4'), + _buildGridTile('Total Disputes', '5') + ], + ), + ], + ), + ), + ); + } + + Widget _buildGridTile(String title, String value) { + return Container( + alignment: Alignment.centerLeft, + padding: const EdgeInsets.symmetric(horizontal: 0.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + title, + style: const TextStyle( + color: Colors.white, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + value, + style: const TextStyle( + color: Colors.white70, + fontSize: 12, + ), + ), + ], + ), + ); + } + + String _formatTradePeriod(Int64 maxTradePeriod) { + final periodInSeconds = maxTradePeriod.toInt(); + if (periodInSeconds < 60) { + return '$periodInSeconds seconds'; + } else if (periodInSeconds < 3600) { + final minutes = (periodInSeconds / 60).floor(); + return '$minutes minutes'; + } else if (periodInSeconds < 86400) { + final hours = (periodInSeconds / 3600).floor(); + return '$hours hours'; + } else { + final days = (periodInSeconds / 86400).floor(); + return '$days days'; + } + } + + Widget _buildPaymentDetails( + BuildContext context, + String title, + Map payload, + List fields, + List tradeCurrencies, + ) { + return Card( + color: Theme.of(context).cardTheme.color, + margin: const EdgeInsets.fromLTRB(8, 8, 8, 0), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + color: Colors.white, + ), + ), + const SizedBox(height: 10), + ...payload.entries.map((entry) { + String label = getHumanReadablePaymentMethodFormFieldLabel(entry, fields); + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: TextFormField( + initialValue: entry.value.toString(), + readOnly: true, + enabled: false, + decoration: InputDecoration( + labelText: label, + border: const OutlineInputBorder(), + ), + style: const TextStyle(color: Colors.white), + ), + ); + }), + const SizedBox(height: 10), + const Text( + 'Accepted Currencies', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + color: Colors.white, + ), + ), + const SizedBox(height: 10), + GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 100.0, // Maximum width for each item + mainAxisSpacing: 8.0, + crossAxisSpacing: 8.0, + childAspectRatio: 3, // Adjust the aspect ratio as needed + ), + itemCount: tradeCurrencies.length, + itemBuilder: (context, index) { + return Container( + alignment: Alignment.center, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.secondary.withOpacity(0.23), + borderRadius: BorderRadius.circular(8.0), + ), + child: Text( + tradeCurrencies[index].code, + style: const TextStyle( + color: Colors.white, + fontSize: 14, + ), + ), + ); + }, + ), + ], + ), + ), + ); + } +} diff --git a/lib/views/screens/seednode_setup_screen.dart b/lib/views/screens/seednode_setup_screen.dart new file mode 100644 index 0000000..2984993 --- /dev/null +++ b/lib/views/screens/seednode_setup_screen.dart @@ -0,0 +1,328 @@ +import 'dart:async'; +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:haveno/grpc_models.dart'; +import 'package:haveno/haveno_client.dart'; + +class SeedNodeSetupScreen extends StatefulWidget { + const SeedNodeSetupScreen({super.key}); + + @override + _SeedNodeSetupScreenState createState() => _SeedNodeSetupScreenState(); +} + +class _SeedNodeSetupScreenState extends State with SingleTickerProviderStateMixin { + final TextEditingController _seedNodeController = TextEditingController(); + final _formKey = GlobalKey(); + bool _isConnecting = false; + bool _connectionSuccessful = false; + bool _connectionError = false; + bool _isHovered = false; + Color _iconColor = Colors.white; + IconData _connectionIcon = Icons.cloud; + + // Animation controller for shaking the icon + late AnimationController _shakeController; + late Animation _shakeAnimation; + + // Regex for validating V3 Onion addresses + final _onionRegex = RegExp(r'^[a-zA-Z2-7]{56}\.onion$'); + + @override + void initState() { + super.initState(); + + // Initialize shake animation + _shakeController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 500), + ); + + _shakeAnimation = Tween(begin: 0, end: 10).chain( + CurveTween(curve: Curves.elasticIn), + ).animate(_shakeController) + ..addStatusListener((status) { + if (status == AnimationStatus.completed) { + _shakeController.reverse(); + } + }); + } + + @override + void dispose() { + _shakeController.dispose(); + _seedNodeController.dispose(); + super.dispose(); + } + + Future _verifySeedNode(String seedNodeInput) async { + setState(() { + _isConnecting = true; + _connectionError = false; + _iconColor = Colors.blue; + }); + + try { + // Extract the seed node (no need for port checking here) + String seedNode = seedNodeInput; + + print('Connecting to $seedNode'); + + // Try to connect to Haveno and check its version + bool isVersionValid = await checkGprcVersion(seedNode, Duration(seconds: 30)); + + if (isVersionValid) { + // Connection successful + setState(() { + _iconColor = Colors.green; + _connectionIcon = Icons.cloud_done; + _connectionSuccessful = true; + }); + + // Proceed to the next screen after a short delay + await Future.delayed(const Duration(seconds: 1)); + Navigator.pop(context, true); + } else { + // Connection failed + _showConnectionError(); + } + } catch (e) { + print('Error: $e'); + _showConnectionError(); + } finally { + setState(() { + _isConnecting = false; + }); + } + } + + Future checkGprcVersion(String address, Duration timeout) async { + try { + HavenoChannel havenoChannel = HavenoChannel(); + havenoChannel.connect(address, 3201, 'boner'); + await havenoChannel.onConnected; + var versionResponse = await havenoChannel.versionClient?.getVersion(GetVersionRequest()); + if (versionResponse?.version != null && versionResponse!.version.isNotEmpty) { + return true; + } else { + return false; + } + } catch (e) { + return false; // Connection failed + } + } + + void _showConnectionError() { + _shakeController.forward(from: 0); // Start the shake animation + setState(() { + _iconColor = Colors.red; + _connectionIcon = Icons.cloud_off; + _connectionError = true; + }); + } + + void _showSeedNodeInfo() { + // Show a dialog with information about seed nodes + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text('What is a Seed Node?'), + content: Text( + 'A seed node is a server that helps your client connect to the network. Please enter the address of a seed node to proceed. You can include the port number if necessary, e.g., your-seed-node.onion.', + ), + actions: [ + TextButton( + child: Text('Got it'), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ], + ); + }, + ); + } + + void _toggleHover(bool hover) { + setState(() { + _isHovered = hover; + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Stack( + children: [ + Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 32.0), + child: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Column( + children: [ + Image.asset( + 'assets/haveno-logo.png', + height: 100, + ), + const SizedBox(height: 16), + AnimatedBuilder( + animation: _shakeAnimation, + builder: (context, child) { + return Transform.translate( + offset: Offset(_shakeAnimation.value, 0), + child: Icon( + _connectionIcon, + size: 48, + color: _iconColor, + ), + ); + }, + ), + const SizedBox(height: 16), + ], + ), + Text( + 'Enter Seed Node', + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 24), + Form( + key: _formKey, + child: Column( + children: [ + TextFormField( + controller: _seedNodeController, + decoration: InputDecoration( + labelText: 'Seed Node Address', + hintText: 'e.g., yourseednodeaddresshere.onion', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8.0), + ), + suffixIcon: IconButton( + icon: Icon( + Icons.info_outline, + ), + onPressed: _showSeedNodeInfo, + ), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a seed node address.'; + } + if (!_onionRegex.hasMatch(value)) { + return 'Invalid V3 Onion Address.'; + } + return null; + }, + ), + const SizedBox(height: 24), + ElevatedButton( + onPressed: _isConnecting + ? null + : () { + if (_formKey.currentState!.validate()) { + _verifySeedNode(_seedNodeController.text); + } + }, + child: _isConnecting + ? SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + color: Colors.white, + strokeWidth: 2, + ), + ) + : Text('Connect'), + ), + if (_connectionError) + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: Text( + 'Connection failed. Please try again.', + style: TextStyle(color: Colors.red), + ), + ), + ], + ), + ), + ], + ), + ), + ), + ), + Positioned( + bottom: 16, + right: 16, + child: MouseRegion( + cursor: SystemMouseCursors.click, + onEnter: (_) => _toggleHover(true), + onExit: (_) => _toggleHover(false), + child: GestureDetector( + onTap: _showSeedNodeInfo, + child: Tooltip( + message: 'Learn more about seed nodes.', + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.help_outline, + color: _isHovered ? Colors.orange : Colors.grey.withOpacity(0.46), + ), + const SizedBox(width: 4), + SizedBox( + height: 20, + child: Stack( + children: [ + Text( + 'What is a Seed Node?', + style: TextStyle( + color: _isHovered ? Colors.white : Colors.grey.withOpacity(0.46), + fontSize: 14, + decoration: TextDecoration.none, + ), + ), + Positioned( + bottom: -2, + left: 0, + right: 0, + child: Container( + height: 2, + color: _isHovered ? Colors.orange : Colors.transparent, + ), + ), + ], + ), + ), + ], + ), + ), + ), + ), + ), + ], + ), + ); + } +} + +class NextScreen extends StatelessWidget { + const NextScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: Text('Connected to Seed Node!'), + ), + ); + } +} diff --git a/lib/views/screens/setup_wizard_screen.dart b/lib/views/screens/setup_wizard_screen.dart new file mode 100644 index 0000000..ba6dcbb --- /dev/null +++ b/lib/views/screens/setup_wizard_screen.dart @@ -0,0 +1,50 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'package:flutter/material.dart'; + +class SetupWizardScreen extends StatefulWidget { + final String? title; + + const SetupWizardScreen({super.key, this.title}); + + @override + _SetupWizardScreenState createState() => _SetupWizardScreenState(); +} + +class _SetupWizardScreenState extends State { + final int _currentPage = 0; + + @override + Widget build(BuildContext context) { + + return Scaffold( + appBar: AppBar( + title: Text(widget.title ?? 'Setup Wizard'), + ), + //body: Onboarding( + // swipeableBody: [MoneroNodeSetupPage(), DaemonSetupPage(), EsablishConnectionScreen(), AccountSetupPage()], + //), + ); + } + +} diff --git a/lib/views/screens/taker_offer_detail_screen.dart b/lib/views/screens/taker_offer_detail_screen.dart new file mode 100644 index 0000000..33a4396 --- /dev/null +++ b/lib/views/screens/taker_offer_detail_screen.dart @@ -0,0 +1,469 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'package:fixnum/fixnum.dart' as fixnum; +import 'package:flutter/material.dart'; +import 'package:grpc/grpc.dart'; +import 'package:haveno/grpc_models.dart'; +import 'package:haveno/profobuf_models.dart'; +import 'package:haveno_app/providers/haveno_client_providers/price_provider.dart'; +import 'package:haveno_app/views/screens/home_screen.dart'; +import 'package:haveno_app/utils/payment_utils.dart'; +import 'package:provider/provider.dart'; +import 'package:haveno_app/providers/haveno_client_providers/trades_provider.dart'; +import 'package:haveno_app/providers/haveno_client_providers/payment_accounts_provider.dart'; +import 'package:haveno_app/views/widgets/loading_button.dart'; + +class OfferDetailScreen extends StatefulWidget { + final OfferInfo offer; + + const OfferDetailScreen({super.key, required this.offer}); + + @override + _OfferDetailScreenState createState() => _OfferDetailScreenState(); +} + +class _OfferDetailScreenState extends State { + final _formKey = GlobalKey(); + final TextEditingController _payController = TextEditingController(); + final TextEditingController _receiveController = TextEditingController(); + final TextEditingController _marketPriceController = TextEditingController(); // Controller for market price display + bool _isLoading = true; + bool _isWithinLimits = true; + OfferInfo? _offer; + String? _selectedPaymentAccountId; + List _paymentAccounts = []; + double _currentMarketPrice = 0.0; // Variable to store the market price + + @override + void initState() { + super.initState(); + _offer = widget.offer; + _loadPaymentAccounts(); + _loadMarketPrices(); + _initializeReceiveAmount(); + _payController.addListener(_updateReceiveAmount); + _receiveController.addListener(_updatePayAmount); + } + + // Determine if the trade is selling XMR or buying XMR + bool get isSellingXMR => !_offer!.isMyOffer && _offer!.direction == 'BUY'; + + // Initialize the receive/pay amounts based on the minimum offer amount + void _initializeReceiveAmount() { + if (_offer != null) { + final minAmountXMR = _offer!.minAmount.toDouble() / 1e12; // Convert from atomic units + if (isSellingXMR) { + // Selling XMR: Prefill "I will pay" with min XMR + _payController.text = minAmountXMR.toStringAsFixed(12); + _updateReceiveAmount(); + } else { + // Buying XMR: Prefill "I will receive" with min XMR + _receiveController.text = minAmountXMR.toStringAsFixed(12); + _updatePayAmount(); + } + } + } + + void _loadMarketPrices() async { + final pricesProvider = Provider.of(context, listen: false); + final marketPrice = pricesProvider.prices.firstWhere( + (price) => price.currencyCode == _offer?.counterCurrencyCode, + orElse: () => MarketPriceInfo(currencyCode: 'USD', price: 0), + ); + + setState(() { + _currentMarketPrice = marketPrice.price; + _marketPriceController.text = _calculateMarketValue().toStringAsFixed(2); + }); + } + + // Updates the receive amount based on the pay amount + void _updateReceiveAmount() { + if (_offer != null && _payController.text.isNotEmpty) { + final payAmount = double.tryParse(_payController.text) ?? 0; + final offerPrice = double.parse(_offer!.price); + + _receiveController.removeListener(_updatePayAmount); + + if (isSellingXMR) { + // Selling XMR: Calculate receive amount in fiat + _receiveController.text = (payAmount * offerPrice).toStringAsFixed(2); + } else { + // Buying XMR: Calculate receive amount in XMR + _receiveController.text = (payAmount / offerPrice).toStringAsFixed(12); + } + + _receiveController.addListener(_updatePayAmount); + _checkLimits(); + _marketPriceController.text = _calculateMarketValue().toStringAsFixed(2); + } + } + + // Updates the pay amount based on the receive amount + void _updatePayAmount() { + if (_offer != null && _receiveController.text.isNotEmpty) { + final receiveAmount = double.tryParse(_receiveController.text) ?? 0; + final offerPrice = double.parse(_offer!.price); + + _payController.removeListener(_updateReceiveAmount); + + if (isSellingXMR) { + // Selling XMR: Calculate pay amount in XMR + _payController.text = (receiveAmount / offerPrice).toStringAsFixed(12); + } else { + // Buying XMR: Calculate pay amount in fiat + _payController.text = (receiveAmount * offerPrice).toStringAsFixed(2); + } + + _payController.addListener(_updateReceiveAmount); + _checkLimits(); + _marketPriceController.text = _calculateMarketValue().toStringAsFixed(2); + } + } + + double _calculateMarketValue() { + // Get the XMR amount based on whether it's a buy or sell trade + final xmrAmount = double.tryParse(isSellingXMR ? _payController.text : _receiveController.text) ?? 0; + + // Calculate the equivalent fiat value of the XMR + return xmrAmount * _currentMarketPrice; + } + + void _loadPaymentAccounts() async { + final paymentAccountsProvider = + Provider.of(context, listen: false); + await paymentAccountsProvider.getPaymentAccounts(); + setState(() { + _paymentAccounts = paymentAccountsProvider.paymentAccounts + .where((account) => + account.paymentMethod.id == _offer?.paymentMethodId) + .toList() ?? + []; + _isLoading = false; + }); + } + + // Ensures the pay/receive amounts are within the offer limits + void _checkLimits() { + final minAmountXMR = _offer!.minAmount.toDouble() / 1e12; + final maxAmountXMR = _offer!.amount.toDouble() / 1e12; + final xmrAmount = double.tryParse(isSellingXMR ? _payController.text : _receiveController.text) ?? 0; + + setState(() { + _isWithinLimits = xmrAmount >= minAmountXMR && xmrAmount <= maxAmountXMR; + }); + } + +Future _confirmOrder() async { + if (_formKey.currentState?.validate() ?? false) { + final tradesProvider = + Provider.of(context, listen: false); + + try { + // Declare variables for handling amounts + BigInt amountBigInt; + + // Check if user is selling or buying + if (!isSellingXMR) { + // User is buying XMR: use receiveAmount + final receiveAmountDouble = double.parse(_receiveController.text); + amountBigInt = BigInt.from((receiveAmountDouble * 1e12).round()); + } else { + // User is selling XMR: use payAmount + final payAmountDouble = double.parse(_payController.text); + amountBigInt = BigInt.from((payAmountDouble * 1e12).round()); + } + + // Call the takeOffer function with the correct amount + await tradesProvider.takeOffer( + widget.offer.id, + _selectedPaymentAccountId, // Use selected payment account ID + fixnum.Int64(amountBigInt.toInt()), // Use the calculated BigInt as Int64 + ); + + // Navigate to HomeScreen after successful order confirmation + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) => HomeScreen(initialIndex: 3), + ), + ); + } on GrpcError catch (e) { + // Handle error: Navigate to a different screen and show error message + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) => HomeScreen(initialIndex: 1), + ), + ); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(e.message ?? 'Unknown server error'), + ), + ); + } + } +} + + + @override + void dispose() { + _payController.dispose(); + _receiveController.dispose(); + _marketPriceController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(isSellingXMR ? "Sell Your Monero" : "Buy Monero"), + ), + body: _isLoading + ? const Center(child: CircularProgressIndicator()) + : _offer == null + ? const Center(child: Text('Offer not found')) + : Padding( + padding: const EdgeInsets.fromLTRB(8, 8, 8, 0), + child: SingleChildScrollView( + child: Form( + key: _formKey, + child: Column( + children: [ + Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Offer Details', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold)), + const SizedBox(height: 16.0), + + // Render the text fields based on the trade direction + if (isSellingXMR) ...[ + TextFormField( + controller: _payController, + decoration: const InputDecoration( + labelText: 'I will pay', + suffixText: 'XMR', + border: OutlineInputBorder(), + ), + keyboardType: TextInputType.number, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter the amount to pay'; + } + return null; + }, + ), + const SizedBox(height: 16.0), + TextFormField( + controller: _receiveController, + decoration: InputDecoration( + labelText: 'I will receive', + suffixText: _offer?.counterCurrencyCode, + border: const OutlineInputBorder(), + ), + keyboardType: TextInputType.number, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter the amount to receive'; + } + return null; + }, + ), + ] else ...[ + TextFormField( + controller: _payController, + decoration: InputDecoration( + labelText: 'I will pay', + suffixText: _offer?.counterCurrencyCode, + border: const OutlineInputBorder(), + ), + keyboardType: TextInputType.number, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter the amount to pay'; + } + return null; + }, + ), + const SizedBox(height: 16.0), + TextFormField( + controller: _receiveController, + decoration: const InputDecoration( + labelText: 'I will receive', + suffixText: 'XMR', + border: OutlineInputBorder(), + ), + keyboardType: TextInputType.number, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter the amount to receive'; + } + return null; + }, + ), + ], + + const SizedBox(height: 16.0), + + // Market value approximation + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Center( + child: Text( + _marketPriceController.text.isEmpty + ? 'Calculating...' + : 'XMR is ~${_marketPriceController.text} ${_offer?.counterCurrencyCode} in Market Value', + style: TextStyle( + fontSize: 16.0, + color: Colors.grey[600]), + ), + ) + ], + ), + if (!_isWithinLimits) + const Padding( + padding: EdgeInsets.only(top: 8.0), + child: Text( + 'Amount is out of the buy limits', + style: TextStyle(color: Colors.red), + ), + ), + ], + ), + ), + ), + const SizedBox(height: 4.0), + Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('${(!_offer!.isMyOffer && _offer!.direction == 'BUY') ? "Paid" : "Pay"} via ${_offer?.paymentMethodShortName}', + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold)), + const SizedBox(height: 16.0), + DropdownButtonFormField( + decoration: InputDecoration( + labelText: (!_offer!.isMyOffer && _offer!.direction == 'BUY') ? "You'll receive to" : "You'll pay with", + border: const OutlineInputBorder(), + ), + items: _paymentAccounts.map((account) { + return DropdownMenuItem( + value: account.id, + child: Text(account.accountName), + ); + }).toList(), + onChanged: (value) { + setState(() { + _selectedPaymentAccountId = value; + }); + }, + validator: (value) { + if (value == null) { + return 'Please select a payment account'; + } + return null; + }, + ), + ], + ), + ), + ), + const SizedBox(height: 4.0), + Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('About this Offer', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold)), + const SizedBox(height: 16.0), + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text((!_offer!.isMyOffer && _offer!.direction == 'BUY') ? "Buyer's Rate" : "Seller's Rate", style: TextStyle(fontWeight: FontWeight.bold)), + Text(isFiatCurrency( + _offer!.counterCurrencyCode) + ? '${double.parse(_offer!.price).toStringAsFixed(2)} ${_offer!.counterCurrencyCode}/${_offer!.baseCurrencyCode}' + : _offer!.price), + ], + ), + const SizedBox(height: 16.0), + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + const Text('Minimum Amount', style: TextStyle(fontWeight: FontWeight.bold)), + Text( + '${_offer!.minVolume} ${_offer!.counterCurrencyCode}'), + ], + ), + const SizedBox(height: 16.0), + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + const Text('Maximum Amount', style: TextStyle(fontWeight: FontWeight.bold)), + Text( + '${_offer!.volume} ${_offer!.counterCurrencyCode}'), + ], + ), + const SizedBox(height: 16.0), + const Text('Offer ID', style: TextStyle(fontWeight: FontWeight.bold)), + Text(_offer!.id), + const SizedBox(height: 16.0), + const Text('Payment Method', style: TextStyle(fontWeight: FontWeight.bold)), + Text(_offer!.paymentMethodShortName), + ], + ), + ), + ), + ], + ), + ), + ), + ), + bottomNavigationBar: Padding( + padding: const EdgeInsets.all(16.0), + child: LoadingButton( + onPressed: _confirmOrder, + child: const Text('Confirm Trade'), + ), + ), + ); + } +} diff --git a/lib/views/screens/trade_chat_screen.dart b/lib/views/screens/trade_chat_screen.dart new file mode 100644 index 0000000..6576356 --- /dev/null +++ b/lib/views/screens/trade_chat_screen.dart @@ -0,0 +1,448 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:haveno/grpc_models.dart'; +import 'package:haveno/profobuf_models.dart'; +import 'package:haveno_app/providers/haveno_client_providers/disputes_provider.dart'; +import 'package:haveno_app/utils/human_readable_helpers.dart'; +import 'package:haveno_app/views/screens/dispute_chat_screen.dart'; +import 'package:provider/provider.dart'; +import 'package:uuid/uuid.dart'; +import 'package:haveno_app/providers/haveno_client_providers/trades_provider.dart'; +import 'package:chatview/chatview.dart'; + +class TradeChatScreen extends StatefulWidget { + final String tradeId; + + const TradeChatScreen({super.key, required this.tradeId}); + + @override + _TradeChatScreenState createState() => _TradeChatScreenState(); +} + +class _TradeChatScreenState extends State { + ChatController? _chatController; + late final ChatUser _systemUser; + late final ChatUser _myUser; + late String _tradePeerId; + late ChatUser _tradePeerUser; + late String _arbitratorId; + late ChatUser _arbitratorUser; + late ChatViewState _chatViewState; + late String _chatUserStatus; + TradeInfo? _trade; + Dispute? _dispute; + late TradesProvider _tradesProvider; + + @override + void initState() { + super.initState(); + _systemUser = ChatUser(id: 'system', name: 'System'); + _myUser = ChatUser(id: 'me', name: 'Me'); + _chatViewState = ChatViewState.hasMessages; + + // Store a reference to the provider + _tradesProvider = Provider.of(context, listen: false); + // Listen for changes in chat messages + _tradesProvider.onNewChatMessage = (chatMessage) { + _handleNewChatMessage(chatMessage); + }; + _tradesProvider.onTradeUpdate = (trade, isNewTrade) { + if (!isNewTrade) { + _handleTradeUpdate(trade); + } + }; + + // Initialize trade data + WidgetsBinding.instance.addPostFrameCallback((_) { + _initializeTradeData(); + }); + } + + void _handleNewChatMessage(ChatMessage chatMessage) { + if (!mounted) return; + + List chatMessages = _tradesProvider.chatMessages[widget.tradeId] ?? []; + + for (var chatMessage in chatMessages) { + bool messageExists = _chatController?.initialMessageList.any((msg) => msg.id == chatMessage.uid) ?? false; + + if (!messageExists) { + final message = _mapChatMessageToMessage(chatMessage); + _chatController?.addMessage(message); + + if (_chatViewState == ChatViewState.noData) { + setState(() { + _chatViewState = ChatViewState.hasMessages; + }); + } + } + } + } + + void _handleTradeUpdate(TradeInfo trade) { + if (trade.tradeId == _trade?.tradeId) { + if (trade.state != _trade?.state) { + _trade = trade; + setState(() { + _chatUserStatus = humanReadablePhaseAs( + _trade!.phase, + _trade!.role.contains('buyer'), + true, + ); + }); + } + } + } + + @override + void dispose() { + super.dispose(); + } + + Future _initializeTradeData() async { + _trade = await _getTrade(widget.tradeId); + if (_trade == null && mounted) { + Navigator.of(context).pop(); // Exit if trade is not found + return; + } + if (_trade != null) { + _chatUserStatus = humanReadablePhaseAs(_trade!.phase, _trade!.role.contains('buyer'), true); + _setUserRoles(); + await _initializeChatController(); + } + } + + void _setUserRoles() { + if (_trade == null) return; + + _tradePeerId = _trade!.tradePeerNodeAddress.split('.').first; + _arbitratorId = _trade!.arbitratorNodeAddress.split('.').first; + + _tradePeerUser = ChatUser(id: _tradePeerId, name: 'Peer'); + _arbitratorUser = ChatUser(id: _arbitratorId, name: 'Arbitrator'); + + print(_myUser.id); + print(_tradePeerUser.id); + } + + Future _getTrade(String tradeId) async { + await _tradesProvider.getTrade(tradeId); + var trade = _tradesProvider.trades.firstWhere((trade) => trade.tradeId == tradeId); + return trade; + } + + Future _initializeChatController() async { + if (_chatController != null || _trade == null) return; + + await _tradesProvider.getChatMessages(_trade!.tradeId); + + List chatMessages = _tradesProvider.chatMessages[_trade!.tradeId] ?? []; + chatMessages.sort((a, b) => a.date.compareTo(b.date)); + + final messageList = chatMessages.map(_mapChatMessageToMessage).toList(); + + _chatController = ChatController( + initialMessageList: messageList, + scrollController: ScrollController(), + currentUser: _myUser, + otherUsers: [_tradePeerUser, _arbitratorUser, _systemUser], + ); + + _chatViewState = _chatController!.initialMessageList.isEmpty + ? ChatViewState.noData + : ChatViewState.hasMessages; + } + + Message _mapChatMessageToMessage(ChatMessage chatMessage) { + final senderNodeAddress = chatMessage.senderNodeAddress.hostName.split('.').first; + + final sentBy = senderNodeAddress == _tradePeerId + ? _tradePeerId + : senderNodeAddress == _arbitratorId + ? _arbitratorId + : chatMessage.isSystemMessage + ? 'system' + : 'me'; // Assuming messages not matching peer or arbitrator are sent by the user + + return Message( + id: chatMessage.uid, + message: chatMessage.message, + createdAt: DateTime.fromMillisecondsSinceEpoch(chatMessage.date.toInt()), + sentBy: sentBy, + status: chatMessage.acknowledged ? MessageStatus.read : MessageStatus.delivered, + ); + } + + void _handlePaymentSentPressed() { + final tradesProvider = Provider.of(context, listen: false); + tradesProvider.confirmPaymentSent(_trade!.tradeId).then((_) { + _addSystemMessage('Payment marked as sent.'); + }).catchError((error) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to confirm payment as sent: $error')), + ); + }); + } + + void _handlePaymentReceivedPressed() async { + final tradesProvider = Provider.of(context, listen: false); + if (_trade!.disputeState != "NO_DISPUTE") { + final disputesProvider = Provider.of(context, listen: false); + try { + await disputesProvider.resolveDispute( + _trade!.tradeId, + DisputeResult_Winner.BUYER, + DisputeResult_Reason.TRADE_ALREADY_SETTLED, + "Seller marks payment as received", + _trade!.sellerPayoutAmount, + ); + } catch (e) { + print(e.toString()); + } + } + tradesProvider.confirmPaymentReceived(_trade!.tradeId).then((_) { + _addSystemMessage('Payment marked as received.'); + }).catchError((error) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to confirm payment as received: $error')), + ); + }); + } + + void _handleDisputePressed() { + final disputesProvider = Provider.of(context, listen: false); + disputesProvider.openDispute(_trade!.tradeId).then((dispute) { + if (dispute != null) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => DisputeChatScreen(tradeId: _trade!.tradeId), + ), + ); + } else { + throw Exception("No dispute object returned"); + } + }).catchError((error) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to dispute trade: $error')), + ); + }); + } + + void _handleSwitchToSupportChatPressed() async { + if (_dispute == null) { + final disputesProvider = Provider.of(context, listen: false); + _dispute = await disputesProvider.getDispute(_trade!.tradeId); + } + + if (_dispute != null && mounted) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => DisputeChatScreen(tradeId: _trade!.tradeId), + ), + ); + } else { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Failed to load dispute chat.')), + ); + } + } + } + +void _handleSendPressed(String messageText, ReplyMessage replyMessage, MessageType messageType) async { + final newMessage = Message( + id: const Uuid().v4(), + message: messageText, + createdAt: DateTime.now(), + sentBy: 'me', + messageType: messageType, + replyMessage: replyMessage, + ); + + _chatController?.addMessage(newMessage); + + // Update the state to reflect that there are now messages in the chat + if (_chatViewState != ChatViewState.hasMessages && newMessage.message.isNotEmpty) { + setState(() { + _chatViewState = ChatViewState.hasMessages; + }); + } + + // If your send message process involves asynchronous calls, you should handle errors like so: + try { + final tradesProvider = Provider.of(context, listen: false); + await tradesProvider.sendChatMessage(_trade!.tradeId, messageText); + newMessage.setStatus = MessageStatus.delivered; + } catch (e) { + newMessage.setStatus = MessageStatus.undelivered; + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('You can only send 1 message per minute!')), + ); + } +} + + + void _addSystemMessage(String text) { + final systemMessage = Message( + id: const Uuid().v1(), + message: text, + createdAt: DateTime.now(), + sentBy: 'system', + ); + + _chatController?.addMessage(systemMessage); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: FutureBuilder( + future: _initializeTradeData(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } + + if (_trade == null) { + return const Center(child: Text("Trade not found")); + } + + return ChatView( + chatController: _chatController!, + onSendTap: _handleSendPressed, + chatViewState: _chatViewState, + chatViewStateConfig: ChatViewStateConfiguration( + noMessageWidgetConfig: ChatViewStateWidgetConfiguration( + showDefaultReloadButton: false, + subTitle: 'Start a conversation below...', + subTitleTextStyle: TextStyle(color: Colors.white.withOpacity(0.5), ), + titleTextStyle: TextStyle(inherit: true, color: Colors.white.withOpacity(0.5), fontSize: 21) + ) + ), + appBar: ChatViewAppBar( + backGroundColor: Theme.of(context).scaffoldBackgroundColor, + chatTitle: 'Trade #${_trade!.shortId}', + userStatus: _chatUserStatus, + actions: [ + PopupMenuButton( + onSelected: (String value) { + switch (value) { + case 'Dispute Trade': + _handleDisputePressed(); + break; + case 'Switch to Support Chat': + _handleSwitchToSupportChatPressed(); + break; + case 'Confirm Transfer of Funds': + _handlePaymentSentPressed(); + break; + case 'Confirm Receipt of Funds': + _handlePaymentReceivedPressed(); + break; + } + }, + itemBuilder: (BuildContext context) { + return >[ + if (_trade!.disputeState == "NO_DISPUTE") + const PopupMenuItem( + value: 'Dispute Trade', + child: Text('Dispute Trade'), + ), + if (_trade!.disputeState != "NO_DISPUTE") + const PopupMenuItem( + value: 'Switch to Support Chat', + child: Text('Switch to Support Chat'), + ), + if (!_trade!.isPaymentSent && !_trade!.role.contains('seller')) + const PopupMenuItem( + value: 'Confirm Transfer of Funds', + child: Text('Confirm Transfer of Funds'), + ), + if (_trade!.role.contains('seller')) + const PopupMenuItem( + value: 'Confirm Receipt of Funds', + child: Text('Confirm Receipt of Funds'), + ), + ]; + }, + icon: const Icon(Icons.more_vert), + ), + ], + ), + featureActiveConfig: const FeatureActiveConfig( + enableSwipeToReply: true, + enableSwipeToSeeTime: true, + ), + chatBackgroundConfig: ChatBackgroundConfiguration( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + messageSorter: (message1, message2) { + return message1.createdAt.compareTo(message2.createdAt); + }, + margin: const EdgeInsets.fromLTRB(4, 4, 4, 0) + ), + sendMessageConfig: SendMessageConfiguration( + defaultSendButtonColor: Theme.of(context).colorScheme.primary, + replyMessageColor: Colors.white, + replyDialogColor: Colors.blue, + replyTitleColor: Colors.black, + closeIconColor: Colors.black, + textFieldBackgroundColor: const Color(0xFF424242), + textFieldConfig: const TextFieldConfiguration( + borderRadius: BorderRadius.all(Radius.circular(8)), + contentPadding: EdgeInsets.all(4), + padding: EdgeInsets.fromLTRB(4, 4, 4, 4), + margin: EdgeInsets.zero, + ), + enableCameraImagePicker: false, + enableGalleryImagePicker: false, + allowRecordingVoice: false, + ), + chatBubbleConfig: ChatBubbleConfiguration( + outgoingChatBubbleConfig: const ChatBubble( + color: Colors.blue, + borderRadius: BorderRadius.only( + topRight: Radius.circular(12), + topLeft: Radius.circular(12), + bottomLeft: Radius.circular(12), + ), + ), + inComingChatBubbleConfig: ChatBubble( + color: Colors.black.withOpacity(0.2), + textStyle: const TextStyle(color: Colors.white), + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(12), + topRight: Radius.circular(12), + bottomRight: Radius.circular(12), + ), + ), + ), + ); + }, + ), + ); + } +} diff --git a/lib/views/screens/trade_dispute_chat_screen.dart b/lib/views/screens/trade_dispute_chat_screen.dart new file mode 100644 index 0000000..3e2283c --- /dev/null +++ b/lib/views/screens/trade_dispute_chat_screen.dart @@ -0,0 +1,20 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . diff --git a/lib/views/screens/trade_timeline/buyer_phases/phase_deposits_confirmed_buyer.dart b/lib/views/screens/trade_timeline/buyer_phases/phase_deposits_confirmed_buyer.dart new file mode 100644 index 0000000..eaa560f --- /dev/null +++ b/lib/views/screens/trade_timeline/buyer_phases/phase_deposits_confirmed_buyer.dart @@ -0,0 +1,28 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'package:haveno_app/views/screens/trade_timeline/phase_base.dart'; + +class PhaseDepositsConfirmedBuyer extends PhaseBase { + const PhaseDepositsConfirmedBuyer({super.key}) + : super(phaseText: "Deposit Received"); +} diff --git a/lib/views/screens/trade_timeline/buyer_phases/phase_deposits_published_buyer.dart b/lib/views/screens/trade_timeline/buyer_phases/phase_deposits_published_buyer.dart new file mode 100644 index 0000000..eacc06d --- /dev/null +++ b/lib/views/screens/trade_timeline/buyer_phases/phase_deposits_published_buyer.dart @@ -0,0 +1,28 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'package:haveno_app/views/screens/trade_timeline/phase_base.dart'; + +class PhaseDepositsPublishedBuyer extends PhaseBase { + const PhaseDepositsPublishedBuyer({super.key}) + : super(phaseText: "Transferring to Escrow"); +} diff --git a/lib/views/screens/trade_timeline/buyer_phases/phase_deposits_unlocked_buyer.dart b/lib/views/screens/trade_timeline/buyer_phases/phase_deposits_unlocked_buyer.dart new file mode 100644 index 0000000..039439f --- /dev/null +++ b/lib/views/screens/trade_timeline/buyer_phases/phase_deposits_unlocked_buyer.dart @@ -0,0 +1,296 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:haveno/grpc_models.dart'; +import 'package:haveno/profobuf_models.dart'; +import 'package:haveno_app/providers/haveno_client_providers/payment_accounts_provider.dart'; +import 'package:haveno_app/providers/haveno_client_providers/trades_provider.dart'; +import 'package:haveno_app/utils/human_readable_helpers.dart'; +import 'package:haveno_app/views/screens/trade_chat_screen.dart'; +import 'package:haveno_app/views/screens/trade_timeline/phase_base.dart'; +import 'package:haveno_app/utils/payment_utils.dart'; +import 'package:haveno_app/views/widgets/loading_button.dart'; +import 'package:provider/provider.dart'; + +class PhaseDepositsUnlockedBuyer extends PhaseBase { + final Map takerPaymentAccountPayload; + final Map makerPaymentAccountPayload; + final TradeInfo trade; + final VoidCallback onPaidInFull; + + const PhaseDepositsUnlockedBuyer({ + super.key, + required this.takerPaymentAccountPayload, + required this.makerPaymentAccountPayload, + required this.trade, + required this.onPaidInFull, + }) : super(phaseText: "Awaiting Your Payment"); + + Future _getPaymentAccountForm(context) async { + final paymentAccountsProvider = Provider.of(context, listen: false); + await paymentAccountsProvider.getPaymentMethods(); + return paymentAccountsProvider.getPaymentAccountForm(trade.offer.paymentMethodId); + } + + + @override +Widget build(BuildContext context) { + final tradeAmount = trade.amount; + final tradePrice = trade.price; + final currencyCode = trade.offer.counterCurrencyCode; + final paymentMethod = trade.offer.paymentMethodShortName; + + final price = double.parse(tradePrice); + final amount = formatXmr(tradeAmount, returnString: false) as double; + final total = amount * price; + + final totalAmountFormatted = "${formatFiat(total)} $currencyCode"; + + // Fetch the PaymentAccountForm using a FutureBuilder + return FutureBuilder( + future: _getPaymentAccountForm(context), // Fetch the form asynchronously + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } else if (snapshot.hasError) { + return Center(child: Text('Error: ${snapshot.error}')); + } else if (!snapshot.hasData) { + return const Center(child: Text('No payment account form available')); + } + + final paymentAccountForm = snapshot.data!; + + return Stack( + children: [ + Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).size.height * 0.1, // Add padding equal to the height of the button container + ), + child: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(height: 20), + const CircularProgressIndicator(), + const SizedBox(height: 20), + Text( + phaseText, + style: const TextStyle(fontSize: 24), + ), + Card( + margin: const EdgeInsets.fromLTRB(8, 8, 8, 4), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Center( + child: Text.rich( + TextSpan( + text: 'You must pay a total of ', + style: const TextStyle( + fontWeight: FontWeight.normal, + fontSize: 16, + ), + children: [ + TextSpan( + text: totalAmountFormatted, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + const TextSpan( + text: ' via ', + style: TextStyle( + fontWeight: FontWeight.normal, + fontSize: 16, + ), + ), + TextSpan( + text: paymentMethod, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + const TextSpan( + text: '. Please be sure the amount is exact.', + style: TextStyle( + fontWeight: FontWeight.normal, + fontSize: 16, + ), + ), + ], + ), + textAlign: TextAlign.center, // Center the text + ), + ), + ), + ), + _buildPaymentDetails( + 'You\'ll send from...', + takerPaymentAccountPayload, + paymentAccountForm + ), + _buildCopyablePaymentDetails( + context, + 'To the seller\'s account...', + makerPaymentAccountPayload, + paymentAccountForm, + ), + ], + ), + ), + ), + Positioned( + bottom: 0.0, + left: 8.0, + right: 8.0, + child: Container( + color: Theme.of(context).scaffoldBackgroundColor, // Same background as the scaffold + padding: const EdgeInsets.symmetric(vertical: 8.0), // Reduced padding + child: Row( + children: [ + Expanded( + child: LoadingButton( + onPressed: () async { + try { + await Provider.of(context, listen: false) + .confirmPaymentSent(trade.tradeId); + onPaidInFull(); + } catch (error) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Failed to confirm payment, please try again in a moment.')), + ); + } + }, + child: const Text('I have paid in full'), + ), + ), + const SizedBox(width: 8.0), + ElevatedButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => TradeChatScreen(tradeId: trade.tradeId), + ), + ); + }, + style: ElevatedButton.styleFrom( + minimumSize: const Size(48, 48), // Match the height of the main button + padding: EdgeInsets.zero, // Remove extra padding + backgroundColor: Theme.of(context).colorScheme.primary, + ), + child: const Icon(Icons.chat), + ), + ], + ), + ), + ), + ], + ); + }, +); + +} + + + Widget _buildPaymentDetails(String title, Map payload, PaymentAccountForm paymentAccountForm) { + return Card( + margin: const EdgeInsets.fromLTRB(8, 8, 8, 4), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), + ), + const SizedBox(height: 10), + ...payload.entries.map((entry) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: TextFormField( + enabled: false, + initialValue: entry.value, + readOnly: true, + decoration: InputDecoration( + labelText: getHumanReadablePaymentMethodFormFieldLabel(entry, paymentAccountForm.fields), + border: const OutlineInputBorder(), + ), + ), + ); + }), + //const SizedBox(height: 20), + ], + ), + ), + ); + } + + Widget _buildCopyablePaymentDetails( + BuildContext context, String title, Map payload, PaymentAccountForm paymentAccountForm) { + return Card( + margin: const EdgeInsets.fromLTRB(8, 8, 8, 4), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), + ), + const SizedBox(height: 10), + ...payload.entries.map((entry) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: TextFormField( + enabled: true, + initialValue: entry.value, + readOnly: true, + decoration: InputDecoration( + labelText: getHumanReadablePaymentMethodFormFieldLabel(entry, paymentAccountForm.fields), + border: const OutlineInputBorder(), + suffixIcon: IconButton( + icon: const Icon(Icons.copy), + onPressed: () { + Clipboard.setData(ClipboardData(text: entry.value)); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Copied to clipboard')), + ); + }, + ), + ), + ), + ); + }), + //const SizedBox(height: 20), + ], + ), + ), + ); + } + +} diff --git a/lib/views/screens/trade_timeline/buyer_phases/phase_init_buyer.dart b/lib/views/screens/trade_timeline/buyer_phases/phase_init_buyer.dart new file mode 100644 index 0000000..2dc8bb1 --- /dev/null +++ b/lib/views/screens/trade_timeline/buyer_phases/phase_init_buyer.dart @@ -0,0 +1,28 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'package:haveno_app/views/screens/trade_timeline/phase_base.dart'; + +class PhaseInitBuyer extends PhaseBase { + const PhaseInitBuyer({super.key}) + : super(phaseText: "Initializing Trade"); +} diff --git a/lib/views/screens/trade_timeline/buyer_phases/phase_payment_received_buyer.dart b/lib/views/screens/trade_timeline/buyer_phases/phase_payment_received_buyer.dart new file mode 100644 index 0000000..87fb86b --- /dev/null +++ b/lib/views/screens/trade_timeline/buyer_phases/phase_payment_received_buyer.dart @@ -0,0 +1,28 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'package:haveno_app/views/screens/trade_timeline/phase_base.dart'; + +class PhasePaymentReceivedBuyer extends PhaseBase { + const PhasePaymentReceivedBuyer({super.key}) + : super(phaseText: "Payment Received"); +} diff --git a/lib/views/screens/trade_timeline/buyer_phases/phase_payment_sent_buyer.dart b/lib/views/screens/trade_timeline/buyer_phases/phase_payment_sent_buyer.dart new file mode 100644 index 0000000..157ab3b --- /dev/null +++ b/lib/views/screens/trade_timeline/buyer_phases/phase_payment_sent_buyer.dart @@ -0,0 +1,79 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'package:flutter/material.dart'; +import 'package:haveno/grpc_models.dart'; +import 'package:haveno_app/views/screens/trade_chat_screen.dart'; +import 'package:haveno_app/views/screens/trade_timeline/phase_base.dart'; + +class PhasePaymentSentBuyer extends PhaseBase { + final TradeInfo trade; + + const PhasePaymentSentBuyer({super.key, required this.trade}) + : super(phaseText: "Seller Confirming Payment"); + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + // Scrollable content (if any, here it's just a placeholder for your phase text) + Padding( + padding: const EdgeInsets.only(bottom: 80.0), // Adjust padding for button space + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const CircularProgressIndicator(), + const SizedBox(height: 20), + Text( + phaseText, + style: const TextStyle(fontSize: 24), + ), + ], + ), + ), + ), + // Static chat button at the bottom + Positioned( + bottom: 16.0, + right: 16.0, + child: ElevatedButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => TradeChatScreen(tradeId: trade.tradeId), + ), + ); + }, + style: ElevatedButton.styleFrom( + minimumSize: const Size(48, 48), // Set the height and width + padding: EdgeInsets.zero, // Remove extra padding + backgroundColor: Theme.of(context).colorScheme.primary, + ), + child: const Icon(Icons.chat), + ), + ), + ], + ); + } +} diff --git a/lib/views/screens/trade_timeline/phase_base.dart b/lib/views/screens/trade_timeline/phase_base.dart new file mode 100644 index 0000000..b4d5d95 --- /dev/null +++ b/lib/views/screens/trade_timeline/phase_base.dart @@ -0,0 +1,46 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'package:flutter/material.dart'; + +abstract class PhaseBase extends StatelessWidget { + final String phaseText; + + const PhaseBase({required this.phaseText, super.key}); + + @override + Widget build(BuildContext context) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const CircularProgressIndicator(), + const SizedBox(height: 20), + Text( + phaseText, + style: const TextStyle(fontSize: 24), + ), + ], + ), + ); + } +} diff --git a/lib/views/screens/trade_timeline/phase_pb_error.dart b/lib/views/screens/trade_timeline/phase_pb_error.dart new file mode 100644 index 0000000..3e2283c --- /dev/null +++ b/lib/views/screens/trade_timeline/phase_pb_error.dart @@ -0,0 +1,20 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . diff --git a/lib/views/screens/trade_timeline/seller_phases/phase_deposits_confirmed_seller.dart b/lib/views/screens/trade_timeline/seller_phases/phase_deposits_confirmed_seller.dart new file mode 100644 index 0000000..146b147 --- /dev/null +++ b/lib/views/screens/trade_timeline/seller_phases/phase_deposits_confirmed_seller.dart @@ -0,0 +1,28 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'package:haveno_app/views/screens/trade_timeline/phase_base.dart'; + +class PhaseDepositsConfirmedSeller extends PhaseBase { + const PhaseDepositsConfirmedSeller({super.key}) + : super(phaseText: "Awaiting Network Confirmations"); +} diff --git a/lib/views/screens/trade_timeline/seller_phases/phase_deposits_published_seller.dart b/lib/views/screens/trade_timeline/seller_phases/phase_deposits_published_seller.dart new file mode 100644 index 0000000..34398ac --- /dev/null +++ b/lib/views/screens/trade_timeline/seller_phases/phase_deposits_published_seller.dart @@ -0,0 +1,27 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import 'package:haveno_app/views/screens/trade_timeline/phase_base.dart'; + +class PhaseDepositsPublishedSeller extends PhaseBase { + const PhaseDepositsPublishedSeller({super.key}) + : super(phaseText: "Reserving Your Funds"); +} diff --git a/lib/views/screens/trade_timeline/seller_phases/phase_deposits_unlocked_seller.dart b/lib/views/screens/trade_timeline/seller_phases/phase_deposits_unlocked_seller.dart new file mode 100644 index 0000000..7aad40e --- /dev/null +++ b/lib/views/screens/trade_timeline/seller_phases/phase_deposits_unlocked_seller.dart @@ -0,0 +1,80 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'package:flutter/material.dart'; +import 'package:haveno/grpc_models.dart'; +import 'package:haveno_app/views/screens/trade_chat_screen.dart'; +import 'package:haveno_app/views/screens/trade_timeline/phase_base.dart'; + +class PhaseDepositsUnlockedSeller extends PhaseBase { + final TradeInfo trade; + + const PhaseDepositsUnlockedSeller({required this.trade, super.key}) + : super(phaseText: "Awaiting Payment"); + + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + // Scrollable content (if any, here it's just a placeholder for your phase text) + Padding( + padding: const EdgeInsets.only(bottom: 80.0), // Adjust padding for button space + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const CircularProgressIndicator(), + const SizedBox(height: 20), + Text( + phaseText, + style: const TextStyle(fontSize: 24), + ), + ], + ), + ), + ), + // Static chat button at the bottom + Positioned( + bottom: 16.0, + right: 16.0, + child: ElevatedButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => TradeChatScreen(tradeId: trade.tradeId), + ), + ); + }, + style: ElevatedButton.styleFrom( + minimumSize: const Size(48, 48), // Set the height and width + padding: EdgeInsets.zero, // Remove extra padding + backgroundColor: Theme.of(context).colorScheme.primary, + ), + child: const Icon(Icons.chat), + ), + ), + ], + ); + } +} \ No newline at end of file diff --git a/lib/views/screens/trade_timeline/seller_phases/phase_init_seller.dart b/lib/views/screens/trade_timeline/seller_phases/phase_init_seller.dart new file mode 100644 index 0000000..a864483 --- /dev/null +++ b/lib/views/screens/trade_timeline/seller_phases/phase_init_seller.dart @@ -0,0 +1,28 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'package:haveno_app/views/screens/trade_timeline/phase_base.dart'; + +class PhaseInitSeller extends PhaseBase { + const PhaseInitSeller({super.key}) + : super(phaseText: "Initializing Trade"); +} diff --git a/lib/views/screens/trade_timeline/seller_phases/phase_payment_received_seller.dart b/lib/views/screens/trade_timeline/seller_phases/phase_payment_received_seller.dart new file mode 100644 index 0000000..b47e45c --- /dev/null +++ b/lib/views/screens/trade_timeline/seller_phases/phase_payment_received_seller.dart @@ -0,0 +1,28 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'package:haveno_app/views/screens/trade_timeline/phase_base.dart'; + +class PhasePaymentReceivedSeller extends PhaseBase { + const PhasePaymentReceivedSeller({super.key}) + : super(phaseText: 'Completed'); +} diff --git a/lib/views/screens/trade_timeline/seller_phases/phase_payment_sent_seller.dart b/lib/views/screens/trade_timeline/seller_phases/phase_payment_sent_seller.dart new file mode 100644 index 0000000..c44d7bc --- /dev/null +++ b/lib/views/screens/trade_timeline/seller_phases/phase_payment_sent_seller.dart @@ -0,0 +1,181 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:haveno/grpc_models.dart'; +import 'package:haveno_app/providers/haveno_client_providers/trades_provider.dart'; +import 'package:haveno_app/utils/payment_utils.dart'; +import 'package:haveno_app/views/screens/trade_chat_screen.dart'; +import 'package:haveno_app/views/screens/trade_timeline/phase_base.dart'; +import 'package:provider/provider.dart'; + +class PhasePaymentSentSeller extends PhaseBase { + final TradeInfo trade; + + const PhasePaymentSentSeller({required this.trade, super.key}) + : super(phaseText: 'Confirm Payment Received'); + + Map _extractAccountPayload(Map json) { + return json.entries + .firstWhere((entry) => entry.key.contains('AccountPayload')) + .value as Map; + } + + @override + Widget build(BuildContext context) { + final price = double.parse(trade.price); + final amount = formatXmr(trade.amount, returnString: false) as double; + final totalAmount = amount * price; + final totalAmountFormatted = "${formatFiat(totalAmount)} ${trade.offer.counterCurrencyCode}"; + + final takerPaymentAccountJson = trade.contract.takerPaymentAccountPayload.toProto3Json(); + final makerPaymentAccountJson = trade.contract.makerPaymentAccountPayload.toProto3Json(); + + final takerPaymentAccountPayload = _extractAccountPayload(jsonDecode(jsonEncode(takerPaymentAccountJson))); + final makerPaymentAccountPayload = _extractAccountPayload(jsonDecode(jsonEncode(makerPaymentAccountJson))); + + return Stack( + children: [ + // Scrollable content + Padding( + padding: const EdgeInsets.only(bottom: 100.0), // Adjusted padding for button space + child: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(height: 20), + const CircularProgressIndicator(), + const SizedBox(height: 20), + const Text( + 'Confirm Payment Received', + style: TextStyle(fontSize: 24), + ), + const SizedBox(height: 20), + Card( + margin: const EdgeInsets.all(8.0), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Text( + 'You should have received a total of $totalAmountFormatted from the following account:', + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + ), + ), + _buildPaymentDetails('From account...', takerPaymentAccountPayload), + const SizedBox(height: 20), + ], + ), + ), + ), + // Static button at the bottom with a solid background + Positioned( + bottom: 0.0, + left: 8.0, + right: 8.0, + child: Container( + color: Theme.of(context).scaffoldBackgroundColor, // Same background as the scaffold + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Row( + children: [ + Expanded( + child: ElevatedButton( + onPressed: () => _onConfirmPaymentReceived(context), + style: ElevatedButton.styleFrom( + minimumSize: const Size.fromHeight(48), // Set the height + ), + child: const Text('Confirm Payment Received'), + ), + ), + const SizedBox(width: 8.0), + ElevatedButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => TradeChatScreen(tradeId: trade.tradeId), + ), + ); + }, + style: ElevatedButton.styleFrom( + minimumSize: const Size(48, 48), // Match the height of the main button + padding: EdgeInsets.zero, // Remove extra padding + backgroundColor: Theme.of(context).colorScheme.primary, + ), + child: const Icon(Icons.chat_bubble_outline), + ), + ], + ), + ), + ), + ], + ); + } + + Widget _buildPaymentDetails(String title, Map payload) { + return Card( + margin: const EdgeInsets.all(8.0), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), + ), + const SizedBox(height: 10), + ...payload.entries.map((entry) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: TextFormField( + initialValue: entry.value, + readOnly: true, + decoration: const InputDecoration( + border: OutlineInputBorder(), + ), + ), + ); + }), + const SizedBox(height: 20), + ], + ), + ), + ); + } + + void _onConfirmPaymentReceived(BuildContext context) { + final tradesProvider = Provider.of(context, listen: false); + tradesProvider + .confirmPaymentReceived(trade.tradeId) + .catchError((error) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Failed to confirm payment received, please try again in a moment.')), + ); + }); + } +} diff --git a/lib/views/screens/wallet_send_screen.dart b/lib/views/screens/wallet_send_screen.dart new file mode 100644 index 0000000..3e2283c --- /dev/null +++ b/lib/views/screens/wallet_send_screen.dart @@ -0,0 +1,20 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . diff --git a/lib/views/tabs/buy/buy_market_offers_tab.dart b/lib/views/tabs/buy/buy_market_offers_tab.dart new file mode 100644 index 0000000..b534973 --- /dev/null +++ b/lib/views/tabs/buy/buy_market_offers_tab.dart @@ -0,0 +1,99 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'package:flutter/material.dart'; +import 'package:haveno/grpc_models.dart'; +import 'package:haveno_app/providers/haveno_client_providers/price_provider.dart'; +import 'package:haveno_app/views/widgets/offer_card_widget.dart'; +import 'package:provider/provider.dart'; +import 'package:haveno_app/providers/haveno_client_providers/offers_provider.dart'; + +class BuyMarketOffersTab extends StatelessWidget { + const BuyMarketOffersTab({super.key}); + + @override + Widget build(BuildContext context) { + final offersProvider = Provider.of(context, listen: false); + final pricesProvider = Provider.of(context, listen: false); + + Future fetchData() async { + if (pricesProvider.prices.isEmpty) { + await pricesProvider.getXmrMarketPrices(); + } + if (offersProvider.offers == null || offersProvider.offers == []) { + await offersProvider.getOffers(); + } + } + + + return FutureBuilder( + future: fetchData(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } else if (snapshot.hasError) { + return Center(child: Text('Error: ${snapshot.error}')); + } else { + final offers = offersProvider.marketBuyOffers; + final marketPrices = pricesProvider.prices; + + if (offers == null || offers.isEmpty) { + return const Center(child: Text('No offers available')); + } else { + // Calculate value score for each offer + final List> sortedOffers = offers.map((offer) { + final marketPriceInfo = marketPrices.firstWhere( + (marketInfo) => marketInfo.currencyCode == offer.counterCurrencyCode, + orElse: () => MarketPriceInfo()..price = 0.0); // Provide a default value if not found + final marketPrice = marketPriceInfo.price; + final offerPrice = double.tryParse(offer.price) ?? 0.0; + + double valueScore = 0; + if (marketPrice > 0) { + valueScore = (marketPrice - offerPrice) / marketPrice * 100; + } + + return { + 'offer': offer, + 'valueScore': valueScore, + }; + }).toList(); + + // Sort offers by value score (lowest value first) + sortedOffers.sort((a, b) => (a['valueScore'] as double).compareTo(b['valueScore'] as double)); + + return Padding( + padding: const EdgeInsets.only(top: 2.0), // Add 2 pixels of padding at the top + child: ListView.builder( + itemCount: sortedOffers.length, + itemBuilder: (context, index) { + final offer = sortedOffers[index]['offer'] as OfferInfo; + return OfferCard(offer: offer); + }, + ), + ); + } + } + }, + ); + } +} diff --git a/lib/views/tabs/buy/buy_my_offers_tab.dart b/lib/views/tabs/buy/buy_my_offers_tab.dart new file mode 100644 index 0000000..71ccaee --- /dev/null +++ b/lib/views/tabs/buy/buy_my_offers_tab.dart @@ -0,0 +1,77 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'package:flutter/material.dart'; +import 'package:haveno_app/views/widgets/offer_card_widget.dart'; +import 'package:provider/provider.dart'; +import 'package:haveno_app/providers/haveno_client_providers/offers_provider.dart'; + +class BuyMyOffersTab extends StatelessWidget { + const BuyMyOffersTab({super.key}); + + @override + Widget build(BuildContext context) { + final offersProvider = Provider.of(context, listen: false); + + Future fetchData() async { + if (offersProvider.offers == null || offersProvider.offers == []) { + try { + await offersProvider.getMyOffers(); + } catch (e) { + print("Tried to update My Buy Offers, but hit cooldown limit"); + } + } + } + + return Consumer( + builder: (context, offersProvider, child) { + return FutureBuilder( + future: fetchData(), // Fetch offers + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } else if (snapshot.hasError) { + return Center(child: Text('Error: ${snapshot.error}')); + } else { + final offers = offersProvider.myBuyOffers; + if (offers == null || offers.isEmpty) { + return const Center(child: Text('No offers available')); + } else { + return Padding( + padding: const EdgeInsets.only( + top: 2.0), // Add 2 pixels of padding at the top + child: ListView.builder( + itemCount: offers.length, + itemBuilder: (context, index) { + final offer = offers[index]; + return OfferCard(offer: offer); + }, + ), + ); + } + } + }, + ); + }, + ); + } +} diff --git a/lib/views/tabs/buy_tab.dart b/lib/views/tabs/buy_tab.dart new file mode 100644 index 0000000..671a822 --- /dev/null +++ b/lib/views/tabs/buy_tab.dart @@ -0,0 +1,184 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'package:flutter/material.dart'; +import 'package:haveno/profobuf_models.dart'; +import 'package:haveno_app/views/widgets/offer_filter_menu.dart'; +import 'package:provider/provider.dart'; +import 'package:haveno_app/providers/haveno_client_providers/offers_provider.dart'; +import 'package:haveno_app/providers/haveno_client_providers/payment_accounts_provider.dart'; +import 'package:haveno_app/views/tabs/buy/buy_market_offers_tab.dart'; +import 'package:haveno_app/views/tabs/buy/buy_my_offers_tab.dart'; +import 'package:haveno_app/views/widgets/new_trade_offer_form.dart'; + +class BuyTab extends StatefulWidget { + const BuyTab({super.key}); + + @override + _BuyTabState createState() => _BuyTabState(); +} + +class _BuyTabState extends State with TickerProviderStateMixin { + final _formKey = GlobalKey(); + TabController? _tabController; + bool _isLoadingPaymentMethods = true; + List _paymentAccounts = []; + bool _isFilterVisible = false; + late AnimationController _filterAnimationController; + late Animation _filterAnimation; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 2, vsync: this); + _filterAnimationController = AnimationController( + duration: const Duration(milliseconds: 300), + vsync: this, + ); + _filterAnimation = CurvedAnimation( + parent: _filterAnimationController, + curve: Curves.easeInOut, + ); + _initializeData(); + } + + Future _initializeData() async { + final paymentAccountsProvider = + Provider.of(context, listen: false); + + await paymentAccountsProvider.getPaymentAccounts(); + + setState(() { + _isLoadingPaymentMethods = false; + _paymentAccounts = paymentAccountsProvider.paymentAccounts; + }); + + } + + void _showNewTradeForm() { + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (BuildContext context) { + return NewTradeOfferForm( + direction: 'BUY', + paymentAccounts: _paymentAccounts, + formKey: _formKey, + ); + }, + ).then((value) { + // After closing the modal, navigate to the "My Offers" tab + if (_tabController != null) { + _tabController?.animateTo(1); // Move to "My Offers" tab + } + }); + } + + + @override + void dispose() { + _tabController?.dispose(); + _filterAnimationController.dispose(); + super.dispose(); + } + + void _toggleFilter() { + setState(() { + _isFilterVisible = !_isFilterVisible; + if (_isFilterVisible) { + _filterAnimationController.forward(); + } else { + _filterAnimationController.reverse(); + } + }); + } + + @override + Widget build(BuildContext context) { + final offersProvider = Provider.of(context); + final totalOpenOffers = offersProvider.offers?.length ?? 'Loading '; + + return Scaffold( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + appBar: AppBar( + title: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('Buy Monero'), + Text( + '$totalOpenOffers total open offers', + style: TextStyle( + fontSize: 14.0, // Reduced font size + color: Colors.white.withOpacity(0.23), // Reduced opacity + ), + ), + ], + ), + actions: [ + IconButton( + icon: const Icon(Icons.filter_list), + onPressed: _toggleFilter, + ), + ], + bottom: TabBar( + controller: _tabController, + tabs: const [ + Tab(text: 'Market Offers'), + Tab(text: 'My Offers'), + ], + ), + ), + body: Column( + children: [ + SizeTransition( + sizeFactor: _filterAnimation, + axisAlignment: -1.0, + child: OfferFilterMenu( + onCurrenciesChanged: (value) { + // Handle currency filter change + }, + onPaymentMethodsChanged: (value) { + // Handle payment method filter change + }, + ), + ), + Expanded( + child: _isLoadingPaymentMethods + ? const Center(child: CircularProgressIndicator()) + : TabBarView( + controller: _tabController, + children: [ + BuyMarketOffersTab(), + BuyMyOffersTab(), + ], + ), + ), + ], + ), + floatingActionButton: FloatingActionButton( + onPressed: _showNewTradeForm, + backgroundColor: Theme.of(context).colorScheme.primary, + child: const Icon(Icons.add), + ), + ); + } +} diff --git a/lib/views/tabs/market_statistics.dart b/lib/views/tabs/market_statistics.dart new file mode 100644 index 0000000..c07919b --- /dev/null +++ b/lib/views/tabs/market_statistics.dart @@ -0,0 +1,110 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'package:flutter/material.dart'; +import 'package:haveno_app/data/mock_data.dart'; +import 'package:haveno_app/providers/haveno_client_providers/trade_statistics_provider.dart'; +import 'package:interactive_chart/interactive_chart.dart'; +import 'package:provider/provider.dart'; + +class MarketStatistics extends StatefulWidget { + const MarketStatistics({super.key}); + + @override + State createState() => _MarketStatisticsState(); +} + +class _MarketStatisticsState extends State { + final List _data = MockDataTesla.candles; + bool _showAverage = false; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + // Access the provider here to initialize or fetch data + Provider.of(context, listen: false) + .getTradeStatistics(); + }); + } + + @override + Widget build(BuildContext context) { + // Access the provider's state here + final tradeStatisticsProvider = + Provider.of(context); + + return Scaffold( + appBar: AppBar( + title: const Text("XMR/USDT"), + actions: [ + IconButton( + icon: Icon( + _showAverage ? Icons.show_chart : Icons.bar_chart_outlined, + ), + onPressed: () { + setState(() => _showAverage = !_showAverage); + if (_showAverage) { + _computeTrendLines(); + } else { + _removeTrendLines(); + } + }, + ), + ], + ), + body: SafeArea( + minimum: const EdgeInsets.symmetric(horizontal: 4, vertical: 12), + child: InteractiveChart( + candles: _data, + style: ChartStyle( + volumeHeightFactor: 0.1, + priceGainColor: Colors.green, + priceLossColor: Colors.red, + trendLineStyles: [ + Paint() + ..strokeWidth = 2.0 + ..strokeCap = StrokeCap.round + ..color = Colors.deepOrange, + ], + ), + ), + ), + ); + } + + void _computeTrendLines() { + final ma7 = CandleData.computeMA(_data, 7); + final ma30 = CandleData.computeMA(_data, 30); + final ma90 = CandleData.computeMA(_data, 90); + + for (int i = 0; i < _data.length; i++) { + _data[i].trends = [ma7[i], ma30[i], ma90[i]]; + } + } + + void _removeTrendLines() { + for (final data in _data) { + data.trends = []; + } + } +} diff --git a/lib/views/tabs/sell/sale_market_offers_tab.dart b/lib/views/tabs/sell/sale_market_offers_tab.dart new file mode 100644 index 0000000..c956974 --- /dev/null +++ b/lib/views/tabs/sell/sale_market_offers_tab.dart @@ -0,0 +1,98 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'package:flutter/material.dart'; +import 'package:haveno/grpc_models.dart'; +import 'package:haveno_app/providers/haveno_client_providers/price_provider.dart'; +import 'package:haveno_app/views/widgets/offer_card_widget.dart'; +import 'package:provider/provider.dart'; +import 'package:haveno_app/providers/haveno_client_providers/offers_provider.dart'; + +class SaleMarketOffersTab extends StatelessWidget { + const SaleMarketOffersTab({super.key}); + + @override + Widget build(BuildContext context) { + final offersProvider = Provider.of(context, listen: false); + final pricesProvider = Provider.of(context, listen: false); + + Future fetchData() async { + if (pricesProvider.prices.isEmpty) { + await pricesProvider.getXmrMarketPrices(); + } + if (offersProvider.offers == null || offersProvider.offers == []) { + await offersProvider.getOffers(); + } + } + + return FutureBuilder( + future: fetchData(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } else if (snapshot.hasError) { + return Center(child: Text('Error: ${snapshot.error}')); + } else { + final offers = offersProvider.marketSellOffers; + final marketPrices = pricesProvider.prices; + + if (offers == null || offers.isEmpty) { + return const Center(child: Text('No offers available')); + } else { + // Calculate value score for each offer + final List> sortedOffers = offers.map((offer) { + final marketPriceInfo = marketPrices.firstWhere( + (marketInfo) => marketInfo.currencyCode == offer.counterCurrencyCode, + orElse: () => MarketPriceInfo()..price = 0.0); // Provide a default value if not found + final marketPrice = marketPriceInfo.price; + final offerPrice = double.tryParse(offer.price) ?? 0.0; + + double valueScore = 0; + if (marketPrice > 0) { + valueScore = (offerPrice - marketPrice) / marketPrice * 100; + } + + return { + 'offer': offer, + 'valueScore': valueScore, + }; + }).toList(); + + // Sort offers by value score (highest value first) + sortedOffers.sort((a, b) => (b['valueScore'] as double).compareTo(a['valueScore'] as double)); + + return Padding( + padding: const EdgeInsets.only(top: 2.0), // Add 2 pixels of padding at the top + child: ListView.builder( + itemCount: sortedOffers.length, + itemBuilder: (context, index) { + final offer = sortedOffers[index]['offer'] as OfferInfo; + return OfferCard(offer: offer); + }, + ), + ); + } + } + }, + ); + } +} diff --git a/lib/views/tabs/sell/sale_my_offers_tab.dart b/lib/views/tabs/sell/sale_my_offers_tab.dart new file mode 100644 index 0000000..df3c973 --- /dev/null +++ b/lib/views/tabs/sell/sale_my_offers_tab.dart @@ -0,0 +1,69 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'package:flutter/material.dart'; +import 'package:haveno_app/views/widgets/offer_card_widget.dart'; +import 'package:provider/provider.dart'; +import 'package:haveno_app/providers/haveno_client_providers/offers_provider.dart'; + +class SaleMyOffersTab extends StatelessWidget { + const SaleMyOffersTab({super.key}); + + @override + Widget build(BuildContext context) { + final offersProvider = Provider.of(context, listen: false); + + Future fetchData() async { + if (offersProvider.offers == null || offersProvider.offers == []) { + await offersProvider.getMyOffers(); + } + } + + return FutureBuilder( + future: fetchData(), // Fetch offers + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } else if (snapshot.hasError) { + return Center(child: Text('Error: ${snapshot.error}')); + } else { + final offers = offersProvider.mySellOffers; + if (offers == null || offers.isEmpty) { + return const Center(child: Text('No offers available')); + } else { + return Padding( + padding: const EdgeInsets.only( + top: 2.0), // Add 2 pixels of padding at the top + child: ListView.builder( + itemCount: offers.length, + itemBuilder: (context, index) { + final offer = offers[index]; + return OfferCard(offer: offer); + }, + ), + ); + } + } + }, + ); + } +} diff --git a/lib/views/tabs/sell_tab.dart b/lib/views/tabs/sell_tab.dart new file mode 100644 index 0000000..3d11491 --- /dev/null +++ b/lib/views/tabs/sell_tab.dart @@ -0,0 +1,181 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import 'package:flutter/material.dart'; +import 'package:haveno/profobuf_models.dart'; +import 'package:haveno_app/views/tabs/sell/sale_market_offers_tab.dart'; +import 'package:haveno_app/views/tabs/sell/sale_my_offers_tab.dart'; +import 'package:provider/provider.dart'; +import 'package:haveno_app/providers/haveno_client_providers/offers_provider.dart'; +import 'package:haveno_app/providers/haveno_client_providers/payment_accounts_provider.dart'; +import 'package:haveno_app/views/widgets/new_trade_offer_form.dart'; +import 'package:haveno_app/views/widgets/offer_filter_menu.dart'; + +class SellTab extends StatefulWidget { + const SellTab({super.key}); + + @override + _SellTabState createState() => _SellTabState(); +} + +class _SellTabState extends State with TickerProviderStateMixin { + final _formKey = GlobalKey(); + TabController? _tabController; + bool _isLoadingPaymentMethods = true; + List _paymentAccounts = []; + bool _isFilterVisible = false; + late AnimationController _filterAnimationController; + late Animation _filterAnimation; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 2, vsync: this); + _filterAnimationController = AnimationController( + duration: const Duration(milliseconds: 300), + vsync: this, + ); + _filterAnimation = CurvedAnimation( + parent: _filterAnimationController, + curve: Curves.easeInOut, + ); + _initializeData(); + } + + Future _initializeData() async { + final paymentAccountsProvider = + Provider.of(context, listen: false); + + await paymentAccountsProvider.getPaymentAccounts(); + + setState(() { + _paymentAccounts = paymentAccountsProvider.paymentAccounts; + _isLoadingPaymentMethods = false; + }); + } + + @override + void dispose() { + _tabController?.dispose(); + _filterAnimationController.dispose(); + super.dispose(); + } + + void _showNewTradeForm() { + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (BuildContext context) { + return NewTradeOfferForm( + direction: 'SELL', + paymentAccounts: _paymentAccounts, + formKey: _formKey, + ); + }, + ).then((value) { + // After closing the modal, navigate to the "My Offers" tab + if (_tabController != null) { + _tabController?.animateTo(1); // Move to "My Offers" tab + } + }); + } + + void _toggleFilter() { + setState(() { + _isFilterVisible = !_isFilterVisible; + if (_isFilterVisible) { + _filterAnimationController.forward(); + } else { + _filterAnimationController.reverse(); + } + }); + } + + @override + Widget build(BuildContext context) { + final offersProvider = Provider.of(context); + final totalOpenOffers = offersProvider.offers?.length ?? 'Loading'; + + return Scaffold( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + appBar: AppBar( + title: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('Sell Monero'), + Text( + '$totalOpenOffers total open offers', + style: TextStyle( + fontSize: 14.0, + color: Colors.white.withOpacity(0.23), + ), + ), + ], + ), + actions: [ + IconButton( + icon: const Icon(Icons.filter_list), + onPressed: _toggleFilter, + ), + ], + bottom: TabBar( + controller: _tabController, + tabs: const [ + Tab(text: 'Market Offers'), + Tab(text: 'My Offers'), + ], + ), + ), + body: Column( + children: [ + SizeTransition( + sizeFactor: _filterAnimation, + axisAlignment: -1.0, + child: OfferFilterMenu( + onCurrenciesChanged: (value) { + // Handle currency filter change + }, + onPaymentMethodsChanged: (value) { + // Handle payment method filter change + }, + ), + ), + Expanded( + child: _isLoadingPaymentMethods + ? const Center(child: CircularProgressIndicator()) + : TabBarView( + controller: _tabController, + children: [ + SaleMarketOffersTab(), + SaleMyOffersTab(), + ], + ), + ), + ], + ), + floatingActionButton: FloatingActionButton( + onPressed: _showNewTradeForm, + backgroundColor: Theme.of(context).colorScheme.primary, + child: const Icon(Icons.add), + ), + ); + } +} diff --git a/lib/views/tabs/trades/trades_active_tab.dart b/lib/views/tabs/trades/trades_active_tab.dart new file mode 100644 index 0000000..f6c0a2f --- /dev/null +++ b/lib/views/tabs/trades/trades_active_tab.dart @@ -0,0 +1,225 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'package:flutter/material.dart'; +import 'package:haveno/profobuf_models.dart'; +import 'package:haveno_app/providers/haveno_client_providers/disputes_provider.dart'; +import 'package:haveno_app/utils/payment_utils.dart'; +import 'package:haveno_app/views/screens/active_buyer_trade_timeline_screen.dart'; +import 'package:haveno_app/views/screens/active_seller_trade_timeline_screen.dart'; +import 'package:haveno_app/views/screens/dispute_chat_screen.dart'; +import 'package:haveno_app/utils/human_readable_helpers.dart'; +import 'package:haveno_app/utils/time_utils.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; +import 'package:haveno_app/providers/haveno_client_providers/trades_provider.dart'; + +class TradesActiveTab extends StatelessWidget { + const TradesActiveTab({super.key}); + + @override + Widget build(BuildContext context) { + final tradesProvider = Provider.of(context); + final activeTrades = tradesProvider.activeTrades; + final disputesProvider = Provider.of(context, listen: false); + + return activeTrades == null + ? const Center(child: CircularProgressIndicator()) + : ListView.builder( + itemCount: activeTrades.length, + itemBuilder: (context, index) { + final trade = activeTrades[index]; + final tradeAmount = trade.amount; + final tradePrice = trade.price; + final currencyCode = trade.offer.counterCurrencyCode; + final paymentMethod = trade.offer.paymentMethodShortName; + final tradeRole = trade.role; + + final price = double.parse(tradePrice); + final amount = + formatXmr(tradeAmount, returnString: false) as double; + final total = amount * price; + + final directionString = tradeRole.contains('seller') ? 'selling' : 'buying'; + final isDisputed = trade.disputeState != 'NO_DISPUTE'; + String tradeStatus = humanReadablePhaseAs(trade.phase, trade.role.contains('buyer'), trade.role.contains('buyer')); + late Dispute? dispute; + if (isDisputed) { + dispute = disputesProvider.getDisputeByTradeId(trade.tradeId); + if (dispute != null) { + tradeStatus = humanReadableDisputeStateAs(trade.disputeState, tradeRole.contains('buyer'), dispute.isOpener); + } else { + throw Exception("Dispute discovered by could not resolve the dispute object"); + } + } + + // Determine badge text and color based on trade role + String badgeText; + Color badgeColor; + + if (tradeRole == 'XMR buyer as maker' || tradeRole == 'XMR seller as maker') { + badgeText = 'Maker'; + badgeColor = Colors.green; + } else { + badgeText = 'Taker'; + badgeColor = Colors.red; + } + + return GestureDetector( + onTap: () { + if (isDisputed) { + if (dispute != null) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + DisputeChatScreen(tradeId: trade.tradeId), + ), + ); + } + } + if (directionString == 'buying') { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + ActiveBuyerTradeTimelineScreen(trade: trade), + ), + ); + } else if (directionString == 'selling') { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + ActiveSellerTradeTimelineScreen(trade: trade), + ), + ); + } + }, + child: Stack( + children: [ + Card( + margin: const EdgeInsets.fromLTRB(8, 8, 8, 2), + color: Theme.of(context).cardTheme.color, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Trade #${trade.shortId}', + style: const TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(height: 8), + Center( + child: Text( + 'You are $directionString ${formatXmr(tradeAmount)} ${trade.offer.baseCurrencyCode} for a total ${formatFiat(total)} $currencyCode at the rate of ${formatFiat(price)} $currencyCode/${trade.offer.baseCurrencyCode} via $paymentMethod', + style: const TextStyle( + color: Colors.white, + fontSize: 16, + ), + textAlign: TextAlign.center, + ), + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // Status Badge + Container( + padding: const EdgeInsets.symmetric( + vertical: 4, horizontal: 8), + decoration: BoxDecoration( + color: isDisputed ? Colors.red : Colors.blue, + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(8), + topRight: Radius.circular(8), + ), + ), + child: Text( + tradeStatus, + style: const TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ), + Text( + 'Opened ${calculateFormattedTimeSince(trade.date)} ago', + style: const TextStyle( + color: Colors.white70, + fontSize: 12, + ), + ), + ], + ), + ], + ), + ), + ), + // Maker/Taker Badge + Positioned( + top: 8, + right: 8, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: badgeColor, + borderRadius: const BorderRadius.only( + topLeft: Radius.zero, + bottomRight: Radius.zero, + bottomLeft: Radius.circular(12), + topRight: Radius.circular(12), + ), + ), + child: Text( + badgeText, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ), + ), + ), + ], + ), + ); + }, + ); + } + + String formatDate(int timestamp) { + final date = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000); + final dateFormat = DateFormat('HH:mm dd-MM-yyyy'); + return dateFormat.format(date); + } +} \ No newline at end of file diff --git a/lib/views/tabs/trades/trades_completed_tab.dart b/lib/views/tabs/trades/trades_completed_tab.dart new file mode 100644 index 0000000..26200ac --- /dev/null +++ b/lib/views/tabs/trades/trades_completed_tab.dart @@ -0,0 +1,199 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'package:flutter/material.dart'; +import 'package:haveno/grpc_models.dart'; +import 'package:haveno_app/utils/human_readable_helpers.dart'; +import 'package:haveno_app/utils/payment_utils.dart'; +import 'package:provider/provider.dart'; +import 'package:haveno_app/providers/haveno_client_providers/trades_provider.dart'; +import 'package:intl/intl.dart'; +import 'package:fixnum/fixnum.dart'; // Import the fixnum package for Int64 +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; // Import FontAwesome package + +class TradesCompletedTab extends StatelessWidget { + const TradesCompletedTab({super.key}); + + @override + Widget build(BuildContext context) { + final tradesProvider = Provider.of(context); + final completedTrades = tradesProvider.completedTrades; + + if (completedTrades == null) { + return const Center(child: CircularProgressIndicator()); + } + + // Sort the completed trades by date in descending order + completedTrades.sort((a, b) => b.date.compareTo(a.date)); + + return ListView.builder( + itemCount: completedTrades.length, + itemBuilder: (context, index) { + final trade = completedTrades[index]; + + return Card( + margin: const EdgeInsets.fromLTRB(8, 8, 8, 2), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + const FaIcon(FontAwesomeIcons.monero, color: Color(0xFFFF6602)), + const SizedBox(width: 8.0), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + trade.role.contains('seller') ? 'Sell XMR' : 'Buy XMR', + style: const TextStyle(fontSize: 18.0, fontWeight: FontWeight.bold), + ), + //const SizedBox(height: 2.0), // Reduced space between the texts + Text( + 'You were the ${trade.role.contains('maker') ? 'Maker' : 'Taker'}', + style: const TextStyle(fontSize: 14.0, color: Colors.grey), + ), + ], + ), + ], + ), + Text( + _formatDate(trade.date), + style: const TextStyle(color: Colors.grey), + ), + ], + ), + const SizedBox(height: 8.0), + Text(trade.offer.paymentMethodShortName), + const SizedBox(height: 8.0), + _buildAmountDisplay(trade), + const SizedBox(height: 8.0), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Tooltip( + message: 'Trade ID: ${trade.shortId}\nTrade Peer: ${trade.tradePeerNodeAddress}\nYou have traded X times with this peer.', + child: const Icon( + Icons.info, + color: Colors.blue, + ), + ), + Text( + humanReadablePayoutStateAs(trade.payoutState, trade.role.contains('buyer'), trade.role.contains('buyer')), + style: const TextStyle( + color: Colors.green, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ], + ), + ), + ); + }, + ); + } + + Widget _buildAmountDisplay(TradeInfo trade) { + final price = double.tryParse(trade.offer.price) ?? 0.0; + final amountAtomicUnits = trade.offer.amount.toDouble(); + final amountXMR = amountAtomicUnits / 1e12; // Convert atomic units to XMR + final payAmount = trade.tradeVolume; + final receiveAmount = double.parse(payAmount) / price; + final isBuyingXmr = trade.role.contains('buyer'); + final payAmountDisplay = autoFormatCurrency(trade.tradeVolume, isBuyingXmr ? trade.offer.counterCurrencyCode : trade.offer.counterCurrencyCode, includeCurrencyCode: false); + final receiveAmountDisplay = autoFormatCurrency(receiveAmount, isBuyingXmr ? trade.offer.baseCurrencyCode : trade.offer.counterCurrencyCode, includeCurrencyCode: false); + print("$payAmountDisplay : $receiveAmountDisplay (${trade.offer.baseCurrencyCode} : ${trade.offer.counterCurrencyCode})"); + if (trade.role.contains('buyer')) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('You pay'), + Text( + '$payAmountDisplay ${trade.offer.counterCurrencyCode}', + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ], + ), + const Icon(Icons.arrow_forward), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + const Text('You receive'), + Text( + '$receiveAmountDisplay ${trade.offer.baseCurrencyCode}', + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ], + ), + ], + ); + } else { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('You pay'), + Text( + '$receiveAmountDisplay ${trade.offer.baseCurrencyCode}', + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ], + ), + const Icon(Icons.arrow_forward), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + const Text('You receive'), + Text( + '$payAmountDisplay ${trade.offer.counterCurrencyCode}', + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ], + ), + ], + ); + } + } + + String _formatDate(Int64? dateInt64) { + try { + if (dateInt64 == null) return 'Unknown Date'; + final date = DateTime.fromMillisecondsSinceEpoch(dateInt64.toInt()); + return DateFormat('dd MMM yyyy, HH:mm').format(date); + } catch (e) { + print('Error formatting date: $e'); + return 'Invalid Date'; + } + } +} diff --git a/lib/views/tabs/trades_tab.dart b/lib/views/tabs/trades_tab.dart new file mode 100644 index 0000000..eddfa12 --- /dev/null +++ b/lib/views/tabs/trades_tab.dart @@ -0,0 +1,96 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'package:flutter/material.dart'; +import 'package:haveno_app/views/tabs/trades/trades_active_tab.dart'; +import 'package:haveno_app/views/tabs/trades/trades_completed_tab.dart'; +import 'package:provider/provider.dart'; +import 'package:haveno_app/providers/haveno_client_providers/trades_provider.dart'; + +class TradesTab extends StatefulWidget { + const TradesTab({super.key}); + + @override + _TradesTabState createState() => _TradesTabState(); +} + +class _TradesTabState extends State + with SingleTickerProviderStateMixin { + TabController? _tabController; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 2, vsync: this); + // Call getTrades when the widget is initialized + final tradesProvider = Provider.of(context, listen: false); + tradesProvider.getTrades(); + } + + @override + void dispose() { + _tabController?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final tradesProvider = Provider.of(context); + final totalTrades = tradesProvider.trades.length; + final activeTrades = tradesProvider.activeTrades.length ?? 0; + + return Scaffold( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + appBar: AppBar( + title: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('Trades'), + Center( + child: Text( + '$activeTrades active out of $totalTrades total trades', + style: TextStyle( + fontSize: 14.0, // Reduced font size + color: Colors.white.withOpacity(0.23), // Reduced opacity + ), + ), + ), + ], + ), + bottom: TabBar( + controller: _tabController, + tabs: [ + Tab(text: 'Active'), + Tab(text: 'Completed'), + ], + ), + ), + body: TabBarView( + controller: _tabController, + children: [ + TradesActiveTab(), + TradesCompletedTab(), + ], + ), + ); + } +} diff --git a/lib/views/widgets/add_node_bottom_sheet.dart b/lib/views/widgets/add_node_bottom_sheet.dart new file mode 100644 index 0000000..a074a62 --- /dev/null +++ b/lib/views/widgets/add_node_bottom_sheet.dart @@ -0,0 +1,64 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'remote_node_form.dart'; + +class AddNodeBottomSheet extends StatelessWidget { + const AddNodeBottomSheet({super.key}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: MediaQuery.of(context).viewInsets, + child: Container( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + 'Add New Node', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 16.0), + if (Platform.isIOS || Platform.isAndroid) + ListTile( + title: const Text('Remote Node'), + onTap: () { + Navigator.pop(context); + _showRemoteNodeForm(context); + }, + ) + else + Column( + children: [ + ListTile( + title: const Text('Local Node'), + onTap: () { + Navigator.pop(context); + // Show form to add local node + }, + ), + ListTile( + title: const Text('Remote Node'), + onTap: () { + Navigator.pop(context); + _showRemoteNodeForm(context); + }, + ), + ], + ), + ], + ), + ), + ); + } + + void _showRemoteNodeForm(BuildContext context) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (BuildContext context) { + return RemoteNodeForm(); + }, + ); + } +} diff --git a/lib/views/widgets/add_payment_account_form.dart b/lib/views/widgets/add_payment_account_form.dart new file mode 100644 index 0000000..4e4175a --- /dev/null +++ b/lib/views/widgets/add_payment_account_form.dart @@ -0,0 +1,377 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'package:flutter/material.dart'; +import 'package:haveno/profobuf_models.dart'; +import 'package:multi_select_flutter/multi_select_flutter.dart'; +import 'package:haveno_app/utils/salt.dart'; +import 'package:provider/provider.dart'; +import 'package:haveno_app/providers/haveno_client_providers/payment_accounts_provider.dart'; + +class DynamicPaymentAccountForm extends StatefulWidget { + final PaymentAccountForm paymentAccountForm; + final String paymentMethodLabel; + final String paymentMethodId; + + const DynamicPaymentAccountForm({super.key, + required this.paymentAccountForm, + required this.paymentMethodLabel, + required this.paymentMethodId, + }); + + @override + _DynamicPaymentAccountFormState createState() => + _DynamicPaymentAccountFormState(); +} + +class _DynamicPaymentAccountFormState extends State { + final _formKey = GlobalKey(); + final Map _controllers = {}; + final Map> _multiSelectValues = {}; + final Map _selectOneValues = {}; + + @override + void initState() { + super.initState(); + for (var field in widget.paymentAccountForm.fields) { + _controllers[field.id.name] = TextEditingController(); + + // Check if the field is "SALT" and set its initial value + if (field.id.name == 'SALT') { + _controllers[field.id.name]?.text = generateHexSalt(); + } + + if (field.component.name == 'SELECT_MULTIPLE') { + _multiSelectValues[field.id.name] = []; + } else if (field.component.name == 'SELECT_ONE') { + _selectOneValues[field.id.name] = null; + } + } + } + + @override + void dispose() { + _controllers.forEach((key, controller) { + controller.dispose(); + }); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: MediaQuery.of(context).viewInsets, + child: DraggableScrollableSheet( + expand: false, + builder: (context, scrollController) { + return SingleChildScrollView( + controller: scrollController, + child: Container( + padding: EdgeInsets.all(16.0), + child: Form( + key: _formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Create new ${widget.paymentMethodLabel} account', + style: TextStyle(fontSize: 18)), + SizedBox(height: 16.0), + ...widget.paymentAccountForm.fields.map((field) { + return Padding( + padding: const EdgeInsets.only(bottom: 16.0), + child: _buildField(field), + ); + }), + ElevatedButton( + onPressed: () { + if (_formKey.currentState?.validate() ?? false) { + for (var field in widget.paymentAccountForm.fields) { + if (field.component.name == 'SELECT_MULTIPLE') { + field.value = _multiSelectValues[field.id.name] + ?.join(',') ?? + ''; + } else if (field.component.name == 'SELECT_ONE') { + field.value = + _selectOneValues[field.id.name] ?? ''; + } else { + field.value = + _controllers[field.id.name]?.text ?? ''; + } + } + // Handle form submission + _submitForm(context); + } + }, + child: const Text('Submit'), + ), + ], + ), + ), + ), + ); + }, + ), + ); + } + + Future _submitForm(BuildContext context) async { + final paymentAccountsProvider = + Provider.of(context, listen: false); + + if (_formKey.currentState?.validate() ?? false) { + for (var field in widget.paymentAccountForm.fields) { + if (field.component.name == 'SELECT_MULTIPLE') { + field.value = _multiSelectValues[field.id.name]?.join(',') ?? ''; + } else if (field.component.name == 'SELECT_ONE') { + field.value = _selectOneValues[field.id.name] ?? ''; + } else { + field.value = _controllers[field.id.name]?.text ?? ''; + } + } + + await paymentAccountsProvider + .createPaymentAccount( + widget.paymentMethodId, widget.paymentAccountForm) + .then((account) { + if (account != null) { + paymentAccountsProvider + .getPaymentAccounts(); // Refresh the accounts list + Navigator.pop(context); // Close the bottom sheet after submission + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text('There was an issue creating your account'), + backgroundColor: Theme.of(context).colorScheme.error, + ), + ); + } + }).catchError((error) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text('There was an issue creating your account'), + backgroundColor: Theme.of(context).colorScheme.error, + ), + ); + }); + } + } + + Widget _buildField(PaymentAccountFormField field) { + switch (field.component.name) { + case 'SELECT_MULTIPLE': + return _buildMultiSelectField(field); + case 'TEXTAREA': + return _buildTextAreaField(field); + case 'SELECT_ONE': + return _buildSelectOneField(field); + case 'TEXT': + default: + return _buildTextField(field, isHidden: (field.id.name == 'SALT')); + } + } + + + Widget _buildTextField(PaymentAccountFormField field, {bool isHidden = false}) { + return Visibility( + visible: !isHidden, + child: TextFormField( + controller: _controllers[field.id.name], + decoration: InputDecoration( + labelText: field.label, + border: const OutlineInputBorder(), + ), + onChanged: (value) { + setState(() {}); + }, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter ${field.label}'; + } + if (field.hasMinLength() && value.length < field.minLength) { + return '${field.label} must be at least ${field.minLength} characters long'; + } + if (field.hasMaxLength() && value.length > field.maxLength) { + return '${field.label} cannot be more than ${field.maxLength} characters long'; + } + return null; + }, + ), + ); + } + + + Widget _buildTextAreaField(PaymentAccountFormField field) { + return TextFormField( + controller: _controllers[field.id.name], + maxLines: 5, + decoration: InputDecoration( + labelText: field.label, + border: const OutlineInputBorder(), + ), + onChanged: (value) { + setState(() {}); + }, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter ${field.label}'; + } + if (field.hasMinLength() && value.length < field.minLength) { + return '${field.label} must be at least ${field.minLength} characters long'; + } + if (field.hasMaxLength() && value.length > field.maxLength) { + return '${field.label} cannot be more than ${field.maxLength} characters long'; + } + return null; + }, + ); + } + +Widget _buildMultiSelectField(PaymentAccountFormField field) { + var items = _getSupportedItems(field); + + if (items.isEmpty) { + return TextFormField( + controller: _controllers[field.id.name], + decoration: InputDecoration( + labelText: field.label, + border: const OutlineInputBorder(), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter ${field.label}'; + } + return null; + }, + ); + } + + return MultiSelectBottomSheetField( + initialChildSize: 0.4, + listType: MultiSelectListType.LIST, + searchable: true, + searchHint: 'Search by code or name...', + buttonText: Text(field.label, style: const TextStyle(fontSize: 16, color: Colors.white)), + title: Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Text( + field.label, + style: const TextStyle(fontSize: 16, color: Colors.white), + ), + ), + items: items, + onConfirm: (values) { + setState(() { + _multiSelectValues[field.id.name] = values.cast(); + }); + }, + validator: (values) { + if (values == null || values.isEmpty) { + return 'Please select ${field.label}'; + } + return null; + }, + chipDisplay: MultiSelectChipDisplay( + textStyle: TextStyle(color: Colors.white), + chipColor: Theme.of(context).primaryColor, + onTap: (item) { + setState(() { + _multiSelectValues[field.id.name]?.remove(item); + }); + }, + ), + decoration: BoxDecoration( + color: Theme.of(context).inputDecorationTheme.fillColor, + border: Border.all(color: Theme.of(context).inputDecorationTheme.border?.borderSide.color ?? Colors.grey), + borderRadius: BorderRadius.circular(4), + ), + buttonIcon: Icon( + Icons.arrow_drop_down, + color: Theme.of(context).inputDecorationTheme.iconColor ?? Colors.grey, + ), + itemsTextStyle: const TextStyle( + fontSize: 16, + color: Colors.white, + ), + selectedItemsTextStyle: const TextStyle( + fontSize: 16, + color: Colors.white, + ), + barrierColor: Colors.black.withOpacity(0.5), + confirmText: Text( + 'OK', + style: TextStyle(color: Theme.of(context).colorScheme.secondary), + ), + cancelText: Text( + 'CANCEL', + style: TextStyle(color: Theme.of(context).colorScheme.secondary), + ), + ); +} + +List> _getSupportedItems(PaymentAccountFormField field) { + if (field.supportedCurrencies.isNotEmpty) { + return field.supportedCurrencies.map((currency) { + return MultiSelectItem(currency.code, '${currency.code} - ${currency.name}'); + }).toList(); + } else if (field.supportedCountries.isNotEmpty) { + return field.supportedCountries.map((country) { + return MultiSelectItem(country.code, '${country.code} - ${country.name}'); + }).toList(); + } + return []; +} + + + + + Widget _buildSelectOneField(PaymentAccountFormField field) { + var items = _getSupportedItems(field); + + if (items.isEmpty) { + return _buildTextField(field); + } + + return DropdownButtonFormField( + decoration: InputDecoration( + labelText: field.label, + border: const OutlineInputBorder(), + ), + items: items.map((item) { + return DropdownMenuItem( + value: item.value, + child: Text(item.label), + ); + }).toList(), + onChanged: (value) { + setState(() { + _selectOneValues[field.id.name] = value; + }); + }, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please select ${field.label}'; + } + return null; + }, + ); + } +} \ No newline at end of file diff --git a/lib/views/widgets/add_payment_method_form.dart b/lib/views/widgets/add_payment_method_form.dart new file mode 100644 index 0000000..073ec69 --- /dev/null +++ b/lib/views/widgets/add_payment_method_form.dart @@ -0,0 +1,106 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'package:flutter/material.dart'; +import 'package:haveno_app/utils/payment_utils.dart'; +import 'package:haveno_app/views/widgets/add_payment_account_form.dart'; +import 'package:provider/provider.dart'; +import 'package:haveno_app/providers/haveno_client_providers/payment_accounts_provider.dart';// Import the utils file + +class PaymentMethodSelectionForm extends StatefulWidget { + final String accountType; + + const PaymentMethodSelectionForm({super.key, required this.accountType}); + + @override + _PaymentMethodSelectionFormState createState() => + _PaymentMethodSelectionFormState(); +} + +class _PaymentMethodSelectionFormState + extends State { + String? _selectedPaymentMethod; + + @override + Widget build(BuildContext context) { + final paymentAccountsProvider = + Provider.of(context); + final paymentMethods = widget.accountType == 'FIAT' + ? paymentAccountsProvider.paymentMethods + : paymentAccountsProvider.cryptoCurrencyPaymentMethods; + + return Padding( + padding: MediaQuery.of(context).viewInsets, + child: Container( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Select Payment Method', style: TextStyle(fontSize: 18)), + const SizedBox(height: 16.0), + DropdownButtonFormField( + decoration: const InputDecoration( + labelText: 'Payment Method', + border: OutlineInputBorder(), + ), + items: paymentMethods.map((method) { + return DropdownMenuItem( + value: method.id, + child: Text(getPaymentMethodLabel(method.id)), + ); + }).toList(), + onChanged: (value) { + setState(() { + _selectedPaymentMethod = value; + }); + + if (value != null) { + paymentAccountsProvider + .getPaymentAccountForm(value) + .then((form) { + Navigator.pop(context); + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (BuildContext context) { + return DynamicPaymentAccountForm( + paymentAccountForm: form!, + paymentMethodLabel: getPaymentMethodLabel(value), + paymentMethodId: value); + }, + ); + }); + } + }, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please select a payment method'; + } + return null; + }, + ), + ], + ), + ), + ); + } +} diff --git a/lib/views/widgets/balance.dart b/lib/views/widgets/balance.dart new file mode 100644 index 0000000..5190bbe --- /dev/null +++ b/lib/views/widgets/balance.dart @@ -0,0 +1,174 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'package:fixnum/fixnum.dart'; +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:haveno_app/utils/payment_utils.dart'; +import 'package:provider/provider.dart'; +import 'package:haveno_app/providers/haveno_client_providers/wallets_provider.dart'; + +class MoneroBalanceWidget extends StatelessWidget { + const MoneroBalanceWidget({super.key}); + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, walletsProvider, child) { + if (walletsProvider.balances == null) { + // Load balances only if not already loaded + walletsProvider.getBalances(); + } + + if (walletsProvider.balances == null) { + // Show loading animation only if it's the first time loading + return const AnimatedDots(); + } else { + // Show the current balance + final balances = walletsProvider.balances; + final balance = (balances?.xmr.balance ?? Int64(0)) + + (balances?.xmr.reservedOfferBalance ?? Int64(0)) + + (balances?.xmr.reservedTradeBalance ?? Int64(0)); + + final formattedBalance = formatXmr(balance); + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), + decoration: BoxDecoration( + color: const Color(0xFF424242), + borderRadius: BorderRadius.circular(8.0), + border: Border.all( + color: const Color(0xFF2E2E2E), + width: 1, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + height: 28.0, // Match this height to the ExchangeRateWidget height + padding: const EdgeInsets.symmetric(horizontal: 6.0), + decoration: BoxDecoration( + color: const Color(0xFF4A4A4A), + borderRadius: BorderRadius.circular(6.0), + ), + child: const Center( + child: FaIcon( + FontAwesomeIcons.monero, + color: Color(0xFFFF6602), + size: 18.0, // Adjust icon size to fit within the height + ), + ), + ), + const SizedBox(width: 6.0), + Container( + height: 28.0, // Ensure the height is consistent with the icon box + padding: const EdgeInsets.symmetric(horizontal: 8.0), + decoration: BoxDecoration( + color: const Color(0xFF3A3A3A), + borderRadius: BorderRadius.circular(6.0), + ), + child: Center( + child: Text( + formattedBalance, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 16.0, + ), + ), + ), + ), + ], + ), + ); + } + }, + ); + } +} + +class AnimatedDots extends StatefulWidget { + const AnimatedDots({super.key}); + + @override + _AnimatedDotsState createState() => _AnimatedDotsState(); +} + +class _AnimatedDotsState extends State with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _animation; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: const Duration(seconds: 1), + )..repeat(); + _animation = Tween(begin: 0.0, end: 3.0).animate(_controller); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), + decoration: BoxDecoration( + color: const Color(0xFF424242), + borderRadius: BorderRadius.circular(8.0), + border: Border.all( + color: const Color(0xFF2E2E2E), + width: 1, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: List.generate(3, (index) { + return AnimatedBuilder( + animation: _animation, + builder: (context, child) { + return Opacity( + opacity: _animation.value > index && _animation.value < index + 1 + ? 1.0 + : 0.3, + child: const Text( + '.', + style: TextStyle( + color: Colors.white, + fontSize: 24.0, + fontWeight: FontWeight.bold, + ), + ), + ); + }, + ); + }), + ), + ); + } +} diff --git a/lib/views/widgets/edit_trade_offer_form.dart b/lib/views/widgets/edit_trade_offer_form.dart new file mode 100644 index 0000000..8293031 --- /dev/null +++ b/lib/views/widgets/edit_trade_offer_form.dart @@ -0,0 +1,159 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'package:flutter/material.dart'; +import 'package:haveno/grpc_models.dart'; +import 'package:haveno_app/utils/payment_utils.dart'; +import 'package:provider/provider.dart'; +import 'package:haveno_app/providers/haveno_client_providers/offers_provider.dart'; + +class EditTradeOfferForm extends StatefulWidget { + final OfferInfo offer; + + const EditTradeOfferForm({super.key, required this.offer}); + + @override + _EditTradeOfferFormState createState() => _EditTradeOfferFormState(); +} + +class _EditTradeOfferFormState extends State { + final TextEditingController _deviationController = TextEditingController(); + final TextEditingController _triggerPriceController = TextEditingController(); + + @override + void initState() { + super.initState(); + // Initialize with current offer data + _deviationController.text = widget.offer.marketPriceMarginPct.toString(); + _triggerPriceController.text = autoFormatCurrency(widget.offer.triggerPrice.toString(), widget.offer.counterCurrencyCode, includeCurrencyCode: false); + } + + @override + Widget build(BuildContext context) { + final isBuy = widget.offer.direction == 'BUY'; + final useMarketBasedPrice = widget.offer.useMarketBasedPrice; + + return Padding( + padding: MediaQuery.of(context).viewInsets, + child: Container( + padding: const EdgeInsets.all(16.0), + child: Form( + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Edit ${isBuy ? 'Buy' : 'Sell'} Offer', + style: const TextStyle(fontSize: 18), + ), + const SizedBox(height: 16.0), + if (useMarketBasedPrice) + TextFormField( + controller: _deviationController, + decoration: const InputDecoration( + labelText: 'Maximum Deviation from Market Price', + border: OutlineInputBorder(), + suffixText: '%', // Suffix text to indicate percentage + ), + keyboardType: TextInputType.number, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter the maximum deviation percentage'; + } + return null; + }, + ), + const SizedBox(height: 16.0), + TextFormField( + controller: _triggerPriceController, + decoration: InputDecoration( + labelText: isBuy + ? 'Delist if Market Price goes above' + : 'Delist if Market Price goes below', + border: const OutlineInputBorder(), + suffixText: widget.offer.counterCurrencyCode, // Suffix is the currency code + ), + keyboardType: TextInputType.number, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a trigger price'; + } + return null; + }, + ), + const SizedBox(height: 16.0), + Row( + children: [ + Expanded( + child: ElevatedButton( + onPressed: () async { + // Validate and update offer details + if (Form.of(context).validate()) { + final offersProvider = + Provider.of(context, listen: false); + await offersProvider.editOffer( + offerId: widget.offer.id, + marketPriceMarginPct: double.tryParse(_deviationController.text), + triggerPrice: _triggerPriceController.text, + ); + Navigator.pop(context); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Offer updated successfully!')), + ); + } + }, + child: const Text('Save Changes'), + ), + ), + ], + ), + const SizedBox(height: 16.0), + Row( + children: [ + Expanded( + child: ElevatedButton( + onPressed: () async { + // Cancel offer action + final offersProvider = + Provider.of(context, listen: false); + await offersProvider.cancelOffer(widget.offer.id); + Navigator.pop(context); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Offer canceled')), + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red.withOpacity(0.8), // Red color with opacity + ), + child: const Text('Cancel Offer'), + ), + ), + ], + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/views/widgets/exchange_rate.dart b/lib/views/widgets/exchange_rate.dart new file mode 100644 index 0000000..09b7f60 --- /dev/null +++ b/lib/views/widgets/exchange_rate.dart @@ -0,0 +1,101 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'package:flutter/material.dart'; +import 'package:haveno/grpc_models.dart'; +import 'package:haveno_app/utils/payment_utils.dart'; +import 'package:provider/provider.dart'; +import 'package:haveno_app/providers/haveno_client_providers/price_provider.dart'; +import 'package:haveno_app/providers/haveno_providers/settings_provider.dart'; + +class ExchangeRateWidget extends StatelessWidget { + const ExchangeRateWidget({super.key}); + + @override + Widget build(BuildContext context) { + return Consumer2( + builder: (context, settingsProvider, pricesProvider, child) { + final preferredCurrency = settingsProvider.preferredCurrency ?? 'USD'; + final marketPrice = pricesProvider.prices.firstWhere( + (price) => price.currencyCode == preferredCurrency, + orElse: () => MarketPriceInfo(currencyCode: preferredCurrency, price: 0), + ); + + final currencyPair = 'XMR/$preferredCurrency'; + final value = "${formatFiat(marketPrice.price)} $preferredCurrency"; + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), + decoration: BoxDecoration( + color: const Color(0xFF424242), + borderRadius: BorderRadius.circular(8.0), + border: Border.all( + color: const Color(0xFF2E2E2E), + width: 1, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + height: 28.0, // Match the height to the MoneroBalanceWidget height + padding: const EdgeInsets.symmetric(horizontal: 6.0), + decoration: BoxDecoration( + color: const Color(0xFF3A3A3A), + borderRadius: BorderRadius.circular(6.0), + ), + child: Center( + child: Text( + currencyPair, + style: const TextStyle( + color: Colors.white, + fontSize: 16.0, + ), + ), + ), + ), + const SizedBox(width: 6.0), + Container( + height: 28.0, // Ensure consistent height + padding: const EdgeInsets.symmetric(horizontal: 8.0), + decoration: BoxDecoration( + color: const Color(0xFF4A4A4A), + borderRadius: BorderRadius.circular(6.0), + ), + child: Center( + child: Text( + value, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 16.0, + ), + ), + ), + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/views/widgets/loading_button.dart b/lib/views/widgets/loading_button.dart new file mode 100644 index 0000000..1709bbd --- /dev/null +++ b/lib/views/widgets/loading_button.dart @@ -0,0 +1,88 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'package:flutter/material.dart'; + +class LoadingButton extends StatefulWidget { + final Future Function() onPressed; + final Widget child; + final ButtonStyle? style; + final double? width; + final double? height; + + const LoadingButton({ + required this.onPressed, + required this.child, + this.style, + this.width, + this.height, + super.key, + }); + + @override + _LoadingButtonState createState() => _LoadingButtonState(); +} + +class _LoadingButtonState extends State { + bool _isLoading = false; + + void _handlePressed() async { + setState(() { + _isLoading = true; + }); + + try { + await widget.onPressed(); + } finally { + setState(() { + _isLoading = false; + }); + } + } + + @override + Widget build(BuildContext context) { + // Define the default style with a minimum height of 48 + final defaultStyle = ElevatedButton.styleFrom( + minimumSize: const Size.fromHeight(48), // Default minimum height + ); + + return SizedBox( + width: widget.width, + height: widget.height, // Optionally provide custom height or leave null + child: ElevatedButton( + onPressed: _isLoading ? null : _handlePressed, + style: widget.style ?? defaultStyle, // Apply custom style or fallback to default + child: _isLoading + ? const SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator( + color: Colors.white, + strokeWidth: 2, + ), + ) + : widget.child, + ), + ); + } +} diff --git a/lib/views/widgets/main_drawer.dart b/lib/views/widgets/main_drawer.dart new file mode 100644 index 0000000..fcdeae7 --- /dev/null +++ b/lib/views/widgets/main_drawer.dart @@ -0,0 +1,250 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:haveno_app/providers/haveno_client_providers/version_provider.dart'; +import 'package:haveno_app/services/mobile_manager_service.dart'; +import 'package:haveno_app/views/drawer/node_manager_screen.dart'; +import 'package:haveno_app/views/screens/onboarding_screen.dart'; +import 'package:provider/provider.dart'; +import 'package:haveno_app/views/drawer/link_to_mobile_screen.dart'; +import 'package:haveno_app/views/drawer/payment_accounts_screen.dart'; +import 'package:haveno_app/views/drawer/settings_screen.dart'; +import 'package:haveno_app/views/drawer/wallet_screen.dart'; +import 'package:package_info_plus/package_info_plus.dart'; + +class MainDrawer extends StatefulWidget { + const MainDrawer({super.key}); + + @override + _MainDrawerState createState() => _MainDrawerState(); +} + +class _MainDrawerState extends State { + String _appVersion = 'Loading...'; + String? _daemonVersion; + + @override + void initState() { + super.initState(); + _loadAppVersion(); + _fetchDaemonVersion(); + } + + Future _loadAppVersion() async { + try { + PackageInfo packageInfo = await PackageInfo.fromPlatform(); + setState(() { + _appVersion = '${packageInfo.version} (${packageInfo.buildNumber})'; + }); + } catch (e) { + print('Failed to load app version: $e'); // Log the error + setState(() { + _appVersion = 'Failed to load version'; + }); + } + } + + + Future _fetchDaemonVersion() async { + final versionProvider = Provider.of(context, listen: false); + await versionProvider.fetchVersion(); + setState(() { + _daemonVersion = versionProvider.version; + }); + } + + @override + Widget build(BuildContext context) { + return Drawer( + child: Column( + children: [ + Expanded( + child: ListView( + padding: EdgeInsets.zero, + children: [ + DrawerHeader( + decoration: const BoxDecoration( + color: Color(0xFF303030), // Header background color + ), + child: Center( + child: Image.asset( + 'assets/haveno-logo.png', + height: 60, + ), + ), + ), + ListTile( + leading: const Icon(Icons.account_balance_wallet, color: Colors.white), + title: const Text('Wallet', style: TextStyle(color: Colors.white)), + onTap: () { + Navigator.pop(context); + Navigator.push( + context, + MaterialPageRoute(builder: (context) => WalletScreen()), + ); + }, + ), + ListTile( + leading: const Icon(Icons.account_circle, color: Colors.white), + title: const Text('Payment Accounts', style: TextStyle(color: Colors.white)), + onTap: () { + Navigator.pop(context); + Navigator.push( + context, + MaterialPageRoute(builder: (context) => PaymentAccountsScreen()), + ); + }, + ), + ListTile( + leading: const FaIcon( + FontAwesomeIcons.monero, + color: Colors.white, + size: 18.0, // Adjust icon size to fit within the height + ), + title: const Text('Nodes', style: TextStyle(color: Colors.white)), + onTap: () { + Navigator.pop(context); + Navigator.push( + context, + MaterialPageRoute(builder: (context) => NodeManagerScreen()), // Make sure you replace this with your Nodes screen + ); + }, + ), + ListTile( + leading: const Icon(Icons.settings, color: Colors.white), + title: const Text('Settings', style: TextStyle(color: Colors.white)), + onTap: () { + Navigator.pop(context); + Navigator.push( + context, + MaterialPageRoute(builder: (context) => SettingsScreen()), + ); + }, + ), + if (Platform.isWindows || Platform.isMacOS || Platform.isLinux) + ListTile( + leading: const Icon(Icons.sync, color: Colors.white), + title: const Text('Link to Mobile Device', style: TextStyle(color: Colors.white)), + onTap: () { + Navigator.pop(context); + Navigator.push( + context, + MaterialPageRoute(builder: (context) => LinkToMobileScreen()), + ); + }, + ), + if (Platform.isIOS || Platform.isAndroid) + ListTile( + leading: const Icon(Icons.logout, color: Colors.white), + title: const Text('Logout', style: TextStyle(color: Colors.white)), + onTap: () { + _showLogoutConfirmation(context); + }, + ), + ], + ), + ), + _buildFooter(context), + ], + ), + ); + } + + Widget _buildFooter(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Daemon Version: ${_daemonVersion ?? "Loading..."}', + style: TextStyle(color: Colors.white.withOpacity(0.66)), // Adjust opacity + ), + Text( + 'App Version: $_appVersion', + style: TextStyle(color: Colors.white.withOpacity(0.66)), // Adjust opacity + ), + const SizedBox(height: 8.0), + _buildStatusRow('Tor Status', true), // Assume true is online, false is offline + _buildStatusRow('Daemon Status', true), + ], + ), + ); + } + + Widget _buildStatusRow(String label, bool isOnline) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + '$label: ', + style: TextStyle(color: Colors.white.withOpacity(0.7)), // Adjust opacity + ), + Icon( + Icons.circle, + color: isOnline ? Colors.green : Colors.red, + size: 12.0, + ), + ], + ); + } + + void _showLogoutConfirmation(BuildContext context) { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Confirm Logout'), + content: const Text('If you logout you will lose connection to your Haveno daemon which will require setup again, are you sure?'), + actions: [ + TextButton( + child: const Text('Cancel'), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + TextButton( + child: const Text('Yes, Logout'), + onPressed: () { + Navigator.of(context).pop(); + _logout(context); + }, + ), + ], + ); + }, + ); + } + + void _logout(BuildContext context) { + final mobileManagerService = MobileManagerService(); + mobileManagerService.logout(); + // Navigate back to the initial setup screen or login screen + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (context) => OnboardingScreen()), // Replace with your initial setup screen + ); + } +} diff --git a/lib/views/widgets/new_trade_offer_form.dart b/lib/views/widgets/new_trade_offer_form.dart new file mode 100644 index 0000000..e6575f4 --- /dev/null +++ b/lib/views/widgets/new_trade_offer_form.dart @@ -0,0 +1,377 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'package:flutter/material.dart'; +import 'package:grpc/grpc.dart'; +import 'package:haveno/profobuf_models.dart'; +import 'package:haveno_app/utils/payment_utils.dart'; +import 'package:haveno_app/views/tabs/buy_tab.dart'; +import 'package:haveno_app/views/widgets/loading_button.dart'; +import 'package:provider/provider.dart'; +import 'package:haveno_app/providers/haveno_client_providers/offers_provider.dart'; +import 'package:fixnum/fixnum.dart' as fixnum; +import 'dart:convert'; // Import the dart:convert library + +class NewTradeOfferForm extends StatefulWidget { + final GlobalKey formKey; + final List paymentAccounts; + final String direction; // New argument for direction ('BUY' or 'SELL') + + const NewTradeOfferForm({super.key, + required this.formKey, + required this.paymentAccounts, + required this.direction, + }); + + @override + __NewTradeOfferFormState createState() => __NewTradeOfferFormState(); +} + +class __NewTradeOfferFormState extends State { + PaymentAccount? _selectedPaymentAccount; + TradeCurrency? _selectedTradeCurrency; + int _selectedPricingTypeIndex = 0; // 0 for Fixed, 1 for Dynamic + bool _reserveExactAmount = false; + + final TextEditingController _priceController = TextEditingController(); + final TextEditingController _depositController = + TextEditingController(text: '15'); + final TextEditingController _marginController = + TextEditingController(text: '0'); + final TextEditingController _amountController = TextEditingController(); + final TextEditingController _minAmountController = TextEditingController(); + final TextEditingController _triggerPriceController = TextEditingController(); + + @override + Widget build(BuildContext context) { + final isBuy = widget.direction == 'BUY'; + + return Padding( + padding: MediaQuery.of(context).viewInsets, + child: Container( + padding: const EdgeInsets.all(16.0), + child: Form( + key: widget.formKey, + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Open a New XMR ${isBuy ? 'Buy' : 'Sell'} Offer', + style: const TextStyle(fontSize: 18), + ), + const SizedBox(height: 16.0), + ToggleButtons( + isSelected: [ + _selectedPricingTypeIndex == 0, + _selectedPricingTypeIndex == 1 + ], + onPressed: (index) { + setState(() { + _selectedPricingTypeIndex = index; + }); + }, + children: const [ + Padding( + padding: EdgeInsets.symmetric(horizontal: 16.0), + child: Text('Fixed'), + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 16.0), + child: Text('Dynamic'), + ), + ], + ), + const SizedBox(height: 16.0), + DropdownButtonFormField( + decoration: InputDecoration( + labelText: 'Your ${isBuy ? 'Sender' : 'Receiver'} Account', + border: const OutlineInputBorder(), + ), + value: _selectedPaymentAccount, + items: widget.paymentAccounts.map((account) { + return DropdownMenuItem( + value: account, + child: Text("${getPaymentMethodLabel(account.paymentMethod.id)} (${account.accountName})"), + ); + }).toList(), + onChanged: (account) { + setState(() { + _selectedPaymentAccount = account; + _selectedTradeCurrency = null; // Reset currency code + }); + }, + validator: (value) { + if (value == null) { + return 'Please select a payment account'; + } + return null; + }, + ), + const SizedBox(height: 16.0), + DropdownButtonFormField( + decoration: const InputDecoration( + labelText: 'Currency Code', + border: OutlineInputBorder(), + ), + value: _selectedTradeCurrency, + items: _selectedPaymentAccount?.tradeCurrencies + .map((tradeCurrency) { + return DropdownMenuItem( + value: tradeCurrency, + child: Text(tradeCurrency.name), + ); + }).toList() ?? + [], + onChanged: (value) { + setState(() { + _selectedTradeCurrency = value; + }); + }, + validator: (value) { + if (value == null) { + return 'Please select a currency code'; + } + return null; + }, + ), + const SizedBox(height: 16.0), + if (_selectedPricingTypeIndex == 0) + TextFormField( + controller: _priceController, + decoration: InputDecoration( + labelText: 'Price', + suffixText: _selectedTradeCurrency?.code ?? '', + border: const OutlineInputBorder(), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter the price'; + } + return null; + }, + ), + if (_selectedPricingTypeIndex == 1) + TextFormField( + controller: _marginController, + decoration: InputDecoration( + labelText: isBuy + ? 'Market Price Below Margin' + : 'Market Price Above Margin', + suffixText: '%', + border: const OutlineInputBorder(), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter the market price margin'; + } + final margin = double.tryParse(value); + if (margin == null || margin < 1 || margin > 90) { + return 'Please enter a value between 1 and 90'; + } + return null; + }, + ), + const SizedBox(height: 16.0), + TextFormField( + controller: _amountController, + decoration: InputDecoration( + labelText: 'Amount to ${isBuy ? 'Buy' : 'Sell'}', + suffixText: 'XMR', + border: const OutlineInputBorder(), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter the maximum amount you wish to ${isBuy ? 'buy' : 'sell'}'; + } + return null; + }, + ), + const SizedBox(height: 16.0), + TextFormField( + controller: _minAmountController, + decoration: const InputDecoration( + labelText: 'Minimum Transaction Amount', + suffixText: 'XMR', + border: OutlineInputBorder(), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter the minimum transaction amount in XMR'; + } + return null; + }, + ), + const SizedBox(height: 16.0), + TextFormField( + controller: _depositController, + decoration: const InputDecoration( + labelText: 'Mutual Security Deposit', + suffixText: '%', + border: OutlineInputBorder(), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter the mutual security deposit'; + } + final deposit = double.tryParse(value); + if (deposit == null || deposit < 0 || deposit > 50) { + return 'Please enter a value between 0 and 50'; + } + return null; + }, + ), + if (_selectedPricingTypeIndex == 1) + const SizedBox(height: 16.0), + if (_selectedPricingTypeIndex == 1) + TextFormField( + controller: _triggerPriceController, + decoration: InputDecoration( + labelText: + 'Delist If Market Price Goes Above', + suffixText: _selectedTradeCurrency?.code ?? '', + border: const OutlineInputBorder(), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter the trigger price to suspend your offer'; + } + return null; + }, + ), + const SizedBox(height: 16.0), + Row( + children: [ + Expanded( + child: CheckboxListTile( + title: const Row( + children: [ + Text('Reserve only the funds needed'), + SizedBox(width: 4), + Tooltip( + message: + 'If selected, only the exact amount of funds needed for this trade will be reserved. This may also incur a mining fee and will require 10 confirmations therefore it will take ~20 minutes longer to post your trade.', + child: Icon( + Icons.info_outline, + color: Colors.white, + ), + ), + ], + ), + value: _reserveExactAmount, + onChanged: (value) { + setState(() { + _reserveExactAmount = value ?? false; + }); + }, + controlAffinity: ListTileControlAffinity.leading, + ), + ), + ], + ), + const SizedBox(height: 16.0), + LoadingButton( + child: const Text('Post Offer'), + onPressed: () async { + if (widget.formKey.currentState?.validate() ?? false) { + // Prepare the data to be sent + final offerData = { + 'currencyCode': _selectedTradeCurrency?.code ?? '', + 'direction': widget.direction, + 'price': _selectedPricingTypeIndex == 0 ? _priceController.text : '', + 'useMarketBasedPrice': _selectedPricingTypeIndex == 1, + 'marketPriceMarginPct': double.parse( + _marginController.text.isNotEmpty ? _marginController.text : '0'), + 'amount': fixnum.Int64( + ((double.tryParse(_amountController.text) ?? 0) * 1000000000000) + .toInt()).toString(), + 'minAmount': fixnum.Int64( + ((double.tryParse(_minAmountController.text) ?? 0) * 1000000000000) + .toInt()).toString(), + 'buyerSecurityDepositPct': double.parse(_depositController.text) / 100, + 'triggerPrice': _selectedPricingTypeIndex == 1 ? _triggerPriceController.text : '', + 'reserveExactAmount': _reserveExactAmount, + 'paymentAccountId': _selectedPaymentAccount?.id ?? '', + }; + + // Print the JSON representation of the offer + print(jsonEncode(offerData)); + + // Call the postOffer method + try { + final offersProvider = Provider.of(context, listen: false); + await offersProvider.postOffer( + currencyCode: _selectedTradeCurrency?.code ?? '', + direction: widget.direction, + price: _selectedPricingTypeIndex == 0 ? _priceController.text : '', + useMarketBasedPrice: _selectedPricingTypeIndex == 1, + marketPriceMarginPct: double.parse( + _marginController.text.isNotEmpty ? _marginController.text : '0'), + amount: fixnum.Int64( + ((double.tryParse(_amountController.text) ?? 0) * 1000000000000) + .toInt()), + minAmount: fixnum.Int64( + ((double.tryParse(_minAmountController.text) ?? 0) * 1000000000000) + .toInt()), + buyerSecurityDepositPct: double.parse(_depositController.text) / 100, + triggerPrice: _selectedPricingTypeIndex == 1 ? _triggerPriceController.text : '', + reserveExactAmount: _reserveExactAmount, + paymentAccountId: _selectedPaymentAccount?.id ?? '', + ); + if (mounted) { + Navigator.pop(context); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Successfully posted offer!'), + ), + ); + await offersProvider.getMyOffers(); + if (widget.direction == 'BUY') { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => BuyTab(), /// there is a way to just animate to the next tab dot aht + ), + ); + } else { + + } + } + } on GrpcError catch (e) { + Navigator.pop(context); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(e.message ?? 'Unknown server error'), + ), + ); + } + } + }, + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/views/widgets/offer_card_widget.dart b/lib/views/widgets/offer_card_widget.dart new file mode 100644 index 0000000..5b19c40 --- /dev/null +++ b/lib/views/widgets/offer_card_widget.dart @@ -0,0 +1,198 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'package:flutter/material.dart'; +import 'package:haveno/grpc_models.dart'; +import 'package:haveno_app/utils/payment_utils.dart'; +import 'package:haveno_app/views/widgets/edit_trade_offer_form.dart'; +import 'package:provider/provider.dart'; +import 'package:haveno_app/views/screens/taker_offer_detail_screen.dart'; +import 'package:haveno_app/providers/haveno_client_providers/offers_provider.dart'; + +class OfferCard extends StatelessWidget { + final OfferInfo offer; + + const OfferCard({super.key, required this.offer}); + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, offersProvider, child) { + final bool isMyOffer = + offersProvider.myOffers?.any((myOffer) => myOffer.id == offer.id) ?? false; + final bool isBuy = offer.direction == 'SELL'; + + String fromCurrencyDisplay; + String toCurrencyDisplay; + + if (isMyOffer) { + fromCurrencyDisplay = isBuy ? offer.baseCurrencyCode : offer.counterCurrencyCode; + toCurrencyDisplay = isBuy ? offer.counterCurrencyCode : offer.baseCurrencyCode; + } else { + fromCurrencyDisplay = isBuy ? offer.counterCurrencyCode : offer.baseCurrencyCode; + toCurrencyDisplay = isBuy ? offer.baseCurrencyCode : offer.counterCurrencyCode; + } + + // Determine badge color and text based on offer status + String badgeText; + Color badgeColor; + + if (isMyOffer) { + badgeText = offer.state == 'PENDING' ? 'Pending' : 'Active'; + badgeColor = offer.state == 'PENDING' ? Colors.blue : Colors.green; + } else { + badgeText = ''; + badgeColor = Colors.transparent; // No badge if it's not the user's offer + } + + return GestureDetector( + onTap: () { + if (isMyOffer) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (_) => EditTradeOfferForm(offer: offer), + ); + } else { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => OfferDetailScreen(offer: offer), + ), + ); + } + }, + child: Stack( + children: [ + Card( + margin: const EdgeInsets.only(top: 1.0), + color: Theme.of(context).cardTheme.color, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.zero, // No border radius + ), + elevation: 0, // No elevation + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + flex: 2, + child: Text( + fromCurrencyDisplay, + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + const SizedBox(width: 8), + const Icon( + Icons.arrow_forward, + color: Colors.white, + ), + const SizedBox(width: 8), + Expanded( + flex: 4, + child: Text( + offer.paymentMethodShortName, + textAlign: TextAlign.center, + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + const SizedBox(width: 8), + const Icon( + Icons.arrow_forward, + color: Colors.white, + ), + const SizedBox(width: 8), + Expanded( + flex: 2, + child: Text( + toCurrencyDisplay, + textAlign: TextAlign.end, + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + const SizedBox(height: 10), + Center( + child: Text( + isFiatCurrency(offer.counterCurrencyCode) + ? '${double.parse(offer.price).toStringAsFixed(2)} ${offer.counterCurrencyCode}' + : offer.price, + style: const TextStyle( + color: Colors.green, + fontWeight: FontWeight.bold, + fontSize: 18, + ), + ), + ), + const SizedBox(height: 5), + Center( + child: Text( + 'Order limit: ${offer.minVolume} - ${offer.volume}', + style: const TextStyle(color: Colors.white), + ), + ), + ], + ), + ), + ), + if (isMyOffer) + Positioned( + bottom: 0, + right: 0, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: badgeColor, + borderRadius: const BorderRadius.only(topLeft: Radius.circular(8)), + ), + child: Text( + badgeText, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], + ), + ); + }, + ); + } +} \ No newline at end of file diff --git a/lib/views/widgets/offer_filter_menu.dart b/lib/views/widgets/offer_filter_menu.dart new file mode 100644 index 0000000..19beeac --- /dev/null +++ b/lib/views/widgets/offer_filter_menu.dart @@ -0,0 +1,216 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'package:flutter/material.dart'; +import 'package:dropdown_search/dropdown_search.dart'; +import 'package:haveno_app/providers/haveno_client_providers/payment_accounts_provider.dart'; +import 'package:haveno_app/utils/payment_utils.dart'; +import 'package:provider/provider.dart'; + +class OfferFilterMenu extends StatefulWidget { + final ValueChanged>? onCurrenciesChanged; + final ValueChanged>? onPaymentMethodsChanged; + + const OfferFilterMenu({ + super.key, + this.onCurrenciesChanged, + this.onPaymentMethodsChanged, + }); + + @override + _OfferFilterMenuState createState() => _OfferFilterMenuState(); +} + +class _OfferFilterMenuState extends State { + List _selectedCurrencies = []; + List _selectedPaymentMethodIds = []; + late Future> _paymentMethodsFuture; + + @override + void initState() { + super.initState(); + _paymentMethodsFuture = _fetchPaymentMethods(); + } + + Future> _fetchPaymentMethods() async { + final paymentAccountsProvider = Provider.of(context, listen: false); + await paymentAccountsProvider.getPaymentMethods(); // Fetch payment methods + return paymentAccountsProvider.paymentMethods.map((method) => method.id).toList(); + } + + @override + Widget build(BuildContext context) { + return Container( + color: Theme.of(context).cardTheme.color, + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Filters', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + _buildDropdownSearchField( + label: 'Currencies', + items: getAllFiatCurrencies().toList(), + selectedValues: _selectedCurrencies, + onChanged: (values) { + setState(() { + _selectedCurrencies = values; + }); + widget.onCurrenciesChanged?.call(values); + }, + ), + const SizedBox(height: 10), + FutureBuilder>( + future: _paymentMethodsFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return CircularProgressIndicator(); + } else if (snapshot.hasError) { + return Text('Error loading payment methods'); + } else if (!snapshot.hasData || snapshot.data!.isEmpty) { + return Text('No payment methods available'); + } else { + return _buildDropdownSearchField( + label: 'Payment Methods', + items: snapshot.data!, + selectedValues: _selectedPaymentMethodIds, + itemAsString: (String id) => getPaymentMethodLabel(id), + onChanged: (values) { + setState(() { + _selectedPaymentMethodIds = values; + }); + widget.onPaymentMethodsChanged?.call(values); + }, + ); + } + }, + ), + ], + ), + ); + } + + Widget _buildDropdownSearchField({ + required String label, + required List items, + required List selectedValues, + required ValueChanged> onChanged, + String Function(String)? itemAsString, + }) { + return DropdownSearch.multiSelection( + key: Key(label), // Use a key to force rebuild + items: items, + selectedItems: selectedValues, + itemAsString: itemAsString, + dropdownDecoratorProps: DropDownDecoratorProps( + dropdownSearchDecoration: InputDecoration( + contentPadding: const EdgeInsets.fromLTRB(6, 6, 3, 0), + labelText: label, + border: const OutlineInputBorder(), + filled: true, + fillColor: Theme.of(context).inputDecorationTheme.fillColor, + ), + ), + dropdownBuilder: (context, selectedItems) { + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Wrap( + spacing: 6.0, + runSpacing: 6.0, + children: selectedItems.map((item) { + return DeletableChip( + label: itemAsString?.call(item) ?? item, + item: item, + onDelete: () { + setState(() { + selectedItems.remove(item); + }); + print(selectedItems.join(' ')); + onChanged(List.from(selectedValues)); // Trigger a UI rebuild + }, + ); + }).toList(), + ), + ); + }, + dropdownButtonProps: const DropdownButtonProps( + alignment: Alignment.center, + color: Colors.white, + ), + popupProps: PopupPropsMultiSelection.modalBottomSheet( + title: Padding( + padding: const EdgeInsets.all(16.0), + child: Text( + label, + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + ), + showSearchBox: true, + modalBottomSheetProps: ModalBottomSheetProps( + backgroundColor: Theme.of(context).primaryColor, + barrierColor: Colors.black.withOpacity(0.5), + ), + searchFieldProps: TextFieldProps( + padding: const EdgeInsets.symmetric(horizontal: 16), + decoration: InputDecoration( + labelText: 'Search $label', + prefixIcon: const Icon(Icons.search), + border: const OutlineInputBorder(), + filled: true, + fillColor: Theme.of(context).inputDecorationTheme.fillColor, + ), + ), + listViewProps: const ListViewProps( + padding: EdgeInsets.fromLTRB(0, 0, 8, 0), + ), + ), + onChanged: onChanged, + ); + } +} + +class DeletableChip extends StatelessWidget { + final String label; + final String item; + final VoidCallback onDelete; + + const DeletableChip({super.key, + required this.label, + required this.item, + required this.onDelete, + }); + + @override + Widget build(BuildContext context) { + return Chip( + label: Text(label), + padding: const EdgeInsets.all(0), + backgroundColor: Theme.of(context).colorScheme.secondary.withOpacity(0.23), + labelStyle: const TextStyle(color: Colors.white), + deleteIconColor: Colors.white, + onDeleted: onDelete, + ); + } +} diff --git a/lib/views/widgets/offer_filter_menu.dart.old b/lib/views/widgets/offer_filter_menu.dart.old new file mode 100644 index 0000000..e86074c --- /dev/null +++ b/lib/views/widgets/offer_filter_menu.dart.old @@ -0,0 +1,153 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'package:flutter/material.dart'; +import 'package:multi_select_flutter/multi_select_flutter.dart'; + +class OfferFilterMenu extends StatefulWidget { + final ValueChanged>? onCurrenciesChanged; + final ValueChanged>? onPaymentMethodsChanged; + + const OfferFilterMenu({ + Key? key, + this.onCurrenciesChanged, + this.onPaymentMethodsChanged, + }) : super(key: key); + + @override + _OfferFilterMenuState createState() => _OfferFilterMenuState(); +} + +class _OfferFilterMenuState extends State { + final Map> _selectedCurrencies = {}; + final Map> _selectedPaymentMethods = {}; + + @override + Widget build(BuildContext context) { + return Container( + color: Theme.of(context).cardTheme.color, + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Filters', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + _buildMultiSelectField( + 'Currencies', + ['USD', 'EUR', 'GBP', 'AUD', 'CAD', 'JPY'], + _selectedCurrencies, + (values) { + widget.onCurrenciesChanged?.call(values); + }, + ), + const SizedBox(height: 8), + _buildMultiSelectField( + 'Payment Methods', + ['PayPal', 'Revolut', 'Zelle', 'Venmo'], + _selectedPaymentMethods, + (values) { + widget.onPaymentMethodsChanged?.call(values); + }, + ), + ], + ), + ); + } + +Widget _buildMultiSelectField( + String label, + List items, + Map> selectedValues, + ValueChanged> onConfirm) { + return MultiSelectBottomSheetField( + separateSelectedItems: true, + initialChildSize: 0.4, + listType: MultiSelectListType.LIST, + searchable: true, + searchHint: 'Search by $label...', + buttonText: Text(label, style: const TextStyle(fontSize: 16, color: Colors.white, height: 0.16)), + title: Padding( + padding: const EdgeInsets.only(left: 16.0), + child: Text( + label, + style: const TextStyle(fontSize: 16, color: Colors.white), + ), + ), + items: items.map((item) => MultiSelectItem(item, item)).toList(), + onConfirm: (values) { + setState(() { + selectedValues[label] = values; + }); + onConfirm(values); + }, + validator: (values) { + if (values == null || values.isEmpty) { + return 'Please select $label'; + } + return null; + }, + chipDisplay: MultiSelectChipDisplay( + textStyle: const TextStyle(color: Colors.white), + chipColor: Theme.of(context).colorScheme.secondary.withOpacity(0.23), + onTap: (item) { + setState(() { + selectedValues[label]?.remove(item); + }); + }, + alignment: Alignment.bottomCenter, + height: 24, + decoration: BoxDecoration(), + ), + decoration: BoxDecoration( + color: Theme.of(context).inputDecorationTheme.fillColor, + border: Border.all(color: Theme.of(context).inputDecorationTheme.border?.borderSide.color ?? Colors.grey), + borderRadius: BorderRadius.circular(4), + ), + buttonIcon: Icon( + Icons.arrow_drop_down, + color: Theme.of(context).inputDecorationTheme.iconColor ?? Colors.grey, + ), + itemsTextStyle: const TextStyle( + fontSize: 16, + color: Colors.white, + ), + selectedItemsTextStyle: const TextStyle( + fontSize: 16, + color: Colors.white, + ), + barrierColor: Colors.black.withOpacity(0.5), + confirmText: Text( + 'OK', + style: TextStyle(color: Theme.of(context).colorScheme.secondary), + ), + cancelText: Text( + 'CANCEL', + style: TextStyle(color: Theme.of(context).colorScheme.secondary), + ), + ); +} + + +} diff --git a/lib/views/widgets/remote_node_form.dart b/lib/views/widgets/remote_node_form.dart new file mode 100644 index 0000000..25c5091 --- /dev/null +++ b/lib/views/widgets/remote_node_form.dart @@ -0,0 +1,175 @@ +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + + +import 'package:flutter/material.dart'; +import 'package:haveno/grpc_models.dart'; +import 'package:haveno_app/utils/string_utils.dart'; +import 'package:haveno_app/views/widgets/loading_button.dart'; +import 'package:provider/provider.dart'; +import 'package:haveno_app/providers/haveno_client_providers/xmr_connections_provider.dart'; + +class RemoteNodeForm extends StatefulWidget { + final UrlConnection? node; + + const RemoteNodeForm({super.key, this.node}); + + @override + _RemoteNodeFormState createState() => _RemoteNodeFormState(); +} + +class _RemoteNodeFormState extends State { + late TextEditingController hostController; + late TextEditingController portController; + late TextEditingController usernameController; + late TextEditingController passwordController; + + @override + void initState() { + super.initState(); + final parsedUrl = parseNodeUrl(widget.node?.url ?? ''); + final String? host = parsedUrl['host']; + final String? port = parsedUrl['port']; + + hostController = TextEditingController(text: host); + portController = TextEditingController(text: port ?? '18081'); + usernameController = TextEditingController(text: widget.node?.username ?? ''); + passwordController = TextEditingController(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom, + top: 16.0, + left: 16.0, + right: 16.0, + ), + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + 'Add Remote Node', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 16.0), + TextField( + controller: hostController, + decoration: const InputDecoration( + labelText: 'Host IP/Domain', + border: OutlineInputBorder(), + ), + ), + const SizedBox(height: 16.0), + TextField( + controller: portController, + decoration: const InputDecoration( + labelText: 'Port', + border: OutlineInputBorder(), + ), + ), + const SizedBox(height: 16.0), + TextField( + controller: usernameController, + decoration: const InputDecoration( + labelText: 'Username (Optional)', + border: OutlineInputBorder(), + ), + ), + const SizedBox(height: 16.0), + TextField( + controller: passwordController, + decoration: const InputDecoration( + labelText: 'Password (Optional)', + border: OutlineInputBorder(), + ), + obscureText: true, + ), + const SizedBox(height: 16.0), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: SizedBox( + height: 48, // Set equal height for both buttons + child: ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); // Cancel button action + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.grey, // Optional: customize the color + ), + child: const Text('Cancel'), + ), + ), + ), + const SizedBox(width: 16.0), // Space between the buttons + Expanded( + child: SizedBox( + height: 48, // Set equal height for both buttons + child: LoadingButton( + onPressed: () async { + try { + final newNode = UrlConnection( + url: hostController.text, + username: usernameController.text, + password: passwordController.text, + ); + + await Provider.of(context, listen: false) + .addConnection(newNode); + + // Show success snackbar + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Node added successfully'), + backgroundColor: Colors.green, + ), + ); + + Navigator.of(context).pop(); // Close the form on success + } catch (e) { + // Show error snackbar + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Failed to add node: $e'), + backgroundColor: Colors.red, + ), + ); + print(e); // Log the error + } + }, + child: const Text('Save'), + ), + ), + ), + ], + ), + const SizedBox(height: 16.0), // Bottom margin to balance the layout + ], + ), + ), + ); + } +} diff --git a/lib/views/widgets/window.dart b/lib/views/widgets/window.dart new file mode 100644 index 0000000..01ac403 --- /dev/null +++ b/lib/views/widgets/window.dart @@ -0,0 +1,104 @@ +/* // Haveno Plus extends the features of Haveno, supporting mobile devices and more. +// Haveno App extends the features of Haveno, supporting mobile devices and more. +// Copyright (C) 2024 Kewbit (https://kewbit.org) +// Source Code: https://git.haveno.com/haveno/haveno-app.git +// +// Author: Kewbit +// Website: https://kewbit.org +// Contact Email: me@kewbit.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import 'package:bitsdojo_window/bitsdojo_window.dart'; +import 'package:flutter/material.dart'; + +class TitleBar extends StatelessWidget { + const TitleBar({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return WindowTitleBarBox( + child: Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Color(0xFF303030), Color(0xFF303030)], + stops: [0.0, 1.0]), + ), + child: Row( + children: [ + Expanded( + child: MoveWindow(), + ), + const WindowButtons() + ], + ), + ), + ); + } +} + +final buttonColors = WindowButtonColors( + iconNormal: const Color(0xFF805306), + mouseOver: const Color(0xFFF6A00C), + mouseDown: const Color(0xFF805306), + iconMouseOver: const Color(0xFF805306), + iconMouseDown: const Color(0xFFFFD500)); + +final closeButtonColors = WindowButtonColors( + mouseOver: const Color(0xFFD32F2F), + mouseDown: const Color(0xFFB71C1C), + iconNormal: const Color(0xFF805306), + iconMouseOver: Colors.white); + +class WindowButtons extends StatelessWidget { + const WindowButtons({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + MinimizeWindowButton(colors: buttonColors), + MaximizeWindowButton(colors: buttonColors), + CloseWindowButton( + colors: closeButtonColors, + onPressed: () { + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Exit Program?'), + content: const Text( + ('The window will be hidden, to exit the program you can use the system menu.')), + actions: [ + TextButton( + child: const Text('OK'), + onPressed: () { + Navigator.of(context).pop(); + appWindow.hide(); + }, + ), + ], + ); + }, + ); + }, + ), + ], + ); + } +} */ \ No newline at end of file diff --git a/linux/.gitignore b/linux/.gitignore new file mode 100644 index 0000000..d3896c9 --- /dev/null +++ b/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt new file mode 100644 index 0000000..def7043 --- /dev/null +++ b/linux/CMakeLists.txt @@ -0,0 +1,145 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.10) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "haveno") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "com.haveno.haveno") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Define the application target. To change its name, change BINARY_NAME above, +# not the value here, or `flutter run` will no longer work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/linux/flutter/CMakeLists.txt b/linux/flutter/CMakeLists.txt new file mode 100644 index 0000000..d5bd016 --- /dev/null +++ b/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..760efa1 --- /dev/null +++ b/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,35 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include +#include +#include +#include +#include + +void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) emoji_picker_flutter_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "EmojiPickerFlutterPlugin"); + emoji_picker_flutter_plugin_register_with_registrar(emoji_picker_flutter_registrar); + g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); + file_selector_plugin_register_with_registrar(file_selector_linux_registrar); + g_autoptr(FlPluginRegistrar) sentry_flutter_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "SentryFlutterPlugin"); + sentry_flutter_plugin_register_with_registrar(sentry_flutter_registrar); + g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin"); + sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar); + g_autoptr(FlPluginRegistrar) tray_manager_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "TrayManagerPlugin"); + tray_manager_plugin_register_with_registrar(tray_manager_registrar); + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); +} diff --git a/linux/flutter/generated_plugin_registrant.h b/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..e0f0a47 --- /dev/null +++ b/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake new file mode 100644 index 0000000..90ef60d --- /dev/null +++ b/linux/flutter/generated_plugins.cmake @@ -0,0 +1,29 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + emoji_picker_flutter + file_selector_linux + sentry_flutter + sqlite3_flutter_libs + tray_manager + url_launcher_linux +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/linux/main.cc b/linux/main.cc new file mode 100644 index 0000000..e7c5c54 --- /dev/null +++ b/linux/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/linux/my_application.cc b/linux/my_application.cc new file mode 100644 index 0000000..20cfcf2 --- /dev/null +++ b/linux/my_application.cc @@ -0,0 +1,124 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "haveno"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "haveno"); + } + + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GApplication::startup. +static void my_application_startup(GApplication* application) { + //MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application startup. + + G_APPLICATION_CLASS(my_application_parent_class)->startup(application); +} + +// Implements GApplication::shutdown. +static void my_application_shutdown(GApplication* application) { + //MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application shutdown. + + G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application); +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_APPLICATION_CLASS(klass)->startup = my_application_startup; + G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, + "flags", G_APPLICATION_NON_UNIQUE, + nullptr)); +} diff --git a/linux/my_application.h b/linux/my_application.h new file mode 100644 index 0000000..72271d5 --- /dev/null +++ b/linux/my_application.h @@ -0,0 +1,18 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..7cb7b3e --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,1303 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + any_link_preview: + dependency: transitive + description: + name: any_link_preview + sha256: "3135873778da5400e784ec4f304229d16f5d748276de2fce88170554907bca46" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + archive: + dependency: "direct main" + description: + name: archive + sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d + url: "https://pub.dev" + source: hosted + version: "3.6.1" + args: + dependency: transitive + description: + name: args + sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 + url: "https://pub.dev" + source: hosted + version: "2.6.0" + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + audio_waveforms: + dependency: transitive + description: + name: audio_waveforms + sha256: "9fbd314cd4f67e8964df960548ea3b56f829716879527caafb8f3d76ee7e6bf0" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + background_downloader: + dependency: "direct main" + description: + path: "." + ref: "V8.7.1" + resolved-ref: "8d075cb528f3082904e02b8f1a7dcc336793742c" + url: "https://git.haveno.com/haveno/external/background_downloader.git" + source: git + version: "8.7.1" + badges: + dependency: "direct main" + description: + name: badges + sha256: a7b6bbd60dce418df0db3058b53f9d083c22cdb5132a052145dc267494df0b84 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + cached_network_image: + dependency: transitive + description: + name: cached_network_image + sha256: "4a5d8d2c728b0f3d0245f69f921d7be90cae4c2fd5288f773088672c0893f819" + url: "https://pub.dev" + source: hosted + version: "3.4.0" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" + url: "https://pub.dev" + source: hosted + version: "4.1.1" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + sha256: "6322dde7a5ad92202e64df659241104a43db20ed594c41ca18de1014598d7996" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + change_app_package_name: + dependency: "direct dev" + description: + name: change_app_package_name + sha256: "1d6ca5fbaba7264f70857941543337b2efe48f19ae2eef29b89927541b52a787" + url: "https://pub.dev" + source: hosted + version: "1.4.0" + characters: + dependency: transitive + description: + name: characters + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + chatview: + dependency: "direct main" + description: + path: "." + ref: "2.1.1" + resolved-ref: c9ad0d705f3db743cc10452ae316aeee2fd810d0 + url: "https://git.haveno.com/haveno/external/flutter_chatview.git" + source: git + version: "2.1.1" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + url: "https://pub.dev" + source: hosted + version: "2.0.3" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c + url: "https://pub.dev" + source: hosted + version: "0.4.2" + clock: + dependency: transitive + description: + name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" + source: hosted + version: "1.1.1" + collection: + dependency: "direct main" + description: + name: collection + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.dev" + source: hosted + version: "1.18.0" + connectivity_plus: + dependency: "direct main" + description: + name: connectivity_plus + sha256: "876849631b0c7dc20f8b471a2a03142841b482438e3b707955464f5ffca3e4c3" + url: "https://pub.dev" + source: hosted + version: "6.1.0" + connectivity_plus_platform_interface: + dependency: transitive + description: + name: connectivity_plus_platform_interface + sha256: "42657c1715d48b167930d5f34d00222ac100475f73d10162ddf43e714932f204" + url: "https://pub.dev" + source: hosted + version: "2.0.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" + url: "https://pub.dev" + source: hosted + version: "0.3.4+2" + crypto: + dependency: "direct main" + description: + name: crypto + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + url: "https://pub.dev" + source: hosted + version: "3.0.6" + cryptography: + dependency: "direct main" + description: + name: cryptography + sha256: d146b76d33d94548cf035233fbc2f4338c1242fa119013bead807d033fc4ae05 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + csslib: + dependency: transitive + description: + name: csslib + sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + dbus: + dependency: transitive + description: + name: dbus + sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac" + url: "https://pub.dev" + source: hosted + version: "0.7.10" + dropdown_search: + dependency: "direct main" + description: + name: dropdown_search + sha256: "55106e8290acaa97ed15bea1fdad82c3cf0c248dd410e651f5a8ac6870f783ab" + url: "https://pub.dev" + source: hosted + version: "5.0.6" + emoji_picker_flutter: + dependency: transitive + description: + name: emoji_picker_flutter + sha256: "7c6681783e06710608df27be0e38aa4ba73ca1ccac370bb0e7a1320723ae4bca" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" + url: "https://pub.dev" + source: hosted + version: "2.1.3" + file: + dependency: "direct main" + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: b2b91daf8a68ecfa4a01b778a6f52edef9b14ecd506e771488ea0f2e0784198b + url: "https://pub.dev" + source: hosted + version: "0.9.3+1" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: "271ab9986df0c135d45c3cdb6bd0faa5db6f4976d3e4b437cf7d0f258d941bfc" + url: "https://pub.dev" + source: hosted + version: "0.9.4+2" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b + url: "https://pub.dev" + source: hosted + version: "2.6.2" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: "8f5d2f6590d51ecd9179ba39c64f722edc15226cc93dcc8698466ad36a4a85a4" + url: "https://pub.dev" + source: hosted + version: "0.9.3+3" + fixnum: + dependency: "direct main" + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_background_service: + dependency: "direct main" + description: + name: flutter_background_service + sha256: d32f078ec57647c9cfd6e1a8da9297f7d8f021d4dcc204a35aaad2cdbfe255f0 + url: "https://pub.dev" + source: hosted + version: "5.0.10" + flutter_background_service_android: + dependency: transitive + description: + name: flutter_background_service_android + sha256: "39da42dddf877beeef82bc2583130d8bedb4d0765e99ca9e7b4a32e8c6abd239" + url: "https://pub.dev" + source: hosted + version: "6.2.7" + flutter_background_service_ios: + dependency: transitive + description: + name: flutter_background_service_ios + sha256: "6037ffd45c4d019dab0975c7feb1d31012dd697e25edc05505a4a9b0c7dc9fba" + url: "https://pub.dev" + source: hosted + version: "5.0.3" + flutter_background_service_platform_interface: + dependency: transitive + description: + name: flutter_background_service_platform_interface + sha256: ca74aa95789a8304f4d3f57f07ba404faa86bed6e415f83e8edea6ad8b904a41 + url: "https://pub.dev" + source: hosted + version: "5.1.2" + flutter_cache_manager: + dependency: transitive + description: + name: flutter_cache_manager + sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" + url: "https://pub.dev" + source: hosted + version: "3.4.1" + flutter_launcher_icons: + dependency: "direct main" + description: + name: flutter_launcher_icons + sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea" + url: "https://pub.dev" + source: hosted + version: "0.13.1" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" + url: "https://pub.dev" + source: hosted + version: "5.0.0" + flutter_local_notifications: + dependency: "direct main" + description: + name: flutter_local_notifications + sha256: ef41ae901e7529e52934feba19ed82827b11baa67336829564aeab3129460610 + url: "https://pub.dev" + source: hosted + version: "18.0.1" + flutter_local_notifications_linux: + dependency: transitive + description: + name: flutter_local_notifications_linux + sha256: "8f685642876742c941b29c32030f6f4f6dacd0e4eaecb3efbb187d6a3812ca01" + url: "https://pub.dev" + source: hosted + version: "5.0.0" + flutter_local_notifications_platform_interface: + dependency: transitive + description: + name: flutter_local_notifications_platform_interface + sha256: "6c5b83c86bf819cdb177a9247a3722067dd8cc6313827ce7c77a4b238a26fd52" + url: "https://pub.dev" + source: hosted + version: "8.0.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: "9b78450b89f059e96c9ebb355fa6b3df1d6b330436e0b885fb49594c41721398" + url: "https://pub.dev" + source: hosted + version: "2.0.23" + flutter_socks_proxy: + dependency: "direct main" + description: + path: "." + ref: "0.0.3" + resolved-ref: "7a1a6fe2f4a0c82eb7d7de3059e11f79f57a153c" + url: "https://git.haveno.com/haveno/external/socks_proxy.git" + source: git + version: "0.0.3" + flutter_svg: + dependency: transitive + description: + name: flutter_svg + sha256: "936d9c1c010d3e234d1672574636f3352b4941ca3decaddd3cafaeb9ad49c471" + url: "https://pub.dev" + source: hosted + version: "2.0.15" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + font_awesome_flutter: + dependency: "direct main" + description: + name: font_awesome_flutter + sha256: d3a89184101baec7f4600d58840a764d2ef760fe1c5a20ef9e6b0e9b24a07a3a + url: "https://pub.dev" + source: hosted + version: "10.8.0" + google_fonts: + dependency: "direct main" + description: + name: google_fonts + sha256: b1ac0fe2832c9cc95e5e88b57d627c5e68c223b9657f4b96e1487aa9098c7b82 + url: "https://pub.dev" + source: hosted + version: "6.2.1" + google_identity_services_web: + dependency: transitive + description: + name: google_identity_services_web + sha256: "55580f436822d64c8ff9a77e37d61f5fb1e6c7ec9d632a43ee324e2a05c3c6c9" + url: "https://pub.dev" + source: hosted + version: "0.3.3" + googleapis_auth: + dependency: transitive + description: + name: googleapis_auth + sha256: befd71383a955535060acde8792e7efc11d2fccd03dd1d3ec434e85b68775938 + url: "https://pub.dev" + source: hosted + version: "1.6.0" + grpc: + dependency: transitive + description: + name: grpc + sha256: "5b99b7a420937d4361ece68b798c9af8e04b5bc128a7859f2a4be87427694813" + url: "https://pub.dev" + source: hosted + version: "4.0.1" + haveno: + dependency: "direct main" + description: + name: haveno + sha256: bd64427841795aaf5b88f22ffeced86911cc3a744667064514d926522a2d8237 + url: "https://pub.dev" + source: hosted + version: "3.0.1" + html: + dependency: transitive + description: + name: html + sha256: "1fc58edeaec4307368c60d59b7e15b9d658b57d7f3125098b6294153c75337ec" + url: "https://pub.dev" + source: hosted + version: "0.15.5" + http: + dependency: "direct main" + description: + name: http + sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 + url: "https://pub.dev" + source: hosted + version: "1.2.2" + http2: + dependency: "direct main" + description: + name: http2 + sha256: "9ced024a160b77aba8fb8674e38f70875e321d319e6f303ec18e87bd5a4b0c1d" + url: "https://pub.dev" + source: hosted + version: "2.3.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + image: + dependency: transitive + description: + name: image + sha256: f31d52537dc417fdcde36088fdf11d191026fd5e4fae742491ebd40e5a8bea7d + url: "https://pub.dev" + source: hosted + version: "4.3.0" + image_picker: + dependency: transitive + description: + name: image_picker + sha256: "021834d9c0c3de46bf0fe40341fa07168407f694d9b2bb18d532dc1261867f7a" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: "8faba09ba361d4b246dc0a17cb4289b3324c2b9f6db7b3d457ee69106a86bd32" + url: "https://pub.dev" + source: hosted + version: "0.8.12+17" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: "717eb042ab08c40767684327be06a5d8dbb341fe791d514e4b92c7bbe1b7bb83" + url: "https://pub.dev" + source: hosted + version: "3.0.6" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: "4f0568120c6fcc0aaa04511cb9f9f4d29fc3d0139884b1d06be88dcec7641d6b" + url: "https://pub.dev" + source: hosted + version: "0.8.12+1" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "4ed1d9bb36f7cd60aa6e6cd479779cc56a4cb4e4de8f49d487b1aaad831300fa" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: "3f5ad1e8112a9a6111c46d0b57a7be2286a9a07fc6e1976fdf5be2bd31d4ff62" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: "9ec26d410ff46f483c5519c29c02ef0e02e13a543f882b152d4bfd2f06802f80" + url: "https://pub.dev" + source: hosted + version: "2.10.0" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + interactive_chart: + dependency: "direct main" + description: + path: "." + ref: "v0.3.5" + resolved-ref: "8de04325796c702746901d5341d56d65c6959b86" + url: "https://git.haveno.com/haveno/external/flutter-interactive-chart.git" + source: git + version: "0.3.5" + internet_connection_checker_plus: + dependency: "direct main" + description: + name: internet_connection_checker_plus + sha256: "95da3194bdb98cb606b921f0e0007cbe55c32d6a7fa92d5debe59ef4183ce2a6" + url: "https://pub.dev" + source: hosted + version: "2.5.2" + intl: + dependency: "direct main" + description: + name: intl + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + url: "https://pub.dev" + source: hosted + version: "0.19.0" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.dev" + source: hosted + version: "4.9.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + url: "https://pub.dev" + source: hosted + version: "10.0.5" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + url: "https://pub.dev" + source: hosted + version: "3.0.5" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + lints: + dependency: transitive + description: + name: lints + sha256: "3315600f3fb3b135be672bf4a178c55f274bebe368325ae18462c89ac1e3b413" + url: "https://pub.dev" + source: hosted + version: "5.0.0" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + url: "https://pub.dev" + source: hosted + version: "0.12.16+1" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + menu_base: + dependency: transitive + description: + name: menu_base + sha256: "820368014a171bd1241030278e6c2617354f492f5c703d7b7d4570a6b8b84405" + url: "https://pub.dev" + source: hosted + version: "0.1.1" + meta: + dependency: transitive + description: + name: meta + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + url: "https://pub.dev" + source: hosted + version: "1.15.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + mobile_scanner: + dependency: "direct main" + description: + path: "." + ref: "v5.1.1" + resolved-ref: c2c98489fd43cd73b2080ba1f570d0c3eb9b9624 + url: "http://git.haveno.com/haveno/external/mobile_scanner.git" + source: git + version: "5.1.1" + multi_select_flutter: + dependency: "direct main" + description: + name: multi_select_flutter + sha256: "503857b415d390d29159df8a9d92d83c6aac17aaf1c307fb7bcfc77d097d20ed" + url: "https://pub.dev" + source: hosted + version: "4.1.3" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + nm: + dependency: transitive + description: + name: nm + sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254" + url: "https://pub.dev" + source: hosted + version: "0.5.0" + octo_image: + dependency: transitive + description: + name: octo_image + sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + onboarding: + dependency: "direct main" + description: + name: onboarding + sha256: bda49643bea9cb05a5da79f3314a2042f454ca76a61235c35e8fa0b7d6ec56e8 + url: "https://pub.dev" + source: hosted + version: "4.0.2" + package_info_plus: + dependency: "direct main" + description: + name: package_info_plus + sha256: da8d9ac8c4b1df253d1a328b7bf01ae77ef132833479ab40763334db13b91cce + url: "https://pub.dev" + source: hosted + version: "8.1.1" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: ac1f4a4847f1ade8e6a87d1f39f5d7c67490738642e2542f559ec38c37489a66 + url: "https://pub.dev" + source: hosted + version: "3.0.1" + path: + dependency: "direct main" + description: + name: path + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + url: "https://pub.dev" + source: hosted + version: "1.9.0" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: c464428172cb986b758c6d1724c603097febb8fb855aa265aeecc9280c294d4a + url: "https://pub.dev" + source: hosted + version: "2.2.12" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 + url: "https://pub.dev" + source: hosted + version: "2.4.0" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + url: "https://pub.dev" + source: hosted + version: "6.0.2" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + pointycastle: + dependency: "direct main" + description: + name: pointycastle + sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe" + url: "https://pub.dev" + source: hosted + version: "3.9.1" + protobuf: + dependency: transitive + description: + name: protobuf + sha256: "68645b24e0716782e58948f8467fd42a880f255096a821f9e7d0ec625b00c84d" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + provider: + dependency: "direct main" + description: + name: provider + sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c + url: "https://pub.dev" + source: hosted + version: "6.1.2" + qr: + dependency: transitive + description: + name: qr + sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + qr_flutter: + dependency: "direct main" + description: + name: qr_flutter + sha256: "5095f0fc6e3f71d08adef8feccc8cea4f12eec18a2e31c2e8d82cb6019f4b097" + url: "https://pub.dev" + source: hosted + version: "4.1.0" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" + sentry: + dependency: transitive + description: + name: sentry + sha256: "2440763ae96fa8fd1bcdfc224f5232e1b7a09af76a72f4e626ee313a261faf6f" + url: "https://pub.dev" + source: hosted + version: "8.10.1" + sentry_flutter: + dependency: "direct main" + description: + name: sentry_flutter + sha256: "3b30038b3b9303540a8b2c8b1c8f0bb93a207f8e4b25691c59d969ddeb4734fd" + url: "https://pub.dev" + source: hosted + version: "8.10.1" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "95f9997ca1fb9799d494d0cb2a780fd7be075818d59f00c43832ed112b158a82" + url: "https://pub.dev" + source: hosted + version: "2.3.3" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "3b9febd815c9ca29c9e3520d50ec32f49157711e143b7a4ca039eb87e8ade5ab" + url: "https://pub.dev" + source: hosted + version: "2.3.3" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "07e050c7cd39bad516f8d64c455f04508d09df104be326d8c02551590a0d513d" + url: "https://pub.dev" + source: hosted + version: "2.5.3" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e + url: "https://pub.dev" + source: hosted + version: "2.4.2" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shortid: + dependency: transitive + description: + name: shortid + sha256: d0b40e3dbb50497dad107e19c54ca7de0d1a274eb9b4404991e443dadb9ebedb + url: "https://pub.dev" + source: hosted + version: "0.1.2" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + socks5_proxy: + dependency: "direct main" + description: + name: socks5_proxy + sha256: "616818a0ea1064a4823b53c9f7eaf8da64ed82dcd51ed71371c7e54751ed5053" + url: "https://pub.dev" + source: hosted + version: "1.0.6" + source_span: + dependency: transitive + description: + name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" + source: hosted + version: "1.10.0" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + sqflite: + dependency: "direct main" + description: + name: sqflite + sha256: "2d7299468485dca85efeeadf5d38986909c5eb0cd71fd3db2c2f000e6c9454bb" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + sqflite_android: + dependency: transitive + description: + name: sqflite_android + sha256: "78f489aab276260cdd26676d2169446c7ecd3484bbd5fead4ca14f3ed4dd9ee3" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: "761b9740ecbd4d3e66b8916d784e581861fd3c3553eda85e167bc49fdb68f709" + url: "https://pub.dev" + source: hosted + version: "2.5.4+6" + sqflite_common_ffi: + dependency: "direct main" + description: + name: sqflite_common_ffi + sha256: b8ba78c1b72a9ee6c2323b06af95d43fd13e03d90c8369cb454fd7f629a72588 + url: "https://pub.dev" + source: hosted + version: "2.3.4+3" + sqflite_darwin: + dependency: transitive + description: + name: sqflite_darwin + sha256: "96a698e2bc82bd770a4d6aab00b42396a7c63d9e33513a56945cbccb594c2474" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + sqflite_platform_interface: + dependency: transitive + description: + name: sqflite_platform_interface + sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + sqlite3: + dependency: transitive + description: + name: sqlite3 + sha256: fde692580bee3379374af1f624eb3e113ab2865ecb161dbe2d8ac2de9735dbdb + url: "https://pub.dev" + source: hosted + version: "2.4.5" + sqlite3_flutter_libs: + dependency: "direct main" + description: + name: sqlite3_flutter_libs + sha256: "7ae52b23366e5295005022e62fa093f64bfe190810223ea0ebf733a4cd140bce" + url: "https://pub.dev" + source: hosted + version: "0.5.26" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + url: "https://pub.dev" + source: hosted + version: "1.11.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + url: "https://pub.dev" + source: hosted + version: "2.1.2" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + string_validator: + dependency: transitive + description: + name: string_validator + sha256: a278d038104aa2df15d0e09c47cb39a49f907260732067d0034dc2f2e4e2ac94 + url: "https://pub.dev" + source: hosted + version: "1.1.0" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225" + url: "https://pub.dev" + source: hosted + version: "3.3.0+3" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + test_api: + dependency: transitive + description: + name: test_api + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + url: "https://pub.dev" + source: hosted + version: "0.7.2" + timeago: + dependency: transitive + description: + name: timeago + sha256: "054cedf68706bb142839ba0ae6b135f6b68039f0b8301cbe8784ae653d5ff8de" + url: "https://pub.dev" + source: hosted + version: "3.7.0" + timezone: + dependency: "direct main" + description: + name: timezone + sha256: "2236ec079a174ce07434e89fcd3fcda430025eb7692244139a9cf54fdcf1fc7d" + url: "https://pub.dev" + source: hosted + version: "0.9.4" + tray_manager: + dependency: "direct main" + description: + name: tray_manager + sha256: bdc3ac6c36f3d12d871459e4a9822705ce5a1165a17fa837103bc842719bf3f7 + url: "https://pub.dev" + source: hosted + version: "0.2.4" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" + url: "https://pub.dev" + source: hosted + version: "6.3.1" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "6fc2f56536ee873eeb867ad176ae15f304ccccc357848b351f6f0d8d4a40d193" + url: "https://pub.dev" + source: hosted + version: "6.3.14" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e + url: "https://pub.dev" + source: hosted + version: "6.3.1" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "769549c999acdb42b8bcfa7c43d72bf79a382ca7441ab18a808e101149daf672" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e" + url: "https://pub.dev" + source: hosted + version: "2.3.3" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "44cf3aabcedde30f2dba119a9dea3b0f2672fbe6fa96e85536251d678216b3c4" + url: "https://pub.dev" + source: hosted + version: "3.1.3" + uuid: + dependency: "direct main" + description: + name: uuid + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff + url: "https://pub.dev" + source: hosted + version: "4.5.1" + vector_graphics: + dependency: transitive + description: + name: vector_graphics + sha256: "27d5fefe86fb9aace4a9f8375b56b3c292b64d8c04510df230f849850d912cb7" + url: "https://pub.dev" + source: hosted + version: "1.1.15" + vector_graphics_codec: + dependency: transitive + description: + name: vector_graphics_codec + sha256: "2430b973a4ca3c4dbc9999b62b8c719a160100dcbae5c819bae0cacce32c9cdb" + url: "https://pub.dev" + source: hosted + version: "1.1.12" + vector_graphics_compiler: + dependency: transitive + description: + name: vector_graphics_compiler + sha256: ab9ff38fc771e9ee1139320adbe3d18a60327370c218c60752068ebee4b49ab1 + url: "https://pub.dev" + source: hosted + version: "1.1.15" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" + url: "https://pub.dev" + source: hosted + version: "14.2.5" + web: + dependency: transitive + description: + name: web + sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" + url: "https://pub.dev" + source: hosted + version: "0.5.1" + win32: + dependency: transitive + description: + name: win32 + sha256: "84ba388638ed7a8cb3445a320c8273136ab2631cd5f2c57888335504ddab1bc2" + url: "https://pub.dev" + source: hosted + version: "5.8.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + url: "https://pub.dev" + source: hosted + version: "6.5.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + url: "https://pub.dev" + source: hosted + version: "3.1.2" +sdks: + dart: ">=3.5.0 <4.0.0" + flutter: ">=3.24.0" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..7005b4b --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,133 @@ +# Haveno App extends the features of Haveno, supporting mobile devices and more. +# Copyright (C) 2024 Kewbit (https://kewbit.org) +# Source Code: https://git.haveno.com/haveno/haveno-app.git +# +# Author: Kewbit +# Website: https://kewbit.org +# Contact Email: me@kewbit.org +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +name: haveno_app +description: "Haveno Multi-Platform Application" +license: AGPL-3 +repository: https://git.haveno.com/haveno/haveno-app + +authors: + - Kewbit +publish_to: 'none' + +version: 0.3.2+1 + +environment: + sdk: '>=3.4.3 <4.0.0' + +dependencies: + flutter: + sdk: flutter + + haveno: ^3.0.1 + socks5_proxy: ^1.0.6 + flutter_socks_proxy: + git: + url: https://git.haveno.com/haveno/external/socks_proxy.git + ref: 0.0.3 + provider: ^6.0.0 + path_provider: ^2.1.4 + multi_select_flutter: ^4.1.3 + uuid: ^4.4.2 + google_fonts: ^6.2.1 + font_awesome_flutter: ^10.7.0 + fixnum: ^1.1.0 + intl: ^0.19.0 + badges: ^3.1.2 + cryptography: ^2.7.0 + file: ^7.0.0 + path: ^1.9.0 + http2: ^2.3.0 + flutter_launcher_icons: ^0.13.1 + qr_flutter: ^4.1.0 + mobile_scanner: + git: + url: http://git.haveno.com/haveno/external/mobile_scanner.git + ref: v5.1.1 + url_launcher: ^6.3.0 + archive: ^3.6.1 + connectivity_plus: ^6.0.5 + internet_connection_checker_plus: ^2.5.1 + interactive_chart: + git: + url: https://git.haveno.com/haveno/external/flutter-interactive-chart.git + ref: v0.3.5 + chatview: + git: + url: https://git.haveno.com/haveno/external/flutter_chatview.git + ref: 2.1.1 + #local_auth: ^2.3.0 ## To do faceid etc + package_info_plus: ^8.0.2 + #workmanager: ^0.5.2 ## Probably needed for ios to check for messafes every 15 mins + flutter_background_service: ^5.0.7 + flutter_local_notifications: ^18.0.1 + sqflite: ^2.3.3+1 + sqflite_common_ffi: ^2.3.3 + timezone: ^0.9.4 + crypto: ^3.0.5 + pointycastle: ^3.9.1 + onboarding: ^4.0.2 + collection: ^1.18.0 + dropdown_search: ^5.0.5 + http: ^1.2.2 + sentry_flutter: ^8.8.0 + shared_preferences: ^2.3.2 + sqlite3_flutter_libs: ^0.5.24 + tray_manager: ^0.2.4 + background_downloader: + git: + url: https://git.haveno.com/haveno/external/background_downloader.git + ref: V8.7.1 + #window_manager: ^0.4.2 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^5.0.0 + change_app_package_name: ^1.4.0 + +flutter_icons: + android: true + ios: + generate: true + image_path: "assets/icon/app_icon_smaller.png" + remove_alpha_ios: true + image_path: "assets/icon/app_icon.png" + windows: + generate: true + image_path: "assets/icon/app_icon.png" + icon_size: 48 + macos: + generate: true + image_path: "assets/icon/app_icon.png" + +flutter: + uses-material-design: true + assets: + - assets/versions.json + - assets/config/default/torrc + - assets/getting-started-logo.png + - assets/arbitration-logo.png + - assets/haveno-logo.png + - assets/tor-logo.png + - assets/icon/app_icon.png + - assets/icon/app_icon.ico + - assets/icon/app_icon_smaller.png \ No newline at end of file