From 3afeb0e93681f2f46d3daf03e40e020af5348dc7 Mon Sep 17 00:00:00 2001 From: Rainer Killinger Date: Wed, 8 Dec 2021 17:26:11 +0100 Subject: [PATCH 01/30] Initial commit --- .dockerignore | 2 + .gitignore | 3 + Dockerfile | 19 ++ Gemfile | 3 + LICENCE | 171 ++++++++++++++ Makefile | 37 +++ README.md | 26 +++ app.conf | 21 ++ .../assets-mobile/android/icon-background.png | Bin 0 -> 1646 bytes .../assets-mobile/android/icon-foreground.png | Bin 0 -> 11216 bytes customizable/assets-mobile/icon.png | Bin 0 -> 82961 bytes customizable/assets-mobile/splash.png | Bin 0 -> 58155 bytes customizable/assets/icon/favicon.png | Bin 0 -> 10836 bytes customizable/assets/imgs/logo.png | Bin 0 -> 18105 bytes playstore_api_key.json.sample | 3 + static/fastlane-android/Appfile | 1 + static/fastlane-android/Fastfile | 34 +++ static/fastlane-ios/Appfile | 0 static/fastlane-ios/Fastfile | 38 ++++ .../actions/update_build_settings.rb | 213 ++++++++++++++++++ static/scripts/android.sh | 35 +++ static/scripts/clone_app.sh | 18 ++ static/scripts/ionic.sh | 35 +++ static/scripts/ios.sh | 9 + 24 files changed, 668 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 Gemfile create mode 100644 LICENCE create mode 100644 Makefile create mode 100644 README.md create mode 100644 app.conf create mode 100644 customizable/assets-mobile/android/icon-background.png create mode 100644 customizable/assets-mobile/android/icon-foreground.png create mode 100644 customizable/assets-mobile/icon.png create mode 100644 customizable/assets-mobile/splash.png create mode 100644 customizable/assets/icon/favicon.png create mode 100644 customizable/assets/imgs/logo.png create mode 100644 playstore_api_key.json.sample create mode 100644 static/fastlane-android/Appfile create mode 100644 static/fastlane-android/Fastfile create mode 100644 static/fastlane-ios/Appfile create mode 100644 static/fastlane-ios/Fastfile create mode 100644 static/fastlane-ios/actions/update_build_settings.rb create mode 100755 static/scripts/android.sh create mode 100755 static/scripts/clone_app.sh create mode 100755 static/scripts/ionic.sh create mode 100755 static/scripts/ios.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..488fa1c0 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +** +!Gemfile \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..f9148d20 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +app +playstore_api_key.json +Gemfile.lock \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..acc897a5 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +FROM registry.gitlab.com/openstapps/app + +ENV LC_ALL=en_US.UTF-8 \ + LANG=en_US.UTF-8 + +ENV TZ=Europe/Berlin +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone + +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + xmlstarlet \ + ruby-full + +RUN gem install bundler + +COPY . /build +WORKDIR /build + +RUN bundler update diff --git a/Gemfile b/Gemfile new file mode 100644 index 00000000..adc90d98 --- /dev/null +++ b/Gemfile @@ -0,0 +1,3 @@ +source "https://rubygems.org" + +gem "fastlane" \ No newline at end of file diff --git a/LICENCE b/LICENCE new file mode 100644 index 00000000..f53176d9 --- /dev/null +++ b/LICENCE @@ -0,0 +1,171 @@ +GNU GENERAL PUBLIC LICENSE +Version 3, 29 June 2007 + +Copyright © 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 General Public License is a free, copyleft license for software and other kinds of works. + +The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is 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. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. + +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. + +To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. + +For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. + +Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. + +For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. + +Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. + +Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. + +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 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. Use with the GNU Affero General Public License. +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 Affero 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 special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. +14. Revised Versions of this License. +The Free Software Foundation may publish revised and/or new versions of the GNU 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 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 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 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 diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..385493a2 --- /dev/null +++ b/Makefile @@ -0,0 +1,37 @@ +SHELL = /bin/bash +APP_DIR = $(PWD)/app +VERSION ?= develop + +clean: + rm -rf app + +clone: clean + sh static/scripts/clone_app.sh ${VERSION} + +install: clone + cd app && NG_CLI_ANALYTICS="false" npm ci --unsafe-perm + +assets: install + cp -rf customizable/assets/. app/src/assets/ && cp -rf customizable/assets-mobile/. app/resources/ + +configuration: assets + source app.conf && sh static/scripts/ionic.sh + +web-build: configuration + cd app && ionic build --prod + +web: web-build + zip -r www.zip app/www + echo "Web application artifact for version ${VERSION} is archived in www.zip" + +prepare-android: configuration + source app.conf && cd app && rm -rf android www && ionic capacitor add android && npm run resources:android && ionic capacitor build android --no-open --prod && cd .. && sh static/scripts/android.sh + +android: prepare-android + source app.conf && cp -rf static/fastlane-android/. app/android/fastlane/ && cp -rf app.conf app/ios/App/fastlane/.env && cd app/android && bundler exec fastlane android release + +prepare-ios: configuration + source app.conf && cd app && rm -rf ios www && ionic capacitor add ios && npm run resources:ios && ionic capacitor build ios --no-open --prod && cd .. && sh static/scripts/ios.sh + +ios: prepare-ios + source app.conf && cp -rf static/fastlane-ios/. app/ios/App/fastlane/ && cp -rf app.conf app/ios/App/fastlane/.env && cd app/ios/App && bundler exec fastlane ios release \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 00000000..df740e9a --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +# App Release Template (ART) + +This project can be used to generate mobile device apps (android / ios) and angular web app. Beware that this is only the app/frontend part and your need a fully functioning backend deployment first. + +## Requirements + +* A fully functioning and publicly accessible [backend](https://gitlab.com/openstapps/backend) deployment +* An active Apple App Store Developer Account and/or Google Playstore Developer Account +* Docker (Android and Angular builds) +* MacOS Device with latest Xcode, fastlane and xmlstartletn, node v14 and npm package @ionic/cli (iOS builds only) + +## Docker + +Build an image from Dockerfile in this project + +```bash +sudo docker build -t openstapps-art . +``` + +## Usage + +0. If neccessary make adjustments to your app / profiles / entitlements in your corresponding developer/store portal +1. Overwrite assets in customizable directory with your own ones (keeping the same size and file format) +2. Edit app.conf to your liking (needs info you can find in your corresponding developer/store portal) +3. Use docker to run `make web` and `make android` (i.e. `docker run -it -v $(pwd):/build --rm openstapps-art:latest make web`) +4. On a macOS device run `make ios` (make sure you have all required certificates / profiles set up in Xcode) diff --git a/app.conf b/app.conf new file mode 100644 index 00000000..ed64eaae --- /dev/null +++ b/app.conf @@ -0,0 +1,21 @@ +# Edit the following entires if necessary + +APP_NAME="Open StApps" +APP_DISPLAY_NAME="StApps" # App name on homescreen =Not much space +BACKEND_URL="https://your.backend.server.tld" # Publicly available backend url +BACKEND_VERSION="2.0.0" # Minimum backend version the app will request +APP_LINK_URL="your.deep.link.host.tld" # Your host used for universal =deep links + +# Android specific configuration + +ANDROID_PACKAGE_NAME="de.any_school.app" # e.g. com.krausefx.app + + +# iOS specific sonfiguration + +LOCATION_USAGE_DESCRIPTION="Die Ortungsdienste werden für die Kartenansichten und bessere Suchergebnisse benötigt" +IOS_APP_IDENTIFIER="com.any_school.app.ABCDEV1234" # The bundle identifier of your ios app +APPLE_ID="example@domain.tld" # Your Apple Developer email address +ITC_TEAM_ID="123412345" # App Store Connect Team ID +TEAM_ID="ABCDEV1234" # Developer Team ID +PROVISIONING_PROFILE_NAME="App Deployment Profile" # Name of the apps own provisioning profile used for signing diff --git a/customizable/assets-mobile/android/icon-background.png b/customizable/assets-mobile/android/icon-background.png new file mode 100644 index 0000000000000000000000000000000000000000..e2f36dc86e5d88ee0d5708c0a25bf9fa08f34040 GIT binary patch literal 1646 zcmeAS@N?(olHy`uVBq!ia0y~yVB7%0985qFcb(<6Ku$}hvvYu_v$H}$QGQxxPAUU~ z#>Co*wjPHaWRAv1FI}W7`$pl>goRd-0-~i?v_uvZ(hf@u1Vns{im6b@L=mjb2th(%2aACShlD;(8-^mgj@0iZ+%B`2bUG|xU!-hFL>UMlC zgYo<&*KbF?;poU=7QdeBdyiSKyrEG&)=0&j>%tdtt^>a%YlHb->}RdIe&=4)Z;M(6 zqbs4W*eonq+`6XhefRO*ymR;d*VZONtU=qlsM<-=BDPAFgO>bCYGe8D3oWGWGJ|M`UZqI z@`*Druta#eIEGZ*dV9rDkb!~Y@P@|v^Umz$oh^1&xy%lmgcu~08WfpmLL_uBv~kdi zpoxp2hSJd4Z5*@;ZLr&E724oet;MDKu!5slHaTNh+Let!D?>9?

)kMrK{Nv;g-?c%y%$Wywt#Aj2 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%0Aq&5 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 eC6bK~>3M3&3 zNG}1DrnE>2y$M2q&_YNk32@#d@cX`V?|*mPamE?rgRR@VkT+|sIiLB=XU??}UOmv& zI(~%j2pb#Q@w<0!KVoA$lE?Ua_#p6|4+}R>va#*^*GXOd!CiIrbDrKFPn}%t+1M_> z34Wt;r|#yN@X_ph8e;4|C%hsbq+gAUy5Y5tL%G#l|5WzPG1%w7wRPjp1zZ zUmHw~4;ibSpzrengC9M6bFxPJh`4RlL|MdY_3Gq;(#_nYl&6rG{ZRK*_-%{UC&mpw z#R43|uZ7v9m^P;Ueesu4^#bP@%`(Pey?0X~0H4Bpt}$mFj}~j~^~DakTp!YueErnG>0X$kwlZO){CRYA_Wu zHYI1!U;av~{?ZCDQ@`_=Q~UL~TWZ`F6K#YwD|Mq@o=R^DriY=eFC^VUpFE>*`oTX3 z`zc}DUl&Kr)_?pZd4>OwqBTFk?$5*Xf5bv3f`pWGC;mLFy5BgU#Tvd}8uqEz%xvZG z^x#1Wv^n(4A&UjJ{d(c9&FE8~F8(Ih(s+aQPfynzsOYV#mc@B(uzi9X|2R7~cH)fq z18N1XvT}e%_0TB{1tmCp7g0hkeFEHa&{kXPHrqDizYo})1mHV=dEPMvv#}lJV*K65 z22DEyeDlz=yE+<&CXXIHd_v+&bYBh|+c~zox77^%2IdB&_5l}l*J^7BA2Y2leDqx` z5diMk`HEEjYxg1GYF?v#|GB!ecfq!21GYUI01>f=gFPECh;a`Gdp6j^0mA}&IM}lR z69#*nxEBjdAnfJFJx<)?#63=ASzs>~_G00GRxCWnp#_dImV9ivRIs6X{4kr*H&K=e z82=Zu;r`FX^LP1-CM@f#_MXXs-N$T?FXsL6pNssH|E(Qo@)zc_`f+36u2}otGF;aN~-{Ko+5bGz@_#cv+4h>Udm?gAMDB^mJrTJl zBKJh(fBMaPI&Dv{wQkN02mVAETZ*iW z4iMPNPk0L_Kfj}{Nd3Dp_p#WBrz7Wnbv}WEMFXNaSH+7f`L&||KtV~@4YPW>4diDZ zD9Zettv5g3dhB$}9reHznWU$GirKMUJa927^4}A0U*A0Z+mhTwTfp`fW)x>&M_agX z3N-RZUFrzYbTxP+R5y4X%XSUfg=XS@>dh8rH4pTA!v_Bz5dxP+mp^dWnp`tzOBLDB znDAfEOH`t@a|!6%SvWsyODA(Ll0(;zh5lgP+kWy7CVwv7xno{LdGQfT@{jhiuQ9+< zU%;_!P6OV0PIXiIc6zAbm8tt%icA5X=JqGE7<=sAhR8|}87y)m&t03yLF8tO_ukVg zOK{Aqal3Y!@$%0Lo4+xumfHL8@%O+v*wu1bJ&&Po+DO%vMz!v4KiDN|!Mvbd9Y5+G z0*iHno@L%8{r(7ZQrD8M=R)~#kFszEpSy;Vk-dWH{^a+xks?hG3}m_pG6Ju4@krNM z-OsqImRIL7bF8T^ks%L?(1K7Un7LguF7=NOC`v$ZHkB*FM7YF6sie;rTBgsqgRNck zs4??_^o-#{)F>hH;gLf2B5I@iBV5;!*zBH=s1hst-jSfepbSOp@^~vx<9CLP=RYru z+MS@4eTNZD=daeU78<5_xG1v2ujB~qJ%{)0d#Pc?`hhKH+f(I+w4>(Rxy+RhG436U z$zq)yYlZW)4`rWpDt+rXkcs<8!)`jW_@i?*yg4`Z4|s<{OLhoKrL>2Pfimu5a}d}r zI?8weHZ$5kY=D)~!e0r;YnC1l>IrTkUD+_yAri_9;(JE)9WrzRT~XGH{^jw?Qp_RQ z9A54q<>_0+MPi9e2b|}cxPH2M+wfcAM=8g2Ha7)V$-hk%-EP4dL$DE!aql>IkZwk^ zqIC6;4W8XO+v7C1g}>4 zPUkF+=t=0V>NvwwV69Wk6QaWvI7>0Dzwty75~p$dchPXa%cL3WR@ogi>O$~8tpc6@ zzSDBTv7M&Yo_+pQ;N8?(h1+&aveV?8hYXBRYI9yy{1k^*enta4>EZtD_%K1F6a z#m(P>r+&-vuPYp(mk$tZdR$S+3g!{BT2sB9S#|=bhF^^4mP)GxB5^dtBRlmxTS7*=$p#Q#o*5NxTU*v zsXhw+hh@a$|1qLTq3a+umFjE4=6LN# zL|Is|c+cBeb^bRm*Zh@(rrnTj=k|(OCj|AUnV4^n!lOxi`vCT}GiKb?W#Ox7=L(j| zKc1S8SnaT>x0#ksD`j9;CES-sGui&;MAsu(>{D%B!RDvWQUzjzuikpVq=d1npLc@J zUUa1#zU<_YVRf;af1>#_>IecdiB~3Fb@QqQMs}Y=7B9jfrRA783=d`#mY|~McV1l?w$qMcBXYZ zMLQn4VZQm^4FJAm71TR)zQ|@2b2diDfZ`N z+@vG~Y}2dKK*tBO|M+BIbL00&iNc~B&IpDq%KNzklAU{!^)UlZn<4t=isVX@*H))g|$Vs4XKJ7PJZR32yj>s>$m6nP~ zq7DR3WK%1hhV5&5`kNKx=269(8Oa!V5vGY9Wd31VCwcJl&lKTDv&woC*9ZoVGHz=_ zuE-wPfXBeNg$OCn_?4`Olo0EVs+VGn2hUSJHNx;=ef-CCxE8zW*Q_FoEkJH2IkKTq zGr>9S#jC) z81kuxOS>1cSkQrghdbrqmMX=R^}oe(by2;@sfwcHGxClt^XmZb99=%WGHOsRtLTz9SeWym&lcWAal@)5-0CylZLsUB)$)6X`QhFVP$DVVeGyDrT-b{B8|2wKr3u2 zHnztOS1p*fZT;IY^uh-IcHm+Qsa(pTv|_ei85Nx9R_+xZIdiW7UTD-@=P(%Kgc69w z3Uurbl1Si>ES=k(X+41;@`VT`aCe= zwb|SOXPFGck3Ah_VKp3~^q-3~UZ#zF8l83|l=Dj&FeT4DUbkjsXp-FUwJfps$B!(7 zCv646_I-HXsFPXI%gRnFgbR%}U3CT@hKn%bL%gNO7m*izt0Q{WJL^-tC)RC2MF9@g zZ8#XKc5$+1v5TDA|Q>$76F z0^T>3x~w*kZ>2cZU+V9ZtSS)b1X?u@Hxdp%-~=X}%ef#Dbj>|cDdWgRN?B?5*kn|wSf1dT9#?yK*P?%Ykyx#rR-a++Yxq+sy`yRRS+)hWdDnd&7mR%$A z5%~jag3Di=oejVH^jjG}9X6K}@A-;1Ft&30GmMY5~%X>d@7$TusFz^-~G!s#HYrq4MFN z2QP~^D&FtIe{U#6+;cGvmTg+QdPS&8=LqWZ)peO? z=10EzzA1%gFoV~VeRmNnw_ZU4-IcZRg*_Ii602>UcgDnGu$=`F+<%(OMR2mZGn_2+ zPs=2U%dg}`^3UEz=b7fC!( z2W1&yX8y4|AZTF@GMkr0LB?T!&PsjD2y_Zfxt?4RRCDYt`Wq5Yu!nR9EvafKu~i0n!A)%2l;^Hf*ak9uaL%mPwFp^yqE+HfljyKjXSPlAF z{%#mmDu1I{?QyB9k#2}GW5&;icR3kehZr=t(ToZ6JW~gUi7g zg&hK0-Yv0u)@8-<5uFT;#(s|M6Z@7EI4PU+i!*2HD((0{=lQ5MmWkC_CLVj^`M@6h znR~AFY-5RNLCLpttU_j?k;lA|iJYEfmx>-#aYESK`5ESW&6NFTyt4;@93W=O4?t4P^Th^2S8_#1xt#R z=vvA$hN&&Tw*%$3C9b0@#Dy+7Sl6>Zt_#@KKn(G@1Nx-L`adBu9-OK=u_0}q?gI-M zC5if+X0edTuBRIPw#0J9jpgEF$oJ$NgkHKH?42^X*i!V@-Y3_=nQrZbqVY3L%Lu&ZTw?4r`W07%&4;jF=4*1e}tI**u@`h*C*D~3fGU3D+bpWS4G}AL^C&ZLp5qC zuBG)FvXY@s>^298@ye~gQ*xq0L&UZ(qPa~hPT|b{Y1cX5)U%i@IT-Mq0~|K9HE_#-E^hlnj{?;u*Q zURi}H`htH_i(4vHUR+PC0AjkxS{$-CCMy~B+$j&V_&E~W@RdLMLakXlSDojq=!c3v zA$>G6J?4w;)}BqhR-PgCVb*=y`3N7gm-mtaYC@~I`P~kfWpNs{aZQ*7Iu`fRzH`}m z6RxKADBqIp#bB4{!l9Du=T@NPrHy9c>~`NoBH1X|I}co14v}?QV~WE6T9UipkOrsk zsD9DP$td$bP3@wc}1T5l%lWjGgP$ksfCWW}C$Y%`0l+|s|ylrZ*C#Xz)A8k4J#4KDp>qxEy zjBp{wi~`?}7zTM=3*`&9Vo8gaJFzi+QLkE5BI?W`shk(N@Q?>8i^1+~y@8fnRy2F( zFYMX62lTq?LoS5>)BTm)4FcDPT$hPbK~c-t7p+clZjb-aQ||G-emF%l$IO}cy5+N5 z2sp^yzzx!|z|2nhVmrx1(>mnWOOgjB;1=yy$v+KQE86B%y{0pRM37+t#5vae17vu~ z0~2&BXx&aynY#Mriep7yI3JwTQ=m$(>uc|{f!F*n^A&3qb|~m6)jGH#VSF+k`tcM1pN9jvi<65BL)sVXDeWu}RNp9k)85&Z! zY%hrUdO0O8qq3C1zxlz{<^ISut=m=0U$iG8VAMoEfL=tFRh08rZw3t#YR+#hbZdnN zUZp~9L7o+g%U=b-6Z}`Wo7xVw9RkNR^2Y>@kLZuTCyxm5NJy-NG4U|>Co6yW{fk|c zUac1c`DoEGu)utQN5hwn@fi-<+=7Qu!#GeMoCJzulG%#|pL(QDedh#uZ~2o(g8)Bl zXiF-P7RRWPBZ86v(+5aD^nGuOq960#OmCeNG|-JTz)C7kg*hPoqmgT0B4DSso3chv z^(xQufsV0kv7_@s*%SzCKOGk1Yv`>iq!F4>wpN$qgd*yS#uJ7nbFbRR%)}$|z0!an>LQx9aA7wP^6NIW2txj%V z2w9edUMDhgUmrTN^hPH!xA! z)s)nUD2EU`<*B#Y^lQwu#V03%+tDnZx!F#Fn{VUF2$(J6!yAyHe0)|1*(mo5ck_KM zc zrs|X4fan#GK{$jPKV)t@idON$*>&1FKKjB^e=4T5aoV)vT=jVNwxl4K^iHcfJ{~)> z9Ty+E9j_&@oo9@3$)M7bWFLXa2eaKhOpeRS<>4#WpM5_&>=D}8wyho10IK=H!Th|- zYsj4r40tqHsN&+IY6pYzq8Rn;Ow<+^zR+>S18c2ykv#419yH>-NiQ#3RNuzESsedb zxfoYN^9ap0@j7$Z5>t*2F+1p?j~9s&Mbz=-wVZK|vrm>a6z)Vo_!k6>Y}>*fwh-XS z5G?N0{E{=@+_U~IU&-zAwQmlp6A^zhR-e2R33JbJ!E{jGNa?K)s8z`Ja(`wQCIlqA z>fElJQRC3R2==A_0LIt%2-W&sxeyN-k~UPsLB^yFWo?MiT&W|4?>Co4HKCdvV;0@+ zmJyG-5ZT0Q?3LRU?5GmogiQACxc!ZFA6%j$*XmVB@Y0&P9W(3%QQto6ibar*SQZtL z`yQf5oXsik03p`Rq1$Py!U&oe@`%oOekGD&qViA$N;kiT3ap&qJNC+ z<1j=Mec8of_|9;geIPVBv@6X&8e-%UG!q7lCt0gfPN%>^};Iqu{>Vu@%> zI^RoCdFR#R9cO`33Cf7dbv{Mw%Pw&n&KN(sh{gXu7iV8qfG#kNkRq$uG4gmA#c!E&8_18^n5si&Lx@l$1U5pmn7<;lyk4OvI9EWKSE2KS z&hNNwhxck1reEG}=}nBX_JBp;PM@6;8`Pgz&kT59((f|Be@SIPB~LvqT0JfWu~p$V zXxlAf_zN#Xx2hPme4yz+P_xx#bSCx?NJVocw_T95nE{2xr zvxBqpX2M#(yV0spy*UZ*PhlHb3f`Ox79nQa@+hOW1pIw-BrC>T;rSO8HMH&w_EW@RgI<}*PQvx!ASrk0JGr->$FMtVmQYUjQQ6J z@9bRov}Vud&YYMqcXM*K;KL+vNUdhevIicTRBbFajQFRsXtp3GXX>Fcq`zWr80}h- z;bh_f!6I!8yKSEP@Js7xyzKjRcNWsaZQD{&(9gjVl1eP9@7o8t3FduhVel8e!pvFV z5~`_mD$dze1&wxuB6^ceXulUSVUx0eEZ&fbKd-8!ZXieURRa1 z*Sc#G%>!_tQGo9Yxh5Y=6$%T1mq%P~j3;0m&Kl`iu41xEA(S4AbeI0Ox-!R@S^B5& z4xN7BtsXid=l0S_%gt#hXAbDFi)exRDx9_F%i{xi;D?{-qOD_f@!~PG?5Xmc!Oq(? zq|$^6y^YGa%XQzK##t?35szO?M~)38+0t940&Th`2^zAO_+Y6nzr98~2GYwPIkau8 zk0<+f1k_}JbpA93uUcT0?!|(D{Ye#TzL(C#NLLA}GNk<%zY}rs6w?5oa(AYfNokoh z$C(9p+VtE>-Je5I#Fn!cQmL!g@)n_*5W9%^`NG7B+2(8%(1Le$;(mczeRq&{Mpfq! zj{JDa;$Ks;VIDr|BNTHl?D;$^Vb`)dz623#r$6q`Fx9?gX+R>Q&FfpO85j9&zHCnd_&a2a!og34*`^yf~}sNH)7z z_EckeX5VW5CE}W9!k8#y3D=}|0W!e{>*6*FoH&Gms~8XW9KlRQn#c^8bFCnX1s7F2 z&!NlR-LULp>FnF7#yPoxS8+G2M1%}dP^+&G(Oh2Y1QfomaWd|R_#y9}42O`s)1<^#4IB3>dCi-}o;(@!fS>9(wV7Z`n;Gsk|(3!2=j`UEi2quk16?lD$|J>vbu#=G0`p+cOgh|26nok>7on3T&H z|IZY18bsb{a#y9`#1MCdj>1dH6by2Bqk8-G4G1q>|eubtxK9Bxj7 z6#tazki3*JX*=9^O^YZCTeAFg!Bf#HiyevH|1sL|wv3R0XP6#l=FrZmGwk#AzRM4NHl($O`NP$HsfHp^fCMHVmeQ|%^U`I&(9pTD;W+{ zET}ZWB^txR_u;O~K@pKeKaZBf&PaD>!J_PkFy-8D17;UvGwUa?nw(*%QI6HuS!nZA z-4!2Z@)_6(^+Pc(A#t$yteCozNT1JL6&}x~%&_lc)62W+2WI|U{1g}XzN4FBx_tSz zB02}kN6^dKoXT_%%qy^v<-@j^xC;L1*z21ZP3hSdSjaDyS7UDfjJ?@udnr8@Xl)&4 zn;jq9T!!#oO-Fn%Gvlv%f4)tF6vU0bKXJ}^)9jF?-gThy{YNUaGSb9ARyP8iM|`PV5(ZdTe7a;i8j#Y#^&yVDUZGSqx*;1lR3X)l^ zSAHY>2s*M&TKI_Ossw=r|GiKC8rm}L=-)X>tQb{fgQg;<5W)Q7ZDG8Pc%v5XmN)kE z-4?NDMlCHvWs!wUT2cOEmkWy!i9P4Lj3)73eFdZPv+GeyPLe;NNbF>{bs=SasZmK% zEOE;Aod&nGWutD(hv^FM`D<&6F`uX&NOp9#+9w(Lno@(dcV|cM%b(!4$Mz(YGmG`- zI=deDG_J>+1YoGT|Aiwa(p&BquRUH zKiglHJ3hs0^hD>OF(~$-K?ytheO43RF@h#)?1~R%CZ^zWHC zmno9f_vxE1N zC=Xjy?J-3*#T$R4$Vngjue&OuBP+JAaLnesUv@-WprqXm?U)3SoU}9D=iN>&hF9GV z7`B)Y#y?;U_Z7`0WLe^6b>?z1JWRA@bU)S4R=jmy7{OO`Iyn~+oHG;!2dcXuvUug( zyn(2NY`lwvbJ^5ajl4xEL!MJ%9#=Ii!H~79usg#n;&=K_@P<=EYQKyw=7Xn><4P6P z72mWvbDu^LYv1Em2WOAuPMq;TH{RyHC$65;;y;^kRV*i4?P53Nluy-`AHKkQ73E>a zAI`M(4gX!}o7IF@bEjN|B}uhgsr>9A7fq@@iu(=+l^E}n6CesUSA2q-Wm(dSgFT#1 zk?%JIQG(-XlZa*KV~v`{?U@7CrvAl2Jh6h41ZBiLf~C5yfp*Eb<;M8cQlNwo_Q63gUFNuLUI9W>>F}RIL4W-FCV%%V zKy$+o>iOalM5RtK4XWvKY=5QysmGRwTqEqjV9ZS4Dzjk9KfY@r$Q{UfHM^LEt>V9m zvepWO!i|~KHg#!7g`6|I(#^kl&S{f4PrPX-`O_?1j;IPTm$>9i z88x6t3lSvPx6BVJ>r0Upiz?#&MK5N@1W!d?=vJ)$aVD)SO&(F^BI@%n{6WNKIK&wI z+k}74wqRh!v*uxg3D~ejPJk@I9Is@@F_q_HkubT6+x6FK%iLae0q33U@ja(F0eMoi zEn*y;NiI*57g7c_8s1O4c)W)fBn0xBigmzpl1?(Sr)~Ah_=>UMy67WrI)BeqOY}SA&7RO%HfQJhd-}zV z-e?n4O4RGQYeswxQS&qb^c&;NF6TDo+46-qc9y2a>}UiFt*zRueGQ-5aj(&noD;P( z%R+|(-{}!=J{1UveT5sOUV({U>f!xYRfwh+A1s2sIoqw<6<&F_4V&{7`VfUj@TFc_ zP}ZpNaWQbA3JLf2r##Xqt$xrjY9J`l2ZLArad1U+ZE?@w=86qC=) zEr5D$LQ_{$H4Sx|w%vw31XOgw)(5|KQ+$oIEQ#sNAzF7;V(oaQ*!V+hTI#U1w6(TY zP>s%?U+}&+By{2wg6mvty?z)rRlQL@{4@I%rH6L5QMc>E$lO+1%QdGc3aC1}-R*N2 zw17BJ%sJsVlW#`M98fcD@;UPoRaaP7kw=s*>|<*GrO4eCd2JvwgmTb zCGj^P(xPzamYV}Tq|1N%YNsJCwrl&he?i&!tQ~H(l*jrk@h0K=IX>hW$CwN7j%j1( zRRJxq9XH4_LN@~Fw0szuEqg({UTb#H6%0-ffMLOC+E!K|Xe@#mm&vg#($CkY?ngfC zmWcgIP;`b0AKcevlLk@FrsNqknGg0TjKG3{nbuE z{h|E-pu+OtQX$IMkNZelVdO*2+WJ5do-Ai@sIiW4JAsJe<*b67lHjJ!V_ipx1^u8IAQ4IC) zWv+F%Vt#JsOF9m8u zyX?@46r8m1m#yq~%?xh}l!))o0*V(?8(Cki9MLb( zgQ&@bFW;O5yLDsL@Vy4DZ;}LMGnF>@4bn0PW5w}sB zhGB0}R3o~VX!t^)-FQ_Tmh+I!=S-f;=KScGBy)dzw30rYLTA!=0WT6>*kJMon(4=7jI01vOO(k?eVg%TJN&5AoFUs@?!mH*OxoD{olDK z^nY{%YHzSz{4+7}{|`WNESYIJ;te=1a1krNP(^NEs~1t_^d0ec7=rX#l78rgTvy#9 z#W!JBN6xMef%S&`3F%(jo2yNeN!Ef%mOiEyL8Jz+$#=$$+{PB1Nmc<)7G4e9z#rp! z`y|WLjAtkV_s5fQpwR=&h^SS#JegML!dS)QW)aZ(kFrhb;6#tPN347`DVCHJx}H>+ zM|K;fx{;2LE*p(F=>u-b|gRymWMI^KBJBL=e$#k|T1t${=>t=YUY5xVLdDxX14Bb2fFD&QEF3gP4=&FFv7HtxZ~Q zO%^`I^;-H`(rY>BH3V;56oi)ev ztT`$zFh-THS#KkCfi}CmfLBT{CahscY1p;6#kIKpshY)V+Bnn&-CwbNc4R4hE!$%w zJDo;bajBuYkfLkWqMM)-mWu(#$`rUBrH-jzJlR=q33xsXSsnIm!fnmr8aXz9a-`#J z4BL;vfGJHFhf<8=RdBHFby($nhBuz?EUz|hV<`f}FB#AgJzk6nMzxIHrZ-sAHHJdB zH=8mDy`g~4_=1f^9Q0dQ=#{w# z7XR{pJI&_PYQ0UdZaQkc_HB(;vmFTBus*E?3b6eL9~sF2kp#1EGsf@D+BBPsQ(7n! z4p}-Bx^z3lE%0{zJ7X(e;EeC6fc%iBx-#jNUW^#J?1Dtabmon1I(Y(!f@u95-3Hr& zNGuk+HqKLX#+p9PGx5(M!AROdrQ||6A){;}PHCJv=n?N*H6cK1_pR>Yd3L&T^1Zq4 zvufj-4LpepMB*a-E28{$k~R1JEY;vsAI712Mly80Wui`KYpGzWe4&Ckt`)kXH38rv zKzZItFS;v7ew(WcuAejB4cGS?@h3EB@K~NQw(_6M#SQz83WU;1L(dAW)gBse^9TV~ zQBWR~&*>Kh1ExRv;gMF5adI^|eKCfodg-MEY4fE`IC?9N-)Zh*lEq1k zUxV0qIWBm?2ydX*Y-nuBZEOiK3?lf3I8@EqOGf+F(1uBXpAI!08NMD5+Q`$L}Z&6&mObMPoI2@gT1p~4JGp5Nf$$Z1^Y7Fnnx-+8{Jdah+W zJJmfZ)uUPgK=Ia9tF7fE+9TWv*w#wU#5+vTWY1;|v9g-@fPC~albh1cvb@yp#WXJf z;rWkE!vKqrpuJU>GU&fWU{VgIS5GrZN!xm(({GzLDg?Zc*u)h*|0v0Ip`Vo_6tp@e zbVl2nj}ZnX3)^Bf%VMN{Tu6@wYmCX=F+%B0p+VD9z1E?@^wdzY$M|@(q^cbbvObyA z6b&G5jS4+1D3)4@%aomrjM?tqiEK?!w|1wNUwlsegK1x14qb$Nz8+Ap6|Js z2SaW|09Xl**nrGyGp)YdA_)>}_xtsdr;(hoTD1mv@2T~g@Eilgp!Eb0KoK0l1!5ic!!#=z9`!el0c)+&e+_&ox;QrTsv7(|?-GVdf2A3H)DS#7lR z9WI7!FK&)3#wK>ok|}^?b8B2M0eIy2#|S}h5(}GGnAqG3r7o!IwkRwLZC66GZDStw zUn%Z%e|Zva;BJ&Fe!e&}@>aQ1dHz*u^zg+`!^OjIUPBU6^C8@BoF9u~{(Ut6Hqu8c zcBVSJ67EzTN=LiY^jd)`r*hk;(ozUy5XE1pi4Y{v#837s!l>wecrj4r4{n8lKppqv z1y#1-28XwsfB#6}LV1i`pFVvGbZ+pUGps9~IbAjVa(jt6*U3a?qxI&2V)x;S zdg(T(EEoUi6-i0SOS)+$vlR94W3MV8#I5Zu`tJli-De{JM`*0E?lcHzo)Zzx{@c2D zYjhTm6uD8|rL2soL5gepYNxma?<`{`$Pv_wx}38!itl zEcinKYzu=xpylOd+x6wiH!zrxOD{Usqk8>PHGR3ou{-ynjN{L9>?$ii*M4z?p3#BI zysBeYx(bzZ<2xkq?DaAGH9epC0q*tn^@k50oIo0(qTY!aHv&_!n{0l^c)7Fk?lBNz zkA{tnjf`{mW#A_~klF56VeE>R*_D@Szqhx?g&#lL2>kL}Y$)AwexO)bOsqv6_zkXX z&m$i&L7tmm;yw)Zn|i*V{RIDZPkXx3M2D7;IAtR(EzNPbf>OF=`A*bu{M%8k|iw4t^w{f0vsK|9*aa*%vx`40!GR*L>hfowMvq(9G}U7#UTul*^`jF zP~3P!^aLPNHcMTA>6`*jwZ4+FYpl=Ih^Eej6n^=_DjnY$&p8imR6%EcM_4 z?2W`!LmfBaIT;%s+tneP8&ZBtV~>P9dh$$3>jao_R9R^g)G zb8^0X`H~@3GzLrDCVq&(Vt`?OTy zDd~!ksHbhvU_wP9PF1Nxbp)WDBQORt2*{84*)i1X=Pf7|3n8LPH5W&)`j2K4ZTii= zJh{6vr~`mDE?ad+=K#Q)mwDQUdn#f160 z#Kd|qP9@1P8}P~JYGVwRI`Bxy7hs8k$6sF@Ng<~xfX7>ByLpW0W#;5c4)s! z*#I1@Q5Sw(8(R$~w7;TC12J$UiB#oJW_agL>f7nf)lnhf3DNU|r7ihs_9g}fr|Kd& z;tvV>)z%#a26$fdk=g%!poRxvMrxhwFP!Mo191zE26+74QR%yQo0ux^At<2u>@sB| z2Nt4egtjoXS+)c?(c)Q$hEplVYof6sl2f>uj-0noZWWGKKFI;1=O1DoId{_7K?1@U zW5wVT(oh|69n_{f_@Akb`o zeo%X8Vv_uxc2*_e8Q^8L@X5dHfFvppC_X_B35Jb2b$5By8INdCAf$0kG{$Pn$a{^~ zPmf>&pDm79M&7<%|Kh;W3m{NUH_-zyI>2MPK^buZN|(&G`|RR1Fjm+L!ddh!X~M#A zrQKX#L4j-kmq$1mm!7K|)cLbbLZLwnVx8?lot1Uzi2)+M33y@_fIvoEBaJa}lGSS$ z7&MYB51eF5;4zAyILt|L_nKzWk7wn4<${djwk)Tu4m0Pvae zZ3Y8GMn;NWv`S(;d|(3r&c*A;#iY58xx_2_ULc07N&@p*@YaNx*kVFr5et5Kn-GR(Atxf0F|6HDusPvFERE(>*A2crS;L zs&WV^+B(tMttMnMGjw~aBU>jm*9seu89YfKG@bRR;$)x=u+dzH17H@bR;a8PpN!+V zI+?bmmpoRHM0BNl{8LQjLdA4PmKMMN?>7&DEEN0&magQh84?o0;NPjXR30GS){+kN zd{mEU1SIJsQ%BuAU}VTVYslR@a%w$bbwNO~hVDH#P^|s>T^~TXC*K*G7ddi@o0kkt zkO8I_>)7bE3Pf2D%%Pd~#2%DgbK(^#@892V{Q8&cB#*f81#fR}#u`X~-fBXXj#Ov` zS{6Rl83N!{04C`2X4`}Vd>B2tMJ;_r0H~5*C}>G-D1-DVCj&z_LHYi18NzChQak6rZCaf)n%|-pmHdq7?nqdXEZRaI5DrGYqzu3W5E;*)o;vPNqF+?Q!t>EqR< zBi{fJ-Z+Ekfh^Dv#w5{U~IO3#n%F-F3TkFZd@IipCCI4jX zndgq3Vt#IdD5J)cXjdfWVO851@CNug-Y#B6E@ZWGRKwlfJHG0EXYgJ`nEyT@=B7v1%Ck-*I2q?8g8|ag=0LrXM zfvpA*oP095ts~425x6-yY5fVX8=L4qj0_D_9qDiFYJk`DBDcvm^*g`>fN%%oB%S01 zJgEP4H(@+0UN!2)ix+*^z|!ak?Lgj?(v0PM=a>yB0NU>%JkmH`E$rY$Xo|Fh?#u5g zVq#+9>;ilMTMc*+lqVDQ$9rqGAekxOzI{tp2`o9*^>-b>0g1rpBFMRA`je*wy&1Ea z^JPWCyZWt-Bhz#`{R}81$O7CZ>ot|)&?wc!&n1%n?9mXAK&1fDX;S3)5b$UFfxBQb z0T-W{nfVA)v)n9}1q2;!jt*GDJCnkpvhu)nv>H1^P<`5p-uL?F?pc6j$G-euA)rM@ ziR(5{bhoNkK*{I(wd2<`yK51zt}&t zu$Z%b_YqUQAqp@PAWi}Diw45N2e$C>-aFBbQjgKw(s!w8ap;<@4F+eWq}Kr56t9h$ z91(^`Bx65;}0;z~pr@yMW@~TU9hOO-)TkN%iWf zARym?ycT_|8a(<%Wgk>y}Q zBf!)NB{Y6nXM?n7op-$m>AsD|*1?nCKKB8_fM3ig570)y8gBzKQ@;cT6f=LDZ3Bt- zwwISzc?%Y>v{OO+e)b#Z7?ZyQT_;*^i`HFl&j%{8&sIuaT%XkEKm-slEQfY} zyqR*lv-EqmS@db~3u0nOKxqJZkjeDCPm>c8xB!{=*$VV*`e(9|FFJzx?mgN&)|sX4 z-`|DfW6a&lFRE^?bOG)b$#lSFIF&Z7lL<|XE?Bahn^|IFVwofDJCM#&9fL=!gD3Lq z*tOTIfu9NiW^nG#?E}jB@*h}@=+jQcFtcLk2B0mT$ne7 z4IT%Yjjx6)eFN7R@t<$aGW!y0wj0jHa5-tBsfpoOhURBSwYgdSY3k2#6w>s1R%@5Gf*P zLcc4w73ycV5aWD z-Mde;g<688YQ2In`${3$@MA#Me|0C|2s)4?8*+`bwYBT+9e7|T+>WL!)v_akVD@6g z9D+d7?wR#FsuDNK-Lgw_@TZN3@>?h24jTbZn}E{UppK5uqV?0jb|Lq6e3w1v)ID+nc!+jmgkMV0} zg14*W4ry}0JeL{`zniDHN0#VxVUSXYYvP9RrJ|$3`uh4rnde`LTzULpoeO*L&6%XO zJdk~q+D?V-PJ`mU?pG@XQ8v6X2wyI)nMDc`+O!3?akinM!Rqeb(5#&#%kLCPn`ccs zb&#pEbOLCdQFRVAckI(V1ywM3K95#x z{;sSh6{y=l;!2wgI^wgDI5=QA1gO}2@Byi6_(onj;2=C(T3YH_m+2OL^yN-Lebd+q z$u5K`$psS;@s+Vg=Uie=tgrcE6a{6GIR-Qc!r$QXAbw1?Z{N-p*+Z1LT^(IBan9v? z-uqtM+;*e-E%X4W?4F1TN_Y&*ePLZC@wJ!O)$Jx0w;^NC8b&c2E?&oIfZp!p6IzLCtB4mmtQiYi-_Sg7|qC?a-pQrqU zd?nY$hbJgYTYkK|p(dLZcV;nNiTIj?;H{3_hb0;rX*UkWRiG)0ZB=_f%4h7=AXuyz zCWbK0RN2y5Bqf?`PzMLdC$}gzDzx&Ldu?qk=VcGX%4xSt_E@17Tz;^6jHd&SH#G-B zvhdt&MW5k54%Ck%+MBGfH~^x^>$OBf9F{Vt_;UdG!XD6FP^S^wIWGAD)8ORI{k7)@ z@NWq3L)^%-OYeVkbL)*Q&fZ4vug7sAA)%;|C1&PMz-c$s=Pp=c5Vp;J+v%p;L=NIi zgCjud=;=F!{Q|d#3&J6ZI8t_IIEaK@DtWXHp;F&d#w`K8wIai%JVM{Y`T1=d@8IB& zsHjDyJ$GCyxK=RAxRJuH`tXhzZ7nTQ?;PhCA7L$M5ndQ0>0XQ6+~Djiu#UZW@vFx=<#&J*tU9-(bwmeoA5=-6 z?{6eIVDpGpMZw2f;S42?yD*79^ymwfV#&Ju=}K6^g_mAJz7iamVU@D6E_b%vYu<&Z zs9iUVddMb*y2Tlfj9K@6++r1Ke|X!?tsKH6zu$eF}vP0g?d&(>zR?6_aP3Q{1m{zn+nb#MCzc;ZzR!!S>tz%Co) z4g8AQ8AMl7HWm++I|})=Y49pYJCca6xVx|Va`bT^cbY@NBw1H_u9GaJ(2= zUXJo~c_dPRiSC>*(f~v~lCnET>y|JRk1?}sA0AGEZ4?*`?(dSle%Xhn8`uHl1{oZE zKZE+1#H*#=N+DJ@lzhv(xZpo8rynl)_r346LUIiZvi;%ttS1#(cqf+?{)Hu&(dOASuN&G*-6y^LS~C z%=rPv>8dLmk`6l<43>*W%FRcI6c<~4$wqWsDnuma~KYvD_g?a7wTO>a5 zwcSsiJgJGxfsC2l!y6eYXDfVX5+wCrB36i=mXvIG5Cr8O>*{lA!qh!h&$KQ4*UGNa zQSlvo12JKyqwDfaS(XOb{3d-PZ5fv*t^|U_>HEJBhc`*sa?s4>Uk?xT#0p~%40OK| zgv5@_C(?E|4{g5@IXq=P)_itHWrBC#i;f%V?Q*-aYuI5N2Ef58DM*8d*H=mAa&`I4wo1izT({P-H zt;r8dz9R^NeY8nO8%F6j8X9J2N68^@3vGvZfX2AmJemx{|qD?rrr+M_+Dmz73ps~-@Gfq)4GMu?_np&&Nu zc=T16mfZQ%o(OZS;=-)*A8;BO;nYv}vq~fGTR3d7S9yRVPF|(K)9ODaSyyJQT5p2= zm)G!W9&W@e(-*SL?Z?2`urXjmCYKISc10mn|8Po5ijVb_aQlLz(D3TkJQ#|9yJ(e- zFmRg@jcqg>z}&&1*e;1yhm6~jYKQJ@y=TXkEz`>ldo8$;Mn1zPc>X1?d@4byOgeX@ zSWQ!9kFc!yJ|Hc0Qmt2y!9b>qqix7e0rx1D+#I&Bd1fB&!FPxXW$Us}{T7{Ly``mP zbB@}^EBq=SWt4M*tq40jaosTjPl_Np&hS`mmiB~@4e;|HKa(r7h+47afRNOdmQQjk zIE5m*khxA3j+S|U;+}{+ivC=EuvtewQh&ex)3}px9Z$L_pCjjpXWcgJvA-@;qZx9Hkk6soLswSAfmrH3T|lwfQF)dskz(L z+HwCLwvATEx;h%q8BZAm*L>+Qyk~p@aFVdFRU%@ z@?nFJtb?-TMgr$DYsD}Xt<(m}ke|!}d;4C;1A|UUZM&8^MZ^HuBVB#K%16E?pW(NK z$#yCUBa|jlA;82{0i;ZES)(1~-XKOvGE6fd(dyepZs^$DezTH&qB^*QGZ5f%oGFOB z-7`R{>Ft$$)62kqt$n$nj`_Ff-Q)2@MUsXXj*BtJRR^ zXc{zU8?Rr!nwzM#by8g+~lvnA;CJUV#fe%uYQH>Qym}m9oOg z>iA-U6XZ~H0sWyLJqU$a9SPxTF`^f#zVg&*>b;0$NC=)?zUqjc%>rDRf}U`5z=lW^R*e~5->;+~FJC}Xo$}`(WqqwMO*`AS)jtsc zz(**8=Q_P?4jfotiEP}uTDmhsp9jh>l8m#p1REQ5!|x_uy#LkxyA}g|2ylxoe6_Y2 z$xUsGBZ_ZM1?;QK{8aFGx(f9xN(M(FUL+S8bMd`xjq%^Wn|oII;{M zPu0ywIXRYuEfK8}Ayq7*;VDd4rA{w9iU)i}*S$Mj2an`8RuH6WY(&%*%$aj>o@pV< zH?wKdCh|?8pYn0wmiUlOfnbhZf;2EI`OhXy`M$$<U$=$qUC~ zTkspf$V&^TZNar^>}-jiTXO^K(M`X?kg_zI86aC`fmOg>d{}4 zyGYp%wo_Q8hk~MF(a2CQffTDJcR(#h)zI_M)?4q{_o)=G2i&~O?_^JDpy^uf|k@b;l&;uhR z4p~uxExXBQpg=riEKb_GPt@P3Nb^9-#~WCdXh|lb;_~uf-KLY;-Ef$#(9%_W?tzjN zVa`t;t;C|^Ziu}W9!NkB22eP{Lr8N>(I>bN5v&^fGZb11y;QM3IJ}WIO~LaYHy8AE zR1LhFIqs&Ju3vxJu9+5RoX}xqG1duga<;B4?_qoMgA-NpsrECU^lFm>cWo?dCz=E1hCNVVf><>rZR zOXsmjHJcPgp~fk8lz;E?8si}xgt8hHrFfijq2jTuc&CzJ<1sxIpS}u{b+tIjI_i1Y zufY?h&XQar%!`YWkxB&x1=Im*5UkQ8{@7O1g6%XyIoj}f9H6bz;L*|z0>(Unkq#eQ zpL&n(Y()47ne9K`oIi~sg$6#_dw&>uYYvScQJxgu+l6D@e)zGNG;*ZRTlO}qSJ^Db zObA?@!Q3YWrCbQ{Bz(F{q3S6vpLRFLd415#X``ZnrWOyiCA*elvuSB*kr_*IcxKs0 zkf?Krfx5zDmn*4dWcj~1B7oE>gc;Kru8YU}TS9x;h2~=xr9M2&3*9#FYsCL5S+9=m zLMEDMqe3wuH+`>3J~+jq-8;YinrjxKRsjxSq77g3`{ohh%dgHTPbWy;_;6}3=roU`2`myGV12z^icK?n^JW^SQSSY{T{ddmg7 zv960+M=epV*@#P}!#-gcE@~R^YQ1P$2o@23Ovx6L(t_St@k0yW!aY{{4VX?@zURq> zNjtC5j(#1hA(7)hbOV>ZYD5P64loj#m-Ycwe-8JNP!_eF659wPA1`hsp5nHER5|eu zibV3Yp;W%f5Dla6Sw!(EN*Qd^WGmr%7VapYpcJO+SQy0VOy$`|()7f&Pl@a`$vhWC zk^|fZR;8Kxwutg?ae7b8KyWd5f-u@K(DLCMl?&H?go8;m`LAr1AINWffVR~BxE2D!8})?^`+B|8M=s9Eiv zn_@v(%{&U!AX>-1gnz$Tw6C|sZcHoDB65pHjO5d2Ml$}_SeK!>x%t-+i!>kxQ{wy= za%%Biwy@=AP^L;VO{5YIhxTAJq`6GDDTpJdp4e)L_Hq{6lRYwkIT|W{&iM)+TTF90 z{=D|;Te}oZ=N#S2jRehGMc&qR>u+nt?d&&?{40>lgSf>>N0%qaaz!T<^i>q z7XXj62qDO9G?Il?qphv2`3ZRM4*7`69;M4!KmTsx<<64`{8UbyIPq!54~trG<=cJD zD6R7}D+sXvlI8|s($6>O7B5Yqz9PUJ%mRZ#JR{YltH-Ppvdb|MZAT{E6<6o(hMWql zhKaT)eg9As&AiZp!aFR2sua2{r6C>~*8+LwLYlg0=zw-02X*N*hPSnwsSoHBrxGD4UAc5 z9h_r4_=_wx4V-GK;-3y{$UBvY?*T|CGJYr?6D^0N&0VM}gq?UFe3mB8fM?KqPJ;(% zvLt!-^YAL8sEpSA=zN#;CF zm02dr$7<{=(9yL-#a>E@;Yhnp)~s1Wm$*IEcFy?3C-T^f;o$?pE?)Tms7Nh=?n@z^G-{s!-l&+8eY>^sZ(pP?$r5=p*)!YQBR+PT_UPIc} zrLX>hKMQLZG1FSeRrFTvL{9aya?MVO2|M42pLH{$> zK|La_%Jbqmo-P_M|*`1dBMd!yKH~xqD0#)V< zRGBYOm4t%QO~HJ1;=kmp6J?k$>0~O6sWfv*7%uHRKP`~hbni$(vI<;?NZNI`f8CR|B(K>AJaOS*2%O^rgi=`+F*po-;JKL z9vqXrwPM+mCa)Qyz`~O&z>`PQ)8j>)~jF~YevS(jH36WWu#w=#WzP5^#ExQt-M3S}aLTIs- zy+tLFwGv6?H`M8Lp68tNob!8qpXaZ`>oenX-|y?X?(2PD%l-MhMA}-L2=Z^{2LJ$q zW~N4V>~Fo*i;tK6d6ne-6aWx{1UejG*K|7emuKn z`pceMnIrEiBWKnHxW>K2J{C_50?l>^2HDtlRunmu)KIe?yw{N%?tI-j9+9lG$c&$@ zKjvO5N}BJmqJ3$({C-BYVKNg@$9TTzk#p(Qwf++}vrV;<0`C&et9`g%>wKwxxoPC< zOvkZ9~*QPTrZWgC2e@m0#tD8mh`!x124dzjf)8N8)gNDc0HAmE`B>xaUfz#@{L9z zm+2B_nBTAymi}=4vko8a0?OcNm7<^z$&NCY; zeT?kiaA%zNG^=rWH2UV${9v?b^>WpC{6nItkcm-d&B&H)VUH6cjWNdsP9C0zHcQ)| zOR-YDp_pk9wR5}K-K2dr+>!A{3Knr`AwpVX=eEB-$N(4lx51Mfaz^dUuXQ-cI;0l# z)c5opd=D-guuDjCA(eAw$_7s5TXfdWqaV+4xosk~wOxuvUrV*UC!o;*CyY4}UEL{5 zlKT&m+V2(=ewrVe?u}Ut8$kpGJ)qc-bOQpmv>i} z2sY-we&pfk&27w5gRn==n- zy8e_$Rxd)AtuEP80t2$IE8AF#j~&ciDzO7&4dqY7jNB;?^f1N=Jc&@*Np>6eUUbhJ zW%To{ckJssXjPbI5n03v+_Bhm>!kLvqU$@}W<5-Nki7SZ5>SQ z?L0f}%IujE+ldKGJp+_2b?;DYT7){&NVlJNUoG?=kor0zrSv8JnQuuXU%3?RCBOZB zh3h)b*7X#Vj}u65dJNLz{mXY%>nKtV6zKlzu9N<8Rzs+6fH?j|P-;n=I>rZaoQt0s zeZs~Y$15MQujJuZd9B6Wa?kyIS9jE!3npCw8A#mca@yx;+jChA&fN zIvnr4s=ajdESY{@{i5HdPS6qRWDS0yx1T?B|LAtLv-aX{xDCU1rUun4bH{cQKYcfhSPySKSS`;g>zl8niTQ-|jYZG^5I2;v&78!U~}2tRwEtmR3&hc4_)>rV<`;`)=`mY0Fq8>_ ztPP-|*ZB)3mffJsuOiwX3@f~SYV;W+z67loE9{66EzxU?f0@>zFj3v~=~an{$m;*7g?{G!i_@0k<4PAbH@9L>a_E&#%CGn>T=Bg5yRaR-i1 z0V8=1uVA;sH#}0_;M)fkdN&(;=Z>r|aC*uD9Mz$@kSSc#R_vQNcT^y0{wDIe?$?6J zeTw4`&N&?yTF!{)oAzihU0gu8OIR%d+g_G;depAEkCtcD+eG{vDtmLe~#aI)c zcWtuDDJPqs8<^(bzrs z5!k8JukHO87sn57G9jCbr@3!^9W?Lj zwrt4B8*Qae^YSjiIb-RmXPITHU*D8QTVP4#Va2&vr@K`P>Ut%VG;~~sWp7FZ`1Dkl znp160SxTY8Db9kVc0->L=FHP-%ec9;4L%z1%IIlaq|nEn_ayfzmnVaxN%Adv8y;cKyc#`VwR9%I$0KWBDvkhwrFbN^l(ZgzALL|k7n{d zgo-BL9Y?&lJ$Zi!3}jj@6%rgUXgyH94NjG2>3~jsa(}|V3#&o=QY>|QEJQPE+mkah zuco*`4kC#CI(~vJQBhG5agq5QVy2&U8s{fLHS+7x_PSZf$Pt5Vx6xArdVO0Xk`i~V z$882ZjZ)ke8}G{y+Ni3;57#U=1zMy9ZP3;m6YFhFIBuE9!`J=bLELh}vC9!mzVnhh z0tk9tmey5#PdQE3$GBWMJnu0?R7$~=X!-CZfwoRqH)Zondsq8bo1M>5%hpvJ8hGj% zw?!2;eI!b^NgDt3{JpkZ30L=)L>YZIo7n65Zi<%qSHk&rIUi86#Z*bRpX>FcCMi0f z-wQh@S~`o0d2=FJT}e{8bUt^axF$GfZ*T0gEeo1gD~6%h1Ma5el^xw#(fIPPTdkeg zLW3ZXaJE3ctSu~mUv%sx5Nv2mQk%#=JISwEK!QZ$bbEAbe3wAuxF6l)=(XiY`O$hzZ(|xv)#)*>*njr-tvUc;6$=1ZjVe zkg6}zC#Rm;pP#!eEc)0X`cy9W9z6q{G;xtzF-PUkkt#nLEOOc%4IedZ+dFf_*?Ci| z`!N9pc?HQcrl*Ud8zs8p9$IFt7pTz{mcJ#(?LH{XOw$ZHnVhccv|9(9TBjPok9q&# z+$6@l1vI2uJ2ci}C+Djn+!PWNnsYqjae2C|D8k@e8n)>=8I`3BdrdHW1f4tvzdnFd6oM_bC~9FzR+rR4+qOH)R4t*RfDO&}?= zY3XM3lM=A3ykyrD!QQyg{GqUCxd9mhwo#d!v|@O2D5_w)X7WU>;)x0yOU3yMKxW(N z11{D+A9wHK6tn(7b4gLYiyYiZR-!vGk084_sIUFtCH_Xpmf)`3O$P@=xJNre-d%p} z+!b=_O21R9R(VfvM!Xq9Nvk?D-Xuo<1E0lJzUX$VM$Ytmn@%`_mWayS{XAlZvcgjdQL&Y; zdz)Bhh@K*4PQ4!H?FVOHp4fUr{?`1LvjLAw{37P{ zNT$_tresd5>$bi^6X7s4I&29UHmINdB+*K%YTSpG?D>V<8-C7_x%s1C7OKC*o^ZV6 z!d<>EI~;TM44s!HMchl@)bA~xp&!qqk}0StUo%9TT?I=q_+= z_us52q}{Rt07%%A*pI9StSr$u8bu9*r(p?d{uDZUJph2Vu0I`vBNJFaEWv|B)d9^` z*Mop0ybj0_VFj_G8xlN8rhyECeW0}iE|84V!h>{o^K1K~*$OBG76#}~@uD)({yLyF zy=eB|tHbIb;2MNQ)&U)`vIQE_7z7|(4Xy?O8~c-dVW8doKy3z|h_*A@^IZk|P6y=4 zV$sp+>VAHHYJM7OG=_&dR7*=s9RgE_!N6<;m>EE2Vf?{Vru?dkZ#s+!OdNwvB8f%? zuIj{KX+G>;4r0#(zbjv**w~*v_}#$j{hE3v3$Jd*zJas-VFT1*5C{qkfq`LK>Oae~ zXRWM$NK={LS!DC6?vJ6XL)9Sa6v}TTm@H%8U+w*_1k-{2=0M$!z@+&wa0Fvt0+l8I zvr)R25A$c6K1{;u(Av7a@I-a?qSh?`EMsD3W&1;Bl|~N|g}x@SivAgi$Nj+3eHdPA z7(7m$;6)V>?fM9BnHN|Tqwq6((k;b6tfGkN=AOBw`97q&`Jqxo+5)=uA zYiXdgG~qCW7EBZMD`-D~!DMH{s!k|G4F+4A!Q;?-*cvhHs3cJ^9t3qd)njeq$JU_@ zX{s*AwYwf#_L#V#6n3=8u^9g@vB7^>v}L80LqXarIP z!q)o}o`xq81O5|wwL}25zb8N1l7?Gz;@iUUYm?t9hylh!{To*O&%pnt$=;LZNBwJh zeue(fqR(LY(HLYKh7I;G0mu4tJ--M3p~;S2yO}ITfZ2a=>R)i$-?GS*ElXnr{AAyr z;Qj6H8$Dj6wNeTMt`#*j2KOx#aBBg8U)w`A&)+6VzmrpcDJ6esVA6;zKMaGQ@4=2b z9gzNNzXIC+Qhk7i_DGB>Acju&B7F-lbzds}dw=+mS)jjSn2GWISED#j4Aq0c z?#ACs@t?8`{R@Tfzs53D6Afio$+evSA2@4l0Ea;KKu{=%fw3`yUF8iSa4k(E)E~M-;%R=&KMJTns7L<;SFtD}OiPQ12Wy}-F<>|XO9X472v9Iy6OYnD z5Hz*0Sj|6m*MxlpSgW=`rL|`AyDomOum1yohzKMSiy&aZT2Le&42PhIU~lH$n6Lihod3NYu*=7P^XGe;`EQy40{=1cxA^^su7BwITMYay@ITS@4_$wYfxiX* zC%XR6=;HtNO(B8GejxE`#9jX%Ue~j}**M^JKGqdPQSEChE@J67yhd?ZZ7hk#e_+{6F8h zHCoc3fg>U{Ol0IYT5K(3K~&N=zg6%)*vq;r#gHw?s6>%_H9`kS=(84vB7JkgoJ;!K}ynA+LW@`GG#% z3nt=U%6_NIi?uK@0<5h5UaY;Gz#dsoH+5nH02{59{%;9 z_Zxfp0016cGb4Qm|Doyh&eJl+60N$|^ZZ;lWt_Q?_(1|8eF`H4EnEp7G2XlFh=}O+ zbv!y(ICOX#4IM;8ZL*z29{I<#hF6V2q-?VN4?weJ9}&E5Q&ZK1cj;CIUA+T#)rc8HXZ8D z*9bb~X7Mzku6|uj+so<2jqAe=P#ZGzVHV=@2NyzjHCK46G!S;r%DAWk=BE7LzVIFy zeC_v`KjV-BK=gL|-OQ^x3b>xL>YKyMZ%~OJdKEf`fIWje!*^1>j*TU?aK@hZzu=~F z8FR04n_}@s{&3h-#Q}wIPQ6!VW>(IlkHLrZ=D4%Q%e4jpVPVg4QirQ-!vOE1FG?5I z!cN_E6Vd|}>`vd9_Tw)u&)hadQ0b`BVuJQoy@o*07IrWI3pMN^}EEf(aK22|~ zI)+q~zB`t;{?XHO+&5u8_;~1cQCq(L^9d5Ix2Dr#Iy1F#!#SKGH(PUDg=s}SoStmh&=BTUeen29Rp#4Ww-dAnzRCw- zD0*_ws~hPaCebu^Ei6ZOu!~wI^Ac0ce>3Xt)FcsdAk%z zw|>5b9(iG1uq+=jUA(oT2^nMiV)+`8+o(v&R2t=QRF`zouk#kwYx&G|mo1>;YzyYS z{H11K?Zcp5Xufc{inK*8mxf3&;KC7dgS|sRgr$(wb%hIJnN23mKYjYmtb~AVTnCDaJ>AzuqY5kd4%bEQi7wUBlaCc|U zT-@9_uxqq*F6uD=;@r!uOxj zQjs>0ThFN{3PXf-sPS4^Pu`f`-qNg`qu;$=((3i31($oEFlfMnKe*;3Ln+}9;&QDG z>~$kxR5EDE(ng;OyKlWKmNjiAj%22w4DCc?hn}GKX0CpChFxF{+8^4>By8hldki!guqPY76dQI=x zMIJf?b+Z_bcHd&Fz*~GG51Ek5@ZB%)iRQQ^qX%jaQjq0LwK=#0Q`k|w3@=8PPm(xu zFJtd?kK0Oq43f5Y?5^6a3Q-ZvOL%Whe=b}_a$jpRrz1sdqpxX4K$Wfoj~h>D z0dV`miH6p?il>>@OYcuPZ@fEGHsi|mHMNyy?8B{Rz_f6CsX{;YK3p%)=!s^-1N)dV z!~H%7>xMMX22G6HX4JXL41%E1G7HmJ1I+&p#jdUZGh=I`A_MoM{|^x|X(a#v literal 0 HcmV?d00001 diff --git a/customizable/assets/imgs/logo.png b/customizable/assets/imgs/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..4dac7c4fe85c9e85a0d40f275d1803a9f0c519f2 GIT binary patch literal 18105 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 literal 0 HcmV?d00001 diff --git a/playstore_api_key.json.sample b/playstore_api_key.json.sample new file mode 100644 index 00000000..bcdf77c3 --- /dev/null +++ b/playstore_api_key.json.sample @@ -0,0 +1,3 @@ +{ + "hint": "Replace this with the content of your valid playstore api key json file and remove '.sample' from file name" +} \ No newline at end of file diff --git a/static/fastlane-android/Appfile b/static/fastlane-android/Appfile new file mode 100644 index 00000000..3efc324a --- /dev/null +++ b/static/fastlane-android/Appfile @@ -0,0 +1 @@ +json_key_file("../../../playstore_api_key.json") # Don't Change diff --git a/static/fastlane-android/Fastfile b/static/fastlane-android/Fastfile new file mode 100644 index 00000000..ede6a3cf --- /dev/null +++ b/static/fastlane-android/Fastfile @@ -0,0 +1,34 @@ +# This file contains the fastlane.tools configuration for Android +# You can find the documentation at https://docs.fastlane.tools +# +# For a list of all available actions, check out +# +# https://docs.fastlane.tools/actions +# +# For a list of all available plugins, check out +# +# https://docs.fastlane.tools/plugins/available-plugins +# + +# Uncomment the line if you want fastlane to automatically update itself +# update_fastlane + +default_platform(:android) + +platform :android do + desc "Runs all the tests" + lane :test do + gradle(task: "test") + end + + desc "Submit a new Beta Build to Crashlytics Beta" + lane :beta do + gradle(task: "clean assembleRelease") + end + + desc "Deploy a new version to the Google Play" + lane :release do + gradle(task: "clean assembleRelease") + #upload_to_play_store + end +end diff --git a/static/fastlane-ios/Appfile b/static/fastlane-ios/Appfile new file mode 100644 index 00000000..e69de29b diff --git a/static/fastlane-ios/Fastfile b/static/fastlane-ios/Fastfile new file mode 100644 index 00000000..f9d0cdf9 --- /dev/null +++ b/static/fastlane-ios/Fastfile @@ -0,0 +1,38 @@ +# This file contains the fastlane.tools configuration for iOS +# You can find the documentation at https://docs.fastlane.tools +# +# For a list of all available actions, check out +# +# https://docs.fastlane.tools/actions +# +# For a list of all available plugins, check out +# +# https://docs.fastlane.tools/plugins/available-plugins +# + +# Uncomment the line if you want fastlane to automatically update itself +# update_fastlane + +default_platform(:ios) + +platform :ios do + desc "Push a new release build to the App Store" + + lane :release do + set_info_plist_value( + path: "App/Info.plist", + key: "NSLocationAlwaysAndWhenInUseUsageDescription", + value: ENV['LOCATION_USAGE_DESCRIPTION'] + ) + update_build_settings( + use_automatic_signing: false, + path: "App.xcodeproj", + team_id: ENV['TEAM_ID'], + bundle_identifier: ENV['IOS_APP_IDENTIFIER'], + profile_name: ENV['PROVISIONING_PROFILE_NAME'], + entitlements_file_path: "App/App.entitlements" + ) + build_app(workspace: "App.xcworkspace", scheme: "App") + # upload_to_app_store(skip_metadata: true, skip_screenshots: true) + end +end \ No newline at end of file diff --git a/static/fastlane-ios/actions/update_build_settings.rb b/static/fastlane-ios/actions/update_build_settings.rb new file mode 100644 index 00000000..5b211cc4 --- /dev/null +++ b/static/fastlane-ios/actions/update_build_settings.rb @@ -0,0 +1,213 @@ +require 'xcodeproj' +module Fastlane + module Actions + class UpdateBuildSettingsAction < Action + def self.run(params) + FastlaneCore::PrintTable.print_values(config: params, title: "Summary for code signing settings") + path = params[:path] + path = File.join(File.expand_path(path), "project.pbxproj") + + project = Xcodeproj::Project.open(params[:path]) + UI.user_error!("Could not find path to project config '#{path}'. Pass the path to your project (not workspace)!") unless File.exist?(path) + UI.message("Updating the Automatic Codesigning flag to #{params[:use_automatic_signing] ? 'enabled' : 'disabled'} for the given project '#{path}'") + + unless project.root_object.attributes["TargetAttributes"] + UI.user_error!("Seems to be a very old project file format - please open your project file in a more recent version of Xcode") + return false + end + + changed_targets = [] + changed_build_configurations = [] + + project.targets.each do |target| + if params[:targets] + unless params[:targets].include?(target.name) + UI.important("Skipping #{target.name} not selected (#{params[:targets].join(',')})") + next + end + end + + target.build_configurations.each do |config| + if params[:build_configurations] + unless params[:build_configurations].include?(config.name) + UI.important("Skipping #{config.name} not selected (#{params[:build_configurations].join(',')})") + next + end + end + + style_value = params[:use_automatic_signing] ? 'Automatic' : 'Manual' + set_build_setting(config, "CODE_SIGN_STYLE", style_value) + + if params[:team_id] + set_build_setting(config, "DEVELOPMENT_TEAM", params[:team_id]) + UI.important("Set Team id to: #{params[:team_id]} for target: #{target.name} for build configuration: #{config.name}") + end + if params[:code_sign_identity] + set_build_setting(config, "CODE_SIGN_IDENTITY", params[:code_sign_identity]) + UI.important("Set Code Sign identity to: #{params[:code_sign_identity]} for target: #{target.name} for build configuration: #{config.name}") + end + if params[:profile_name] + set_build_setting(config, "PROVISIONING_PROFILE_SPECIFIER", params[:profile_name]) + UI.important("Set Provisioning Profile name to: #{params[:profile_name]} for target: #{target.name} for build configuration: #{config.name}") + end + if params[:entitlements_file_path] + set_build_setting(config, "CODE_SIGN_ENTITLEMENTS", params[:entitlements_file_path]) + UI.important("Set Entitlements file path to: #{params[:entitlements_file_path]} for target: #{target.name} for build configuration: #{config.name}") + end + # Since Xcode 8, this is no longer needed, you simply use PROVISIONING_PROFILE_SPECIFIER + if params[:profile_uuid] + set_build_setting(config, "PROVISIONING_PROFILE", params[:profile_uuid]) + UI.important("Set Provisioning Profile UUID to: #{params[:profile_uuid]} for target: #{target.name} for build configuration: #{config.name}") + end + if params[:bundle_identifier] + set_build_setting(config, "PRODUCT_BUNDLE_IDENTIFIER", params[:bundle_identifier]) + UI.important("Set Bundle identifier to: #{params[:bundle_identifier]} for target: #{target.name} for build configuration: #{config.name}") + end + + changed_build_configurations << config.name + end + + changed_targets << target.name + end + project.save + + if changed_targets.empty? + UI.important("None of the specified targets has been modified") + UI.important("available targets:") + project.targets.each do |target| + UI.important("\t* #{target.name}") + end + else + UI.success("Successfully updated project settings to use Code Sign Style = '#{params[:use_automatic_signing] ? 'Automatic' : 'Manual'}'") + UI.success("Modified Targets:") + changed_targets.each do |target| + UI.success("\t * #{target}") + end + + UI.success("Modified Build Configurations:") + changed_build_configurations.each do |name| + UI.success("\t * #{name}") + end + end + + params[:use_automatic_signing] + end + + def self.set_build_setting(configuration, name, value) + # Iterate over any keys that start with this name + # This will also set keys that have filtering like [sdk=iphoneos*] + keys = configuration.build_settings.keys.select { |key| key.to_s.match(/#{name}.*/) } + keys.each do |key| + configuration.build_settings[key] = value + end + + # Explicitly set the key with value if keys don't exist + configuration.build_settings[name] = value + end + + def self.description + "Configures Xcode's Codesigning options" + end + + def self.details + "Configures Xcode's Codesigning options of all targets in the project" + end + + def self.available_options + [ + FastlaneCore::ConfigItem.new(key: :path, + env_name: "FL_PROJECT_SIGNING_PROJECT_PATH", + description: "Path to your Xcode project", + code_gen_sensitive: true, + default_value: Dir['*.xcodeproj'].first, + default_value_dynamic: true, + verify_block: proc do |value| + UI.user_error!("Path is invalid") unless File.exist?(File.expand_path(value)) + end), + FastlaneCore::ConfigItem.new(key: :use_automatic_signing, + env_name: "FL_PROJECT_USE_AUTOMATIC_SIGNING", + description: "Defines if project should use automatic signing", + type: Boolean, + default_value: false), + FastlaneCore::ConfigItem.new(key: :team_id, + env_name: "FASTLANE_TEAM_ID", + optional: true, + description: "Team ID, is used when upgrading project"), + FastlaneCore::ConfigItem.new(key: :targets, + env_name: "FL_PROJECT_SIGNING_TARGETS", + optional: true, + type: Array, + description: "Specify targets you want to toggle the signing mech. (default to all targets)"), + FastlaneCore::ConfigItem.new(key: :build_configurations, + env_name: "FL_PROJECT_SIGNING_BUILD_CONFIGURATIONS", + optional: true, + type: Array, + description: "Specify build_configurations you want to toggle the signing mech. (default to all configurations)"), + FastlaneCore::ConfigItem.new(key: :code_sign_identity, + env_name: "FL_CODE_SIGN_IDENTITY", + description: "Code signing identity type (iPhone Developer, iPhone Distribution)", + optional: true), + FastlaneCore::ConfigItem.new(key: :entitlements_file_path, + env_name: "FL_CODE_SIGN_ENTITLEMENTS_FILE_PATH", + description: "Path to your entitlements file", + optional: true), + FastlaneCore::ConfigItem.new(key: :profile_name, + env_name: "FL_PROVISIONING_PROFILE_SPECIFIER", + description: "Provisioning profile name to use for code signing", + optional: true), + FastlaneCore::ConfigItem.new(key: :profile_uuid, + env_name: "FL_PROVISIONING_PROFILE", + description: "Provisioning profile UUID to use for code signing", + optional: true), + FastlaneCore::ConfigItem.new(key: :bundle_identifier, + env_name: "FL_APP_IDENTIFIER", + description: "Application Product Bundle Identifier", + optional: true) + ] + end + + def self.output + end + + def self.example_code + [ + ' # manual code signing + update_code_signing_settings( + use_automatic_signing: false, + path: "demo-project/demo/demo.xcodeproj" + )', + ' # automatic code signing + update_code_signing_settings( + use_automatic_signing: true, + path: "demo-project/demo/demo.xcodeproj" + )', + ' # more advanced manual code signing + update_code_signing_settings( + use_automatic_signing: true, + path: "demo-project/demo/demo.xcodeproj", + team_id: "QABC123DEV", + bundle_identifier: "com.demoapp.QABC123DEV", + profile_name: "Demo App Deployment Profile", + entitlements_file_path: "Demo App/generated/New.entitlements" + )' + ] + end + + def self.category + :code_signing + end + + def self.return_value + "The current status (boolean) of codesigning after modification" + end + + def self.authors + ["mathiasAichinger", "hjanuschka", "p4checo", "portellaa", "aeons", "att55", "abcdev"] + end + + def self.is_supported?(platform) + [:ios, :mac].include?(platform) + end + end + end +end diff --git a/static/scripts/android.sh b/static/scripts/android.sh new file mode 100755 index 00000000..287b28da --- /dev/null +++ b/static/scripts/android.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env sh + +DEFAULT_APP_LINK_URL="your.deep.link.host.tld" +APP_LINK_URL="${APP_LINK_URL:-$DEFAULT_APP_LINK_URL}" + +if [ -n "$(xmlstarlet sel -T -t -v "/manifest/application/activity[intent-filter/@android:autoVerify='true']" app/android/app/src/main/AndroidManifest.xml)" ]; then + echo "Deep link capability already defined in AndroidManifest.xml" +else + echo "Adding deep link capability to AndroidManifest.xml" + xmlstarlet edit -L -s /manifest/application/activity -t elem -n tmpElement -v "" \ + -i //tmpElement -t attr -n "android:autoVerify" -v "true" \ + -r //tmpElement -v intent-filter \ + app/android/app/src/main/AndroidManifest.xml + + xmlstarlet edit -L -s "/manifest/application/activity/intent-filter[@android:autoVerify='true']" -t elem -n tmpElement -v "" \ + -i //tmpElement -t attr -n "android:name" -v "android.intent.action.VIEW" \ + -r //tmpElement -v action \ + app/android/app/src/main/AndroidManifest.xml + + xmlstarlet edit -L -s "/manifest/application/activity/intent-filter[@android:autoVerify='true']" -t elem -n tmpElement -v "" \ + -i //tmpElement -t attr -n "android:name" -v "android.intent.action.DEFAULT" \ + -r //tmpElement -v category \ + app/android/app/src/main/AndroidManifest.xml + + xmlstarlet edit -L -s "/manifest/application/activity/intent-filter[@android:autoVerify='true']" -t elem -n tmpElement -v "" \ + -i //tmpElement -t attr -n "android:name" -v "android.intent.action.BROWSABLE" \ + -r //tmpElement -v category \ + app/android/app/src/main/AndroidManifest.xml + + xmlstarlet edit -L -s "/manifest/application/activity/intent-filter[@android:autoVerify='true']" -t elem -n tmpElement -v "" \ + -i //tmpElement -t attr -n "android:scheme" -v "https" \ + -i //tmpElement -t attr -n "android:host" -v "$APP_LINK_URL" \ + -r //tmpElement -v data \ + app/android/app/src/main/AndroidManifest.xml +fi \ No newline at end of file diff --git a/static/scripts/clone_app.sh b/static/scripts/clone_app.sh new file mode 100755 index 00000000..3f72fb88 --- /dev/null +++ b/static/scripts/clone_app.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env sh + +# get semantical versioning string linke 2.0.0 (if $1 is v2.0.0) or $1 +DEFAULT_VERSION="0.0.0" +APP_VERSION="${APP_VERSION:-$DEFAULT_VERSION}" + +if echo -n $1 | grep -Eq 'v[0-9]+\.[0-9]+\.[0-9]+'; then + APP_VERSION=$(echo -n "$1" | cut -c 2-); +else + APP_VERSION=$1; +fi + +if [ "$APP_VERSION" = "0.0.0" ]; then + echo "Unsupported app version was set!" + return 1 +fi + +git clone --depth 1 --branch $APP_VERSION https://gitlab.com/openstapps/app.git app diff --git a/static/scripts/ionic.sh b/static/scripts/ionic.sh new file mode 100755 index 00000000..b0058ece --- /dev/null +++ b/static/scripts/ionic.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env sh + +DEFAULT_APP_NAME="Open StApps" +DEFAULT_APP_DISPLAY_NAME="StApps" +DEFAULT_APP_ID="de.any_school.app" + +APP_NAME="${APP_NAME:-$DEFAULT_APP_NAME}" +APP_DISPLAY_NAME="${APP_DISPLAY_NAME:-$DEFAULT_APP_DISPLAY_NAME}" +APP_ID="${APP_ID:-$DEFAULT_APP_ID}" + +APP_VERSION=$(jq '.version' app/package.json) + +# ionic config +cat app/ionic.config.json | jq '.name = $newName' --arg newName "$APP_NAME" > tmp.$$.json && mv tmp.$$.json app/ionic.config.json + +# capacitor config +awk "/appName:.*,/ && !done { gsub(/appName:.*,/, \"appName: '$APP_NAME',\"); done=1}; 1" app/capacitor.config.ts > tmp.$$.json && mv tmp.$$.json app/capacitor.config.ts +awk "/appId:.*,/ && !done { gsub(/appId:.*,/, \"appId: '$APP_ID',\"); done=1}; 1" app/capacitor.config.ts > tmp.$$.json && mv tmp.$$.json app/capacitor.config.ts + +# cordova config +xmlstarlet edit -L --update "/widget/@id" --value "$APP_ID" app/config.xml +xmlstarlet edit -L --update "/widget/@version" --value "$APP_VERSION" app/config.xml +xmlstarlet edit -L -N x="http://www.w3.org/ns/widgets" --update "//x:name" --value "$APP_NAME" app/config.xml + +if [ -n $(xmlstarlet sel -N x="http://www.w3.org/ns/widgets" -T -t -v "//x:name[@short]" app/config.xml) ]; then + #insert + xmlstarlet edit -L -N x="http://www.w3.org/ns/widgets" -s "//x:name" -t attr -n "short" -v "$APP_DISPLAY_NAME" app/config.xml +else + #update + xmlstarlet edit -L -N x="http://www.w3.org/ns/widgets" --update "//x:name[@short]" -v "$APP_DISPLAY_NAME" app/config.xml +fi + +# environment config +awk "/backend_url:.*,/ && !done { gsub(/backend_url:.*,/, \"backend_url: '$BACKEND_URL',\"); done=1}; 1" app/src/environments/environment.prod.ts > tmp.$$.json && mv tmp.$$.json app/src/environments/environment.prod.t +awk "/backend_version:.*,/ && !done { gsub(/backend_version:.*,/, \"backend_version: '$BACKEND_VERSION',\"); done=1}; 1" app/src/environments/environment.prod.t > tmp.$$.json && mv tmp.$$.json app/src/environments/environment.prod.t diff --git a/static/scripts/ios.sh b/static/scripts/ios.sh new file mode 100755 index 00000000..2d18480a --- /dev/null +++ b/static/scripts/ios.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env sh + +DEFAULT_APP_LINK_URL="your.deep.link.host.tld" +APP_LINK_URL="${APP_LINK_URL:-$DEFAULT_APP_LINK_URL}" +ENTITLEMENTS_FILE="app/ios/App/App/App.entitlements" + +/usr/libexec/PlistBuddy -c "Delete :com.apple.developer.associated-domains string $APP_LINK_URL" $ENTITLEMENTS_FILE +/usr/libexec/PlistBuddy -c "Add :com.apple.developer.associated-domains array" $ENTITLEMENTS_FILE +/usr/libexec/PlistBuddy -c "Add :com.apple.developer.associated-domains:0 string $APP_LINK_URL" $ENTITLEMENTS_FILE \ No newline at end of file From d22e6ff87f0abe0f92fae99257ef874cb58f0e92 Mon Sep 17 00:00:00 2001 From: Rainer Killinger Date: Fri, 28 Jan 2022 12:25:48 +0100 Subject: [PATCH 02/30] refactor: add mulitple customization features --- .gitignore | 2 + Makefile | 42 ++-- README.md | 6 +- app.conf | 21 -- app.conf.sample | 23 ++ customizable/theme/variables.scss | 7 + playstore.keystore.sample | 1 + static/fastlane-android/Appfile | 3 +- static/fastlane-android/Fastfile | 47 +++- static/fastlane-ios/Fastfile | 47 +++- .../actions/update_build_settings.rb | 213 ------------------ static/scripts/android.sh | 58 ++--- static/scripts/ionic.sh | 42 +++- static/scripts/ios.sh | 18 +- 14 files changed, 226 insertions(+), 304 deletions(-) delete mode 100644 app.conf create mode 100644 app.conf.sample create mode 100644 customizable/theme/variables.scss create mode 100644 playstore.keystore.sample delete mode 100644 static/fastlane-ios/actions/update_build_settings.rb diff --git a/.gitignore b/.gitignore index f9148d20..d98a0bec 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ app playstore_api_key.json +playstore.keystore +app.conf Gemfile.lock \ No newline at end of file diff --git a/Makefile b/Makefile index 385493a2..32dcb7e9 100644 --- a/Makefile +++ b/Makefile @@ -1,12 +1,12 @@ -SHELL = /bin/bash -APP_DIR = $(PWD)/app -VERSION ?= develop +SHELL := /bin/bash +APP_DIR := $(PWD)/app +BRANCH ?= develop clean: rm -rf app clone: clean - sh static/scripts/clone_app.sh ${VERSION} + sh static/scripts/clone_app.sh ${BRANCH} install: clone cd app && NG_CLI_ANALYTICS="false" npm ci --unsafe-perm @@ -14,24 +14,38 @@ install: clone assets: install cp -rf customizable/assets/. app/src/assets/ && cp -rf customizable/assets-mobile/. app/resources/ -configuration: assets - source app.conf && sh static/scripts/ionic.sh +configuration-web: assets + sh static/scripts/ionic.sh -web-build: configuration +configuration-android: assets + CONFIG_MODE=ANDROID sh static/scripts/ionic.sh + +configuration-ios: assets + CONFIG_MODE=IOS sh static/scripts/ionic.sh + +web-build: configuration-web cd app && ionic build --prod web: web-build - zip -r www.zip app/www + cd app && zip -r ../www.zip www echo "Web application artifact for version ${VERSION} is archived in www.zip" -prepare-android: configuration - source app.conf && cd app && rm -rf android www && ionic capacitor add android && npm run resources:android && ionic capacitor build android --no-open --prod && cd .. && sh static/scripts/android.sh +prepare-android: configuration-android + source app.conf && cd app && rm -rf android www && ionic capacitor add android && npm run resources:android && ionic capacitor build android --no-open --prod && cd .. && sh static/scripts/android.sh + cp -rf static/fastlane-android/. app/android/fastlane/ && cp -rf app.conf app/android/.env android: prepare-android - source app.conf && cp -rf static/fastlane-android/. app/android/fastlane/ && cp -rf app.conf app/ios/App/fastlane/.env && cd app/android && bundler exec fastlane android release + cd app/android && bundler exec fastlane android release -prepare-ios: configuration +android-beta: prepare-android + cd app/android && bundler exec fastlane android beta + +prepare-ios: configuration-ios source app.conf && cd app && rm -rf ios www && ionic capacitor add ios && npm run resources:ios && ionic capacitor build ios --no-open --prod && cd .. && sh static/scripts/ios.sh + cp -rf static/fastlane-ios/. app/ios/App/fastlane/ && cp -rf app.conf app/ios/App/fastlane/.env -ios: prepare-ios - source app.conf && cp -rf static/fastlane-ios/. app/ios/App/fastlane/ && cp -rf app.conf app/ios/App/fastlane/.env && cd app/ios/App && bundler exec fastlane ios release \ No newline at end of file +ios: prepare-ios + cd app/ios/App && bundler exec fastlane ios release + +ios-beta: prepare-ios + cd app/ios/App && bundler exec fastlane ios beta diff --git a/README.md b/README.md index df740e9a..b3f18109 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@ # App Release Template (ART) -This project can be used to generate mobile device apps (android / ios) and angular web app. Beware that this is only the app/frontend part and your need a fully functioning backend deployment first. +This project can be used to generate mobile device apps (Android / iOS) and the standalone Angular web app. Beware that this is only the app/frontend part and your need a fully functioning backend deployment first. ## Requirements * A fully functioning and publicly accessible [backend](https://gitlab.com/openstapps/backend) deployment * An active Apple App Store Developer Account and/or Google Playstore Developer Account * Docker (Android and Angular builds) -* MacOS Device with latest Xcode, fastlane and xmlstartletn, node v14 and npm package @ionic/cli (iOS builds only) +* MacOS Device with latest Xcode, fastlane and xmlstarlet, node v14 and npm package @ionic/cli (iOS builds only) ## Docker @@ -21,6 +21,6 @@ sudo docker build -t openstapps-art . 0. If neccessary make adjustments to your app / profiles / entitlements in your corresponding developer/store portal 1. Overwrite assets in customizable directory with your own ones (keeping the same size and file format) -2. Edit app.conf to your liking (needs info you can find in your corresponding developer/store portal) +2. Edit app.conf.sample to your liking and rename it to app.conf (needs info you can find in your corresponding developer/store portal) 3. Use docker to run `make web` and `make android` (i.e. `docker run -it -v $(pwd):/build --rm openstapps-art:latest make web`) 4. On a macOS device run `make ios` (make sure you have all required certificates / profiles set up in Xcode) diff --git a/app.conf b/app.conf deleted file mode 100644 index ed64eaae..00000000 --- a/app.conf +++ /dev/null @@ -1,21 +0,0 @@ -# Edit the following entires if necessary - -APP_NAME="Open StApps" -APP_DISPLAY_NAME="StApps" # App name on homescreen =Not much space -BACKEND_URL="https://your.backend.server.tld" # Publicly available backend url -BACKEND_VERSION="2.0.0" # Minimum backend version the app will request -APP_LINK_URL="your.deep.link.host.tld" # Your host used for universal =deep links - -# Android specific configuration - -ANDROID_PACKAGE_NAME="de.any_school.app" # e.g. com.krausefx.app - - -# iOS specific sonfiguration - -LOCATION_USAGE_DESCRIPTION="Die Ortungsdienste werden für die Kartenansichten und bessere Suchergebnisse benötigt" -IOS_APP_IDENTIFIER="com.any_school.app.ABCDEV1234" # The bundle identifier of your ios app -APPLE_ID="example@domain.tld" # Your Apple Developer email address -ITC_TEAM_ID="123412345" # App Store Connect Team ID -TEAM_ID="ABCDEV1234" # Developer Team ID -PROVISIONING_PROFILE_NAME="App Deployment Profile" # Name of the apps own provisioning profile used for signing diff --git a/app.conf.sample b/app.conf.sample new file mode 100644 index 00000000..3e56fd8e --- /dev/null +++ b/app.conf.sample @@ -0,0 +1,23 @@ +# Edit the following entires if necessary + +APP_NAME="Open StApps" # Full app name +APP_DISPLAY_NAME="StApps" # App name on mobile device homescreen (Not much space) +BACKEND_URL="https://your.backend.server.tld" # Publicly available backend url +BACKEND_VERSION="2.0.0" # Minimum backend version the app will request +APP_LINK_HOST="your.deep.link.host.tld" # Your host used for universal (deep) links +APP_URL_SCHEME="de.anyschool.app" # Custom url scheme for native app versions + +# Android specific configuration + +ANDROID_PACKAGE_NAME="de.anyschool.app.droid" # e.g. com.krausefx.app + + +# iOS specific sonfiguration + +LOCATION_USAGE_DESCRIPTION="Die Ortungsdienste werden für die Kartenansichten und bessere Suchergebnisse benötigt" +CALENDAR_USAGE_DESCRIPTION="Zugriff auf Kalender wird für die Synchronisierung deiner Uni Termine benötigt" +IOS_BUNDLE_IDENTIFIER="com.anyschool.app.ios" # The bundle identifier of your ios app +APPLE_ID="example@domain.tld" # Your Apple Developer email address +ITC_TEAM_ID="123412345" # App Store Connect Team ID +TEAM_ID="ABCDEV1234" # Developer Team ID +PROVISIONING_PROFILE_NAME="App Deployment Profile" # Name of the apps own provisioning profile used for signing diff --git a/customizable/theme/variables.scss b/customizable/theme/variables.scss new file mode 100644 index 00000000..7abfb042 --- /dev/null +++ b/customizable/theme/variables.scss @@ -0,0 +1,7 @@ +:root { + /* + Use this section to overwrite theme variables + A color scheme can be tested and generated at: + https://ionicframework.com/docs/theming/color-generator + */ +} diff --git a/playstore.keystore.sample b/playstore.keystore.sample new file mode 100644 index 00000000..6ace0b73 --- /dev/null +++ b/playstore.keystore.sample @@ -0,0 +1 @@ +Replace this with your valid playstore keystore file used for signing your binaries and remove '.sample' from file name \ No newline at end of file diff --git a/static/fastlane-android/Appfile b/static/fastlane-android/Appfile index 3efc324a..dd52a0e0 100644 --- a/static/fastlane-android/Appfile +++ b/static/fastlane-android/Appfile @@ -1 +1,2 @@ -json_key_file("../../../playstore_api_key.json") # Don't Change +json_key_file("../../playstore_api_key.json") # Don't Change +package_name ENV["ANDROID_PACKAGE_NAME"] || "de.anyschool.app.droid" # Don't Change \ No newline at end of file diff --git a/static/fastlane-android/Fastfile b/static/fastlane-android/Fastfile index ede6a3cf..55b6133e 100644 --- a/static/fastlane-android/Fastfile +++ b/static/fastlane-android/Fastfile @@ -13,22 +13,61 @@ # Uncomment the line if you want fastlane to automatically update itself # update_fastlane +require 'json' + default_platform(:android) +version_code = 1 +playstore_track = "internal" +package_json = JSON.parse(File.read('../../package.json')) + platform :android do desc "Runs all the tests" lane :test do gradle(task: "test") end - desc "Submit a new Beta Build to Crashlytics Beta" + lane :fetch_version_code do + version_code = google_play_track_version_codes( + package_name: ENV['ANDROID_PACKAGE_NAME'], + track: playstore_track, + json_key: '../../playstore_api_key.json' + ).max + end + + lane :build do + gradle( + task: "clean assemble", + build_type: "Release", + print_command: false, + properties: { + "android.injected.signing.store.file" => "../../../playstore.keystore", + "android.injected.signing.store.password" => "", + "android.injected.signing.key.alias" => "", + "android.injected.signing.key.password" => "", + "versionCode" => version_code, + "versionName" => package_json['version'] + } + ) + end + + desc "Submit a new beta build to internal testing track" lane :beta do - gradle(task: "clean assembleRelease") + playstore_track = "internal" + + build + upload_to_play_store( + track: playstore_track, + json_key: '../../playstore_api_key.json', + skip_upload_metadata: true, + skip_upload_images: true, + skip_upload_screenshots: true + ) end desc "Deploy a new version to the Google Play" lane :release do - gradle(task: "clean assembleRelease") - #upload_to_play_store + build + #upload_to_play_store(json_key: '../../playstore_api_key.json', skip_upload_metadata: true, skip_upload_images: true) end end diff --git a/static/fastlane-ios/Fastfile b/static/fastlane-ios/Fastfile index f9d0cdf9..12ebb78e 100644 --- a/static/fastlane-ios/Fastfile +++ b/static/fastlane-ios/Fastfile @@ -13,26 +13,61 @@ # Uncomment the line if you want fastlane to automatically update itself # update_fastlane +require 'json' + default_platform(:ios) +package_json = JSON.parse(File.read('../../package.json')) + platform :ios do desc "Push a new release build to the App Store" - lane :release do + lane :configure do set_info_plist_value( path: "App/Info.plist", key: "NSLocationAlwaysAndWhenInUseUsageDescription", value: ENV['LOCATION_USAGE_DESCRIPTION'] ) - update_build_settings( - use_automatic_signing: false, + set_info_plist_value( + path: "App/Info.plist", + key: "NSLocationWhenInUseUsageDescription", + value: ENV['LOCATION_USAGE_DESCRIPTION'] + ) + set_info_plist_value( + path: "App/Info.plist", + key: "NSCalendarsUsageDescription", + value: ENV['CALENDAR_USAGE_DESCRIPTION'] + ) + update_url_schemes( + path: "App/Info.plist", + url_schemes: [ENV['APP_URL_SCHEME']] + ) + update_code_signing_settings( + use_automatic_signing: true, path: "App.xcodeproj", team_id: ENV['TEAM_ID'], - bundle_identifier: ENV['IOS_APP_IDENTIFIER'], - profile_name: ENV['PROVISIONING_PROFILE_NAME'], + bundle_identifier: ENV['IOS_BUNDLE_IDENTIFIER'], entitlements_file_path: "App/App.entitlements" ) + increment_version_number( + version_number: package_json['version'], + xcodeproj: "App.xcodeproj" + ) + end + + lane :build do build_app(workspace: "App.xcworkspace", scheme: "App") - # upload_to_app_store(skip_metadata: true, skip_screenshots: true) + end + + lane :release do + configure + build + #upload_to_app_store(skip_metadata: true, skip_screenshots: true) + end + + lane :beta do + configure + build + upload_to_testflight(skip_submission: true) end end \ No newline at end of file diff --git a/static/fastlane-ios/actions/update_build_settings.rb b/static/fastlane-ios/actions/update_build_settings.rb deleted file mode 100644 index 5b211cc4..00000000 --- a/static/fastlane-ios/actions/update_build_settings.rb +++ /dev/null @@ -1,213 +0,0 @@ -require 'xcodeproj' -module Fastlane - module Actions - class UpdateBuildSettingsAction < Action - def self.run(params) - FastlaneCore::PrintTable.print_values(config: params, title: "Summary for code signing settings") - path = params[:path] - path = File.join(File.expand_path(path), "project.pbxproj") - - project = Xcodeproj::Project.open(params[:path]) - UI.user_error!("Could not find path to project config '#{path}'. Pass the path to your project (not workspace)!") unless File.exist?(path) - UI.message("Updating the Automatic Codesigning flag to #{params[:use_automatic_signing] ? 'enabled' : 'disabled'} for the given project '#{path}'") - - unless project.root_object.attributes["TargetAttributes"] - UI.user_error!("Seems to be a very old project file format - please open your project file in a more recent version of Xcode") - return false - end - - changed_targets = [] - changed_build_configurations = [] - - project.targets.each do |target| - if params[:targets] - unless params[:targets].include?(target.name) - UI.important("Skipping #{target.name} not selected (#{params[:targets].join(',')})") - next - end - end - - target.build_configurations.each do |config| - if params[:build_configurations] - unless params[:build_configurations].include?(config.name) - UI.important("Skipping #{config.name} not selected (#{params[:build_configurations].join(',')})") - next - end - end - - style_value = params[:use_automatic_signing] ? 'Automatic' : 'Manual' - set_build_setting(config, "CODE_SIGN_STYLE", style_value) - - if params[:team_id] - set_build_setting(config, "DEVELOPMENT_TEAM", params[:team_id]) - UI.important("Set Team id to: #{params[:team_id]} for target: #{target.name} for build configuration: #{config.name}") - end - if params[:code_sign_identity] - set_build_setting(config, "CODE_SIGN_IDENTITY", params[:code_sign_identity]) - UI.important("Set Code Sign identity to: #{params[:code_sign_identity]} for target: #{target.name} for build configuration: #{config.name}") - end - if params[:profile_name] - set_build_setting(config, "PROVISIONING_PROFILE_SPECIFIER", params[:profile_name]) - UI.important("Set Provisioning Profile name to: #{params[:profile_name]} for target: #{target.name} for build configuration: #{config.name}") - end - if params[:entitlements_file_path] - set_build_setting(config, "CODE_SIGN_ENTITLEMENTS", params[:entitlements_file_path]) - UI.important("Set Entitlements file path to: #{params[:entitlements_file_path]} for target: #{target.name} for build configuration: #{config.name}") - end - # Since Xcode 8, this is no longer needed, you simply use PROVISIONING_PROFILE_SPECIFIER - if params[:profile_uuid] - set_build_setting(config, "PROVISIONING_PROFILE", params[:profile_uuid]) - UI.important("Set Provisioning Profile UUID to: #{params[:profile_uuid]} for target: #{target.name} for build configuration: #{config.name}") - end - if params[:bundle_identifier] - set_build_setting(config, "PRODUCT_BUNDLE_IDENTIFIER", params[:bundle_identifier]) - UI.important("Set Bundle identifier to: #{params[:bundle_identifier]} for target: #{target.name} for build configuration: #{config.name}") - end - - changed_build_configurations << config.name - end - - changed_targets << target.name - end - project.save - - if changed_targets.empty? - UI.important("None of the specified targets has been modified") - UI.important("available targets:") - project.targets.each do |target| - UI.important("\t* #{target.name}") - end - else - UI.success("Successfully updated project settings to use Code Sign Style = '#{params[:use_automatic_signing] ? 'Automatic' : 'Manual'}'") - UI.success("Modified Targets:") - changed_targets.each do |target| - UI.success("\t * #{target}") - end - - UI.success("Modified Build Configurations:") - changed_build_configurations.each do |name| - UI.success("\t * #{name}") - end - end - - params[:use_automatic_signing] - end - - def self.set_build_setting(configuration, name, value) - # Iterate over any keys that start with this name - # This will also set keys that have filtering like [sdk=iphoneos*] - keys = configuration.build_settings.keys.select { |key| key.to_s.match(/#{name}.*/) } - keys.each do |key| - configuration.build_settings[key] = value - end - - # Explicitly set the key with value if keys don't exist - configuration.build_settings[name] = value - end - - def self.description - "Configures Xcode's Codesigning options" - end - - def self.details - "Configures Xcode's Codesigning options of all targets in the project" - end - - def self.available_options - [ - FastlaneCore::ConfigItem.new(key: :path, - env_name: "FL_PROJECT_SIGNING_PROJECT_PATH", - description: "Path to your Xcode project", - code_gen_sensitive: true, - default_value: Dir['*.xcodeproj'].first, - default_value_dynamic: true, - verify_block: proc do |value| - UI.user_error!("Path is invalid") unless File.exist?(File.expand_path(value)) - end), - FastlaneCore::ConfigItem.new(key: :use_automatic_signing, - env_name: "FL_PROJECT_USE_AUTOMATIC_SIGNING", - description: "Defines if project should use automatic signing", - type: Boolean, - default_value: false), - FastlaneCore::ConfigItem.new(key: :team_id, - env_name: "FASTLANE_TEAM_ID", - optional: true, - description: "Team ID, is used when upgrading project"), - FastlaneCore::ConfigItem.new(key: :targets, - env_name: "FL_PROJECT_SIGNING_TARGETS", - optional: true, - type: Array, - description: "Specify targets you want to toggle the signing mech. (default to all targets)"), - FastlaneCore::ConfigItem.new(key: :build_configurations, - env_name: "FL_PROJECT_SIGNING_BUILD_CONFIGURATIONS", - optional: true, - type: Array, - description: "Specify build_configurations you want to toggle the signing mech. (default to all configurations)"), - FastlaneCore::ConfigItem.new(key: :code_sign_identity, - env_name: "FL_CODE_SIGN_IDENTITY", - description: "Code signing identity type (iPhone Developer, iPhone Distribution)", - optional: true), - FastlaneCore::ConfigItem.new(key: :entitlements_file_path, - env_name: "FL_CODE_SIGN_ENTITLEMENTS_FILE_PATH", - description: "Path to your entitlements file", - optional: true), - FastlaneCore::ConfigItem.new(key: :profile_name, - env_name: "FL_PROVISIONING_PROFILE_SPECIFIER", - description: "Provisioning profile name to use for code signing", - optional: true), - FastlaneCore::ConfigItem.new(key: :profile_uuid, - env_name: "FL_PROVISIONING_PROFILE", - description: "Provisioning profile UUID to use for code signing", - optional: true), - FastlaneCore::ConfigItem.new(key: :bundle_identifier, - env_name: "FL_APP_IDENTIFIER", - description: "Application Product Bundle Identifier", - optional: true) - ] - end - - def self.output - end - - def self.example_code - [ - ' # manual code signing - update_code_signing_settings( - use_automatic_signing: false, - path: "demo-project/demo/demo.xcodeproj" - )', - ' # automatic code signing - update_code_signing_settings( - use_automatic_signing: true, - path: "demo-project/demo/demo.xcodeproj" - )', - ' # more advanced manual code signing - update_code_signing_settings( - use_automatic_signing: true, - path: "demo-project/demo/demo.xcodeproj", - team_id: "QABC123DEV", - bundle_identifier: "com.demoapp.QABC123DEV", - profile_name: "Demo App Deployment Profile", - entitlements_file_path: "Demo App/generated/New.entitlements" - )' - ] - end - - def self.category - :code_signing - end - - def self.return_value - "The current status (boolean) of codesigning after modification" - end - - def self.authors - ["mathiasAichinger", "hjanuschka", "p4checo", "portellaa", "aeons", "att55", "abcdev"] - end - - def self.is_supported?(platform) - [:ios, :mac].include?(platform) - end - end - end -end diff --git a/static/scripts/android.sh b/static/scripts/android.sh index 287b28da..f61d1df7 100755 --- a/static/scripts/android.sh +++ b/static/scripts/android.sh @@ -1,35 +1,37 @@ #!/usr/bin/env sh -DEFAULT_APP_LINK_URL="your.deep.link.host.tld" -APP_LINK_URL="${APP_LINK_URL:-$DEFAULT_APP_LINK_URL}" +. $PWD/app.conf -if [ -n "$(xmlstarlet sel -T -t -v "/manifest/application/activity[intent-filter/@android:autoVerify='true']" app/android/app/src/main/AndroidManifest.xml)" ]; then - echo "Deep link capability already defined in AndroidManifest.xml" -else - echo "Adding deep link capability to AndroidManifest.xml" - xmlstarlet edit -L -s /manifest/application/activity -t elem -n tmpElement -v "" \ - -i //tmpElement -t attr -n "android:autoVerify" -v "true" \ - -r //tmpElement -v intent-filter \ - app/android/app/src/main/AndroidManifest.xml +DEFAULT_APP_URL_SCHEME="de.anyschool.app" +APP_URL_SCHEME="${APP_URL_SCHEME:-$DEFAULT_APP_URL_SCHEME}" - xmlstarlet edit -L -s "/manifest/application/activity/intent-filter[@android:autoVerify='true']" -t elem -n tmpElement -v "" \ - -i //tmpElement -t attr -n "android:name" -v "android.intent.action.VIEW" \ - -r //tmpElement -v action \ - app/android/app/src/main/AndroidManifest.xml +ANDROID_MANIFEST_PATH="app/android/app/src/main/AndroidManifest.xml" +ANDROID_STRINGS_PATH="app/android/app/src/main/res/values/strings.xml" - xmlstarlet edit -L -s "/manifest/application/activity/intent-filter[@android:autoVerify='true']" -t elem -n tmpElement -v "" \ - -i //tmpElement -t attr -n "android:name" -v "android.intent.action.DEFAULT" \ - -r //tmpElement -v category \ - app/android/app/src/main/AndroidManifest.xml - xmlstarlet edit -L -s "/manifest/application/activity/intent-filter[@android:autoVerify='true']" -t elem -n tmpElement -v "" \ - -i //tmpElement -t attr -n "android:name" -v "android.intent.action.BROWSABLE" \ - -r //tmpElement -v category \ - app/android/app/src/main/AndroidManifest.xml +git -C app checkout -- android/build.gradle +git -C app checkout -- android/app/src/main/AndroidManifest.xml - xmlstarlet edit -L -s "/manifest/application/activity/intent-filter[@android:autoVerify='true']" -t elem -n tmpElement -v "" \ - -i //tmpElement -t attr -n "android:scheme" -v "https" \ - -i //tmpElement -t attr -n "android:host" -v "$APP_LINK_URL" \ - -r //tmpElement -v data \ - app/android/app/src/main/AndroidManifest.xml -fi \ No newline at end of file +# AndroidManifest.xml +xmlstarlet edit --pf --inplace \ + --update "/manifest/@package" --value "$ANDROID_PACKAGE_NAME" \ + $ANDROID_MANIFEST_PATH + +xmlstarlet edit --pf --inplace \ + --update "/manifest/application/activity/@android:name" --value "$ANDROID_PACKAGE_NAME".MainActivity \ + $ANDROID_MANIFEST_PATH + +# strings.xml +xmlstarlet edit --pf --inplace \ + --update "/resources/string[@name='package_name']" --value "$ANDROID_PACKAGE_NAME" \ + $ANDROID_STRINGS_PATH + +xmlstarlet edit --pf --inplace \ + --update "/resources/string[@name='custom_url_scheme']" --value "$APP_URL_SCHEME" \ + $ANDROID_STRINGS_PATH + +xmlstarlet edit --inplace \ + --delete "/resources/string[@name='app_host']" \ + --append "/resources/string[@name='custom_url_scheme']" --type elem -n string --value "$APP_LINK_HOST" \ + --insert "/resources/string[not(@name)]" --type attr -n name --value "app_host" \ + $ANDROID_STRINGS_PATH diff --git a/static/scripts/ionic.sh b/static/scripts/ionic.sh index b0058ece..970de081 100755 --- a/static/scripts/ionic.sh +++ b/static/scripts/ionic.sh @@ -1,17 +1,37 @@ #!/usr/bin/env sh -DEFAULT_APP_NAME="Open StApps" -DEFAULT_APP_DISPLAY_NAME="StApps" -DEFAULT_APP_ID="de.any_school.app" +. $PWD/app.conf + +DEFAULT_APP_ID="de.anyschool.app" +DEFAULT_CONFIG_MODE="WEB" -APP_NAME="${APP_NAME:-$DEFAULT_APP_NAME}" -APP_DISPLAY_NAME="${APP_DISPLAY_NAME:-$DEFAULT_APP_DISPLAY_NAME}" APP_ID="${APP_ID:-$DEFAULT_APP_ID}" - APP_VERSION=$(jq '.version' app/package.json) +CONFIG_MODE="${CONFIG_MODE:-$DEFAULT_CONFIG_MODE}" + +if [ "$CONFIG_MODE" = 'ANDROID' ]; then + APP_ID="$ANDROID_PACKAGE_NAME" +fi + +if [ "$CONFIG_MODE" = 'IOS' ]; then + APP_ID="$IOS_BUNDLE_IDENTIFIER" +fi # ionic config cat app/ionic.config.json | jq '.name = $newName' --arg newName "$APP_NAME" > tmp.$$.json && mv tmp.$$.json app/ionic.config.json +sed -i -e 's@StApps<\/title>@<title>'"$APP_DISPLAY_NAME"'<\/title>@g' app/src/index.html + +ROOT_THEME_DEFINITONS=$(cat app/src/theme/variables.scss | grep -o :root | wc -l) + +if [ $ROOT_THEME_DEFINITONS -gt 1 ]; then + #update + echo "SCSS theme has already been set" +else + #insert + cat customizable/theme/variables.scss >> app/src/theme/variables.scss +fi + + # capacitor config awk "/appName:.*,/ && !done { gsub(/appName:.*,/, \"appName: '$APP_NAME',\"); done=1}; 1" app/capacitor.config.ts > tmp.$$.json && mv tmp.$$.json app/capacitor.config.ts @@ -22,7 +42,9 @@ xmlstarlet edit -L --update "/widget/@id" --value "$APP_ID" app/config.xml xmlstarlet edit -L --update "/widget/@version" --value "$APP_VERSION" app/config.xml xmlstarlet edit -L -N x="http://www.w3.org/ns/widgets" --update "//x:name" --value "$APP_NAME" app/config.xml -if [ -n $(xmlstarlet sel -N x="http://www.w3.org/ns/widgets" -T -t -v "//x:name[@short]" app/config.xml) ]; then +SHORT_EXISTS=$(xmlstarlet sel -N x='http://www.w3.org/ns/widgets' -T -t -v '//x:name[@short]' app/config.xml) + +if [ -z "${SHORT_EXISTS:-}" ]; then #insert xmlstarlet edit -L -N x="http://www.w3.org/ns/widgets" -s "//x:name" -t attr -n "short" -v "$APP_DISPLAY_NAME" app/config.xml else @@ -31,5 +53,7 @@ else fi # environment config -awk "/backend_url:.*,/ && !done { gsub(/backend_url:.*,/, \"backend_url: '$BACKEND_URL',\"); done=1}; 1" app/src/environments/environment.prod.ts > tmp.$$.json && mv tmp.$$.json app/src/environments/environment.prod.t -awk "/backend_version:.*,/ && !done { gsub(/backend_version:.*,/, \"backend_version: '$BACKEND_VERSION',\"); done=1}; 1" app/src/environments/environment.prod.t > tmp.$$.json && mv tmp.$$.json app/src/environments/environment.prod.t +awk "/backend_url:.*,/ && !done { gsub(/backend_url:.*,/, \"backend_url: '$BACKEND_URL',\"); done=1}; 1" app/src/environments/environment.prod.ts > tmp.$$.json && mv tmp.$$.json app/src/environments/environment.prod.ts +awk "/backend_version:.*,/ && !done { gsub(/backend_version:.*,/, \"backend_version: '$BACKEND_VERSION',\"); done=1}; 1" app/src/environments/environment.prod.ts > tmp.$$.json && mv tmp.$$.json app/src/environments/environment.prod.ts +awk "/app_host:.*,/ && !done { gsub(/app_host:.*,/, \"app_host: '$APP_LINK_HOST',\"); done=1}; 1" app/src/environments/environment.prod.ts > tmp.$$.json && mv tmp.$$.json app/src/environments/environment.prod.ts +awk "/custom_url_scheme:.*,/ && !done { gsub(/custom_url_scheme:.*,/, \"custom_url_scheme: '$APP_URL_SCHEME',\"); done=1}; 1" app/src/environments/environment.prod.ts > tmp.$$.json && mv tmp.$$.json app/src/environments/environment.prod.ts diff --git a/static/scripts/ios.sh b/static/scripts/ios.sh index 2d18480a..37ed1714 100755 --- a/static/scripts/ios.sh +++ b/static/scripts/ios.sh @@ -1,9 +1,17 @@ #!/usr/bin/env sh -DEFAULT_APP_LINK_URL="your.deep.link.host.tld" -APP_LINK_URL="${APP_LINK_URL:-$DEFAULT_APP_LINK_URL}" -ENTITLEMENTS_FILE="app/ios/App/App/App.entitlements" +. $PWD/app.conf -/usr/libexec/PlistBuddy -c "Delete :com.apple.developer.associated-domains string $APP_LINK_URL" $ENTITLEMENTS_FILE +ENTITLEMENTS_FILE="app/ios/App/App/App.entitlements" +INFOPLIST_FILE="app/ios/App/App/Info.plist" + + +/usr/libexec/PlistBuddy -c "Delete :com.apple.developer.associated-domains string applinks:$APP_LINK_HOST" $ENTITLEMENTS_FILE /usr/libexec/PlistBuddy -c "Add :com.apple.developer.associated-domains array" $ENTITLEMENTS_FILE -/usr/libexec/PlistBuddy -c "Add :com.apple.developer.associated-domains:0 string $APP_LINK_URL" $ENTITLEMENTS_FILE \ No newline at end of file +/usr/libexec/PlistBuddy -c "Add :com.apple.developer.associated-domains:0 string applinks:$APP_LINK_HOST" $ENTITLEMENTS_FILE + +/usr/libexec/PlistBuddy -c "Delete :BGTaskSchedulerPermittedIdentifiers string com.transistorsoft.fetch" $INFOPLIST_FILE +/usr/libexec/PlistBuddy -c "Add :BGTaskSchedulerPermittedIdentifiers array" $INFOPLIST_FILE +/usr/libexec/PlistBuddy -c "Add :BGTaskSchedulerPermittedIdentifiers:0 string com.transistorsoft.fetch" $INFOPLIST_FILE + +git -C app checkout -- ios/App/App/AppDelegate.swift From e78496fb8ba2da7257202e23af6766a8882f656f Mon Sep 17 00:00:00 2001 From: Rainer Killinger <killinger@hrz.uni-frankfurt.de> Date: Thu, 10 Feb 2022 15:19:06 +0100 Subject: [PATCH 03/30] refactor: read android secrets from environment --- app.conf.sample | 18 ++++++++++++------ static/fastlane-android/Fastfile | 8 ++++---- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/app.conf.sample b/app.conf.sample index 3e56fd8e..95b5e37d 100644 --- a/app.conf.sample +++ b/app.conf.sample @@ -7,17 +7,23 @@ BACKEND_VERSION="2.0.0" # Minimum backend version th APP_LINK_HOST="your.deep.link.host.tld" # Your host used for universal (deep) links APP_URL_SCHEME="de.anyschool.app" # Custom url scheme for native app versions -# Android specific configuration - -ANDROID_PACKAGE_NAME="de.anyschool.app.droid" # e.g. com.krausefx.app - # iOS specific sonfiguration -LOCATION_USAGE_DESCRIPTION="Die Ortungsdienste werden für die Kartenansichten und bessere Suchergebnisse benötigt" +LOCATION_USAGE_DESCRIPTION="Ortungsdienste werden für die Kartenansichten und bessere Suchergebnisse benötigt" CALENDAR_USAGE_DESCRIPTION="Zugriff auf Kalender wird für die Synchronisierung deiner Uni Termine benötigt" IOS_BUNDLE_IDENTIFIER="com.anyschool.app.ios" # The bundle identifier of your ios app APPLE_ID="example@domain.tld" # Your Apple Developer email address ITC_TEAM_ID="123412345" # App Store Connect Team ID TEAM_ID="ABCDEV1234" # Developer Team ID -PROVISIONING_PROFILE_NAME="App Deployment Profile" # Name of the apps own provisioning profile used for signing + + +# Android specific configuration + +ANDROID_PACKAGE_NAME="de.anyschool.app.android" # Your Google Playconsole app package name + +# Provide the following environment variables in a secure fashion. +# Don't change the following lines! +ANDROID_KEYSTORE_PASSWORD="${ANDROID_KEYSTORE_PASSWORD:-'unsed'}" # Passwort to your keyfile +ANDROID_KEYSTORE_KEY_ALIAS="${ANDROID_KEYSTORE_KEY_ALIAS:-'unsed'}" # Name/Alias of the key used for signing within the keyfile +ANDROID_KEYSTORE_KEY_PASSWORD="${ANDROID_KEYSTORE_KEY_PASSWORD:-'unsed'}" # Passwort to the this very key diff --git a/static/fastlane-android/Fastfile b/static/fastlane-android/Fastfile index 55b6133e..a252a25d 100644 --- a/static/fastlane-android/Fastfile +++ b/static/fastlane-android/Fastfile @@ -41,10 +41,10 @@ platform :android do build_type: "Release", print_command: false, properties: { - "android.injected.signing.store.file" => "../../../playstore.keystore", - "android.injected.signing.store.password" => "", - "android.injected.signing.key.alias" => "", - "android.injected.signing.key.password" => "", + "android.injected.signing.store.file" => "/build/playstore.keystore", + "android.injected.signing.store.password" => ENV['ANDROID_KEYSTORE_PASSWORD'], + "android.injected.signing.key.alias" => ENV['ANDROID_KEYSTORE_KEY_ALIAS'], + "android.injected.signing.key.password" => ENV['ANDROID_KEYSTORE_KEY_PASSWORD'], "versionCode" => version_code, "versionName" => package_json['version'] } From 8abd993667ff4578fe46ab5bdbc234172b4b9d46 Mon Sep 17 00:00:00 2001 From: Rainer Killinger <killinger@hrz.uni-frankfurt.de> Date: Thu, 10 Feb 2022 21:10:40 +0100 Subject: [PATCH 04/30] refactor: version code handling --- static/fastlane-android/Fastfile | 22 ++++++++++++++++++---- static/fastlane-ios/Fastfile | 2 +- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/static/fastlane-android/Fastfile b/static/fastlane-android/Fastfile index a252a25d..4c4c3634 100644 --- a/static/fastlane-android/Fastfile +++ b/static/fastlane-android/Fastfile @@ -17,7 +17,7 @@ require 'json' default_platform(:android) -version_code = 1 +current_version_code = 1 playstore_track = "internal" package_json = JSON.parse(File.read('../../package.json')) @@ -28,14 +28,28 @@ platform :android do end lane :fetch_version_code do - version_code = google_play_track_version_codes( + current_version_code = google_play_track_version_codes( package_name: ENV['ANDROID_PACKAGE_NAME'], track: playstore_track, json_key: '../../playstore_api_key.json' ).max end + lane :fetch_highest_version_code do + version_code_candidates = [1] + tracks = ['production', 'beta', 'internal'] + tracks.each do |t| + version_code_candidates += google_play_track_version_codes( + package_name: ENV['ANDROID_PACKAGE_NAME'], + track: t, + json_key: '../../playstore_api_key.json' + ) + end + current_version_code = version_code_candidates.compact.max + end + lane :build do + fetch_highest_version_code gradle( task: "clean assemble", build_type: "Release", @@ -45,8 +59,8 @@ platform :android do "android.injected.signing.store.password" => ENV['ANDROID_KEYSTORE_PASSWORD'], "android.injected.signing.key.alias" => ENV['ANDROID_KEYSTORE_KEY_ALIAS'], "android.injected.signing.key.password" => ENV['ANDROID_KEYSTORE_KEY_PASSWORD'], - "versionCode" => version_code, - "versionName" => package_json['version'] + "android.injected.version.code" => current_version_code + 1, + "android.injected.version.name" => package_json['version'] } ) end diff --git a/static/fastlane-ios/Fastfile b/static/fastlane-ios/Fastfile index 12ebb78e..ecf7ee4f 100644 --- a/static/fastlane-ios/Fastfile +++ b/static/fastlane-ios/Fastfile @@ -17,7 +17,7 @@ require 'json' default_platform(:ios) -package_json = JSON.parse(File.read('../../package.json')) +package_json = JSON.parse(File.read('../../../package.json')) platform :ios do desc "Push a new release build to the App Store" From 4b258234e0019f09b4025acf1c9fa40be4e96c2b Mon Sep 17 00:00:00 2001 From: Rainer Killinger <killinger@hrz.uni-frankfurt.de> Date: Fri, 18 Feb 2022 11:43:57 +0100 Subject: [PATCH 05/30] refactor: enable CI only configuration --- .gitignore | 3 --- app.conf.sample | 37 +++++++++++++++++------------ playstore_api_key.json.sample | 3 --- static/fastlane-android/Appfile | 2 +- static/fastlane-android/Fastfile | 15 +++++------- static/fastlane-ios/Fastfile | 40 +++++++++++++++++++++++++++++--- 6 files changed, 66 insertions(+), 34 deletions(-) delete mode 100644 playstore_api_key.json.sample diff --git a/.gitignore b/.gitignore index d98a0bec..58e1763a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,2 @@ app -playstore_api_key.json -playstore.keystore -app.conf Gemfile.lock \ No newline at end of file diff --git a/app.conf.sample b/app.conf.sample index 95b5e37d..202b77f1 100644 --- a/app.conf.sample +++ b/app.conf.sample @@ -1,21 +1,27 @@ -# Edit the following entires if necessary +# Edit the following entires and save the file without .sample extension -APP_NAME="Open StApps" # Full app name -APP_DISPLAY_NAME="StApps" # App name on mobile device homescreen (Not much space) -BACKEND_URL="https://your.backend.server.tld" # Publicly available backend url -BACKEND_VERSION="2.0.0" # Minimum backend version the app will request -APP_LINK_HOST="your.deep.link.host.tld" # Your host used for universal (deep) links -APP_URL_SCHEME="de.anyschool.app" # Custom url scheme for native app versions +APP_NAME="Open StApps" # Full app name +APP_DISPLAY_NAME="StApps" # App name on mobile device homescreen (Not much space) +BACKEND_URL="https://your.backend.server.tld" # Publicly available backend url +BACKEND_VERSION="2.0.0" # Minimum backend version the app will request +APP_LINK_HOST="your.deep.link.host.tld" # Your host used for universal (deep) links +APP_URL_SCHEME="de.anyschool.app" # Custom url scheme for native app versions -# iOS specific sonfiguration +# iOS specific configuration LOCATION_USAGE_DESCRIPTION="Ortungsdienste werden für die Kartenansichten und bessere Suchergebnisse benötigt" CALENDAR_USAGE_DESCRIPTION="Zugriff auf Kalender wird für die Synchronisierung deiner Uni Termine benötigt" -IOS_BUNDLE_IDENTIFIER="com.anyschool.app.ios" # The bundle identifier of your ios app -APPLE_ID="example@domain.tld" # Your Apple Developer email address -ITC_TEAM_ID="123412345" # App Store Connect Team ID -TEAM_ID="ABCDEV1234" # Developer Team ID +IOS_BUNDLE_IDENTIFIER="com.anyschool.app.ios" # The bundle identifier of your ios app +APPLE_ID="example@domain.tld" # Your Apple Developer email address +ITC_TEAM_ID="123412345" # App Store Connect Team ID +TEAM_ID="ABCDEV1234" # Developer Team ID +APPLE_API_KEY_ID="123ACAB456" # Your API key id +APPLE_API_KEY_ISSUER_ID="1234578-1234-1234-1234-12345678901" # Your API key issuer id + +# Provide the following environment variable in a secure fashion. +# Don't change the following line! +APPLE_API_KEY_CONTENT="${APPLE_API_KEY_CONTENT:-'unset'}" # Base64 encoded contents of Apple API key file (.p8 file) # Android specific configuration @@ -24,6 +30,7 @@ ANDROID_PACKAGE_NAME="de.anyschool.app.android" # Your # Provide the following environment variables in a secure fashion. # Don't change the following lines! -ANDROID_KEYSTORE_PASSWORD="${ANDROID_KEYSTORE_PASSWORD:-'unsed'}" # Passwort to your keyfile -ANDROID_KEYSTORE_KEY_ALIAS="${ANDROID_KEYSTORE_KEY_ALIAS:-'unsed'}" # Name/Alias of the key used for signing within the keyfile -ANDROID_KEYSTORE_KEY_PASSWORD="${ANDROID_KEYSTORE_KEY_PASSWORD:-'unsed'}" # Passwort to the this very key +ANDROID_API_KEY_CONTENT="${ANDROID_API_KEY_CONTENT:-'unset'}" # Base64 encoded contents of your API key file (.json file) +ANDROID_KEYSTORE_PASSWORD="${ANDROID_KEYSTORE_PASSWORD:-'unset'}" # Passwort to your keyfile +ANDROID_KEYSTORE_KEY_ALIAS="${ANDROID_KEYSTORE_KEY_ALIAS:-'unset'}" # Name/Alias of the key used for signing within the keyfile +ANDROID_KEYSTORE_KEY_PASSWORD="${ANDROID_KEYSTORE_KEY_PASSWORD:-'unset'}" # Passwort to the this very key diff --git a/playstore_api_key.json.sample b/playstore_api_key.json.sample deleted file mode 100644 index bcdf77c3..00000000 --- a/playstore_api_key.json.sample +++ /dev/null @@ -1,3 +0,0 @@ -{ - "hint": "Replace this with the content of your valid playstore api key json file and remove '.sample' from file name" -} \ No newline at end of file diff --git a/static/fastlane-android/Appfile b/static/fastlane-android/Appfile index dd52a0e0..e6c604b0 100644 --- a/static/fastlane-android/Appfile +++ b/static/fastlane-android/Appfile @@ -1,2 +1,2 @@ -json_key_file("../../playstore_api_key.json") # Don't Change +#json_key_file("../../playstore_api_key.json") # Don't Change package_name ENV["ANDROID_PACKAGE_NAME"] || "de.anyschool.app.droid" # Don't Change \ No newline at end of file diff --git a/static/fastlane-android/Fastfile b/static/fastlane-android/Fastfile index 4c4c3634..c758d6d5 100644 --- a/static/fastlane-android/Fastfile +++ b/static/fastlane-android/Fastfile @@ -14,6 +14,7 @@ # update_fastlane require 'json' +require 'base64' default_platform(:android) @@ -22,16 +23,12 @@ playstore_track = "internal" package_json = JSON.parse(File.read('../../package.json')) platform :android do - desc "Runs all the tests" - lane :test do - gradle(task: "test") - end lane :fetch_version_code do current_version_code = google_play_track_version_codes( package_name: ENV['ANDROID_PACKAGE_NAME'], track: playstore_track, - json_key: '../../playstore_api_key.json' + json_key_data: ENV['ANDROID_API_KEY_CONTENT'] ).max end @@ -40,9 +37,9 @@ platform :android do tracks = ['production', 'beta', 'internal'] tracks.each do |t| version_code_candidates += google_play_track_version_codes( - package_name: ENV['ANDROID_PACKAGE_NAME'], + package_name: ENV['ANDROID_PACKAGE_NAME'], track: t, - json_key: '../../playstore_api_key.json' + json_key_data: Base64.decode64(ENV['ANDROID_API_KEY_CONTENT']) ) end current_version_code = version_code_candidates.compact.max @@ -72,14 +69,14 @@ platform :android do build upload_to_play_store( track: playstore_track, - json_key: '../../playstore_api_key.json', + json_key_data: Base64.decode64(ENV['ANDROID_API_KEY_CONTENT']), skip_upload_metadata: true, skip_upload_images: true, skip_upload_screenshots: true ) end - desc "Deploy a new version to the Google Play" + desc "Submit a new version to the Google Play" lane :release do build #upload_to_play_store(json_key: '../../playstore_api_key.json', skip_upload_metadata: true, skip_upload_images: true) diff --git a/static/fastlane-ios/Fastfile b/static/fastlane-ios/Fastfile index ecf7ee4f..a5ea4e53 100644 --- a/static/fastlane-ios/Fastfile +++ b/static/fastlane-ios/Fastfile @@ -17,10 +17,34 @@ require 'json' default_platform(:ios) +current_build_number = 1 package_json = JSON.parse(File.read('../../../package.json')) +api_key = app_store_connect_api_key( + key_id: ENV['APPLE_API_KEY_ID'], + issuer_id: ENV['APPLE_API_KEY_ISSUER_ID'], + key_content: ENV['APPLE_API_KEY_CONTENT'], + is_key_content_base64: true, + duration: 1000, + in_house: false +) + platform :ios do - desc "Push a new release build to the App Store" + + lane :fetch_highest_build_number do + build_number_candidates = [1] + build_number_candidates += latest_testflight_build_number( + version: package_json['version'], + initial_build_number: 1, + api_key: api_key + ) + build_number_candidates += app_store_build_number( + version: package_json['version'], + initial_build_number: 1, + api_key: api_key + ) + current_build_number = build_number_candidates.compact.max + end lane :configure do set_info_plist_value( @@ -53,21 +77,31 @@ platform :ios do version_number: package_json['version'], xcodeproj: "App.xcodeproj" ) + fetch_highest_build_number + increment_build_number( + build_number: current_build_number + 1, + xcodeproj: "App.xcodeproj" + ) end lane :build do build_app(workspace: "App.xcworkspace", scheme: "App") end + desc "Submit a new version to iOS App Store" lane :release do configure build - #upload_to_app_store(skip_metadata: true, skip_screenshots: true) + #upload_to_app_store(skip_metadata: true, skip_screenshots: true, api_key: api_key) end + desc "Submit a new version to Testflight" lane :beta do configure build - upload_to_testflight(skip_submission: true) + upload_to_testflight( + skip_submission: true, + api_key: api_key + ) end end \ No newline at end of file From 7596759f772448dc754f0d2557ed56157442f55a Mon Sep 17 00:00:00 2001 From: Rainer Killinger <killinger@hrz.uni-frankfurt.de> Date: Fri, 18 Feb 2022 14:47:39 +0100 Subject: [PATCH 06/30] ci: add image building --- .gitlab-ci.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .gitlab-ci.yml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 00000000..5a57f1f0 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,22 @@ +image: docker:stable + +stages: + - image + +docker image: + stage: image + only: + variables: + - $BUILD_IMAGE == "true" + variables: + DOCKER_DRIVER: overlay2 + services: + - docker:dind + script: + - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN registry.gitlab.com + - docker build -t registry.gitlab.com/openstapps/app-release-template . + - docker push registry.gitlab.com/openstapps/app-release-template + only: + - main + tags: + - docker \ No newline at end of file From 6a5bad7896b07748e4f450653766cd7e76261611 Mon Sep 17 00:00:00 2001 From: Rainer Killinger <killinger@hrz.uni-frankfurt.de> Date: Fri, 18 Feb 2022 15:50:59 +0100 Subject: [PATCH 07/30] ci: add deploy jobs for app targets --- .gitlab-ci.yml | 72 +++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 66 insertions(+), 6 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 5a57f1f0..56e27638 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -2,12 +2,10 @@ image: docker:stable stages: - image + - deploy docker image: stage: image - only: - variables: - - $BUILD_IMAGE == "true" variables: DOCKER_DRIVER: overlay2 services: @@ -16,7 +14,69 @@ docker image: - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN registry.gitlab.com - docker build -t registry.gitlab.com/openstapps/app-release-template . - docker push registry.gitlab.com/openstapps/app-release-template - only: - - main + rules: + - if: '$CI_COMMIT_BRANCH == "main" && $BUILD_IMAGE == "true"' tags: - - docker \ No newline at end of file + - docker + +web: + image: registry.gitlab.com/openstapps/app-release-template + stage: deploy + script: + - > + if [ "$RELEASE_TYPE" == "staging" ]; then + echo "Handle staging artifact here"; + fi + + if [ "$RELEASE_TYPE" == "production" ]; then + echo "Handle production artifact here"; + fi + artifacts: + untracked: false + paths: + - www.zip + tags: + - secrecy + rules: + - if: '$CI_COMMIT_BRANCH == "main" && $RELEASE_TYPE == "staging"' + - if: '$CI_COMMIT_BRANCH == "main" && $RELEASE_TYPE == "production"' + +ios: + image: registry.gitlab.com/openstapps/app-release-template + stage: deploy + script: + - > + if [ "$RELEASE_TYPE" == "staging" ]; then + make ios-beta; + fi + + if [ "$RELEASE_TYPE" == "production" ]; then + make ios; + fi + artifacts: + untracked: false + tags: + - secrecy + rules: + - if: '$CI_COMMIT_BRANCH == "main" && $RELEASE_TYPE == "staging"' + - if: '$CI_COMMIT_BRANCH == "main" && $RELEASE_TYPE == "production"' + +android: + image: registry.gitlab.com/openstapps/app-release-template + stage: deploy + script: + - > + if [ "$RELEASE_TYPE" == "staging" ]; then + make android-beta; + fi + + if [ "$RELEASE_TYPE" == "production" ]; then + make android; + fi + artifacts: + untracked: false + tags: + - secrecy + rules: + - if: '$CI_COMMIT_BRANCH == "main" && $RELEASE_TYPE == "staging"' + - if: '$CI_COMMIT_BRANCH == "main" && $RELEASE_TYPE == "production"' From 9537833247c6d3353abb11768930f24a6c2007e0 Mon Sep 17 00:00:00 2001 From: Rainer Killinger <killinger@hrz.uni-frankfurt.de> Date: Fri, 4 Mar 2022 14:02:57 +0100 Subject: [PATCH 08/30] ci: tag ios jib with macos runner --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 56e27638..15f70bc3 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -56,7 +56,7 @@ ios: artifacts: untracked: false tags: - - secrecy + - macos rules: - if: '$CI_COMMIT_BRANCH == "main" && $RELEASE_TYPE == "staging"' - if: '$CI_COMMIT_BRANCH == "main" && $RELEASE_TYPE == "production"' From 0edc59866750a68131e2f12af9b843c399395aa1 Mon Sep 17 00:00:00 2001 From: Rainer Killinger <killinger@hrz.uni-frankfurt.de> Date: Thu, 10 Mar 2022 11:45:57 +0100 Subject: [PATCH 09/30] docs: update README and file paths --- README.md | 10 ++++++++-- static/scripts/ionic.sh | 8 ++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index b3f18109..006b448c 100644 --- a/README.md +++ b/README.md @@ -11,10 +11,16 @@ This project can be used to generate mobile device apps (Android / iOS) and the ## Docker -Build an image from Dockerfile in this project +Use weelkly updated image from this repo ```bash -sudo docker build -t openstapps-art . +docker pull registry.gitlab.com/openstapps/app-release-template:latest +``` + +Or build your own image from Dockerfile within this project + +```bash +docker build -t openstapps-art . ``` ## Usage diff --git a/static/scripts/ionic.sh b/static/scripts/ionic.sh index 970de081..7d24745a 100755 --- a/static/scripts/ionic.sh +++ b/static/scripts/ionic.sh @@ -53,7 +53,7 @@ else fi # environment config -awk "/backend_url:.*,/ && !done { gsub(/backend_url:.*,/, \"backend_url: '$BACKEND_URL',\"); done=1}; 1" app/src/environments/environment.prod.ts > tmp.$$.json && mv tmp.$$.json app/src/environments/environment.prod.ts -awk "/backend_version:.*,/ && !done { gsub(/backend_version:.*,/, \"backend_version: '$BACKEND_VERSION',\"); done=1}; 1" app/src/environments/environment.prod.ts > tmp.$$.json && mv tmp.$$.json app/src/environments/environment.prod.ts -awk "/app_host:.*,/ && !done { gsub(/app_host:.*,/, \"app_host: '$APP_LINK_HOST',\"); done=1}; 1" app/src/environments/environment.prod.ts > tmp.$$.json && mv tmp.$$.json app/src/environments/environment.prod.ts -awk "/custom_url_scheme:.*,/ && !done { gsub(/custom_url_scheme:.*,/, \"custom_url_scheme: '$APP_URL_SCHEME',\"); done=1}; 1" app/src/environments/environment.prod.ts > tmp.$$.json && mv tmp.$$.json app/src/environments/environment.prod.ts +awk "/backend_url:.*,/ && !done { gsub(/backend_url:.*,/, \"backend_url: '$BACKEND_URL',\"); done=1}; 1" app/src/environments/environment.production.ts > tmp.$$.json && mv tmp.$$.json app/src/environments/environment.production.ts +awk "/backend_version:.*,/ && !done { gsub(/backend_version:.*,/, \"backend_version: '$BACKEND_VERSION',\"); done=1}; 1" app/src/environments/environment.production.ts > tmp.$$.json && mv tmp.$$.json app/src/environments/environment.production.ts +awk "/app_host:.*,/ && !done { gsub(/app_host:.*,/, \"app_host: '$APP_LINK_HOST',\"); done=1}; 1" app/src/environments/environment.production.ts > tmp.$$.json && mv tmp.$$.json app/src/environments/environment.production.ts +awk "/custom_url_scheme:.*,/ && !done { gsub(/custom_url_scheme:.*,/, \"custom_url_scheme: '$APP_URL_SCHEME',\"); done=1}; 1" app/src/environments/environment.production.ts > tmp.$$.json && mv tmp.$$.json app/src/environments/environment.production.ts From 3eeab34bc10bf880ed645c25fa52bce76da6a9e0 Mon Sep 17 00:00:00 2001 From: Rainer Killinger <killinger@hrz.uni-frankfurt.de> Date: Thu, 10 Mar 2022 12:01:01 +0100 Subject: [PATCH 10/30] fix: add zip package to docker image --- Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index acc897a5..2231ad9b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,7 +9,8 @@ RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone RUN apt-get update && \ apt-get install -y --no-install-recommends \ xmlstarlet \ - ruby-full + ruby-full \ + zip RUN gem install bundler From 58ce196507c686bd513ae9bdab3d484a7837c163 Mon Sep 17 00:00:00 2001 From: Rainer Killinger <killinger@hrz.uni-frankfurt.de> Date: Mon, 14 Mar 2022 14:50:49 +0100 Subject: [PATCH 11/30] ci: refinie web deploy job --- .gitlab-ci.yml | 10 +++++-- Dockerfile | 3 +- Makefile | 2 +- app.conf.sample | 2 ++ static/scripts/ssh_deploy.sh | 53 ++++++++++++++++++++++++++++++++++++ 5 files changed, 66 insertions(+), 4 deletions(-) create mode 100644 static/scripts/ssh_deploy.sh diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 15f70bc3..f398c7ac 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -25,11 +25,17 @@ web: script: - > if [ "$RELEASE_TYPE" == "staging" ]; then - echo "Handle staging artifact here"; + # USE GITLAB PROTECTED & MASKED CI VARIABLES TO PROVIDE THE FOLLOWING DATA! + # USE AN UNPRIVILIGED USER WITH ACCESS ONLY TO THIS DIRECTORY + # example: $STAGING_SCP_TARGET = deployuser@staging.environment.com:/path/for/web/data + sh static/scripts/ssh_deploy.sh $STAGING_SCP_TARGET $STAGING_TARGET_SSH_PRIVATE_KEY fi if [ "$RELEASE_TYPE" == "production" ]; then - echo "Handle production artifact here"; + # USE GITLAB PROTECTED & MASKED CI VARIABLES TO PROVIDE THE FOLLOWING DATA! + # USE AN UNPRIVILIGED USER WITH ACCESS ONLY TO THIS DIRECTORY + # example: $PRODUCTION_SCP_TARGET = deployuser@production.environment.com:/path/for/web/data + sh static/scripts/ssh_deploy.sh $PRODUCTION_SCP_TARGET $PRODUCTION_TARGET_SSH_PRIVATE_KEY fi artifacts: untracked: false diff --git a/Dockerfile b/Dockerfile index 2231ad9b..ca732d17 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,7 +10,8 @@ RUN apt-get update && \ apt-get install -y --no-install-recommends \ xmlstarlet \ ruby-full \ - zip + zip \ + openssh-client RUN gem install bundler diff --git a/Makefile b/Makefile index 32dcb7e9..7c5aa2c4 100644 --- a/Makefile +++ b/Makefile @@ -27,7 +27,7 @@ web-build: configuration-web cd app && ionic build --prod web: web-build - cd app && zip -r ../www.zip www + cd app/www && zip -r ../../www.zip . echo "Web application artifact for version ${VERSION} is archived in www.zip" prepare-android: configuration-android diff --git a/app.conf.sample b/app.conf.sample index 202b77f1..4c218e55 100644 --- a/app.conf.sample +++ b/app.conf.sample @@ -20,6 +20,7 @@ APPLE_API_KEY_ID="123ACAB456" # Your API key i APPLE_API_KEY_ISSUER_ID="1234578-1234-1234-1234-12345678901" # Your API key issuer id # Provide the following environment variable in a secure fashion. +# When used in CI protect and mask the variable. # Don't change the following line! APPLE_API_KEY_CONTENT="${APPLE_API_KEY_CONTENT:-'unset'}" # Base64 encoded contents of Apple API key file (.p8 file) @@ -29,6 +30,7 @@ APPLE_API_KEY_CONTENT="${APPLE_API_KEY_CONTENT:-'unset'}" # Base64 encoded ANDROID_PACKAGE_NAME="de.anyschool.app.android" # Your Google Playconsole app package name # Provide the following environment variables in a secure fashion. +# When used in CI protect and mask the variables. # Don't change the following lines! ANDROID_API_KEY_CONTENT="${ANDROID_API_KEY_CONTENT:-'unset'}" # Base64 encoded contents of your API key file (.json file) ANDROID_KEYSTORE_PASSWORD="${ANDROID_KEYSTORE_PASSWORD:-'unset'}" # Passwort to your keyfile diff --git a/static/scripts/ssh_deploy.sh b/static/scripts/ssh_deploy.sh new file mode 100644 index 00000000..2d1bab2d --- /dev/null +++ b/static/scripts/ssh_deploy.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash + +set -e + +SSH_DEPLOY_TARGET=$1 +SSH_DEPLOY_TARGET="${SSH_DEPLOY_TARGET:-"missingtarget"}" + +SSH_PRIVATE_KEY=$2 +SSH_PRIVATE_KEY="${SSH_PRIVATE_KEY:-"missingkey"}" + +GOTO_FAIL=false + +if [ "$SSH_DEPLOY_TARGET" = "missingtarget" ]; then + echo "SSH target for web deployment job is unset!" + GOTO_FAIL=true +fi + +if [ "$SSH_PRIVATE_KEY" = "missingkey" ]; then + echo "SSH key for web deployment job is unset!" + GOTO_FAIL=true +fi + +if [ "$GOTO_FAIL" = true ]; then + return 1 +fi + +IFS='@' read -ra TARGET_COMPONENTS <<< "$SSH_DEPLOY_TARGET" + +SSH_DEPLOY_TARGET_USER="${TARGET_COMPONENTS[0]:-"missinguser"}" + +IFS=':' read -ra TARGET_COMPONENTS <<< "$TARGET_COMPONENTS" + +SSH_DEPLOY_TARGET_HOST="${TARGET_COMPONENTS[0]:-"missinghost"}" +SSH_DEPLOY_TARGET_PATH="${TARGET_COMPONENTS[1]:-"missingpath"}" + +## Add the SSH key stored in SSH_PRIVATE_KEY variable to the agent store +## We're using tr to fix line endings which makes ed25519 keys work +## without extra base64 encoding. +## https://gitlab.com/gitlab-examples/ssh-private-key/issues/1#note_48526556 +## +echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - +## +## Use ssh-keyscan to scan the keys of your private server. Replace gitlab.com +## with your own domain name. You can copy and repeat that command if you have +## more than one server to connect to. + +mkdir -p ~/.ssh +chmod 700 ~/.ssh +ssh-keyscan $SSH_DEPLOY_TARGET_HOST >> ~/.ssh/known_hosts +chmod 644 ~/.ssh/known_hosts + +scp www.zip "$SSH_DEPLOY_TARGET" +ssh "$SSH_DEPLOY_TARGET_USER@$SSH_DEPLOY_TARGET_HOST" "cd $SSH_DEPLOY_TARGET_PATH && unzip -o www.zip && rm -f www.zip" From 368c26ce8ca0819c280274b6f66332b242c5961b Mon Sep 17 00:00:00 2001 From: Rainer Killinger <killinger@hrz.uni-frankfurt.de> Date: Tue, 15 Mar 2022 13:27:39 +0100 Subject: [PATCH 12/30] fix: iOS fastfile behavior --- static/fastlane-ios/Fastfile | 69 +++++++++++++++++++----------------- 1 file changed, 37 insertions(+), 32 deletions(-) diff --git a/static/fastlane-ios/Fastfile b/static/fastlane-ios/Fastfile index a5ea4e53..201b6af0 100644 --- a/static/fastlane-ios/Fastfile +++ b/static/fastlane-ios/Fastfile @@ -13,37 +13,38 @@ # Uncomment the line if you want fastlane to automatically update itself # update_fastlane -require 'json' - -default_platform(:ios) - -current_build_number = 1 -package_json = JSON.parse(File.read('../../../package.json')) - -api_key = app_store_connect_api_key( - key_id: ENV['APPLE_API_KEY_ID'], - issuer_id: ENV['APPLE_API_KEY_ISSUER_ID'], - key_content: ENV['APPLE_API_KEY_CONTENT'], - is_key_content_base64: true, - duration: 1000, - in_house: false -) - -platform :ios do - - lane :fetch_highest_build_number do - build_number_candidates = [1] - build_number_candidates += latest_testflight_build_number( - version: package_json['version'], - initial_build_number: 1, - api_key: api_key - ) - build_number_candidates += app_store_build_number( - version: package_json['version'], - initial_build_number: 1, - api_key: api_key - ) - current_build_number = build_number_candidates.compact.max +require 'json' +require 'base64' + +default_platform(:ios) + +current_build_number = 1 +package_json = JSON.parse(File.read('../../../package.json')) + +api_key = app_store_connect_api_key( + key_id: ENV['APPLE_API_KEY_ID'], + issuer_id: ENV['APPLE_API_KEY_ISSUER_ID'], + key_content: "#{Base64.decode64(ENV['APPLE_API_KEY_CONTENT'])}".gsub('\n', '\\n'), + in_house: false +) + +platform :ios do + + lane :fetch_highest_build_number do + build_number_candidates = [1] + build_number_candidates << latest_testflight_build_number( + version: package_json['version'], + initial_build_number: 1, + app_identifier: ENV['IOS_BUNDLE_IDENTIFIER'], + api_key: api_key + ) + build_number_candidates << app_store_build_number( + version: package_json['version'], + initial_build_number: 1, + app_identifier: ENV['IOS_BUNDLE_IDENTIFIER'], + api_key: api_key + ) + current_build_number = build_number_candidates.max end lane :configure do @@ -92,7 +93,11 @@ platform :ios do lane :release do configure build - #upload_to_app_store(skip_metadata: true, skip_screenshots: true, api_key: api_key) + upload_to_app_store( + skip_metadata: true, + skip_screenshots: true, + api_key: api_key + ) end desc "Submit a new version to Testflight" From ec8b553d2ba98b0eb74746329b420f9fa3395031 Mon Sep 17 00:00:00 2001 From: Rainer Killinger <killinger@hrz.uni-frankfurt.de> Date: Tue, 15 Mar 2022 17:56:15 +0100 Subject: [PATCH 13/30] refactor: handle ssh secrets as base64 encoded --- .gitlab-ci.yml | 2 ++ static/fastlane-ios/Fastfile | 64 ++++++++++++++++++------------------ static/scripts/ssh_deploy.sh | 26 ++++++++------- 3 files changed, 49 insertions(+), 43 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f398c7ac..69777991 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -26,6 +26,7 @@ web: - > if [ "$RELEASE_TYPE" == "staging" ]; then # USE GITLAB PROTECTED & MASKED CI VARIABLES TO PROVIDE THE FOLLOWING DATA! + # THUS $STAGING_TARGET_SSH_PRIVATE_KEY HAS TO BE BASE64 ENCODED # USE AN UNPRIVILIGED USER WITH ACCESS ONLY TO THIS DIRECTORY # example: $STAGING_SCP_TARGET = deployuser@staging.environment.com:/path/for/web/data sh static/scripts/ssh_deploy.sh $STAGING_SCP_TARGET $STAGING_TARGET_SSH_PRIVATE_KEY @@ -33,6 +34,7 @@ web: if [ "$RELEASE_TYPE" == "production" ]; then # USE GITLAB PROTECTED & MASKED CI VARIABLES TO PROVIDE THE FOLLOWING DATA! + # THUS $PRODUCTION_TARGET_SSH_PRIVATE_KEY HAS TO BE BASE64 ENCODED # USE AN UNPRIVILIGED USER WITH ACCESS ONLY TO THIS DIRECTORY # example: $PRODUCTION_SCP_TARGET = deployuser@production.environment.com:/path/for/web/data sh static/scripts/ssh_deploy.sh $PRODUCTION_SCP_TARGET $PRODUCTION_TARGET_SSH_PRIVATE_KEY diff --git a/static/fastlane-ios/Fastfile b/static/fastlane-ios/Fastfile index 201b6af0..ffb1ea19 100644 --- a/static/fastlane-ios/Fastfile +++ b/static/fastlane-ios/Fastfile @@ -13,38 +13,38 @@ # Uncomment the line if you want fastlane to automatically update itself # update_fastlane -require 'json' -require 'base64' - -default_platform(:ios) - -current_build_number = 1 -package_json = JSON.parse(File.read('../../../package.json')) - -api_key = app_store_connect_api_key( - key_id: ENV['APPLE_API_KEY_ID'], - issuer_id: ENV['APPLE_API_KEY_ISSUER_ID'], - key_content: "#{Base64.decode64(ENV['APPLE_API_KEY_CONTENT'])}".gsub('\n', '\\n'), - in_house: false -) - -platform :ios do - - lane :fetch_highest_build_number do - build_number_candidates = [1] - build_number_candidates << latest_testflight_build_number( - version: package_json['version'], - initial_build_number: 1, - app_identifier: ENV['IOS_BUNDLE_IDENTIFIER'], - api_key: api_key - ) - build_number_candidates << app_store_build_number( - version: package_json['version'], - initial_build_number: 1, - app_identifier: ENV['IOS_BUNDLE_IDENTIFIER'], - api_key: api_key - ) - current_build_number = build_number_candidates.max +require 'json' +require 'base64' + +default_platform(:ios) + +current_build_number = 1 +package_json = JSON.parse(File.read('../../../package.json')) + +api_key = app_store_connect_api_key( + key_id: ENV['APPLE_API_KEY_ID'], + issuer_id: ENV['APPLE_API_KEY_ISSUER_ID'], + key_content: "#{Base64.decode64(ENV['APPLE_API_KEY_CONTENT'])}".gsub('\n', '\\n'), + in_house: false +) + +platform :ios do + + lane :fetch_highest_build_number do + build_number_candidates = [1] + build_number_candidates << latest_testflight_build_number( + version: package_json['version'], + initial_build_number: 1, + app_identifier: ENV['IOS_BUNDLE_IDENTIFIER'], + api_key: api_key + ) + build_number_candidates << app_store_build_number( + version: package_json['version'], + initial_build_number: 1, + app_identifier: ENV['IOS_BUNDLE_IDENTIFIER'], + api_key: api_key + ) + current_build_number = build_number_candidates.max end lane :configure do diff --git a/static/scripts/ssh_deploy.sh b/static/scripts/ssh_deploy.sh index 2d1bab2d..56b5da2d 100644 --- a/static/scripts/ssh_deploy.sh +++ b/static/scripts/ssh_deploy.sh @@ -1,12 +1,12 @@ -#!/usr/bin/env bash +#!/usr/bin/env sh set -e SSH_DEPLOY_TARGET=$1 -SSH_DEPLOY_TARGET="${SSH_DEPLOY_TARGET:-"missingtarget"}" +SSH_DEPLOY_TARGET="${SSH_DEPLOY_TARGET:-'missingtarget'}" SSH_PRIVATE_KEY=$2 -SSH_PRIVATE_KEY="${SSH_PRIVATE_KEY:-"missingkey"}" +SSH_PRIVATE_KEY="${SSH_PRIVATE_KEY:-'missingkey'}" GOTO_FAIL=false @@ -24,28 +24,32 @@ if [ "$GOTO_FAIL" = true ]; then return 1 fi -IFS='@' read -ra TARGET_COMPONENTS <<< "$SSH_DEPLOY_TARGET" -SSH_DEPLOY_TARGET_USER="${TARGET_COMPONENTS[0]:-"missinguser"}" +TARGET_COMPONENTS=$(echo "$SSH_DEPLOY_TARGET" | tr '@' "\n") +TARGET_COMPONENTS=$(echo "$TARGET_COMPONENTS" | tr ':' "\n") -IFS=':' read -ra TARGET_COMPONENTS <<< "$TARGET_COMPONENTS" +SSH_DEPLOY_TARGET_USER=$(echo "$TARGET_COMPONENTS" | head -n 1 | tail -n 1) +SSH_DEPLOY_TARGET_HOST=$(echo "$TARGET_COMPONENTS" | head -n 2 | tail -n 1) +SSH_DEPLOY_TARGET_PATH=$(echo "$TARGET_COMPONENTS" | head -n 3 | tail -n 1) -SSH_DEPLOY_TARGET_HOST="${TARGET_COMPONENTS[0]:-"missinghost"}" -SSH_DEPLOY_TARGET_PATH="${TARGET_COMPONENTS[1]:-"missingpath"}" +SSH_DEPLOY_TARGET_USER="${SSH_DEPLOY_TARGET_USER:-'missinguser'}" +SSH_DEPLOY_TARGET_HOST="${SSH_DEPLOY_TARGET_HOST:-'missinghost'}" +SSH_DEPLOY_TARGET_PATH="${SSH_DEPLOY_TARGET_PATH:-'missingpath'}" ## Add the SSH key stored in SSH_PRIVATE_KEY variable to the agent store ## We're using tr to fix line endings which makes ed25519 keys work ## without extra base64 encoding. ## https://gitlab.com/gitlab-examples/ssh-private-key/issues/1#note_48526556 ## -echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - +mkdir -p ~/.ssh +chmod 700 ~/.ssh +eval `ssh-agent -s` +echo "$SSH_PRIVATE_KEY" | base64 -d | tr -d '\r' | ssh-add - ## ## Use ssh-keyscan to scan the keys of your private server. Replace gitlab.com ## with your own domain name. You can copy and repeat that command if you have ## more than one server to connect to. -mkdir -p ~/.ssh -chmod 700 ~/.ssh ssh-keyscan $SSH_DEPLOY_TARGET_HOST >> ~/.ssh/known_hosts chmod 644 ~/.ssh/known_hosts From 87323e70cfcf1773c9f2e8ebbcebedad112cf4af Mon Sep 17 00:00:00 2001 From: Rainer Killinger <killinger@hrz.uni-frankfurt.de> Date: Mon, 21 Mar 2022 13:51:49 +0100 Subject: [PATCH 14/30] fix: shell variable substitution statements --- app.conf.sample | 10 +++++----- static/scripts/ssh_deploy.sh | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) mode change 100644 => 100755 static/scripts/ssh_deploy.sh diff --git a/app.conf.sample b/app.conf.sample index 4c218e55..0583c368 100644 --- a/app.conf.sample +++ b/app.conf.sample @@ -22,7 +22,7 @@ APPLE_API_KEY_ISSUER_ID="1234578-1234-1234-1234-12345678901" # Your API key i # Provide the following environment variable in a secure fashion. # When used in CI protect and mask the variable. # Don't change the following line! -APPLE_API_KEY_CONTENT="${APPLE_API_KEY_CONTENT:-'unset'}" # Base64 encoded contents of Apple API key file (.p8 file) +APPLE_API_KEY_CONTENT="${APPLE_API_KEY_CONTENT:-"unset"}" # Base64 encoded contents of Apple API key file (.p8 file) # Android specific configuration @@ -32,7 +32,7 @@ ANDROID_PACKAGE_NAME="de.anyschool.app.android" # Your # Provide the following environment variables in a secure fashion. # When used in CI protect and mask the variables. # Don't change the following lines! -ANDROID_API_KEY_CONTENT="${ANDROID_API_KEY_CONTENT:-'unset'}" # Base64 encoded contents of your API key file (.json file) -ANDROID_KEYSTORE_PASSWORD="${ANDROID_KEYSTORE_PASSWORD:-'unset'}" # Passwort to your keyfile -ANDROID_KEYSTORE_KEY_ALIAS="${ANDROID_KEYSTORE_KEY_ALIAS:-'unset'}" # Name/Alias of the key used for signing within the keyfile -ANDROID_KEYSTORE_KEY_PASSWORD="${ANDROID_KEYSTORE_KEY_PASSWORD:-'unset'}" # Passwort to the this very key +ANDROID_API_KEY_CONTENT="${ANDROID_API_KEY_CONTENT:-"unset"}" # Base64 encoded contents of your API key file (.json file) +ANDROID_KEYSTORE_PASSWORD="${ANDROID_KEYSTORE_PASSWORD:-"unset"}" # Passwort to your keyfile +ANDROID_KEYSTORE_KEY_ALIAS="${ANDROID_KEYSTORE_KEY_ALIAS:-"unset"}" # Name/Alias of the key used for signing within the keyfile +ANDROID_KEYSTORE_KEY_PASSWORD="${ANDROID_KEYSTORE_KEY_PASSWORD:-"unset"}" # Passwort to the this very key diff --git a/static/scripts/ssh_deploy.sh b/static/scripts/ssh_deploy.sh old mode 100644 new mode 100755 index 56b5da2d..f3d735e7 --- a/static/scripts/ssh_deploy.sh +++ b/static/scripts/ssh_deploy.sh @@ -3,10 +3,10 @@ set -e SSH_DEPLOY_TARGET=$1 -SSH_DEPLOY_TARGET="${SSH_DEPLOY_TARGET:-'missingtarget'}" +SSH_DEPLOY_TARGET="${SSH_DEPLOY_TARGET:-"missingtarget"}" SSH_PRIVATE_KEY=$2 -SSH_PRIVATE_KEY="${SSH_PRIVATE_KEY:-'missingkey'}" +SSH_PRIVATE_KEY="${SSH_PRIVATE_KEY:-"missingkey"}" GOTO_FAIL=false @@ -32,9 +32,9 @@ SSH_DEPLOY_TARGET_USER=$(echo "$TARGET_COMPONENTS" | head -n 1 | tail -n 1) SSH_DEPLOY_TARGET_HOST=$(echo "$TARGET_COMPONENTS" | head -n 2 | tail -n 1) SSH_DEPLOY_TARGET_PATH=$(echo "$TARGET_COMPONENTS" | head -n 3 | tail -n 1) -SSH_DEPLOY_TARGET_USER="${SSH_DEPLOY_TARGET_USER:-'missinguser'}" -SSH_DEPLOY_TARGET_HOST="${SSH_DEPLOY_TARGET_HOST:-'missinghost'}" -SSH_DEPLOY_TARGET_PATH="${SSH_DEPLOY_TARGET_PATH:-'missingpath'}" +SSH_DEPLOY_TARGET_USER="${SSH_DEPLOY_TARGET_USER:-"missinguser"}" +SSH_DEPLOY_TARGET_HOST="${SSH_DEPLOY_TARGET_HOST:-"missinghost"}" +SSH_DEPLOY_TARGET_PATH="${SSH_DEPLOY_TARGET_PATH:-"missingpath"}" ## Add the SSH key stored in SSH_PRIVATE_KEY variable to the agent store ## We're using tr to fix line endings which makes ed25519 keys work From e9bd2b7a6c05f33420f2dc62c70b44f6b7e85e88 Mon Sep 17 00:00:00 2001 From: Rainer Killinger <killinger@hrz.uni-frankfurt.de> Date: Thu, 14 Apr 2022 16:09:13 +0200 Subject: [PATCH 15/30] ci: actually make web target --- .gitlab-ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 69777991..cfc6ff9a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -23,6 +23,7 @@ web: image: registry.gitlab.com/openstapps/app-release-template stage: deploy script: + - make web - > if [ "$RELEASE_TYPE" == "staging" ]; then # USE GITLAB PROTECTED & MASKED CI VARIABLES TO PROVIDE THE FOLLOWING DATA! From 25171fe4d7c2531cd591e8acd8d8329dd81f1db8 Mon Sep 17 00:00:00 2001 From: Rainer Killinger <killinger@hrz.uni-frankfurt.de> Date: Mon, 25 Apr 2022 17:19:30 +0200 Subject: [PATCH 16/30] refactor: check out gradle props on android builds --- static/scripts/android.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/static/scripts/android.sh b/static/scripts/android.sh index f61d1df7..09ca6624 100755 --- a/static/scripts/android.sh +++ b/static/scripts/android.sh @@ -10,6 +10,7 @@ ANDROID_STRINGS_PATH="app/android/app/src/main/res/values/strings.xml" git -C app checkout -- android/build.gradle +git -C app checkout -- android/gradle/wrapper/gradle-wrapper.properties git -C app checkout -- android/app/src/main/AndroidManifest.xml # AndroidManifest.xml From 7111f44fb0f2d1b2eac9bd52d120e7de2253ade3 Mon Sep 17 00:00:00 2001 From: Rainer Killinger <killinger@hrz.uni-frankfurt.de> Date: Fri, 20 May 2022 11:44:15 +0200 Subject: [PATCH 17/30] refactor: make local copy of keystore for gradle --- Makefile | 2 +- static/fastlane-android/Fastfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 7c5aa2c4..d8cc32ba 100644 --- a/Makefile +++ b/Makefile @@ -32,7 +32,7 @@ web: web-build prepare-android: configuration-android source app.conf && cd app && rm -rf android www && ionic capacitor add android && npm run resources:android && ionic capacitor build android --no-open --prod && cd .. && sh static/scripts/android.sh - cp -rf static/fastlane-android/. app/android/fastlane/ && cp -rf app.conf app/android/.env + cp -rf static/fastlane-android/. app/android/fastlane/ && cp -rf app.conf app/android/.env && cp -rf playstore.keystore app/android/playstore.keystore android: prepare-android cd app/android && bundler exec fastlane android release diff --git a/static/fastlane-android/Fastfile b/static/fastlane-android/Fastfile index c758d6d5..ebe207ce 100644 --- a/static/fastlane-android/Fastfile +++ b/static/fastlane-android/Fastfile @@ -52,7 +52,7 @@ platform :android do build_type: "Release", print_command: false, properties: { - "android.injected.signing.store.file" => "/build/playstore.keystore", + "android.injected.signing.store.file" => "playstore.keystore", "android.injected.signing.store.password" => ENV['ANDROID_KEYSTORE_PASSWORD'], "android.injected.signing.key.alias" => ENV['ANDROID_KEYSTORE_KEY_ALIAS'], "android.injected.signing.key.password" => ENV['ANDROID_KEYSTORE_KEY_PASSWORD'], From 9db2489ab27c210fe2ece9de4b174d45e5c4bfc8 Mon Sep 17 00:00:00 2001 From: Rainer Killinger <killinger@hrz.uni-frankfurt.de> Date: Fri, 20 May 2022 13:30:36 +0200 Subject: [PATCH 18/30] fix: relative keystore file location for gradle --- Makefile | 2 +- static/fastlane-android/Fastfile | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index d8cc32ba..7c5aa2c4 100644 --- a/Makefile +++ b/Makefile @@ -32,7 +32,7 @@ web: web-build prepare-android: configuration-android source app.conf && cd app && rm -rf android www && ionic capacitor add android && npm run resources:android && ionic capacitor build android --no-open --prod && cd .. && sh static/scripts/android.sh - cp -rf static/fastlane-android/. app/android/fastlane/ && cp -rf app.conf app/android/.env && cp -rf playstore.keystore app/android/playstore.keystore + cp -rf static/fastlane-android/. app/android/fastlane/ && cp -rf app.conf app/android/.env android: prepare-android cd app/android && bundler exec fastlane android release diff --git a/static/fastlane-android/Fastfile b/static/fastlane-android/Fastfile index ebe207ce..af82a501 100644 --- a/static/fastlane-android/Fastfile +++ b/static/fastlane-android/Fastfile @@ -15,12 +15,14 @@ require 'json' require 'base64' +require 'pathname' default_platform(:android) current_version_code = 1 playstore_track = "internal" package_json = JSON.parse(File.read('../../package.json')) +keystorePath = Pathname.getwd.parent + "../../playstore.keystore" platform :android do @@ -52,7 +54,7 @@ platform :android do build_type: "Release", print_command: false, properties: { - "android.injected.signing.store.file" => "playstore.keystore", + "android.injected.signing.store.file" => keystorePath.to_s, "android.injected.signing.store.password" => ENV['ANDROID_KEYSTORE_PASSWORD'], "android.injected.signing.key.alias" => ENV['ANDROID_KEYSTORE_KEY_ALIAS'], "android.injected.signing.key.password" => ENV['ANDROID_KEYSTORE_KEY_PASSWORD'], From 68877fe85069efdb2cc8c6cc54b76fd80928622c Mon Sep 17 00:00:00 2001 From: Rainer Killinger <killinger@hrz.uni-frankfurt.de> Date: Mon, 8 Aug 2022 15:07:49 +0200 Subject: [PATCH 19/30] refactor: update .gitignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 58e1763a..f5227102 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ app -Gemfile.lock \ No newline at end of file +Gemfile.lock +.vscode \ No newline at end of file From e9bedb78c6283313ebcec9619ba647ff6f95dc5b Mon Sep 17 00:00:00 2001 From: Rainer Killinger <killinger@hrz.uni-frankfurt.de> Date: Mon, 8 Aug 2022 15:10:20 +0200 Subject: [PATCH 20/30] refactor: add profile card dummy icon --- .../assets/imgs/profile-card-head.svg | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 customizable/assets/imgs/profile-card-head.svg diff --git a/customizable/assets/imgs/profile-card-head.svg b/customizable/assets/imgs/profile-card-head.svg new file mode 100644 index 00000000..c3ca1a54 --- /dev/null +++ b/customizable/assets/imgs/profile-card-head.svg @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + class="ionicon" + viewBox="0 0 512 512" + version="1.1" + id="svg6" + sodipodi:docname="person.svg" + inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"> + <metadata + id="metadata12"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title>Person</dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <defs + id="defs10" /> + <sodipodi:namedview + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1" + objecttolerance="10" + gridtolerance="10" + guidetolerance="10" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:window-width="2586" + inkscape:window-height="1500" + id="namedview8" + showgrid="false" + inkscape:zoom="1.3037281" + inkscape:cx="242.86595" + inkscape:cy="256.12612" + inkscape:window-x="72" + inkscape:window-y="27" + inkscape:window-maximized="0" + inkscape:current-layer="svg6" /> + <title + id="title2">Person + + From 9ad402842d80a127f6a6692ee594e47487f9df73 Mon Sep 17 00:00:00 2001 From: Rainer Killinger Date: Fri, 26 Aug 2022 12:02:29 +0200 Subject: [PATCH 21/30] feat: automate generation for universal link files --- .gitignore | 3 +- Makefile | 2 + static/scripts/universal_link_files.sh | 58 ++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 static/scripts/universal_link_files.sh diff --git a/.gitignore b/.gitignore index f5227102..fe63bea5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ app Gemfile.lock -.vscode \ No newline at end of file +.vscode +.DS_Store \ No newline at end of file diff --git a/Makefile b/Makefile index 7c5aa2c4..95dc195f 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,7 @@ SHELL := /bin/bash APP_DIR := $(PWD)/app BRANCH ?= develop +START_TIME := $(date +%s) clean: rm -rf app @@ -27,6 +28,7 @@ web-build: configuration-web cd app && ionic build --prod web: web-build + mkdir -p app/www/.well-known && source app.conf && sh static/scripts/universal_link_files.sh cd app/www && zip -r ../../www.zip . echo "Web application artifact for version ${VERSION} is archived in www.zip" diff --git a/static/scripts/universal_link_files.sh b/static/scripts/universal_link_files.sh new file mode 100644 index 00000000..3595ad4e --- /dev/null +++ b/static/scripts/universal_link_files.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env sh + +. $PWD/app.conf + +# iOS universal link file generation +if [ -z "$TEAM_ID" ] && [ -z "$IOS_BUNDLE_IDENTIFIER" ] +then + echo "Unable to find Apple Team ID and bundle identifier. Skipping apple-app-site-association generation..." +else + file_content=$(cat < $PWD/app/www/.well-known/apple-app-site-association +fi + + +# Google Play/Android universal link file generation +KEYTOOL_INFO=$(keytool -list -v -keystore ./playstore.keystore -alias $ANDROID_KEYSTORE_KEY_ALIAS -storepass $ANDROID_KEYSTORE_PASSWORD -keypass $ANDROID_KEYSTORE_KEY_PASSWORD || true) +CERT_FINGERPRINT=$(echo $KEYTOOL_INFO | sed -n 's/.*SHA256: //p') + +if [ -z "$CERT_FINGERPRINT" ] +then + echo "Unable to retrieve Android sigining SHA256 fingerprint. Skipping assetlinks.json generation..." +else + file_content=$(cat < $PWD/app/www/.well-known/assetlinks.json +fi From 7bfc3fc026b94979698d85374caf9577e36ceaa2 Mon Sep 17 00:00:00 2001 From: Rainer Killinger Date: Mon, 12 Sep 2022 14:49:54 +0200 Subject: [PATCH 22/30] fix: setting version code via fastlane plugin --- Gemfile | 3 ++- static/fastlane-android/Fastfile | 15 ++++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/Gemfile b/Gemfile index adc90d98..b8c8b339 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,4 @@ source "https://rubygems.org" -gem "fastlane" \ No newline at end of file +gem "fastlane" +gem "fastlane-plugin-versioning_android" \ No newline at end of file diff --git a/static/fastlane-android/Fastfile b/static/fastlane-android/Fastfile index af82a501..3d614e0d 100644 --- a/static/fastlane-android/Fastfile +++ b/static/fastlane-android/Fastfile @@ -45,10 +45,17 @@ platform :android do ) end current_version_code = version_code_candidates.compact.max + puts "Version code chosen for build lane: %d" % [current_version_code + 1] end lane :build do fetch_highest_version_code + android_set_version_name( + version_name: package_json["version"], + ) + android_set_version_code( + version_code: current_version_code + 1, + ) gradle( task: "clean assemble", build_type: "Release", @@ -57,16 +64,14 @@ platform :android do "android.injected.signing.store.file" => keystorePath.to_s, "android.injected.signing.store.password" => ENV['ANDROID_KEYSTORE_PASSWORD'], "android.injected.signing.key.alias" => ENV['ANDROID_KEYSTORE_KEY_ALIAS'], - "android.injected.signing.key.password" => ENV['ANDROID_KEYSTORE_KEY_PASSWORD'], - "android.injected.version.code" => current_version_code + 1, - "android.injected.version.name" => package_json['version'] + "android.injected.signing.key.password" => ENV['ANDROID_KEYSTORE_KEY_PASSWORD'] } ) end - desc "Submit a new beta build to internal testing track" + desc "Submit a new beta build to open beta testing track" lane :beta do - playstore_track = "internal" + playstore_track = "beta" build upload_to_play_store( From 1af9140ceaa6873d47272bda24c081365e0cd6f6 Mon Sep 17 00:00:00 2001 From: Rainer Killinger Date: Tue, 20 Sep 2022 16:11:53 +0200 Subject: [PATCH 23/30] refactor: set supported orientations for iOS build --- static/scripts/ios.sh | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/static/scripts/ios.sh b/static/scripts/ios.sh index 37ed1714..9a61e663 100755 --- a/static/scripts/ios.sh +++ b/static/scripts/ios.sh @@ -14,4 +14,24 @@ INFOPLIST_FILE="app/ios/App/App/Info.plist" /usr/libexec/PlistBuddy -c "Add :BGTaskSchedulerPermittedIdentifiers array" $INFOPLIST_FILE /usr/libexec/PlistBuddy -c "Add :BGTaskSchedulerPermittedIdentifiers:0 string com.transistorsoft.fetch" $INFOPLIST_FILE +/usr/libexec/PlistBuddy -c "Delete :UISupportedInterfaceOrientations array" $INFOPLIST_FILE +/usr/libexec/PlistBuddy -c "Add :UISupportedInterfaceOrientations:0 string \"UIInterfaceOrientationPortrait\"" $INFOPLIST_FILE + +/usr/libexec/PlistBuddy -c "Delete :UISupportedInterfaceOrientations~iphone array" $INFOPLIST_FILE + +/usr/libexec/PlistBuddy -c "Delete :UISupportedInterfaceOrientations~ipad array" $INFOPLIST_FILE +/usr/libexec/PlistBuddy -c "Add :UISupportedInterfaceOrientations~ipad:0 string \"UIInterfaceOrientationPortrait\"" $INFOPLIST_FILE +/usr/libexec/PlistBuddy -c "Add :UISupportedInterfaceOrientations~ipad:1 string \"UIInterfaceOrientationPortraitUpsideDown\"" $INFOPLIST_FILE +/usr/libexec/PlistBuddy -c "Add :UISupportedInterfaceOrientations~ipad:2 string \"UIInterfaceOrientationLandscapeLeft\"" $INFOPLIST_FILE +/usr/libexec/PlistBuddy -c "Add :UISupportedInterfaceOrientations~ipad:3 string \"UIInterfaceOrientationLandscapeRight\"" $INFOPLIST_FILE + +/usr/libexec/PlistBuddy -c "Delete :UIViewControllerBasedStatusBarAppearance bool" $INFOPLIST_FILE +/usr/libexec/PlistBuddy -c "Add :UIViewControllerBasedStatusBarAppearance bool" $INFOPLIST_FILE +/usr/libexec/PlistBuddy -c "Set :UIViewControllerBasedStatusBarAppearance NO" $INFOPLIST_FILE + +/usr/libexec/PlistBuddy -c "Delete :UIStatusBarStyle string" $INFOPLIST_FILE +/usr/libexec/PlistBuddy -c "Add :UIStatusBarStyle string" $INFOPLIST_FILE +/usr/libexec/PlistBuddy -c "Set :UIStatusBarStyle UIStatusBarStyleLightContent" $INFOPLIST_FILE + + git -C app checkout -- ios/App/App/AppDelegate.swift From 2da9e8ad1eea716424fc3d8ca2c7276d78df3715 Mon Sep 17 00:00:00 2001 From: Rainer Killinger Date: Fri, 30 Sep 2022 14:54:13 +0200 Subject: [PATCH 24/30] fix: include styles.xml for android builds --- static/scripts/android.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/static/scripts/android.sh b/static/scripts/android.sh index 09ca6624..23df3e8f 100755 --- a/static/scripts/android.sh +++ b/static/scripts/android.sh @@ -12,6 +12,7 @@ ANDROID_STRINGS_PATH="app/android/app/src/main/res/values/strings.xml" git -C app checkout -- android/build.gradle git -C app checkout -- android/gradle/wrapper/gradle-wrapper.properties git -C app checkout -- android/app/src/main/AndroidManifest.xml +git -C app checkout -- android/app/src/main/res/values/styles.xml # AndroidManifest.xml xmlstarlet edit --pf --inplace \ From 7799b5187a39ae8da42bd1379de84c66ff8988f4 Mon Sep 17 00:00:00 2001 From: Rainer Killinger Date: Fri, 30 Sep 2022 15:09:07 +0200 Subject: [PATCH 25/30] fix: malformed supported orientations on iOS --- static/scripts/ios.sh | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/static/scripts/ios.sh b/static/scripts/ios.sh index 9a61e663..feac3642 100755 --- a/static/scripts/ios.sh +++ b/static/scripts/ios.sh @@ -14,12 +14,14 @@ INFOPLIST_FILE="app/ios/App/App/Info.plist" /usr/libexec/PlistBuddy -c "Add :BGTaskSchedulerPermittedIdentifiers array" $INFOPLIST_FILE /usr/libexec/PlistBuddy -c "Add :BGTaskSchedulerPermittedIdentifiers:0 string com.transistorsoft.fetch" $INFOPLIST_FILE -/usr/libexec/PlistBuddy -c "Delete :UISupportedInterfaceOrientations array" $INFOPLIST_FILE -/usr/libexec/PlistBuddy -c "Add :UISupportedInterfaceOrientations:0 string \"UIInterfaceOrientationPortrait\"" $INFOPLIST_FILE - /usr/libexec/PlistBuddy -c "Delete :UISupportedInterfaceOrientations~iphone array" $INFOPLIST_FILE +/usr/libexec/PlistBuddy -c "Delete :UISupportedInterfaceOrientations array" $INFOPLIST_FILE +/usr/libexec/PlistBuddy -c "Add :UISupportedInterfaceOrientations array" $INFOPLIST_FILE +/usr/libexec/PlistBuddy -c "Add :UISupportedInterfaceOrientations:0 string \"UIInterfaceOrientationPortrait\"" $INFOPLIST_FILE + /usr/libexec/PlistBuddy -c "Delete :UISupportedInterfaceOrientations~ipad array" $INFOPLIST_FILE +/usr/libexec/PlistBuddy -c "Add :UISupportedInterfaceOrientations~ipad array" $INFOPLIST_FILE /usr/libexec/PlistBuddy -c "Add :UISupportedInterfaceOrientations~ipad:0 string \"UIInterfaceOrientationPortrait\"" $INFOPLIST_FILE /usr/libexec/PlistBuddy -c "Add :UISupportedInterfaceOrientations~ipad:1 string \"UIInterfaceOrientationPortraitUpsideDown\"" $INFOPLIST_FILE /usr/libexec/PlistBuddy -c "Add :UISupportedInterfaceOrientations~ipad:2 string \"UIInterfaceOrientationLandscapeLeft\"" $INFOPLIST_FILE From d1b78491c36edd3d1259051241c74e065af87b17 Mon Sep 17 00:00:00 2001 From: Rainer Killinger Date: Wed, 7 Dec 2022 20:18:32 +0100 Subject: [PATCH 26/30] fix: invalid assetlinks.json key fingerprint --- static/scripts/universal_link_files.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/scripts/universal_link_files.sh b/static/scripts/universal_link_files.sh index 3595ad4e..a037f4a4 100644 --- a/static/scripts/universal_link_files.sh +++ b/static/scripts/universal_link_files.sh @@ -31,7 +31,7 @@ fi # Google Play/Android universal link file generation KEYTOOL_INFO=$(keytool -list -v -keystore ./playstore.keystore -alias $ANDROID_KEYSTORE_KEY_ALIAS -storepass $ANDROID_KEYSTORE_PASSWORD -keypass $ANDROID_KEYSTORE_KEY_PASSWORD || true) -CERT_FINGERPRINT=$(echo $KEYTOOL_INFO | sed -n 's/.*SHA256: //p') +CERT_FINGERPRINT=$(echo $KEYTOOL_INFO | grep -Po '(?:[a-fA-F0-9]{2}:){31}[a-fA-F0-9]{2}') if [ -z "$CERT_FINGERPRINT" ] then From 7f0c54a826f3f5f06723d51e1bb4e3138771b817 Mon Sep 17 00:00:00 2001 From: Rainer Killinger Date: Wed, 11 Jan 2023 15:18:30 +0100 Subject: [PATCH 27/30] ci: allow develop branch to deploy to web staging --- .gitlab-ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index cfc6ff9a..74b6e8b5 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -47,6 +47,7 @@ web: tags: - secrecy rules: + - if: '$CI_COMMIT_BRANCH == "develop" && $RELEASE_TYPE == "staging"' - if: '$CI_COMMIT_BRANCH == "main" && $RELEASE_TYPE == "staging"' - if: '$CI_COMMIT_BRANCH == "main" && $RELEASE_TYPE == "production"' From a24e2f36fd2ba3d86482e348110a861a8b8f64f9 Mon Sep 17 00:00:00 2001 From: Rainer Killinger Date: Mon, 13 Feb 2023 18:46:35 +0100 Subject: [PATCH 28/30] refactor: default to production releases --- Makefile | 2 +- static/fastlane-android/Fastfile | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 95dc195f..24738b1b 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ SHELL := /bin/bash APP_DIR := $(PWD)/app -BRANCH ?= develop +BRANCH ?= main START_TIME := $(date +%s) clean: diff --git a/static/fastlane-android/Fastfile b/static/fastlane-android/Fastfile index 3d614e0d..7701d456 100644 --- a/static/fastlane-android/Fastfile +++ b/static/fastlane-android/Fastfile @@ -85,7 +85,15 @@ platform :android do desc "Submit a new version to the Google Play" lane :release do + playstore_track = "production" + build - #upload_to_play_store(json_key: '../../playstore_api_key.json', skip_upload_metadata: true, skip_upload_images: true) + upload_to_play_store( + track: playstore_track, + json_key_data: Base64.decode64(ENV['ANDROID_API_KEY_CONTENT']), + skip_upload_metadata: true, + skip_upload_images: true, + skip_upload_screenshots: true + ) end end From fdf799943758eefc8ebe3e2b3b85ece83e01d811 Mon Sep 17 00:00:00 2001 From: Rainer Killinger Date: Wed, 15 Feb 2023 11:46:23 +0100 Subject: [PATCH 29/30] feat: add apk deploy via scp --- .gitlab-ci.yml | 12 ++++++++++-- README.md | 1 + static/scripts/ssh_deploy.sh | 29 +++++++++++++++++++++-------- 3 files changed, 32 insertions(+), 10 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 74b6e8b5..9659dcdf 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -30,7 +30,7 @@ web: # THUS $STAGING_TARGET_SSH_PRIVATE_KEY HAS TO BE BASE64 ENCODED # USE AN UNPRIVILIGED USER WITH ACCESS ONLY TO THIS DIRECTORY # example: $STAGING_SCP_TARGET = deployuser@staging.environment.com:/path/for/web/data - sh static/scripts/ssh_deploy.sh $STAGING_SCP_TARGET $STAGING_TARGET_SSH_PRIVATE_KEY + sh static/scripts/ssh_deploy.sh web $STAGING_SCP_TARGET $STAGING_TARGET_SSH_PRIVATE_KEY fi if [ "$RELEASE_TYPE" == "production" ]; then @@ -38,7 +38,7 @@ web: # THUS $PRODUCTION_TARGET_SSH_PRIVATE_KEY HAS TO BE BASE64 ENCODED # USE AN UNPRIVILIGED USER WITH ACCESS ONLY TO THIS DIRECTORY # example: $PRODUCTION_SCP_TARGET = deployuser@production.environment.com:/path/for/web/data - sh static/scripts/ssh_deploy.sh $PRODUCTION_SCP_TARGET $PRODUCTION_TARGET_SSH_PRIVATE_KEY + sh static/scripts/ssh_deploy.sh web $PRODUCTION_SCP_TARGET $PRODUCTION_TARGET_SSH_PRIVATE_KEY fi artifacts: untracked: false @@ -82,6 +82,14 @@ android: if [ "$RELEASE_TYPE" == "production" ]; then make android; + + # USE GITLAB PROTECTED & MASKED CI VARIABLES TO PROVIDE THE FOLLOWING DATA! + # THUS $APK_TARGET_SSH_PRIVATE_KEY HAS TO BE BASE64 ENCODED + # USE AN UNPRIVILIGED USER WITH ACCESS ONLY TO THIS DIRECTORY + # example: $APK_SCP_TARGET = deployuser@your.apk.storage.com:/path/to/app.apk + if [ -n "$APK_SCP_TARGET" ]; then + sh static/scripts/ssh_deploy.sh apk $APK_SCP_TARGET $APK_TARGET_SSH_PRIVATE_KEY + fi fi artifacts: untracked: false diff --git a/README.md b/README.md index 006b448c..573d43e6 100644 --- a/README.md +++ b/README.md @@ -30,3 +30,4 @@ docker build -t openstapps-art . 2. Edit app.conf.sample to your liking and rename it to app.conf (needs info you can find in your corresponding developer/store portal) 3. Use docker to run `make web` and `make android` (i.e. `docker run -it -v $(pwd):/build --rm openstapps-art:latest make web`) 4. On a macOS device run `make ios` (make sure you have all required certificates / profiles set up in Xcode) +5. Gitlab CI runs need to be provided with SCP targets and Base64 encoded SSH private keys via env variables for web and (optionally) apk deployments diff --git a/static/scripts/ssh_deploy.sh b/static/scripts/ssh_deploy.sh index f3d735e7..90a3b019 100755 --- a/static/scripts/ssh_deploy.sh +++ b/static/scripts/ssh_deploy.sh @@ -2,10 +2,10 @@ set -e -SSH_DEPLOY_TARGET=$1 +SSH_DEPLOY_TARGET=$2 SSH_DEPLOY_TARGET="${SSH_DEPLOY_TARGET:-"missingtarget"}" -SSH_PRIVATE_KEY=$2 +SSH_PRIVATE_KEY=$3 SSH_PRIVATE_KEY="${SSH_PRIVATE_KEY:-"missingkey"}" GOTO_FAIL=false @@ -45,13 +45,26 @@ mkdir -p ~/.ssh chmod 700 ~/.ssh eval `ssh-agent -s` echo "$SSH_PRIVATE_KEY" | base64 -d | tr -d '\r' | ssh-add - -## -## Use ssh-keyscan to scan the keys of your private server. Replace gitlab.com -## with your own domain name. You can copy and repeat that command if you have -## more than one server to connect to. +## Use ssh-keyscan to scan the keys of your private server. +## You can copy and repeat that command if you have more than +## one server to connect to. ssh-keyscan $SSH_DEPLOY_TARGET_HOST >> ~/.ssh/known_hosts chmod 644 ~/.ssh/known_hosts -scp www.zip "$SSH_DEPLOY_TARGET" -ssh "$SSH_DEPLOY_TARGET_USER@$SSH_DEPLOY_TARGET_HOST" "cd $SSH_DEPLOY_TARGET_PATH && unzip -o www.zip && rm -f www.zip" +web() { + scp www.zip "$SSH_DEPLOY_TARGET" + ssh "$SSH_DEPLOY_TARGET_USER@$SSH_DEPLOY_TARGET_HOST" "cd $SSH_DEPLOY_TARGET_PATH && unzip -o www.zip && rm -f www.zip" +} + +apk() { + scp app/android/app/build/outputs/apk/release/app-release.apk "$SSH_DEPLOY_TARGET" +} + +if declare -f "$1" > /dev/null +then + "$@" +else + echo "'$1' is not a known function name" + exit 1 +fi From 4f91f46af69e402b135b5875a8939e33b91db2a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thea=20Sch=C3=B6bl?= Date: Tue, 14 Mar 2023 17:26:49 +0100 Subject: [PATCH 30/30] refactor: move app-release-template to monorepo --- .../app-release-template/.dockerignore | 0 .../app-release-template/.gitignore | 0 .../app-release-template/.gitlab-ci.yml | 0 .../app-release-template/Dockerfile | 0 Gemfile => frontend/app-release-template/Gemfile | 0 LICENCE => frontend/app-release-template/LICENCE | 0 Makefile => frontend/app-release-template/Makefile | 0 .../app-release-template/README.md | 0 .../app-release-template/app.conf.sample | 0 .../assets-mobile/android/icon-background.png | Bin .../assets-mobile/android/icon-foreground.png | Bin .../customizable}/assets-mobile/icon.png | Bin .../customizable}/assets-mobile/splash.png | Bin .../customizable}/assets/icon/favicon.png | Bin .../customizable}/assets/imgs/logo.png | Bin .../customizable}/assets/imgs/profile-card-head.svg | 0 .../customizable}/theme/variables.scss | 0 .../app-release-template/playstore.keystore.sample | 0 .../static}/fastlane-android/Appfile | 0 .../static}/fastlane-android/Fastfile | 0 .../static}/fastlane-ios/Appfile | 0 .../static}/fastlane-ios/Fastfile | 0 .../app-release-template/static}/scripts/android.sh | 0 .../static}/scripts/clone_app.sh | 0 .../app-release-template/static}/scripts/ionic.sh | 0 .../app-release-template/static}/scripts/ios.sh | 0 .../static}/scripts/ssh_deploy.sh | 0 .../static}/scripts/universal_link_files.sh | 0 28 files changed, 0 insertions(+), 0 deletions(-) rename .dockerignore => frontend/app-release-template/.dockerignore (100%) rename .gitignore => frontend/app-release-template/.gitignore (100%) rename .gitlab-ci.yml => frontend/app-release-template/.gitlab-ci.yml (100%) rename Dockerfile => frontend/app-release-template/Dockerfile (100%) rename Gemfile => frontend/app-release-template/Gemfile (100%) rename LICENCE => frontend/app-release-template/LICENCE (100%) rename Makefile => frontend/app-release-template/Makefile (100%) rename README.md => frontend/app-release-template/README.md (100%) rename app.conf.sample => frontend/app-release-template/app.conf.sample (100%) rename {customizable => frontend/app-release-template/customizable}/assets-mobile/android/icon-background.png (100%) rename {customizable => frontend/app-release-template/customizable}/assets-mobile/android/icon-foreground.png (100%) rename {customizable => frontend/app-release-template/customizable}/assets-mobile/icon.png (100%) rename {customizable => frontend/app-release-template/customizable}/assets-mobile/splash.png (100%) rename {customizable => frontend/app-release-template/customizable}/assets/icon/favicon.png (100%) rename {customizable => frontend/app-release-template/customizable}/assets/imgs/logo.png (100%) rename {customizable => frontend/app-release-template/customizable}/assets/imgs/profile-card-head.svg (100%) rename {customizable => frontend/app-release-template/customizable}/theme/variables.scss (100%) rename playstore.keystore.sample => frontend/app-release-template/playstore.keystore.sample (100%) rename {static => frontend/app-release-template/static}/fastlane-android/Appfile (100%) rename {static => frontend/app-release-template/static}/fastlane-android/Fastfile (100%) rename {static => frontend/app-release-template/static}/fastlane-ios/Appfile (100%) rename {static => frontend/app-release-template/static}/fastlane-ios/Fastfile (100%) rename {static => frontend/app-release-template/static}/scripts/android.sh (100%) mode change 100755 => 100644 rename {static => frontend/app-release-template/static}/scripts/clone_app.sh (100%) mode change 100755 => 100644 rename {static => frontend/app-release-template/static}/scripts/ionic.sh (100%) mode change 100755 => 100644 rename {static => frontend/app-release-template/static}/scripts/ios.sh (100%) mode change 100755 => 100644 rename {static => frontend/app-release-template/static}/scripts/ssh_deploy.sh (100%) mode change 100755 => 100644 rename {static => frontend/app-release-template/static}/scripts/universal_link_files.sh (100%) diff --git a/.dockerignore b/frontend/app-release-template/.dockerignore similarity index 100% rename from .dockerignore rename to frontend/app-release-template/.dockerignore diff --git a/.gitignore b/frontend/app-release-template/.gitignore similarity index 100% rename from .gitignore rename to frontend/app-release-template/.gitignore diff --git a/.gitlab-ci.yml b/frontend/app-release-template/.gitlab-ci.yml similarity index 100% rename from .gitlab-ci.yml rename to frontend/app-release-template/.gitlab-ci.yml diff --git a/Dockerfile b/frontend/app-release-template/Dockerfile similarity index 100% rename from Dockerfile rename to frontend/app-release-template/Dockerfile diff --git a/Gemfile b/frontend/app-release-template/Gemfile similarity index 100% rename from Gemfile rename to frontend/app-release-template/Gemfile diff --git a/LICENCE b/frontend/app-release-template/LICENCE similarity index 100% rename from LICENCE rename to frontend/app-release-template/LICENCE diff --git a/Makefile b/frontend/app-release-template/Makefile similarity index 100% rename from Makefile rename to frontend/app-release-template/Makefile diff --git a/README.md b/frontend/app-release-template/README.md similarity index 100% rename from README.md rename to frontend/app-release-template/README.md diff --git a/app.conf.sample b/frontend/app-release-template/app.conf.sample similarity index 100% rename from app.conf.sample rename to frontend/app-release-template/app.conf.sample diff --git a/customizable/assets-mobile/android/icon-background.png b/frontend/app-release-template/customizable/assets-mobile/android/icon-background.png similarity index 100% rename from customizable/assets-mobile/android/icon-background.png rename to frontend/app-release-template/customizable/assets-mobile/android/icon-background.png diff --git a/customizable/assets-mobile/android/icon-foreground.png b/frontend/app-release-template/customizable/assets-mobile/android/icon-foreground.png similarity index 100% rename from customizable/assets-mobile/android/icon-foreground.png rename to frontend/app-release-template/customizable/assets-mobile/android/icon-foreground.png diff --git a/customizable/assets-mobile/icon.png b/frontend/app-release-template/customizable/assets-mobile/icon.png similarity index 100% rename from customizable/assets-mobile/icon.png rename to frontend/app-release-template/customizable/assets-mobile/icon.png diff --git a/customizable/assets-mobile/splash.png b/frontend/app-release-template/customizable/assets-mobile/splash.png similarity index 100% rename from customizable/assets-mobile/splash.png rename to frontend/app-release-template/customizable/assets-mobile/splash.png diff --git a/customizable/assets/icon/favicon.png b/frontend/app-release-template/customizable/assets/icon/favicon.png similarity index 100% rename from customizable/assets/icon/favicon.png rename to frontend/app-release-template/customizable/assets/icon/favicon.png diff --git a/customizable/assets/imgs/logo.png b/frontend/app-release-template/customizable/assets/imgs/logo.png similarity index 100% rename from customizable/assets/imgs/logo.png rename to frontend/app-release-template/customizable/assets/imgs/logo.png diff --git a/customizable/assets/imgs/profile-card-head.svg b/frontend/app-release-template/customizable/assets/imgs/profile-card-head.svg similarity index 100% rename from customizable/assets/imgs/profile-card-head.svg rename to frontend/app-release-template/customizable/assets/imgs/profile-card-head.svg diff --git a/customizable/theme/variables.scss b/frontend/app-release-template/customizable/theme/variables.scss similarity index 100% rename from customizable/theme/variables.scss rename to frontend/app-release-template/customizable/theme/variables.scss diff --git a/playstore.keystore.sample b/frontend/app-release-template/playstore.keystore.sample similarity index 100% rename from playstore.keystore.sample rename to frontend/app-release-template/playstore.keystore.sample diff --git a/static/fastlane-android/Appfile b/frontend/app-release-template/static/fastlane-android/Appfile similarity index 100% rename from static/fastlane-android/Appfile rename to frontend/app-release-template/static/fastlane-android/Appfile diff --git a/static/fastlane-android/Fastfile b/frontend/app-release-template/static/fastlane-android/Fastfile similarity index 100% rename from static/fastlane-android/Fastfile rename to frontend/app-release-template/static/fastlane-android/Fastfile diff --git a/static/fastlane-ios/Appfile b/frontend/app-release-template/static/fastlane-ios/Appfile similarity index 100% rename from static/fastlane-ios/Appfile rename to frontend/app-release-template/static/fastlane-ios/Appfile diff --git a/static/fastlane-ios/Fastfile b/frontend/app-release-template/static/fastlane-ios/Fastfile similarity index 100% rename from static/fastlane-ios/Fastfile rename to frontend/app-release-template/static/fastlane-ios/Fastfile diff --git a/static/scripts/android.sh b/frontend/app-release-template/static/scripts/android.sh old mode 100755 new mode 100644 similarity index 100% rename from static/scripts/android.sh rename to frontend/app-release-template/static/scripts/android.sh diff --git a/static/scripts/clone_app.sh b/frontend/app-release-template/static/scripts/clone_app.sh old mode 100755 new mode 100644 similarity index 100% rename from static/scripts/clone_app.sh rename to frontend/app-release-template/static/scripts/clone_app.sh diff --git a/static/scripts/ionic.sh b/frontend/app-release-template/static/scripts/ionic.sh old mode 100755 new mode 100644 similarity index 100% rename from static/scripts/ionic.sh rename to frontend/app-release-template/static/scripts/ionic.sh diff --git a/static/scripts/ios.sh b/frontend/app-release-template/static/scripts/ios.sh old mode 100755 new mode 100644 similarity index 100% rename from static/scripts/ios.sh rename to frontend/app-release-template/static/scripts/ios.sh diff --git a/static/scripts/ssh_deploy.sh b/frontend/app-release-template/static/scripts/ssh_deploy.sh old mode 100755 new mode 100644 similarity index 100% rename from static/scripts/ssh_deploy.sh rename to frontend/app-release-template/static/scripts/ssh_deploy.sh diff --git a/static/scripts/universal_link_files.sh b/frontend/app-release-template/static/scripts/universal_link_files.sh similarity index 100% rename from static/scripts/universal_link_files.sh rename to frontend/app-release-template/static/scripts/universal_link_files.sh