diff --git a/.github/workflows/beta.yml b/.github/workflows/beta.yml new file mode 100644 index 0000000..a9dfb5c --- /dev/null +++ b/.github/workflows/beta.yml @@ -0,0 +1,51 @@ +name: AppDistribution + +# Only deploy a beta to AppDistribution when we are preparing a new release +on: + push: + branches: + - release/** + +jobs: + deploy-android: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-java@v1 + with: + java-version: '11' + + - uses: subosito/flutter-action@master + with: + channel: stable + + - name: Fetch dependencies + run: flutter pub get + + - name: Run Code Generate + run: ./regenerate.sh + + - name: Setup Secrets + run: | + echo $GOOGLE_SERVICES_JSON | tee android/app/google-services.json + echo $CONFIG_DART | tee lib/config.dart + echo $KEY_PROPERTIES | tee android/key.properties + echo $SIGNING_KEY | base64 --decode > android/distribution/aah.jks + env: + GOOGLE_SERVICES_JSON: ${{ secrets.GOOGLE_SERVICES_JSON }} + CONFIG_DART: ${{ secrets.CONFIG_DART }} + KEY_PROPERTIES: ${{ secrets.KEY_PROPERTIES }} + SIGNING_KEY: ${{ secrets.SIGNING_KEY }} + + - name: Build APK + run: flutter build apk + + - name: Upload to Firebase App Distribution + uses: wzieba/Firebase-Distribution-Github-Action@v1.2.1 + with: + appId: ${{secrets.ANDROID_FIREBASE_APP_ID}} + token: ${{secrets.FIREBASE_TOKEN}} + groups: 52inc,friends-&-family + releaseNotesFile: android/distribution/release_notes.txt + file: ${{ env.SIGNED_RELEASE_FILE }} + diff --git a/.github/workflows/production.yml b/.github/workflows/production.yml new file mode 100644 index 0000000..fee2fd9 --- /dev/null +++ b/.github/workflows/production.yml @@ -0,0 +1,54 @@ +name: Play Store + +# Only deploy a beta to AppDistribution when we are preparing a new release +on: + push: + tags: + - v** + +jobs: + deploy-android: + runs-on: ubuntu-latest + container: + image: google/dart:latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-java@v1 + with: + java-version: '11' + + - uses: subosito/flutter-action@master + with: + channel: stable + + - name: Fetch dependencies + run: flutter pub get + + - name: Setup Secrets + run: | + echo $GOOGLE_SERVICES_JSON | tee android/app/google-services.json + echo $CONFIG_DART | tee lib/config.dart + echo $KEY_PROPERTIES | tee android/key.properties + echo $SIGNING_KEY | base64 --decode > android/distribution/aah.jks + env: + GOOGLE_SERVICES_JSON: ${{ secrets.GOOGLE_SERVICES_JSON }} + CONFIG_DART: ${{ secrets.CONFIG_DART }} + KEY_PROPERTIES: ${{ secrets.KEY_PROPERTIES }} + SIGNING_KEY: ${{ secrets.SIGNING_KEY }} + + - name: Run Code Generate + run: ./regenerate.sh + + - name: Build APK + run: flutter build appbundle + + - name: Upload to Google Play Store + uses: r0adkll/upload-google-play@v1 + with: + serviceAccountJsonRaw: ${{ secrets.SERVICE_ACCOUNT_JSON }} + packageName: com.ftinc.appsagainsthumanity + releaseFile: build/app/outputs/bundle/release/app-release.aab + track: production + whatsNewDirectory: distribution/whatsnew + + diff --git a/.gitignore b/.gitignore index 3124ee4..a0b2976 100644 --- a/.gitignore +++ b/.gitignore @@ -34,7 +34,6 @@ # Misc tools lib/config.json lib/config.dart -credentials.json *.g.dart web/firebase_config.js @@ -49,3 +48,5 @@ app.*.map.json # Exceptions to above rules. !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages + +web/firebase-messaging-sw.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c0fb29..9395904 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 1.0.2 - 3/16/2021 + +* Added ability to wave at players, sending them a push notification. - [@r0adkll](https://github.com/R0ADKLL) + +## 1.0.1 - 6/5/2020 + +* Updated Home/Sign-In screen UI to be more unique and user friendly. - [@r0adkll](https://github.com/R0ADKLL) +* Fixed handling of dynamic links so you can now join games by link. - [@r0adkll](https://github.com/R0ADKLL) + ## 1.0.0 - 4/10/2020 * Initial Release. - [@r0adkll](https://github.com/R0ADKLL) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 670652d..0dc2a16 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,4 @@ -# Contributing to SwitchShare +# Contributing to Apps Against Humanity ## Issues diff --git a/LICENSE b/LICENSE index d22f041..7d7ef4f 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,25 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 + “Commons Clause” License Condition v1.0 + + The Software is provided to you by the Licensor under the License, +as defined below, subject to the following condition. + + Without limiting other conditions in the License, the grant of rights +under the License will not include, and the License does not grant to +you, the right to Sell the Software. + + For purposes of the foregoing, “Sell” means practicing any or all of the +rights granted to you under the License to provide to third parties, for +a fee or other consideration (including without limitation fees for hosting +or consulting/ support services related to the Software), a product or +service whose value derives, entirely or substantially, from the functionality +of the Software. Any license notice or attribution required by the License +must also include this Commons Clause License Condition notice. + + Software: Apps Against Humanity + + License: GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 + + Licensor: 52inc Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies @@ -631,8 +651,8 @@ to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. - DeckBox - Copyright (C) 2018 Drew Heavner + Apps Against Humanity + Copyright (C) 2020 52inc This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -652,7 +672,7 @@ Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: - SwitchShare Copyright (C) 2020 52inc + Apps Against Humanity Copyright (C) 2020 52inc This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. diff --git a/README.md b/README.md index e48432f..0cc6681 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ -# Apps Against Humanity - _WIP_ +# Apps Against ~Humanity~ Fellowships +_Working title for 'rebrand'. The idea is to shift to a build-your-own-cards kinds of layout where users can create/import their own card sets and share them on a 'marketplace' where you can download any shared cardset. This would allow us to further de-couple the Cards Against Humanity cards and branding to better comply with the app store reviews_ A Flutter Cards Against Humanity app for Android, iOS, and Web @@ -10,6 +11,66 @@ Setup a [Firebase Project](https://firebase.com/) with an Android and iOS applic 1. Download the `google-services.json` file for your Android application to `android/app/google-services.json` 2. Download the `GoogleServices-Info.plist` file for your iOS application to `ios/Runner/GoogleServices-Info.plist` +3. Add the following contents to a file named `firebase_config.js` to the `web/` folder + +```javascript +// Your web app's Firebase configuration +var firebaseConfig = { + apiKey: "some_api_key", + authDomain: "some_firebase_project_id.firebaseapp.com", + databaseURL: "https://some_firebase_project_id.firebaseio.com", + projectId: "some_firebase_project_id", + storageBucket: "some_firebase_project_id.appspot.com", + messagingSenderId: "00000000000", + appId: "some_web_app_id", + measurementId: "some_analytics_id" +}; +// Initialize Firebase +firebase.initializeApp(firebaseConfig); +firebase.analytics(); +``` + +### Import Card Data +You can find and import all the card data from this [Google Sheet](https://docs.google.com/spreadsheets/d/1lsy7lIwBe-DWOi2PALZPf5DgXHx9MEvKfRw1GaWQkzg/edit) (or the [mirror here](https://docs.google.com/spreadsheets/d/1H808p0SA8zCfU44PG_ZoNi4eBawB2CiQFI7ttVTGRNQ/edit)) using the node.js script located at `/importer` + +### Setup Wiredash +Setup an account @ [wiredash.io](https://wiredash.io/) and create a new project to generate a `projectId` and `secret` that you will use in the following config. Or, you can remove the `Wiredash` widget from the `lib/app.dart` widget tree. + +### Setup Config + +Add your own configuration file to the `/lib` folder. Use this example: `example.config.json` + +```json +{ + "privacyPolicyUrl": "https://example.com/privacy.html", + "termsOfServiceUrl": "https://example.com/tos.html", + "sourceUrl": "https://github.com/52inc/AppsAgainstHumanity", + "wiredashProjectId": "some_wiredash_projectId", + "wiredashSecret": "some_wiredash_secret" +} +``` + +Then run + +```shell +flutter generate +``` + +### Generate JSON models + +Just run this: + +```shell +flutter packages pub run build_runner build --delete-conflicting-outputs +``` + +### Setup Functions + +Fork the repository here: https://github.com/52inc/AppsAgainstHumanity-Firebase and deploy the functions to your firebase project using: + +```bash +firebase deploy --only functions +``` # Contributing @@ -22,3 +83,11 @@ Please follow the guidelines set forth in the [CONTRIBUTING](CONTRIBUTING.md) do GNU General Public License v3.0 See [LICENSE](LICENSE) to see the full text. + +# Attribution + +All [CAH or "Cards Against Humanity"](https://cardsagainsthumanity.com/) question and answer text are hosted externally and are never included in the binary itself for the app that is uploaded to Google Play and Apple's App Store. + +All [CAH or "Cards Against Humanity"](https://cardsagainsthumanity.com/) question and answer text are licensed under Creative Commons BY-NC-SA 4.0 by the owner Cards Against Humanity, LLC. This application is NOT official, produced, endorsed or supported by Cards Against Humanity, LLC. + +[![CC-BY-NC-SA](assets/cc_by_nc_sa.png)](https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode) diff --git a/android/.gitignore b/android/.gitignore index 6e1ec20..69fd9d6 100644 --- a/android/.gitignore +++ b/android/.gitignore @@ -5,5 +5,6 @@ gradle-wrapper.jar /gradlew.bat /local.properties GeneratedPluginRegistrant.java -distribution/*.jks +distribution/aah.jks key.properties +google-services.json diff --git a/android/app/build.gradle b/android/app/build.gradle index 53c7984..88c025f 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -72,8 +72,9 @@ flutter { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation 'com.google.firebase:firebase-analytics:17.3.0' - implementation 'com.google.firebase:firebase-crashlytics:17.0.0-beta04' + implementation 'com.google.firebase:firebase-analytics:17.4.1' + implementation 'com.google.firebase:firebase-messaging:20.1.7' + implementation 'com.google.firebase:firebase-crashlytics:17.0.0' } apply plugin: 'com.google.gms.google-services' diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index c220634..4721ca5 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -39,11 +39,40 @@ + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/drawable-hdpi/ic_notification.png b/android/app/src/main/res/drawable-hdpi/ic_notification.png new file mode 100644 index 0000000..9177dd8 Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/ic_notification.png differ diff --git a/android/app/src/main/res/drawable-mdpi/ic_notification.png b/android/app/src/main/res/drawable-mdpi/ic_notification.png new file mode 100644 index 0000000..c92982c Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/ic_notification.png differ diff --git a/android/app/src/main/res/drawable-xhdpi/ic_notification.png b/android/app/src/main/res/drawable-xhdpi/ic_notification.png new file mode 100644 index 0000000..7b49573 Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/ic_notification.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/ic_notification.png b/android/app/src/main/res/drawable-xxhdpi/ic_notification.png new file mode 100644 index 0000000..bf3c52b Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/ic_notification.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/ic_notification.png b/android/app/src/main/res/drawable-xxxhdpi/ic_notification.png new file mode 100644 index 0000000..acf5ce5 Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/ic_notification.png differ diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..82e35de --- /dev/null +++ b/android/app/src/main/res/values/colors.xml @@ -0,0 +1,4 @@ + + + #AB47BC + diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..e2ed97d --- /dev/null +++ b/android/app/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + + aah_game_notifications + diff --git a/android/distribution/groups-alpha.txt b/android/distribution/groups-alpha.txt new file mode 100644 index 0000000..6e10cb5 --- /dev/null +++ b/android/distribution/groups-alpha.txt @@ -0,0 +1 @@ +52inc,friends-&-family diff --git a/android/distribution/release-notes.txt b/android/distribution/release-notes.txt new file mode 100644 index 0000000..4c762e2 --- /dev/null +++ b/android/distribution/release-notes.txt @@ -0,0 +1,3 @@ +• Improved disclaimer language in the settings +• Added analytic calls +• Developer packs are now super-secret and require a special action to unlock them diff --git a/appdistribution.sh b/appdistribution.sh new file mode 100755 index 0000000..8d8d0ee --- /dev/null +++ b/appdistribution.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +if [ "$1" == --help ] +then + echo "Usage: ./appdistribution.sh [--build]" +else + + if [[ "$3" == --build ]] + then + flutter clean + flutter build apk + fi + + firebase appdistribution:distribute build/app/outputs/apk/release/app-release.apk \ + --app 1:100898335413:android:88a8670f87c5c1ea8cacfc \ + --token "$1" \ + --release-notes-file android/distribution/release-notes.txt \ + --groups-file "$2" \ + +fi diff --git a/assets/1.5x/rando_cardrissian.png b/assets/1.5x/rando_cardrissian.png index fe53db4..fe24756 100644 Binary files a/assets/1.5x/rando_cardrissian.png and b/assets/1.5x/rando_cardrissian.png differ diff --git a/assets/2.0x/rando_cardrissian.png b/assets/2.0x/rando_cardrissian.png index e963f50..379b432 100644 Binary files a/assets/2.0x/rando_cardrissian.png and b/assets/2.0x/rando_cardrissian.png differ diff --git a/assets/3.0x/rando_cardrissian.png b/assets/3.0x/rando_cardrissian.png index 8c73864..611e476 100644 Binary files a/assets/3.0x/rando_cardrissian.png and b/assets/3.0x/rando_cardrissian.png differ diff --git a/assets/4.0x/rando_cardrissian.png b/assets/4.0x/rando_cardrissian.png index 38d8ed4..468c5d8 100644 Binary files a/assets/4.0x/rando_cardrissian.png and b/assets/4.0x/rando_cardrissian.png differ diff --git a/assets/cc_by_nc_sa.png b/assets/cc_by_nc_sa.png new file mode 100644 index 0000000..b9a5553 Binary files /dev/null and b/assets/cc_by_nc_sa.png differ diff --git a/assets/rando_cardrissian.png b/assets/rando_cardrissian.png index 6169699..1062795 100644 Binary files a/assets/rando_cardrissian.png and b/assets/rando_cardrissian.png differ diff --git a/assets/tos.html b/assets/tos.html deleted file mode 100644 index 2751873..0000000 --- a/assets/tos.html +++ /dev/null @@ -1,994 +0,0 @@ - - - - - -

Apps Against Humanity Terms of Use -

-

-

APPS AGAINST HUMANITY is not affiliated with Cards Against Humanity, LLC in any way. This app was written and published by 52inc (https://52inc.com) as a way for us to play one of our favorite games while on lockdown during the COVID-19 pandemic in 2020. -

-

-

CARDS AGAINST HUMANITY is a Trademark of Cards Against Humanity, LLC and CARDS AGAINST HUMANITY playing cards and game rules are used by APPS AGAINST HUMANITY under a Creative Commons BY-NC-SA-2 Licence (https://creativecommons.org/licenses/by-nc-sa/2.0/).

-

-

If you are Cards Against Humanity LLC reading this: we believe this being a digital product on mobile phones instead of a physical product, having the word Apps instead of Cards in the name, and using a conspicuously different font would prevent any consumer confusion and would not violate your trademark, but if the name “Apps Against Humanity” is too close for comfort – first we are sorry, no harm intended – and we will change it. (our contact is at the bottom) (ps, we are big fans!)   -

-

-

The source-code for this app is released under a Common Clause - GNUv3 License and can be found on 52inc’s Github account. (https://github.com/52inc/AppsAgainstHumanity/) These terms do not restrict or alter the licence for the source code.

-

-

Changes to this Terms and Conditions

-
    -
  • April 21st, 2020 – first published
  • -
-

AGREEMENT TO TERMS

-

-

These Terms and Conditions constitute a legally binding agreement made between you, whether personally or on behalf of an entity (“you”) and 52apps Inc (“we,” “us” or “our”), concerning your access to and use of our mobile application (the “Application”). You agree that by accessing the Application, you have read, understood, and agree to be bound by all of these Terms and Conditions Use. IF YOU DO NOT AGREE WITH ALL OF THESE TERMS AND CONDITIONS, THEN YOU ARE EXPRESSLY PROHIBITED FROM USING THE APPLICATION AND YOU MUST DISCONTINUE USE IMMEDIATELY. -

-

-

Supplemental terms and conditions or documents that may be posted on the Application from time to time are hereby expressly incorporated herein by reference. We reserve the right, in our sole discretion, to make changes or modifications to these Terms and Conditions at any time and for any reason. We will alert you about any changes by updating the “Last updated” date of these Terms and Conditions and you waive any right to receive specific notice of each such change. It is your responsibility to periodically review these Terms and Conditions to stay informed of updates.  You will be subject to, and will be deemed to have been made aware of and to have accepted, the changes in any revised Terms and Conditions by your continued use of the Application after the date such revised Terms are posted. -

-

-

The information provided on the Application is not intended for distribution to or use by any person or entity in any jurisdiction or country where such distribution or use would be contrary to law or regulation or which would subject us to any registration requirement within such jurisdiction or country. Accordingly, those persons who choose to access the Application from other locations do so on their own initiative and are solely responsible for compliance with local laws, if and to the extent local laws are applicable. -

-

-

The Application is intended for users who are at least 18 years old. Persons under the age of 13 are not permitted to register for the Application. -

-

-

3RD PARTY TERMS OF SERVICE

-

You also agree to the terms and conditions of these 3rd party providers:

- -

-

We will make our best efforts to keep the links above updated, but you are ultimately responsible for finding and reviewing the terms and conditions of all affiliated services. -

-

INTELLECTUAL PROPERTY RIGHTS

-

Unless otherwise indicated, the Application is available under a Common Clause - GNUv3 License and all source code, databases, functionality, software, website designs, audio, video, text, photographs, and graphics on the Application (collectively, the “Content”)  and the trademarks, service marks, and logos contained therein (the “Marks”), excluding the content owned by Cards Against Humanity™ used under a Creative Commons BY-NC-SA-2 Licence, are owned or controlled by us or licensed to us, and are released by us under the same license. The Content and the Marks are provided on the Application “AS IS” for your information and personal use only.  Except as expressly provided in these Terms of Use, no part of the Application and no Content or Marks may be copied, reproduced, aggregated, republished, uploaded, posted, publicly displayed, encoded, translated, transmitted, distributed, sold, licensed, or otherwise exploited for any commercial purpose whatsoever, except in compliance with Common Clause - GNUv3 License for Software and Source Code, and Creative Commons BY-NC-SA-2 Licence for all other.

-

-

Provided that you are eligible to use the Application, you are granted a limited license to access and use the Application and to download or print a copy of any portion of the Content to which you have properly gained access solely for your personal, non-commercial use. We reserve all rights not expressly granted to you in and to the Application, Content, and the Marks. -

-

USER REPRESENTATIONS

-

By using the Application, you represent and warrant that: (1) all registration information you submit will be true, accurate, current, and complete; (2) you will maintain the accuracy of such information and promptly update such registration information as necessary; (3) you have the legal capacity and you agree to comply with these Terms of Use; (4) you are not under the age of 13; (5) not a minor in the jurisdiction in which you reside, or if a minor, you have received parental permission to use the Site; (6) you will not access the Application through automated or non-human means, whether through a bot, script or otherwise; (7) you will not use the Application for any illegal or unauthorized purpose; and (8) your use of the Application will not violate any applicable law or regulation. -

-

-

If you provide any information that is untrue, inaccurate, not current, or incomplete, we have the right to suspend or terminate your account and refuse any and all current or future use of the Application(or any portion thereof). -

-

USER REGISTRATION

-

You may be required to register with the Application. You agree to keep your password or auth code confidential and will be responsible for all use of your account, password, and auth codes. We reserve the right to remove, reclaim, or change a username you select if we determine, in our sole discretion, that such username is inappropriate, obscene, or otherwise objectionable. -

-

PROHIBITED ACTIVITIES

-

You may not access or use the Application for any purpose other than that for which we make the Application available. The Application may not be used in connection with any commercial endeavors except those that are specifically endorsed or approved by us. -

-

As a user of the Application, you agree not to:

-
    -
  1. Systematically retrieve data or other content from the Application to create or compile, directly or indirectly, a collection, compilation, database, or directory without written permission from us. -
  2. -
  3. Make any unauthorized use of the Application, including collecting usernames, phone numbers and/or email addresses of users by electronic or other means for the purpose of sending unsolicited email, texts, or calls, or creating user accounts by automated means or under false pretenses. -
  4. -
  5. Use a buying agent or purchasing agent to make purchases on the Application. -
  6. -
  7. Use the Application to advertise or offer to sell goods and services.
  8. -
  9. Circumvent, disable, or otherwise interfere with security-related features of the Application, including features that prevent or restrict the use or copying of any Content or enforce limitations on the use of the Application and/or the Content contained therein. -
  10. -
  11. Engage in unauthorized framing of or linking to the Application.
  12. -
  13. Trick, defraud, or mislead us and other users, especially in any attempt to learn sensitive account information such as user passwords; -
  14. -
  15. Make improper use of our support services or submit false reports of abuse or misconduct. -
  16. -
  17. Engage in any automated use of the system, such as using scripts to send comments or messages, or using any data mining, robots, or similar data gathering and extraction tools. -
  18. -
  19. Interfere with, disrupt, or create an undue burden on the Application or the networks or services connected to the Application. -
  20. -
  21. Attempt to impersonate another user or person or use the username of another user.
  22. -
  23. Sell or otherwise transfer your profile.
  24. -
  25. Use any information obtained from the Application in order to harass, abuse, or harm another person. -
  26. -
  27. Use the Application as part of any effort to compete with us or otherwise use the Application and/or the Content for any revenue-generating endeavor or commercial enterprise. -
  28. -
  29. Decipher, decompile, disassemble, or reverse engineer any of the software comprising or in any way making up a part of the Application. Except for use in security testing strictly limited to your own account and immediately reporting any and all finding to us. -
  30. -
  31. Delete the copyright or other proprietary rights notice from any Content. -
  32. -
  33. Upload or transmit (or attempt to upload or to transmit) viruses, Trojan horses, or other material, including excessive use of capital letters and spamming (continuous posting of repetitive text), that interferes with any party’s uninterrupted use and enjoyment of the Application or modifies, impairs, disrupts, alters, or interferes with the use, features, functions, operation, or maintenance of the Application. -
  34. -
  35. Upload or transmit (or attempt to upload or to transmit) any material that acts as a passive or active information collection or transmission mechanism, including without limitation, clear graphics interchange formats (“gifs”), 1×1 pixels, web bugs, cookies, or other similar devices (sometimes referred to as “spyware” or “passive collection mechanisms” or “pcms”). -
  36. -
  37. Except as may be the result of standard search engine or Internet browser usage, use, launch, develop, or distribute any automated system, including without limitation, any spider, robot, cheat utility, scraper, or offline reader that accesses the Application, or using or launching any unauthorized script or other software. -
  38. -
  39. Disparage, tarnish, or otherwise harm, in our opinion, us and/or the Application.
  40. -
  41. Use the Application in a manner inconsistent with any applicable laws or regulations. -
  42. -
-

USER GENERATED CONTRIBUTIONS

-

The Application may invite you to chat, contribute to, or participate in blogs, message boards, online forums, and other functionality, and may provide you with the opportunity to create, submit, post, display, transmit, perform, publish, distribute, or broadcast content and materials to us or on the Application, including but not limited to text, writings, video, audio, photographs, graphics, comments, suggestions, or personal information or other material (collectively, “Contributions”). Contributions may be viewable by other users of the Application and through third-party websites. As such, any Contributions you transmit may be treated as non-confidential and non-proprietary. When you create or make available any Contributions, you thereby represent and warrant that: -

-
    -
  1. The creation, distribution, transmission, public display, or performance, and the accessing, downloading, or copying of your Contributions do not and will not infringe the proprietary rights, including but not limited to the copyright, patent, trademark, trade secret, or moral rights of any third party. -
  2. -
  3. You are the creator and owner of or have the necessary licenses, rights, consents, releases, and permissions to use and to authorize us, the Application, and other users of the Application to use your Contributions in any manner contemplated by the Application and these Terms of Use. -
  4. -
  5. You have the written consent, release, and/or permission of each and every identifiable individual person in your Contributions to use the name or likeness of each and every such identifiable individual person to enable inclusion and use of your Contributions in any manner contemplated by the Application and these Terms of Use. -
  6. -
  7. Your Contributions are not false, inaccurate, or misleading.
  8. -
  9. Your Contributions are not unsolicited or unauthorized advertising, promotional materials, pyramid schemes, chain letters, spam, mass mailings, or other forms of solicitation. -
  10. -
  11. Your Contributions are not obscene, lewd, lascivious, filthy, violent, harassing, libelous, slanderous, or otherwise objectionable (as determined by us). -
  12. -
  13. Your Contributions do not ridicule, mock, disparage, intimidate, or abuse anyone.
  14. -
  15. Your Contributions do not advocate the violent overthrow of any government or incite, encourage, or threaten physical harm against another. -
  16. -
  17. Your Contributions do not violate any applicable law, regulation, or rule. -
  18. -
  19. Your Contributions do not violate the privacy or publicity rights of any third party. -
  20. -
  21. Your Contributions do not contain any material that solicits personal information from anyone under the age of 18 or exploits people under the age of 18 in a sexual or violent manner. -
  22. -
  23. Your Contributions do not violate any federal or state law concerning child pornography, or otherwise intended to protect the health or well-being of minors; -
  24. -
  25. Your Contributions do not include any offensive comments that are connected to race, national origin, gender, sexual preference, or physical handicap. -
  26. -
  27. Your Contributions do not otherwise violate, or link to material that violates, any provision of these Terms of Use, or any applicable law or regulation. -
  28. -
-

Any use of the Application in violation of the foregoing violates these Terms of Use and may result in, among other things, termination or suspension of your rights to use the Application. -

-

CONTRIBUTION LICENSE

-

By posting your Contributions to any part of the Application, or making Contributions accessible to the Application by linking your account from the Application to any of your social networking accounts, you automatically grant, and you represent and warrant that you have the right to grant, to us an unrestricted, unlimited, irrevocable, perpetual, non-exclusive, transferable, royalty-free, fully-paid, worldwide right, and license to host, use, copy, reproduce, disclose, sell, resell, publish, broadcast, retitle, archive, store, cache, publicly perform, publicly display, reformat, translate, transmit, excerpt (in whole or in part), and distribute such Contributions (including, without limitation, your image and voice) for any purpose, commercial, advertising, or otherwise, and to prepare derivative works of, or incorporate into other works, such Contributions, and grant and authorize sublicenses of the foregoing. The use and distribution may occur in any media formats and through any media channels. -

-

This license will apply to any form, media, or technology now known or hereafter developed, and includes our use of your name, company name, and franchise name, as applicable, and any of the trademarks, service marks, trade names, logos, and personal and commercial images you provide.  You waive all moral rights in your Contributions, and you warrant that moral rights have not otherwise been asserted in your Contributions. -

-

We do not assert any ownership over your Contributions.  You retain full ownership of all of your Contributions and any intellectual property rights or other proprietary rights associated with your Contributions.  We are not liable for any statements or representations in your Contributions provided by you in any area on the Application.  You are solely responsible for your Contributions to the Application and you expressly agree to exonerate us from any and all responsibility and to refrain from any legal action against us regarding your Contributions. -

-

We have the right, in our sole and absolute discretion, (1) to edit, redact, or otherwise change any Contributions; (2) to re-categorize any Contributions to place them in more appropriate locations on the Application; and (3) to pre-screen or delete any Contributions at any time and for any reason, without notice. We have no obligation to monitor your Contributions. -

-

GUIDELINES FOR REVIEWS

-

We may provide you areas on the Application to leave reviews or ratings. When posting a review, you must comply with the following criteria: (1) you should have firsthand experience with the person/entity being reviewed; (2) your reviews should not contain offensive profanity, or abusive, racist, offensive, or hate language; (3) your reviews should not contain discriminatory references based on religion, race, gender, national origin, age, marital status, sexual orientation, or disability; (4) your reviews should not contain references to illegal activity; (5) you should not be affiliated with competitors if posting negative reviews; (6) you should not make any conclusions as to the legality of conduct; (7) you may not post any false or misleading statements; and (8) you may not organize a campaign encouraging others to post reviews, whether positive or negative. -

-

-

We may accept, reject, or remove reviews in our sole discretion. We have absolutely no obligation to screen reviews or to delete reviews, even if anyone considers reviews objectionable or inaccurate.  Reviews are not endorsed by us, and do not necessarily represent our opinions or  the views  of any of our affiliates or partners.  We do not assume liability for any review or for any claims, liabilities, or losses resulting from any review. By posting a review, you hereby grant to us a perpetual, non-exclusive, worldwide, royalty-free, fully-paid, assignable, and sublicensable right and license to reproduce, modify, translate, transmit by any means, display, perform, and/or distribute all content relating to reviews. 

-

MOBILE APPLICATION LICENSE

- Use License

-

If you access the Application via a mobile application, then we grant you a revocable, non-exclusive, non-transferable, limited right to install and use the mobile application on wireless electronic devices owned or controlled by you, and to access and use the mobile application on such devices strictly in accordance with the terms and conditions of this mobile application license contained in these Terms of Use. You shall not: (1) decompile, reverse engineer, disassemble, attempt to derive the source code of, or decrypt the application; (2) make any modification, adaptation, improvement, enhancement, translation, or derivative work from the application; (3) violate any applicable laws, rules, or regulations in connection with your access or use of the application; (4) remove, alter, or obscure any proprietary notice (including any notice of copyright or trademark) posted by us or the licensors of the application; (5) use the application for any revenue generating endeavor, commercial enterprise, or other purpose for which it is not designed or intended; (6) make the application available over a network or other environment permitting access or use by multiple devices or users at the same time; (7) use the application for creating a product, service, or software that is, directly or indirectly, competitive with or in any way a substitute for the application; (8) use the application to send automated queries to any website or to send any unsolicited commercial e-mail; or (9) use any proprietary information or any of our interfaces or our other intellectual property in the design, development, manufacture, licensing, or distribution of any applications, accessories, or devices for use with the application. -

Apple and Android Devices

-

The following terms apply when you use a mobile application obtained from either the Apple Store or Google Play (each an “App Distributor”) to access the Application: (1) the license granted to you for our mobile application is limited to a non-transferable license to use the application on a device that utilizes the Apple iOS or Android operating systems, as applicable, and in accordance with the usage rules set forth in the applicable App Distributor’s terms of service; (2) we are responsible for providing any maintenance and support services with respect to the mobile application as specified in the terms and conditions of this mobile application license contained in these Terms of Use or as otherwise required under applicable law, and you acknowledge that each App Distributor has no obligation whatsoever to furnish any maintenance and support services with respect to the mobile application; (3) in the event of any failure of the mobile application to conform to any applicable warranty, you may notify the applicable App Distributor, and the App Distributor, in accordance with its terms and policies, may refund the purchase price, if any, paid for the mobile application, and to the maximum extent permitted by applicable law, the App Distributor will have no other warranty obligation whatsoever with respect to the mobile application; (4) you represent and warrant that (i) you are not located in a country that is subject to a U.S. government embargo, or that has been designated by the U.S. government as a “terrorist supporting” country and (ii) you are not listed on any U.S. government list of prohibited or restricted parties; (5) you must comply with applicable third-party terms of agreement when using the mobile application, e.g., if you have a VoIP application, then you must not be in violation of their wireless data service agreement when using the mobile application; and (6) you acknowledge and agree that the App Distributors are third-party beneficiaries of the terms and conditions in this mobile application license contained in these Terms of Use, and that each App Distributor will have the right (and will be deemed to have accepted the right) to enforce the terms and conditions in this mobile application license contained in these Terms of Use against you as a third-party beneficiary thereof. -

-

SOCIAL MEDIA

-

As part of the functionality of the Application, you may link your account with online accounts you have with third-party service providers (each such account, a “Third-Party Account”) by either: (1) providing your Third-Party Account login information through the Application; or (2) allowing us to access your Third-Party Account, as is permitted under the applicable terms and conditions that govern your use of each Third-Party Account. You represent and warrant that you are entitled to disclose your Third-Party Account login information to us and/or grant us access to your Third-Party Account, without breach by you of any of the terms and conditions that govern your use of the applicable Third-Party Account, and without obligating us to pay any fees or making us subject to any usage limitations imposed by the third-party service provider of the Third-Party Account.  By granting us access to any Third-Party Accounts, you understand that (1) we may access, make available, and store (if applicable) any content that you have provided to and stored in your Third-Party Account (the “Social Network Content”) so that it is available on and through the Application via your account, including without limitation any friend lists and (2) we may submit to and receive from your Third-Party Account additional information to the extent you are notified when you link your account with the Third-Party Account.  Depending on the Third-Party Accounts you choose and subject to the privacy settings that you have set in such Third-Party Accounts, personally identifiable information that you post to your Third-Party Accounts may be available on and through your account on the Application. Please note that if a Third-Party Account or associated service becomes unavailable or our access to such Third-Party Account is terminated by the third-party service provider, then Social Network Content may no longer be available on and through the Application. You will have the ability to disable the connection between your account on the Application and your Third-Party Accounts at any time. PLEASE NOTE THAT YOUR RELATIONSHIP WITH THE THIRD-PARTY SERVICE PROVIDERS ASSOCIATED WITH YOUR THIRD-PARTY ACCOUNTS IS GOVERNED SOLELY BY YOUR AGREEMENT(S) WITH SUCH THIRD-PARTY SERVICE PROVIDERS.  We make no effort to review any Social Network Content for any purpose, including but not limited to, for accuracy, legality, or non-infringement, and we are not responsible for any Social Network Content. You acknowledge and agree that we may access your email address book associated with a Third-Party Account and your contacts list stored on your mobile device or tablet computer solely for purposes of identifying and informing you of those contacts who have also registered to use the Application. You can deactivate the connection between the Application and your Third-Party Account by contacting us using the contact information below or through your account settings (if applicable). We will attempt to delete any information stored on our servers that was obtained through such Third-Party Account, except the username and profile picture that become associated with your account. -

-

SUBMISSIONS

-

You acknowledge and agree that any questions, comments, suggestions, ideas, feedback, or other information regarding the Application (“Submissions”) provided by you to us are non-confidential and shall become our sole property. We shall own exclusive rights, including all intellectual property rights, and shall be entitled to the unrestricted use and dissemination of these Submissions for any lawful purpose, commercial or otherwise, without acknowledgment or compensation to you. You hereby waive all moral rights to any such Submissions, and you hereby warrant that any such Submissions are original with you or that you have the right to submit such Submissions. You agree there shall be no recourse against us for any alleged or actual infringement or misappropriation of any proprietary right in your Submissions. -

-

THIRD-PARTY WEBSITES AND CONTENT

-

The Application may contain (or you may be sent via the Application) links to other websites (“Third-Party Websites”) as well as articles, photographs, text, graphics, pictures, designs, music, sound, video, information, applications, software, and other content or items belonging to or originating from third parties (“Third-Party Content”). Such Third-Party Websites and Third-Party Content are not investigated, monitored, or checked for accuracy, appropriateness, or completeness by us, and we are not responsible for any Third-Party Websites accessed through the Application or any Third-Party Content posted on, available through, or installed from the Application, including the content, accuracy, offensiveness, opinions, reliability, privacy practices, or other policies of or contained in the Third-Party Websites or the Third-Party Content.  Inclusion of, linking to, or permitting the use or installation of any Third-Party Websites or any Third-Party Content does not imply approval or endorsement thereof by us. If you decide to leave the Application and access the Third-Party Websites or to use or install any Third-Party Content, you do so at your own risk, and you should be aware these Terms of Use no longer govern. You should review the applicable terms and policies, including privacy and data gathering practices, of any website to which you navigate from the Application or relating to any applications you use or install from the Application. Any purchases you make through Third-Party Websites will be through other websites and from other companies, and we take no responsibility whatsoever in relation to such purchases which are exclusively between you and the applicable third party. You agree and acknowledge that we do not endorse the products or services offered on Third-Party Websites and you shall hold us harmless from any harm caused by your purchase of such products or services. Additionally, you shall hold us harmless from any losses sustained by you or harm caused to you relating to or resulting in any way from any Third-Party Content or any contact with Third-Party Websites. -

-

ADVERTISERS

-

We allow advertisers to display their advertisements and other information in certain areas of the Application, such as sidebar advertisements or banner advertisements.  If you are an advertiser, you shall take full responsibility for any advertisements you place on the Application and any services provided on the Application or products sold through those advertisements.  Further, as an advertiser, you warrant and represent that you possess all rights and authority to place advertisements on the Application, including, but not limited to, intellectual property rights, publicity rights, and contractual rights. As an advertiser, you agree that such advertisements are subject to our Digital Millennium Copyright Act (“DMCA”) Notice and Policy provisions as described below, and you understand and agree there will be no refund or other compensation for DMCA takedown-related issues. We simply provide the space to place such advertisements, and we have no other relationship with advertisers. -

-

APP MANAGEMENT

-

We reserve the right, but not the obligation, to: (1) monitor the Application for violations of these Terms of Use; (2) take appropriate legal action against anyone who, in our sole discretion, violates the law or these Terms of Use, including without limitation, reporting such user to law enforcement authorities; (3) in our sole discretion and without limitation, refuse, restrict access to, limit the availability of, or disable (to the extent technologically feasible) any of your Contributions or any portion thereof; (4) in our sole discretion and without limitation, notice, or liability, to remove from the Application or otherwise disable all files and content that are excessive in size or are in any way burdensome to our systems; and (5) otherwise manage the Application in a manner designed to protect our rights and property and to facilitate the proper functioning of the Application. -

-

PRIVACY POLICY

-

We care about data privacy and security. Please review our Privacy Policy [link your privacy policy]. By using the Application, you agree to be bound by our Privacy Policy, which is incorporated into these Terms of Use. Please be advised the Application is hosted in the United States. If you access the Application from the European Union, Asia, or any other region of the world with laws or other requirements governing personal data collection, use, or disclosure that differ from applicable laws in the United States, then through your continued use of the Application or Services, you are transferring your data to the United States, and you expressly consent to have your data transferred to and processed in the United States. Further, we do not knowingly accept, request, or solicit information from children or knowingly market to children.  Therefore, in accordance with the U.S. Children’s Online Privacy Protection Act, if we receive actual knowledge that anyone under the age of 13 has provided personal information to us without the requisite and verifiable parental consent, we will delete that information from the Application as quickly as is reasonably practical. -

-

DIGITAL MILLENNIUM COPYRIGHT ACT (DMCA) NOTICE AND POLICY -

Notifications

-

We respect the intellectual property rights of others.  If you believe that any material available on or through the Application infringes upon any copyright you own or control, please immediately notify our Designated Copyright Agent using the contact information provided below (a “Notification”).  A copy of your Notification will be sent to the person who posted or stored the material addressed in the Notification.  Please be advised that pursuant to federal law you may be held liable for damages if you make material misrepresentations in a Notification. Thus, if you are not sure that material located on or linked to by the Application infringes your copyright, you should consider first contacting an attorney. -

-

-

All Notifications should meet the requirements of DMCA 17 U.S.C. § 512(c)(3) and include the following information: (1) A physical or electronic signature of a person authorized to act on behalf of the owner of an exclusive right that is allegedly infringed; (2) identification of the copyrighted work claimed to have been infringed, or, if multiple copyrighted works on the Application are covered by the Notification, a representative list of such works on the Application; (3) identification of the material that is claimed to be infringing or to be the subject of infringing activity and that is to be removed or access to which is to be disabled, and information reasonably sufficient to permit us to locate the material; (4) information reasonably sufficient to permit us to contact the complaining party, such as an address, telephone number, and, if available, an email address at which the complaining party may be contacted; (5) a statement that the complaining party has a good faith belief that use of the material in the manner complained of is not authorized by the copyright owner, its agent, or the law; and (6) a statement that the information in the notification is accurate, and under penalty of perjury, that the complaining party is authorized to act on behalf of the owner of an exclusive right that is allegedly infringed upon. -

Counter Notification

-

If you believe your own copyrighted material has been removed from the Application as a result of a mistake or misidentification, you may submit a written counter notification to [us/our Designated Copyright Agent] using the contact information provided below (a “Counter Notification”). To be an effective Counter Notification under the DMCA, your Counter Notification must include substantially the following: (1) identification of the material that has been removed or disabled and the location at which the material appeared before it was removed or disabled; (2) a statement that you consent to the jurisdiction of the Federal District Court in which your address is located, or if your address is outside the United States, for any judicial district in which we are located; (3) a statement that you will accept service of process from the party that filed the Notification or the party’s agent; (4) your name, address, and telephone number; (5) a statement under penalty of perjury that you have a good faith belief that the material in question was removed or disabled as a result of a mistake or misidentification of the material to be removed or disabled; and (6) your physical or electronic signature. -

-

If you send us a valid, written Counter Notification meeting the requirements described above, we will restore your removed or disabled material, unless we first receive notice from the party filing the Notification informing us that such party has filed a court action to restrain you from engaging in infringing activity related to the material in question. Please note that if you materially misrepresent that the disabled or removed content was removed by mistake or misidentification, you may be liable for damages, including costs and attorney’s fees. Filing a false Counter Notification constitutes perjury. -

-

Designated Copyright Agent

-

-

Christopher Thibault

-

-

Attn: 52apps Inc Copyright Agent

-

721B Lady St

-

Columbia, SC, 29201

-

support@52inc.com 

-

COPYRIGHT INFRINGEMENTS

-

We respect the intellectual property rights of others. If you believe that any material available on or through the Application infringes upon any copyright you own or control, please immediately notify us using the contact information provided below (a “Notification”). A copy of your Notification will be sent to the person who posted or stored the material addressed in the Notification. Please be advised that pursuant to federal law you may be held liable for damages if you make material misrepresentations in a Notification. Thus, if you are not sure that material located on or linked to by the Application infringes your copyright, you should consider first contacting an attorney. -

-

TERM AND TERMINATION

-

These Terms of Use shall remain in full force and effect while you use the Application.

-

-

WITHOUT LIMITING ANY OTHER PROVISION OF THESE TERMS OF USE, WE RESERVE THE RIGHT TO, IN OUR SOLE DISCRETION AND WITHOUT NOTICE OR LIABILITY, DENY ACCESS TO AND USE OF THE SITE (INCLUDING BLOCKING CERTAIN IP ADDRESSES), TO ANY PERSON FOR ANY REASON OR FOR NO REASON, INCLUDING WITHOUT LIMITATION FOR BREACH OF ANY REPRESENTATION, WARRANTY, OR COVENANT CONTAINED IN THESE TERMS OF USE OR OF ANY APPLICABLE LAW OR REGULATION. WE MAY TERMINATE YOUR USE OR PARTICIPATION IN THE SITE OR DELETE YOUR ACCOUNT AND ANY CONTENT OR INFORMATION THAT YOU POSTED AT ANY TIME, WITHOUT WARNING, IN OUR SOLE DISCRETION. -

-

-

If we terminate or suspend your account for any reason, you are prohibited from registering and creating a new account under your name, a fake or borrowed name, or the name of any third party, even if you may be acting on behalf of the third party. In addition to terminating or suspending your account, we reserve the right to take appropriate legal action, including without limitation pursuing civil, criminal, and injunctive redress. -

-

MODIFICATIONS AND INTERRUPTIONS

-

We reserve the right to change, modify, or remove the contents of the Application at any time or for any reason at our sole discretion without notice. However, we have no obligation to update any information on our Application. We also reserve the right to modify or discontinue all or part of the Application without notice at any time. We will not be liable to you or any third party for any modification, price change, suspension, or discontinuance of the Application. -

-

We cannot guarantee the Application will be available at all times. We may experience hardware, software, or other problems or need to perform maintenance related to the Application, resulting in interruptions, delays, or errors.  We reserve the right to change, revise, update, suspend, discontinue, or otherwise modify the Application at any time or for any reason without notice to you.  You agree that we have no liability whatsoever for any loss, damage, or inconvenience caused by your inability to access or use the Application during any downtime or discontinuance of the Application. Nothing in these Terms of Use will be construed to obligate us to maintain and support the Application or to supply any corrections, updates, or releases in connection therewith. -

-

GOVERNING LAW

-

These Terms of Use and your use of the Application are governed by and construed in accordance with the laws of the State of South Carolina applicable to agreements made and to be entirely performed within the State/Commonwealth of South Carolina, without regard to its conflict of law principles.

-

DISPUTE RESOLUTION

-

Informal Negotiations

-

To expedite resolution and control the cost of any dispute, controversy, or claim related to these Terms of Use (each a “Dispute” and collectively, the “Disputes”) brought by either you or us (individually, a “Party” and collectively, the “Parties”), the Parties agree to first attempt to negotiate any Dispute (except those Disputes expressly provided below) informally for at least 365 days before initiating arbitration. Such informal negotiations commence upon written notice from one Party to the other Party. -

-

Binding Arbitration

-

If the Parties are unable to resolve a Dispute through informal negotiations, the Dispute (except those Disputes expressly excluded below) will be finally and exclusively resolved by binding arbitration. YOU UNDERSTAND THAT WITHOUT THIS PROVISION, YOU WOULD HAVE THE RIGHT TO SUE IN COURT AND HAVE A JURY TRIAL. The arbitration shall be commenced and conducted under the Commercial Arbitration Rules of the American Arbitration Association (“AAA”) and, where appropriate, the AAA’s Supplementary Procedures for Consumer Related Disputes (“AAA Consumer Rules”), both of which are available at the AAA website www.adr.org. Your arbitration fees and your share of arbitrator compensation shall be governed by the AAA Consumer Rules and, where appropriate, limited by the AAA Consumer Rules. The arbitration may be conducted in person, through the submission of documents, by phone, or online. The arbitrator will make a decision in writing, but need not provide a statement of reasons unless requested by either Party. The arbitrator must follow applicable law, and any award may be challenged if the arbitrator fails to do so. Except where otherwise required by the applicable AAA rules or applicable law, the arbitration will take place in Richland County, South Carolina. Except as otherwise provided herein, the Parties may litigate in court to compel arbitration, stay proceedings pending arbitration, or to confirm, modify, vacate, or enter judgment on the award entered by the arbitrator. -

-

-

If for any reason, a Dispute proceeds in court rather than arbitration, the Dispute shall be commenced or prosecuted in the state and federal courts located in Richland County, South Carolina, and the Parties hereby consent to, and waive all defenses of lack of personal jurisdiction, and forum non convenient with respect to venue and jurisdiction in such state and federal courts. Application of the United Nations Convention on Contracts for the International Sale of Goods and the the Uniform Computer Information Transaction Act (UCITA) are excluded from these Terms of Use. -

-

-

In no event shall any Dispute brought by either Party related in any way to the Application be commenced more than one (1) year after the cause of action arose. If this provision is found to be illegal or unenforceable, then neither Party will elect to arbitrate any Dispute falling within that portion of this provision found to be illegal or unenforceable and such Dispute shall be decided by a court of competent jurisdiction within the courts listed for jurisdiction above, and the Parties agree to submit to the personal jurisdiction of that court. -

-

CORRECTIONS

-

There may be information on the Application that contains typographical errors, inaccuracies, or omissions that may relate to the Application, including descriptions, pricing, availability, and various other information. We reserve the right to correct any errors, inaccuracies, or omissions and to change or update the information on the Application at any time, without prior notice. -

-

DISCLAIMER

-

THE APPLICATION IS PROVIDED ON AN AS-IS AND AS-AVAILABLE BASIS.  YOU AGREE THAT YOUR USE OF THE APPLICATION SERVICES WILL BE AT YOUR SOLE RISK. TO THE FULLEST EXTENT PERMITTED BY LAW, WE DISCLAIM ALL WARRANTIES, EXPRESS OR IMPLIED, IN CONNECTION WITH THE APPLICATION AND YOUR USE THEREOF, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. WE MAKE NO WARRANTIES OR REPRESENTATIONS ABOUT THE ACCURACY OR COMPLETENESS OF THE APPLICATION’S CONTENT OR THE CONTENT OF ANY WEBSITES LINKED TO THIS APPLICATION AND WE WILL ASSUME NO LIABILITY OR RESPONSIBILITY FOR ANY (1) ERRORS, MISTAKES, OR INACCURACIES OF CONTENT AND MATERIALS, (2) PERSONAL INJURY OR PROPERTY DAMAGE, OF ANY NATURE WHATSOEVER, RESULTING FROM YOUR ACCESS TO AND USE OF THE APPLICATION, (3) ANY UNAUTHORIZED ACCESS TO OR USE OF OUR SECURE SERVERS AND/OR ANY AND ALL PERSONAL INFORMATION AND/OR FINANCIAL INFORMATION STORED THEREIN, (4) ANY INTERRUPTION OR CESSATION OF TRANSMISSION TO OR FROM THE APPLICATION, (5) ANY BUGS, VIRUSES, TROJAN HORSES, OR THE LIKE WHICH MAY BE TRANSMITTED TO OR THROUGH THE APPLICATION BY ANY THIRD PARTY, AND/OR (6) ANY ERRORS OR OMISSIONS IN ANY CONTENT AND MATERIALS OR FOR ANY LOSS OR DAMAGE OF ANY KIND INCURRED AS A RESULT OF THE USE OF ANY CONTENT POSTED, TRANSMITTED, OR OTHERWISE MADE AVAILABLE VIA THE APPLICATION. WE DO NOT WARRANT, ENDORSE, GUARANTEE, OR ASSUME RESPONSIBILITY FOR ANY PRODUCT OR SERVICE ADVERTISED OR OFFERED BY A THIRD PARTY THROUGH THE APPLICATION, ANY HYPERLINKED WEBSITE, OR ANY WEBSITE OR MOBILE APPLICATION FEATURED IN ANY BANNER OR OTHER ADVERTISING, AND WE WILL NOT BE A PARTY TO OR IN ANY WAY BE RESPONSIBLE FOR MONITORING ANY TRANSACTION BETWEEN YOU AND ANY THIRD-PARTY PROVIDERS OF PRODUCTS OR SERVICES.  AS WITH THE PURCHASE OF A PRODUCT OR SERVICE THROUGH ANY MEDIUM OR IN ANY ENVIRONMENT, YOU SHOULD USE YOUR BEST JUDGMENT AND EXERCISE CAUTION WHERE APPROPRIATE. -

-

LIMITATIONS OF LIABILITY

-

IN NO EVENT WILL WE OR OUR DIRECTORS, EMPLOYEES, OR AGENTS BE LIABLE TO YOU OR ANY THIRD PARTY FOR ANY DIRECT, INDIRECT, CONSEQUENTIAL, EXEMPLARY, INCIDENTAL, SPECIAL, OR PUNITIVE DAMAGES, INCLUDING LOST PROFIT, LOST REVENUE, LOSS OF DATA, OR OTHER DAMAGES ARISING FROM YOUR USE OF THE APPLICATION, EVEN IF WE HAVE BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. [NOTWITHSTANDING ANYTHING TO THE CONTRARY CONTAINED HEREIN, OUR LIABILITY TO YOU FOR ANY CAUSE WHATSOEVER AND REGARDLESS OF THE FORM OF THE ACTION, WILL AT ALL TIMES BE LIMITED TO THE AMOUNT PAID, IF ANY, BY YOU TO US DURING THE TWELVE (12) MONTH PERIOD PRIOR TO ANY CAUSE OF ACTION ARISING.  CERTAIN STATE LAWS DO NOT ALLOW LIMITATIONS ON IMPLIED WARRANTIES OR THE EXCLUSION OR LIMITATION OF CERTAIN DAMAGES. IF THESE LAWS APPLY TO YOU, SOME OR ALL OF THE ABOVE DISCLAIMERS OR LIMITATIONS MAY NOT APPLY TO YOU, AND YOU MAY HAVE ADDITIONAL RIGHTS. -

-

INDEMNIFICATION

-

You agree to defend, indemnify, and hold us harmless, including our subsidiaries, affiliates, and all of our respective officers, agents, partners, and employees, from and against any loss, damage, liability, claim, or demand, including reasonable attorneys’ fees and expenses, made by any third party due to or arising out of: (1) your Contributions; (2) use of the Site;  (3) breach of these Terms of Use; (4) any breach of your representations and warranties set forth in these Terms of Use; (5) your violation of the rights of a third party, including but not limited to intellectual property rights; or (6) any overt harmful act toward any other user of the Site with whom you connected via the Site. Notwithstanding the foregoing, we reserve the right, at your expense, to assume the exclusive defense and control of any matter for which you are required to indemnify us, and you agree to cooperate, at your expense, with our defense of such claims. We will use reasonable efforts to notify you of any such claim, action, or proceeding which is subject to this indemnification upon becoming aware of it. -

-

USER DATA

-

We will maintain certain data that you transmit to the Site for the purpose of managing the Site, as well as data relating to your use of the Site. Although we perform regular routine backups of data, you are solely responsible for all data that you transmit or that relates to any activity you have undertaken using the Site.  You agree that we shall have no liability to you for any loss or corruption of any such data, and you hereby waive any right of action against us arising from any such loss or corruption of such data. -

-

ELECTRONIC COMMUNICATIONS,  TRANSACTIONS, AND SIGNATURES

-

Visiting the Site, sending us emails, and completing online forms constitute electronic communications.  You consent to receive electronic communications, and you agree that all agreements, notices, disclosures, and other communications we provide to you electronically, via email and on the Site, satisfy any legal requirement that such communication be in writing. YOU HEREBY AGREE TO THE USE OF ELECTRONIC SIGNATURES, CONTRACTS, ORDERS, AND OTHER RECORDS, AND TO ELECTRONIC DELIVERY OF NOTICES, POLICIES, AND RECORDS OF TRANSACTIONS INITIATED OR COMPLETED BY US OR VIA THE SITE.  You hereby waive any rights or requirements under any statutes, regulations, rules, ordinances, or other laws in any jurisdiction which require an original signature or delivery or retention of non-electronic records, or to payments or the granting of credits by any means other than electronic means. -

-

CALIFORNIA USERS AND RESIDENTS

-

If any complaint with us is not satisfactorily resolved, you can contact the Complaint Assistance Unit of the Division of Consumer Services of the California Department of Consumer Affairs in writing at 1625 North Market Blvd., Suite N 112, Sacramento, California 95834 or by telephone at (800) 952-5210 or (916) 445-1254. -

-

MISCELLANEOUS

-

These Terms of Use and any policies or operating rules posted by us on the Application constitute the entire agreement and understanding between you and us. Our failure to exercise or enforce any right or provision of these Terms of Use shall not operate as a waiver of such right or provision.  These Terms of Use operate to the fullest extent permissible by law. We may assign any or all of our rights and obligations to others at any time.  We shall not be responsible or liable for any loss, damage, delay, or failure to act caused by any cause beyond our reasonable control. If any provision or part of a provision of these Terms of Use is determined to be unlawful, void, or unenforceable, that provision or part of the provision is deemed severable from these Terms of Use and does not affect the validity and enforceability of any remaining provisions. There is no joint venture, partnership, employment or agency relationship created between you and us as a result of these Terms of Use or use of the Site.  You agree that these Terms of Use will not be construed against us by virtue of having drafted them. You hereby waive any and all defenses you may have based on the electronic form of these Terms of Use and the lack of signing by the parties hereto to execute these Terms of Use. -

-

CONTACT US

-

In order to resolve a complaint regarding the Application or to receive further information regarding use of the Application, please contact us at: -

-

52apps Inc (52inc)

-

721B Lady St
Columbia, SC, 29201

-

hello@52inc.com

-

- diff --git a/distribution/whatsnew/whatsnew-en-US.txt b/distribution/whatsnew/whatsnew-en-US.txt new file mode 100644 index 0000000..797bc5d --- /dev/null +++ b/distribution/whatsnew/whatsnew-en-US.txt @@ -0,0 +1 @@ +First Release diff --git a/example.config.json b/example.config.json new file mode 100644 index 0000000..5207f5e --- /dev/null +++ b/example.config.json @@ -0,0 +1,7 @@ +{ + "privacyPolicyUrl": "https://example.com/privacy.html", + "termsOfServiceUrl": "https://example.com/tos.html", + "sourceUrl": "https://github.com/52inc/AppsAgainstHumanity", + "wiredashProjectId": "some_project_id", + "wiredashSecret": "some_secret_key" +} diff --git a/importer/package-lock.json b/importer/package-lock.json index f32cd7f..7001dcd 100644 --- a/importer/package-lock.json +++ b/importer/package-lock.json @@ -1,97 +1,1833 @@ { "name": "importer", "version": "1.0.0", - "lockfileVersion": 1, + "lockfileVersion": 2, "requires": true, + "packages": { + "": { + "name": "importer", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@types/minimist": "^1.2.0", + "@types/node": "^13.11.1", + "firebase-admin": "^10.1.0", + "google-spreadsheet": "^3.0.11", + "minimist": "^1.2.5" + }, + "devDependencies": { + "typescript": "^3.8.3" + } + }, + "node_modules/@firebase/app": { + "version": "0.7.22", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.7.22.tgz", + "integrity": "sha512-v3AXSCwAvZyIFzOGgPAYtzjltm1M9R4U4yqsIBPf5B4ryaT1EGK+3ETZUOckNl5y2YwdKRJVPDDore+B2xg0Ug==", + "peer": true, + "dependencies": { + "@firebase/component": "0.5.13", + "@firebase/logger": "0.3.2", + "@firebase/util": "1.5.2", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/app-compat": { + "version": "0.1.23", + "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.1.23.tgz", + "integrity": "sha512-c0QOhU2UVxZ7N5++nLQgKZ899ZC8+/ESa8VCzsQDwBw1T3MFAD1cG40KhB+CGtp/uYk/w6Jtk8k0xyZu6O2LOg==", + "peer": true, + "dependencies": { + "@firebase/app": "0.7.22", + "@firebase/component": "0.5.13", + "@firebase/logger": "0.3.2", + "@firebase/util": "1.5.2", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/app-types": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.7.0.tgz", + "integrity": "sha512-6fbHQwDv2jp/v6bXhBw2eSRbNBpxHcd1NBF864UksSMVIqIyri9qpJB1Mn6sGZE+bnDsSQBC5j2TbMxYsJQkQg==" + }, + "node_modules/@firebase/auth-interop-types": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.1.6.tgz", + "integrity": "sha512-etIi92fW3CctsmR9e3sYM3Uqnoq861M0Id9mdOPF6PWIg38BXL5k4upCNBggGUpLIS0H1grMOvy/wn1xymwe2g==", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/component": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.5.13.tgz", + "integrity": "sha512-hxhJtpD8Ppf/VU2Rlos6KFCEV77TGIGD5bJlkPK1+B/WUe0mC6dTjW7KhZtXTc+qRBp9nFHWcsIORnT8liHP9w==", + "dependencies": { + "@firebase/util": "1.5.2", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database": { + "version": "0.12.8", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.12.8.tgz", + "integrity": "sha512-JBQVfFLzfhxlQbl4OU6ov9fdsddkytBQdtSSR49cz48homj38ccltAhK6seum+BI7f28cV2LFHF9672lcN+qxA==", + "dependencies": { + "@firebase/auth-interop-types": "0.1.6", + "@firebase/component": "0.5.13", + "@firebase/logger": "0.3.2", + "@firebase/util": "1.5.2", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database-compat": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-0.1.8.tgz", + "integrity": "sha512-dhXr5CSieBuKNdU96HgeewMQCT9EgOIkfF1GNy+iRrdl7BWLxmlKuvLfK319rmIytSs/vnCzcD9uqyxTeU/A3A==", + "dependencies": { + "@firebase/component": "0.5.13", + "@firebase/database": "0.12.8", + "@firebase/database-types": "0.9.7", + "@firebase/logger": "0.3.2", + "@firebase/util": "1.5.2", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/database-types": { + "version": "0.9.7", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.9.7.tgz", + "integrity": "sha512-EFhgL89Fz6DY3kkB8TzdHvdu8XaqqvzcF2DLVOXEnQ3Ms7L755p5EO42LfxXoJqb9jKFvgLpFmKicyJG25WFWw==", + "dependencies": { + "@firebase/app-types": "0.7.0", + "@firebase/util": "1.5.2" + } + }, + "node_modules/@firebase/logger": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.3.2.tgz", + "integrity": "sha512-lzLrcJp9QBWpo40OcOM9B8QEtBw2Fk1zOZQdvv+rWS6gKmhQBCEMc4SMABQfWdjsylBcDfniD1Q+fUX1dcBTXA==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/util": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.5.2.tgz", + "integrity": "sha512-YvBH2UxFcdWG2HdFnhxZptPl2eVFlpOyTH66iDo13JPEYraWzWToZ5AMTtkyRHVmu7sssUpQlU9igy1KET7TOw==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@google-cloud/firestore": { + "version": "4.15.1", + "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-4.15.1.tgz", + "integrity": "sha512-2PWsCkEF1W02QbghSeRsNdYKN1qavrHBP3m72gPDMHQSYrGULOaTi7fSJquQmAtc4iPVB2/x6h80rdLHTATQtA==", + "optional": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "functional-red-black-tree": "^1.0.1", + "google-gax": "^2.24.1", + "protobufjs": "^6.8.6" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@google-cloud/paginator": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-3.0.7.tgz", + "integrity": "sha512-jJNutk0arIQhmpUUQJPJErsojqo834KcyB6X7a1mxuic8i1tKXxde8E69IZxNZawRIlZdIK2QY4WALvlK5MzYQ==", + "optional": true, + "dependencies": { + "arrify": "^2.0.0", + "extend": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@google-cloud/projectify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-2.1.1.tgz", + "integrity": "sha512-+rssMZHnlh0twl122gXY4/aCrk0G1acBqkHFfYddtsqpYXGxA29nj9V5V9SfC+GyOG00l650f6lG9KL+EpFEWQ==", + "optional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@google-cloud/promisify": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-2.0.4.tgz", + "integrity": "sha512-j8yRSSqswWi1QqUGKVEKOG03Q7qOoZP6/h2zN2YO+F5h2+DHU0bSrHCK9Y7lo2DI9fBd8qGAw795sf+3Jva4yA==", + "optional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@google-cloud/storage": { + "version": "5.19.4", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-5.19.4.tgz", + "integrity": "sha512-Jz7ugcPHhsEmMVvIxM9uoBsdEbKIYwDkh3u07tifsIymEWs47F4/D6+/Tv/W7kLhznqjyOjVJ/0frtBeIC0lJA==", + "optional": true, + "dependencies": { + "@google-cloud/paginator": "^3.0.7", + "@google-cloud/projectify": "^2.0.0", + "@google-cloud/promisify": "^2.0.0", + "abort-controller": "^3.0.0", + "arrify": "^2.0.0", + "async-retry": "^1.3.3", + "compressible": "^2.0.12", + "configstore": "^5.0.0", + "date-and-time": "^2.0.0", + "duplexify": "^4.0.0", + "ent": "^2.2.0", + "extend": "^3.0.2", + "gaxios": "^4.0.0", + "get-stream": "^6.0.0", + "google-auth-library": "^7.14.1", + "hash-stream-validation": "^0.2.2", + "mime": "^3.0.0", + "mime-types": "^2.0.8", + "p-limit": "^3.0.1", + "pumpify": "^2.0.0", + "retry-request": "^4.2.2", + "snakeize": "^0.1.0", + "stream-events": "^1.0.4", + "teeny-request": "^7.1.3", + "xdg-basedir": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.6.7.tgz", + "integrity": "sha512-eBM03pu9hd3VqDQG+kHahiG1x80RGkkqqRb1Pchcwqej/KkAH95gAvKs6laqaHCycYaPK+TKuNQnOz9UXYA8qw==", + "optional": true, + "dependencies": { + "@grpc/proto-loader": "^0.6.4", + "@types/node": ">=12.12.47" + }, + "engines": { + "node": "^8.13.0 || >=10.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.6.9", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.6.9.tgz", + "integrity": "sha512-UlcCS8VbsU9d3XTXGiEVFonN7hXk+oMXZtoHHG2oSA1/GcDP1q6OUgs20PzHDGizzyi8ufGSUDlk3O2NyY7leg==", + "optional": true, + "dependencies": { + "@types/long": "^4.0.1", + "lodash.camelcase": "^4.3.0", + "long": "^4.0.0", + "protobufjs": "^6.10.0", + "yargs": "^16.2.0" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@panva/asn1.js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@panva/asn1.js/-/asn1.js-1.0.0.tgz", + "integrity": "sha512-UdkG3mLEqXgnlKsWanWcgb6dOjUzJ+XC5f+aWw30qrtjxeNUSfKX1cd5FBzOaXQumoe9nIqeZUvrRJS03HCCtw==", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=", + "optional": true + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "optional": true + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "optional": true + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=", + "optional": true + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=", + "optional": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=", + "optional": true + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=", + "optional": true + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=", + "optional": true + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=", + "optional": true + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=", + "optional": true + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "optional": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", + "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", + "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.13", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", + "integrity": "sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.18", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-jwt": { + "version": "0.0.42", + "resolved": "https://registry.npmjs.org/@types/express-jwt/-/express-jwt-0.0.42.tgz", + "integrity": "sha512-WszgUddvM1t5dPpJ3LhWNH8kfNN8GPIBrAGxgIYXVCEGx6Bx4A036aAuf/r5WH9DIEdlmp7gHOYvSM6U87B0ag==", + "dependencies": { + "@types/express": "*", + "@types/express-unless": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.17.28", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.28.tgz", + "integrity": "sha512-P1BJAEAW3E2DJUlkgq4tOL3RyMunoWXqbSCygWo5ZIWTjUgN1YnaXWW4VWl/oc8vs/XoYibEGBKP0uZyF4AHig==", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, + "node_modules/@types/express-unless": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@types/express-unless/-/express-unless-0.5.3.tgz", + "integrity": "sha512-TyPLQaF6w8UlWdv4gj8i46B+INBVzURBNRahCozCSXfsK2VTlL1wNyTlMKw817VHygBtlcl5jfnPadlydr06Yw==", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", + "optional": true + }, + "node_modules/@types/mime": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", + "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==" + }, + "node_modules/@types/minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-aaI6OtKcrwCX8G7aWbNh7i8GOfY=" + }, + "node_modules/@types/node": { + "version": "13.11.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-13.11.1.tgz", + "integrity": "sha512-eWQGP3qtxwL8FGneRrC5DwrJLGN4/dH1clNTuLfN81HCrxVtxRjygDTUoZJ5ASlDEeo0ppYFQjQIlXhtXpOn6g==" + }, + "node_modules/@types/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==" + }, + "node_modules/@types/range-parser": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", + "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" + }, + "node_modules/@types/serve-static": { + "version": "1.13.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz", + "integrity": "sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/agent-base": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.0.tgz", + "integrity": "sha512-j1Q7cSCqN+AwrmDd+pzgqc0/NpC655x2bUf5ZjRIO77DcNBFmh+OgRNzF6OKdCC9RSCb19fGd99+bhXFdkRNqw==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "optional": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "engines": { + "node": ">=8" + } + }, + "node_modules/async-retry": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", + "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", + "optional": true, + "dependencies": { + "retry": "0.13.1" + } + }, + "node_modules/axios": { + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", + "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "dependencies": { + "follow-redirects": "^1.14.0" + } + }, + "node_modules/base64-js": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", + "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" + }, + "node_modules/bignumber.js": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.2.tgz", + "integrity": "sha512-GAcQvbpsM0pUb0zw1EI0KhQEZ+lRwR5fYaAp3vPOYuP7aDvGy6cVN6XHLauvF8SOga2y0dcLcjt3iQDTSEliyw==", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "optional": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "optional": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "optional": true + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "optional": true, + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/configstore": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", + "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==", + "optional": true, + "dependencies": { + "dot-prop": "^5.2.0", + "graceful-fs": "^4.1.2", + "make-dir": "^3.0.0", + "unique-string": "^2.0.0", + "write-file-atomic": "^3.0.0", + "xdg-basedir": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/date-and-time": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/date-and-time/-/date-and-time-2.3.1.tgz", + "integrity": "sha512-OaIRmSJXifwEN21rMVVDs0Kz8uhJ3wWPYd86atkRiqN54liaMQYEbbrgjZQea75YXOBWL4ZFb3rG/waenw1TEg==", + "optional": true + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dicer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.3.0.tgz", + "integrity": "sha512-MdceRRWqltEG2dZqO769g27N/3PXfcKl04VhYnBlo2YhH7zPi88VebsjTKclaOyiuMaGU72hTfw3VkUitGcVCA==", + "dependencies": { + "streamsearch": "0.1.2" + }, + "engines": { + "node": ">=4.5.0" + } + }, + "node_modules/dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "optional": true, + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/duplexify": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.2.tgz", + "integrity": "sha512-fz3OjcNCHmRP12MJoZMPglx8m4rrFP8rovnk4vT8Fs+aonZoCwGg10dSsQsfP/E62eZcPTMSMP6686fu9Qlqtw==", + "optional": true, + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.0" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "optional": true + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "optional": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/ent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", + "integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0=", + "optional": true + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "optional": true + }, + "node_modules/fast-text-encoding": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz", + "integrity": "sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig==" + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/firebase-admin": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-10.1.0.tgz", + "integrity": "sha512-4i4wu+EFgNfY4+D4DxXkZcmbD832ozUMNvHMkOFQrf8upyp51n6jrDJS+wLok9sd62yeqcImbnsLOympGlISPA==", + "dependencies": { + "@firebase/database-compat": "^0.1.1", + "@firebase/database-types": "^0.9.3", + "@types/node": ">=12.12.47", + "dicer": "^0.3.0", + "jsonwebtoken": "^8.5.1", + "jwks-rsa": "^2.0.2", + "node-forge": "^1.3.1" + }, + "engines": { + "node": ">=12.7.0" + }, + "optionalDependencies": { + "@google-cloud/firestore": "^4.15.1", + "@google-cloud/storage": "^5.18.3" + } + }, + "node_modules/follow-redirects": { + "version": "1.14.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz", + "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "optional": true + }, + "node_modules/gaxios": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-4.3.3.tgz", + "integrity": "sha512-gSaYYIO1Y3wUtdfHmjDUZ8LWaxJQpiavzbF5Kq53akSzvmVg0RfyOcFDbO1KJ/KCGRFz2qG+lS81F0nkr7cRJA==", + "dependencies": { + "abort-controller": "^3.0.0", + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.7" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/gcp-metadata": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-4.3.1.tgz", + "integrity": "sha512-x850LS5N7V1F3UcV7PoupzGsyD6iVwTVvsh3tbXfkctZnBnjW5yu5z1/3k3SehF7TyoTIe78rJs02GMMy+LF+A==", + "dependencies": { + "gaxios": "^4.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "optional": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/google-auth-library": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-7.14.1.tgz", + "integrity": "sha512-5Rk7iLNDFhFeBYc3s8l1CqzbEBcdhwR193RlD4vSNFajIcINKI8W8P0JLmBpwymHqqWbX34pJDQu39cSy/6RsA==", + "optional": true, + "dependencies": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^4.0.0", + "gcp-metadata": "^4.2.0", + "gtoken": "^5.0.4", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/google-gax": { + "version": "2.30.2", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-2.30.2.tgz", + "integrity": "sha512-BCNCT26kb0iC52zj2SosyOZMhI5sVfXuul1h0Aw5uT9nGAbmS5eOvQ49ft53ft6XotDj11sUSDV6XESEiQqCqg==", + "optional": true, + "dependencies": { + "@grpc/grpc-js": "~1.6.0", + "@grpc/proto-loader": "^0.6.1", + "@types/long": "^4.0.0", + "abort-controller": "^3.0.0", + "duplexify": "^4.0.0", + "fast-text-encoding": "^1.0.3", + "google-auth-library": "^7.14.0", + "is-stream-ended": "^0.1.4", + "node-fetch": "^2.6.1", + "object-hash": "^3.0.0", + "proto3-json-serializer": "^0.1.8", + "protobufjs": "6.11.2", + "retry-request": "^4.0.0" + }, + "bin": { + "compileProtos": "build/tools/compileProtos.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/google-p12-pem": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-3.1.4.tgz", + "integrity": "sha512-HHuHmkLgwjdmVRngf5+gSmpkyaRI6QmOg77J8tkNBHhNEI62sGHyw4/+UkgyZEI7h84NbWprXDJ+sa3xOYFvTg==", + "dependencies": { + "node-forge": "^1.3.1" + }, + "bin": { + "gp12-pem": "build/src/bin/gp12-pem.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/google-spreadsheet": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/google-spreadsheet/-/google-spreadsheet-3.2.0.tgz", + "integrity": "sha512-z7XMaqb+26rdo8p51r5O03u8aPLAPzn5YhOXYJPcf2hdMVr0dUbIARgdkRdmGiBeoV/QoU/7VNhq1MMCLZv3kQ==", + "dependencies": { + "axios": "^0.21.4", + "google-auth-library": "^6.1.3", + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/google-spreadsheet/node_modules/google-auth-library": { + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-6.1.6.tgz", + "integrity": "sha512-Q+ZjUEvLQj/lrVHF/IQwRo6p3s8Nc44Zk/DALsN+ac3T4HY/g/3rrufkgtl+nZ1TW7DNAw5cTChdVp4apUXVgQ==", + "dependencies": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^4.0.0", + "gcp-metadata": "^4.2.0", + "gtoken": "^5.0.4", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "optional": true + }, + "node_modules/gtoken": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-5.3.2.tgz", + "integrity": "sha512-gkvEKREW7dXWF8NV8pVrKfW7WqReAmjjkMBh6lNCCGOM4ucS0r0YyXXl0r/9Yj8wcW/32ISkfc8h5mPTDbtifQ==", + "dependencies": { + "gaxios": "^4.0.0", + "google-p12-pem": "^3.1.3", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hash-stream-validation": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/hash-stream-validation/-/hash-stream-validation-0.2.4.tgz", + "integrity": "sha512-Gjzu0Xn7IagXVkSu9cSFuK1fqzwtLwFhNhVL8IFJijRNMgUttFbBSIAzKuSIrsFMO1+g1RlsoN49zPIbwPDMGQ==", + "optional": true + }, + "node_modules/http-parser-js": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.6.tgz", + "integrity": "sha512-vDlkRPDJn93swjcjqMSaGSPABbIarsr1TLAui/gLDXzV5VsJNdXNzMYDyNBLQkjWQCJ1uizu8T2oDMhmGt0PRA==" + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "optional": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", + "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "optional": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "optional": true + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream-ended": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-stream-ended/-/is-stream-ended-0.1.4.tgz", + "integrity": "sha512-xj0XPvmr7bQFTvirqnFr50o0hQIh6ZItDqloxt5aJrR4NQsYeSsyFQERYGCAzfindAcnKjINnwEEgLx4IqVzQw==", + "optional": true + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "optional": true + }, + "node_modules/jose": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/jose/-/jose-2.0.5.tgz", + "integrity": "sha512-BAiDNeDKTMgk4tvD0BbxJ8xHEHBZgpeRZ1zGPPsitSyMgjoMWiLGYAE7H7NpP5h0lPppQajQs871E8NHUrzVPA==", + "dependencies": { + "@panva/asn1.js": "^1.0.0" + }, + "engines": { + "node": ">=10.13.0 < 13 || >=13.7.0" + }, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/jsonwebtoken": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", + "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=4", + "npm": ">=1.4.28" + } + }, + "node_modules/jsonwebtoken/node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jsonwebtoken/node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jsonwebtoken/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jwks-rsa": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-2.1.0.tgz", + "integrity": "sha512-GKOSDBWWBCiQTzawei6mEdRQvji5gecj8F9JwMt0ZOPnBPSmTjo5CKFvvbhE7jGPkU159Cpi0+OTLuABFcNOQQ==", + "dependencies": { + "@types/express-jwt": "0.0.42", + "debug": "^4.3.4", + "jose": "^2.0.5", + "limiter": "^1.1.5", + "lru-memoizer": "^2.1.4" + }, + "engines": { + "node": ">=10 < 13 || >=14" + } + }, + "node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/limiter": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", + "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==" + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=", + "optional": true + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" + }, + "node_modules/long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", + "optional": true + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/lru-memoizer": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.1.4.tgz", + "integrity": "sha512-IXAq50s4qwrOBrXJklY+KhgZF+5y98PDaNo0gi/v2KQBFLyWr+JyFvijZXkGKjQj/h9c0OwoE+JZbwUXce76hQ==", + "dependencies": { + "lodash.clonedeep": "^4.5.0", + "lru-cache": "~4.0.0" + } + }, + "node_modules/lru-memoizer/node_modules/lru-cache": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.0.2.tgz", + "integrity": "sha1-HRdnnAac2l0ECZGgnbwsDbN35V4=", + "dependencies": { + "pseudomap": "^1.0.1", + "yallist": "^2.0.0" + } + }, + "node_modules/lru-memoizer/node_modules/yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "optional": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "optional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "optional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "optional": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "optional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "optional": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "optional": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/proto3-json-serializer": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-0.1.8.tgz", + "integrity": "sha512-ACilkB6s1U1gWnl5jtICpnDai4VCxmI9GFxuEaYdxtDG2oVI3sVFIUsvUZcQbJgtPM6p+zqKbjTKQZp6Y4FpQw==", + "optional": true, + "dependencies": { + "protobufjs": "^6.11.2" + } + }, + "node_modules/protobufjs": { + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.2.tgz", + "integrity": "sha512-4BQJoPooKJl2G9j3XftkIXjoC9C0Av2NOrWmbLWT1vH32GcSUHjM0Arra6UfTsVyfMAuFzaLucXn1sadxJydAw==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/long": "^4.0.1", + "@types/node": ">=13.7.0", + "long": "^4.0.0" + }, + "bin": { + "pbjs": "bin/pbjs", + "pbts": "bin/pbts" + } + }, + "node_modules/pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "optional": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/pumpify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-2.0.1.tgz", + "integrity": "sha512-m7KOje7jZxrmutanlkS1daj1dS6z6BgslzOXmcSEpIlCxM3VJH7lG5QLeck/6hgF6F4crFf01UtQmNsJfweTAw==", + "optional": true, + "dependencies": { + "duplexify": "^4.1.1", + "inherits": "^2.0.3", + "pump": "^3.0.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "optional": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/retry-request": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-4.2.2.tgz", + "integrity": "sha512-xA93uxUD/rogV7BV59agW/JHPGXeREMWiZc9jhcwY4YdZ7QOtC7qbomYg0n4wyk2lJhggjvKvhNX8wln/Aldhg==", + "optional": true, + "dependencies": { + "debug": "^4.1.1", + "extend": "^3.0.2" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", + "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" + }, + "node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "optional": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "optional": true + }, + "node_modules/snakeize": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/snakeize/-/snakeize-0.1.0.tgz", + "integrity": "sha1-EMCI2LWOsHazIpu1oE4jLOEmQi0=", + "optional": true + }, + "node_modules/stream-events": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", + "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", + "optional": true, + "dependencies": { + "stubs": "^3.0.0" + } + }, + "node_modules/stream-shift": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", + "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==", + "optional": true + }, + "node_modules/streamsearch": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", + "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "optional": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "optional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha1-6NK6H6nJBXAwPAMLaQD31fiavls=", + "optional": true + }, + "node_modules/teeny-request": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-7.2.0.tgz", + "integrity": "sha512-SyY0pek1zWsi0LRVAALem+avzMLc33MKW/JLLakdP4s9+D7+jHcy5x6P+h94g2QNZsAqQNfX5lsbd3WSeJXrrw==", + "optional": true, + "dependencies": { + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.1", + "stream-events": "^1.0.5", + "uuid": "^8.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" + }, + "node_modules/tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + }, + "node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "optional": true, + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, + "node_modules/typescript": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz", + "integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "optional": true, + "dependencies": { + "crypto-random-string": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "optional": true + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "optional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "optional": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "optional": true + }, + "node_modules/write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "optional": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "node_modules/xdg-basedir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", + "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "optional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "optional": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "optional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + }, "dependencies": { + "@firebase/app": { + "version": "0.7.22", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.7.22.tgz", + "integrity": "sha512-v3AXSCwAvZyIFzOGgPAYtzjltm1M9R4U4yqsIBPf5B4ryaT1EGK+3ETZUOckNl5y2YwdKRJVPDDore+B2xg0Ug==", + "peer": true, + "requires": { + "@firebase/component": "0.5.13", + "@firebase/logger": "0.3.2", + "@firebase/util": "1.5.2", + "tslib": "^2.1.0" + } + }, + "@firebase/app-compat": { + "version": "0.1.23", + "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.1.23.tgz", + "integrity": "sha512-c0QOhU2UVxZ7N5++nLQgKZ899ZC8+/ESa8VCzsQDwBw1T3MFAD1cG40KhB+CGtp/uYk/w6Jtk8k0xyZu6O2LOg==", + "peer": true, + "requires": { + "@firebase/app": "0.7.22", + "@firebase/component": "0.5.13", + "@firebase/logger": "0.3.2", + "@firebase/util": "1.5.2", + "tslib": "^2.1.0" + } + }, "@firebase/app-types": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.6.0.tgz", - "integrity": "sha512-ld6rzjXk/SUauHiQZJkeuSJpxIZ5wdnWuF5fWBFQNPaxsaJ9kyYg9GqEvwZ1z2e6JP5cU9gwRBlfW1WkGtGDYA==" + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.7.0.tgz", + "integrity": "sha512-6fbHQwDv2jp/v6bXhBw2eSRbNBpxHcd1NBF864UksSMVIqIyri9qpJB1Mn6sGZE+bnDsSQBC5j2TbMxYsJQkQg==" }, "@firebase/auth-interop-types": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.1.4.tgz", - "integrity": "sha512-CLKNS84KGAv5lRnHTQZFWoR11Ti7gIPFirDDXWek/fSU+TdYdnxJFR5XSD4OuGyzUYQ3Dq7aVj5teiRdyBl9hA==" + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.1.6.tgz", + "integrity": "sha512-etIi92fW3CctsmR9e3sYM3Uqnoq861M0Id9mdOPF6PWIg38BXL5k4upCNBggGUpLIS0H1grMOvy/wn1xymwe2g==", + "requires": {} }, "@firebase/component": { - "version": "0.1.9", - "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.1.9.tgz", - "integrity": "sha512-i58GsVpxBGnKn1rx2RCAH0rk1Ldp6WterfBNDHyxmuyRO6BaZAgvxrZ3Ku1/lqiI7XMbmmRpP3emmwrStbFt9Q==", + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.5.13.tgz", + "integrity": "sha512-hxhJtpD8Ppf/VU2Rlos6KFCEV77TGIGD5bJlkPK1+B/WUe0mC6dTjW7KhZtXTc+qRBp9nFHWcsIORnT8liHP9w==", "requires": { - "@firebase/util": "0.2.44", - "tslib": "1.11.1" + "@firebase/util": "1.5.2", + "tslib": "^2.1.0" } }, "@firebase/database": { - "version": "0.5.25", - "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.5.25.tgz", - "integrity": "sha512-qUIpgDoODWs/FEdCQoH/VwRDvW7nn7m99TGxbMhdiE2WV/nzKbCo/PbbGm0dltdZzQ/SE87E2lfpPGK89Riw6Q==", + "version": "0.12.8", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.12.8.tgz", + "integrity": "sha512-JBQVfFLzfhxlQbl4OU6ov9fdsddkytBQdtSSR49cz48homj38ccltAhK6seum+BI7f28cV2LFHF9672lcN+qxA==", + "requires": { + "@firebase/auth-interop-types": "0.1.6", + "@firebase/component": "0.5.13", + "@firebase/logger": "0.3.2", + "@firebase/util": "1.5.2", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + } + }, + "@firebase/database-compat": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-0.1.8.tgz", + "integrity": "sha512-dhXr5CSieBuKNdU96HgeewMQCT9EgOIkfF1GNy+iRrdl7BWLxmlKuvLfK319rmIytSs/vnCzcD9uqyxTeU/A3A==", "requires": { - "@firebase/auth-interop-types": "0.1.4", - "@firebase/component": "0.1.9", - "@firebase/database-types": "0.4.14", - "@firebase/logger": "0.2.1", - "@firebase/util": "0.2.44", - "faye-websocket": "0.11.3", - "tslib": "1.11.1" + "@firebase/component": "0.5.13", + "@firebase/database": "0.12.8", + "@firebase/database-types": "0.9.7", + "@firebase/logger": "0.3.2", + "@firebase/util": "1.5.2", + "tslib": "^2.1.0" } }, "@firebase/database-types": { - "version": "0.4.14", - "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.4.14.tgz", - "integrity": "sha512-+D41HWac0HcvwMi+0dezEdSOZHpVjPKPNmpQiW2GDuS5kk27/v1jxc9v7F4ALLtpxbVcn16UZl5PqEkcS9H2Xg==", + "version": "0.9.7", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.9.7.tgz", + "integrity": "sha512-EFhgL89Fz6DY3kkB8TzdHvdu8XaqqvzcF2DLVOXEnQ3Ms7L755p5EO42LfxXoJqb9jKFvgLpFmKicyJG25WFWw==", "requires": { - "@firebase/app-types": "0.6.0" + "@firebase/app-types": "0.7.0", + "@firebase/util": "1.5.2" } }, "@firebase/logger": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.2.1.tgz", - "integrity": "sha512-H4nttTqUzEw3TA/JYl8ma6oMSNKHcdpEWV2L2qA+ZEcpM2OLAzagi//DrYBFR5xpPb17IGagpzSxFgx937Sq/A==" - }, - "@firebase/util": { - "version": "0.2.44", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.2.44.tgz", - "integrity": "sha512-yWnFdeuz7P0QC4oC77JyPdAQ/rTGPDfhHcR5WsoMsKBBHTyqEhaKWL9HeRird+p3AL9M4++ep0FYFNd1UKU3Wg==", + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.3.2.tgz", + "integrity": "sha512-lzLrcJp9QBWpo40OcOM9B8QEtBw2Fk1zOZQdvv+rWS6gKmhQBCEMc4SMABQfWdjsylBcDfniD1Q+fUX1dcBTXA==", "requires": { - "tslib": "1.11.1" + "tslib": "^2.1.0" } }, - "@google-cloud/common": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-2.4.0.tgz", - "integrity": "sha512-zWFjBS35eI9leAHhjfeOYlK5Plcuj/77EzstnrJIZbKgF/nkqjcQuGiMCpzCwOfPyUbz8ZaEOYgbHa759AKbjg==", - "optional": true, + "@firebase/util": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.5.2.tgz", + "integrity": "sha512-YvBH2UxFcdWG2HdFnhxZptPl2eVFlpOyTH66iDo13JPEYraWzWToZ5AMTtkyRHVmu7sssUpQlU9igy1KET7TOw==", "requires": { - "@google-cloud/projectify": "^1.0.0", - "@google-cloud/promisify": "^1.0.0", - "arrify": "^2.0.0", - "duplexify": "^3.6.0", - "ent": "^2.2.0", - "extend": "^3.0.2", - "google-auth-library": "^5.5.0", - "retry-request": "^4.0.0", - "teeny-request": "^6.0.0" + "tslib": "^2.1.0" } }, "@google-cloud/firestore": { - "version": "3.7.4", - "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-3.7.4.tgz", - "integrity": "sha512-RBMG4uZFHeQPFMHTRFMyQ7LDQTLa0f+U0hLAa/7XWjpZHgxKuOWBonsv+C3geymAwShIZSoV/NpNh9tBK7YF5g==", + "version": "4.15.1", + "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-4.15.1.tgz", + "integrity": "sha512-2PWsCkEF1W02QbghSeRsNdYKN1qavrHBP3m72gPDMHQSYrGULOaTi7fSJquQmAtc4iPVB2/x6h80rdLHTATQtA==", "optional": true, "requires": { - "deep-equal": "^2.0.0", + "fast-deep-equal": "^3.1.1", "functional-red-black-tree": "^1.0.1", - "google-gax": "^1.13.0", - "readable-stream": "^3.4.0", - "through2": "^3.0.0" + "google-gax": "^2.24.1", + "protobufjs": "^6.8.6" } }, "@google-cloud/paginator": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-2.0.3.tgz", - "integrity": "sha512-kp/pkb2p/p0d8/SKUu4mOq8+HGwF8NPzHWkj+VKrIPQPyMRw8deZtrO/OcSiy9C/7bpfU5Txah5ltUNfPkgEXg==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-3.0.7.tgz", + "integrity": "sha512-jJNutk0arIQhmpUUQJPJErsojqo834KcyB6X7a1mxuic8i1tKXxde8E69IZxNZawRIlZdIK2QY4WALvlK5MzYQ==", "optional": true, "requires": { "arrify": "^2.0.0", @@ -99,81 +1835,78 @@ } }, "@google-cloud/projectify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-1.0.4.tgz", - "integrity": "sha512-ZdzQUN02eRsmTKfBj9FDL0KNDIFNjBn/d6tHQmA/+FImH5DO6ZV8E7FzxMgAUiVAUq41RFAkb25p1oHOZ8psfg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-2.1.1.tgz", + "integrity": "sha512-+rssMZHnlh0twl122gXY4/aCrk0G1acBqkHFfYddtsqpYXGxA29nj9V5V9SfC+GyOG00l650f6lG9KL+EpFEWQ==", "optional": true }, "@google-cloud/promisify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-1.0.4.tgz", - "integrity": "sha512-VccZDcOql77obTnFh0TbNED/6ZbbmHDf8UMNnzO1d5g9V0Htfm4k5cllY8P1tJsRKC3zWYGRLaViiupcgVjBoQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-2.0.4.tgz", + "integrity": "sha512-j8yRSSqswWi1QqUGKVEKOG03Q7qOoZP6/h2zN2YO+F5h2+DHU0bSrHCK9Y7lo2DI9fBd8qGAw795sf+3Jva4yA==", "optional": true }, "@google-cloud/storage": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-4.7.0.tgz", - "integrity": "sha512-f0guAlbeg7Z0m3gKjCfBCu7FG9qS3M3oL5OQQxlvGoPtK7/qg3+W+KQV73O2/sbuS54n0Kh2mvT5K2FWzF5vVQ==", + "version": "5.19.4", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-5.19.4.tgz", + "integrity": "sha512-Jz7ugcPHhsEmMVvIxM9uoBsdEbKIYwDkh3u07tifsIymEWs47F4/D6+/Tv/W7kLhznqjyOjVJ/0frtBeIC0lJA==", "optional": true, "requires": { - "@google-cloud/common": "^2.1.1", - "@google-cloud/paginator": "^2.0.0", - "@google-cloud/promisify": "^1.0.0", + "@google-cloud/paginator": "^3.0.7", + "@google-cloud/projectify": "^2.0.0", + "@google-cloud/promisify": "^2.0.0", + "abort-controller": "^3.0.0", "arrify": "^2.0.0", + "async-retry": "^1.3.3", "compressible": "^2.0.12", - "concat-stream": "^2.0.0", - "date-and-time": "^0.13.0", - "duplexify": "^3.5.0", + "configstore": "^5.0.0", + "date-and-time": "^2.0.0", + "duplexify": "^4.0.0", + "ent": "^2.2.0", "extend": "^3.0.2", - "gaxios": "^3.0.0", - "gcs-resumable-upload": "^2.2.4", + "gaxios": "^4.0.0", + "get-stream": "^6.0.0", + "google-auth-library": "^7.14.1", "hash-stream-validation": "^0.2.2", - "mime": "^2.2.0", + "mime": "^3.0.0", "mime-types": "^2.0.8", - "onetime": "^5.1.0", - "p-limit": "^2.2.0", + "p-limit": "^3.0.1", "pumpify": "^2.0.0", - "readable-stream": "^3.4.0", + "retry-request": "^4.2.2", "snakeize": "^0.1.0", - "stream-events": "^1.0.1", - "through2": "^3.0.0", + "stream-events": "^1.0.4", + "teeny-request": "^7.1.3", "xdg-basedir": "^4.0.0" - }, - "dependencies": { - "gaxios": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-3.0.2.tgz", - "integrity": "sha512-cLOetrsKOBLPwjzVyFzirYaGjrhtYjbKUHp6fQpsio2HH8Mil35JTFQLgkV5D3CCXV7Gnd5V69/m4C9rMBi9bA==", - "optional": true, - "requires": { - "abort-controller": "^3.0.0", - "extend": "^3.0.2", - "https-proxy-agent": "^5.0.0", - "is-stream": "^2.0.0", - "node-fetch": "^2.3.0" - } - } } }, "@grpc/grpc-js": { - "version": "0.7.9", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-0.7.9.tgz", - "integrity": "sha512-ihn9xWOqubMPBlU77wcYpy7FFamGo5xtsK27EAILL/eoOvGEAq29UOrqRvqYPwWfl2+3laFmGKNR7uCdJhKu4Q==", + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.6.7.tgz", + "integrity": "sha512-eBM03pu9hd3VqDQG+kHahiG1x80RGkkqqRb1Pchcwqej/KkAH95gAvKs6laqaHCycYaPK+TKuNQnOz9UXYA8qw==", "optional": true, "requires": { - "semver": "^6.2.0" + "@grpc/proto-loader": "^0.6.4", + "@types/node": ">=12.12.47" } }, "@grpc/proto-loader": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.5.4.tgz", - "integrity": "sha512-HTM4QpI9B2XFkPz7pjwMyMgZchJ93TVkL3kWPW8GDMDKYxsMnmf4w2TNMJK7+KNiYHS5cJrCEAFlF+AwtXWVPA==", + "version": "0.6.9", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.6.9.tgz", + "integrity": "sha512-UlcCS8VbsU9d3XTXGiEVFonN7hXk+oMXZtoHHG2oSA1/GcDP1q6OUgs20PzHDGizzyi8ufGSUDlk3O2NyY7leg==", "optional": true, "requires": { + "@types/long": "^4.0.1", "lodash.camelcase": "^4.3.0", - "protobufjs": "^6.8.6" + "long": "^4.0.0", + "protobufjs": "^6.10.0", + "yargs": "^16.2.0" } }, + "@panva/asn1.js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@panva/asn1.js/-/asn1.js-1.0.0.tgz", + "integrity": "sha512-UdkG3mLEqXgnlKsWanWcgb6dOjUzJ+XC5f+aWw30qrtjxeNUSfKX1cd5FBzOaXQumoe9nIqeZUvrRJS03HCCtw==" + }, "@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -239,26 +1972,77 @@ "optional": true }, "@tootallnate/once": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.0.0.tgz", - "integrity": "sha512-KYyTT/T6ALPkIRd2Ge080X/BsXvy9O0hcWTtMWkPvwAwF99+vn6Dv4GzrFT/Nn1LePr+FFDbRXXlqmsy9lw2zA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", "optional": true }, - "@types/fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-UoOfVEzAUpeSPmjm7h1uk5MH6KZma2z2O7a75onTGjnNvAvMVrPzPL/vBbT65iIGHWj6rokwfmYcmxmlSf2uwg==", - "optional": true, + "@types/body-parser": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", + "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "@types/connect": { + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", + "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", "requires": { "@types/node": "*" } }, + "@types/express": { + "version": "4.17.13", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", + "integrity": "sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==", + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.18", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "@types/express-jwt": { + "version": "0.0.42", + "resolved": "https://registry.npmjs.org/@types/express-jwt/-/express-jwt-0.0.42.tgz", + "integrity": "sha512-WszgUddvM1t5dPpJ3LhWNH8kfNN8GPIBrAGxgIYXVCEGx6Bx4A036aAuf/r5WH9DIEdlmp7gHOYvSM6U87B0ag==", + "requires": { + "@types/express": "*", + "@types/express-unless": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.17.28", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.28.tgz", + "integrity": "sha512-P1BJAEAW3E2DJUlkgq4tOL3RyMunoWXqbSCygWo5ZIWTjUgN1YnaXWW4VWl/oc8vs/XoYibEGBKP0uZyF4AHig==", + "requires": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, + "@types/express-unless": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@types/express-unless/-/express-unless-0.5.3.tgz", + "integrity": "sha512-TyPLQaF6w8UlWdv4gj8i46B+INBVzURBNRahCozCSXfsK2VTlL1wNyTlMKw817VHygBtlcl5jfnPadlydr06Yw==", + "requires": { + "@types/express": "*" + } + }, "@types/long": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz", - "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", "optional": true }, + "@types/mime": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", + "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==" + }, "@types/minimist": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.0.tgz", @@ -269,6 +2053,25 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-13.11.1.tgz", "integrity": "sha512-eWQGP3qtxwL8FGneRrC5DwrJLGN4/dH1clNTuLfN81HCrxVtxRjygDTUoZJ5ASlDEeo0ppYFQjQIlXhtXpOn6g==" }, + "@types/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==" + }, + "@types/range-parser": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", + "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" + }, + "@types/serve-static": { + "version": "1.13.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz", + "integrity": "sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==", + "requires": { + "@types/mime": "^1", + "@types/node": "*" + } + }, "abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", @@ -281,37 +2084,45 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.0.tgz", "integrity": "sha512-j1Q7cSCqN+AwrmDd+pzgqc0/NpC655x2bUf5ZjRIO77DcNBFmh+OgRNzF6OKdCC9RSCb19fGd99+bhXFdkRNqw==", - "optional": true, "requires": { "debug": "4" } }, - "array-filter": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-filter/-/array-filter-1.0.0.tgz", - "integrity": "sha1-uveeYubvTCpMC4MSMtr/7CUfnYM=", + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "optional": true }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "optional": true, + "requires": { + "color-convert": "^2.0.1" + } + }, "arrify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==" }, - "available-typed-arrays": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.2.tgz", - "integrity": "sha512-XWX3OX8Onv97LMk/ftVyBibpGwY5a8SmuxZPzeOxqmuEqUCOM9ZE+uIaD1VNJ5QnvU2UQusvmKbuM1FR8QWGfQ==", + "async-retry": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", + "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", "optional": true, "requires": { - "array-filter": "^1.0.0" + "retry": "0.13.1" } }, "axios": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz", - "integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==", + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", + "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", "requires": { - "follow-redirects": "1.5.10" + "follow-redirects": "^1.14.0" } }, "base64-js": { @@ -320,19 +2131,39 @@ "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" }, "bignumber.js": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-7.2.1.tgz", - "integrity": "sha512-S4XzBk5sMB+Rcb/LNcpzXr57VRTxgAvaAEDAl1AwRx27j00hT84O6OkteE7u8UB3NuaaygCRrEpqox4uDOrbdQ==" + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.2.tgz", + "integrity": "sha512-GAcQvbpsM0pUb0zw1EI0KhQEZ+lRwR5fYaAp3vPOYuP7aDvGy6cVN6XHLauvF8SOga2y0dcLcjt3iQDTSEliyw==" }, "buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" }, - "buffer-from": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "optional": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "optional": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "optional": true }, "compressible": { @@ -344,18 +2175,6 @@ "mime-db": ">= 1.43.0 < 2" } }, - "concat-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", - "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", - "optional": true, - "requires": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.0.2", - "typedarray": "^0.0.6" - } - }, "configstore": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", @@ -370,12 +2189,6 @@ "xdg-basedir": "^4.0.0" } }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "optional": true - }, "crypto-random-string": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", @@ -383,47 +2196,17 @@ "optional": true }, "date-and-time": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/date-and-time/-/date-and-time-0.13.1.tgz", - "integrity": "sha512-/Uge9DJAT+s+oAcDxtBhyR8+sKjUnZbYmyhbmWjTHNtX7B7oWD8YyYdeXcBRbwSj6hVvj+IQegJam7m7czhbFw==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/date-and-time/-/date-and-time-2.3.1.tgz", + "integrity": "sha512-OaIRmSJXifwEN21rMVVDs0Kz8uhJ3wWPYd86atkRiqN54liaMQYEbbrgjZQea75YXOBWL4ZFb3rG/waenw1TEg==", "optional": true }, "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "optional": true, - "requires": { - "ms": "^2.1.1" - } - }, - "deep-equal": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.0.2.tgz", - "integrity": "sha512-kX0bjV7tdMuhrhzKPEnVwqfQCuf+IEfN+4Xqv4eKd75xGRyn8yzdQ9ujPY6a221rgJKyQC4KBu1PibDTpa6m9w==", - "optional": true, - "requires": { - "es-abstract": "^1.17.5", - "es-get-iterator": "^1.1.0", - "is-arguments": "^1.0.4", - "is-date-object": "^1.0.2", - "is-regex": "^1.0.5", - "isarray": "^2.0.5", - "object-is": "^1.0.2", - "object-keys": "^1.1.1", - "regexp.prototype.flags": "^1.3.0", - "side-channel": "^1.0.2", - "which-boxed-primitive": "^1.0.1", - "which-collection": "^1.0.1", - "which-typed-array": "^1.1.1" - } - }, - "define-properties": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "requires": { - "object-keys": "^1.0.12" + "ms": "2.1.2" } }, "dicer": { @@ -435,53 +2218,24 @@ } }, "dot-prop": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.2.0.tgz", - "integrity": "sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", "optional": true, "requires": { "is-obj": "^2.0.0" } }, "duplexify": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", - "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.2.tgz", + "integrity": "sha512-fz3OjcNCHmRP12MJoZMPglx8m4rrFP8rovnk4vT8Fs+aonZoCwGg10dSsQsfP/E62eZcPTMSMP6686fu9Qlqtw==", "optional": true, "requires": { - "end-of-stream": "^1.0.0", - "inherits": "^2.0.1", - "readable-stream": "^2.0.0", + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", "stream-shift": "^1.0.0" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "optional": true - }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "optional": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "optional": true - } } }, "ecdsa-sig-formatter": { @@ -492,6 +2246,12 @@ "safe-buffer": "^5.0.1" } }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "optional": true + }, "end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -507,48 +2267,11 @@ "integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0=", "optional": true }, - "es-abstract": { - "version": "1.17.5", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz", - "integrity": "sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==", - "requires": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.1.5", - "is-regex": "^1.0.5", - "object-inspect": "^1.7.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.0", - "string.prototype.trimleft": "^2.1.1", - "string.prototype.trimright": "^2.1.1" - } - }, - "es-get-iterator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.0.tgz", - "integrity": "sha512-UfrmHuWQlNMTs35e1ypnvikg6jCz3SK8v8ImvmDsh36fCVUR1MqoFDiyn0/k52C8NqO3YsO8Oe0azeesNuqSsQ==", - "optional": true, - "requires": { - "es-abstract": "^1.17.4", - "has-symbols": "^1.0.1", - "is-arguments": "^1.0.4", - "is-map": "^2.0.1", - "is-set": "^2.0.1", - "is-string": "^1.0.5", - "isarray": "^2.0.5" - } - }, - "es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", - "requires": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - } + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "optional": true }, "event-target-shim": { "version": "5.0.1", @@ -560,73 +2283,45 @@ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "optional": true + }, "fast-text-encoding": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.1.tgz", - "integrity": "sha512-x4FEgaz3zNRtJfLFqJmHWxkMDDvXVtaznj2V9jiP8ACUJrUgist4bP9FmDL2Vew2Y9mEQI/tG4GqabaitYp9CQ==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz", + "integrity": "sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig==" }, "faye-websocket": { - "version": "0.11.3", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.3.tgz", - "integrity": "sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA==", + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", "requires": { "websocket-driver": ">=0.5.1" } }, "firebase-admin": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-8.10.0.tgz", - "integrity": "sha512-QzJZ1sBh9xzKjb44aP6m1duy0Xe1ixexwh0eaOt1CkJYCOq2b6bievK4GNWMl5yGQ7FFBEbZO6hyDi+5wrctcg==", - "requires": { - "@firebase/database": "^0.5.17", - "@google-cloud/firestore": "^3.0.0", - "@google-cloud/storage": "^4.1.2", - "@types/node": "^8.10.59", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-10.1.0.tgz", + "integrity": "sha512-4i4wu+EFgNfY4+D4DxXkZcmbD832ozUMNvHMkOFQrf8upyp51n6jrDJS+wLok9sd62yeqcImbnsLOympGlISPA==", + "requires": { + "@firebase/database-compat": "^0.1.1", + "@firebase/database-types": "^0.9.3", + "@google-cloud/firestore": "^4.15.1", + "@google-cloud/storage": "^5.18.3", + "@types/node": ">=12.12.47", "dicer": "^0.3.0", - "jsonwebtoken": "8.1.0", - "node-forge": "0.7.4" - }, - "dependencies": { - "@types/node": { - "version": "8.10.60", - "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.60.tgz", - "integrity": "sha512-YjPbypHFuiOV0bTgeF07HpEEqhmHaZqYNSdCKeBJa+yFoQ/7BC+FpJcwmi34xUIIRVFktnUyP1dPU8U0612GOg==" - } - } - }, - "follow-redirects": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", - "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", - "requires": { - "debug": "=3.1.0" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - } + "jsonwebtoken": "^8.5.1", + "jwks-rsa": "^2.0.2", + "node-forge": "^1.3.1" } }, - "foreach": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", - "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=", - "optional": true - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "follow-redirects": { + "version": "1.14.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz", + "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==" }, "functional-red-black-tree": { "version": "1.0.1", @@ -635,307 +2330,146 @@ "optional": true }, "gaxios": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-2.3.4.tgz", - "integrity": "sha512-US8UMj8C5pRnao3Zykc4AAVr+cffoNKRTg9Rsf2GiuZCW69vgJj38VK2PzlPuQU73FZ/nTk9/Av6/JGcE1N9vA==", - "optional": true, + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-4.3.3.tgz", + "integrity": "sha512-gSaYYIO1Y3wUtdfHmjDUZ8LWaxJQpiavzbF5Kq53akSzvmVg0RfyOcFDbO1KJ/KCGRFz2qG+lS81F0nkr7cRJA==", "requires": { "abort-controller": "^3.0.0", "extend": "^3.0.2", "https-proxy-agent": "^5.0.0", "is-stream": "^2.0.0", - "node-fetch": "^2.3.0" + "node-fetch": "^2.6.7" } }, "gcp-metadata": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-3.5.0.tgz", - "integrity": "sha512-ZQf+DLZ5aKcRpLzYUyBS3yo3N0JSa82lNDO8rj3nMSlovLcz2riKFBsYgDzeXcv75oo5eqB2lx+B14UvPoCRnA==", - "optional": true, + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-4.3.1.tgz", + "integrity": "sha512-x850LS5N7V1F3UcV7PoupzGsyD6iVwTVvsh3tbXfkctZnBnjW5yu5z1/3k3SehF7TyoTIe78rJs02GMMy+LF+A==", "requires": { - "gaxios": "^2.1.0", - "json-bigint": "^0.3.0" + "gaxios": "^4.0.0", + "json-bigint": "^1.0.0" } }, - "gcs-resumable-upload": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/gcs-resumable-upload/-/gcs-resumable-upload-2.3.3.tgz", - "integrity": "sha512-sf896I5CC/1AxeaGfSFg3vKMjUq/r+A3bscmVzZm10CElyRanN0XwPu/MxeIO4LSP+9uF6yKzXvNsaTsMXUG6Q==", - "optional": true, - "requires": { - "abort-controller": "^3.0.0", - "configstore": "^5.0.0", - "gaxios": "^2.0.0", - "google-auth-library": "^5.0.0", - "pumpify": "^2.0.0", - "stream-events": "^1.0.4" - } + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "optional": true + }, + "get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "optional": true }, "google-auth-library": { - "version": "5.10.1", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-5.10.1.tgz", - "integrity": "sha512-rOlaok5vlpV9rSiUu5EpR0vVpc+PhN62oF4RyX/6++DG1VsaulAFEMlDYBLjJDDPI6OcNOCGAKy9UVB/3NIDXg==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-7.14.1.tgz", + "integrity": "sha512-5Rk7iLNDFhFeBYc3s8l1CqzbEBcdhwR193RlD4vSNFajIcINKI8W8P0JLmBpwymHqqWbX34pJDQu39cSy/6RsA==", "optional": true, "requires": { "arrify": "^2.0.0", "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "fast-text-encoding": "^1.0.0", - "gaxios": "^2.1.0", - "gcp-metadata": "^3.4.0", - "gtoken": "^4.1.0", + "gaxios": "^4.0.0", + "gcp-metadata": "^4.2.0", + "gtoken": "^5.0.4", "jws": "^4.0.0", - "lru-cache": "^5.0.0" + "lru-cache": "^6.0.0" } }, "google-gax": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-1.15.2.tgz", - "integrity": "sha512-yNNiRf9QxWpZNfQQmSPz3rIDTBDDKnLKY/QEsjCaJyDxttespr6v8WRGgU5KrU/6ZM7QRlgBAYXCkxqHhJp0wA==", + "version": "2.30.2", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-2.30.2.tgz", + "integrity": "sha512-BCNCT26kb0iC52zj2SosyOZMhI5sVfXuul1h0Aw5uT9nGAbmS5eOvQ49ft53ft6XotDj11sUSDV6XESEiQqCqg==", "optional": true, "requires": { - "@grpc/grpc-js": "^0.7.4", - "@grpc/proto-loader": "^0.5.1", - "@types/fs-extra": "^8.0.1", + "@grpc/grpc-js": "~1.6.0", + "@grpc/proto-loader": "^0.6.1", "@types/long": "^4.0.0", "abort-controller": "^3.0.0", - "duplexify": "^3.6.0", - "google-auth-library": "^5.0.0", + "duplexify": "^4.0.0", + "fast-text-encoding": "^1.0.3", + "google-auth-library": "^7.14.0", "is-stream-ended": "^0.1.4", - "lodash.at": "^4.6.0", - "lodash.has": "^4.5.2", - "node-fetch": "^2.6.0", - "protobufjs": "^6.8.9", - "retry-request": "^4.0.0", - "semver": "^6.0.0", - "walkdir": "^0.4.0" + "node-fetch": "^2.6.1", + "object-hash": "^3.0.0", + "proto3-json-serializer": "^0.1.8", + "protobufjs": "6.11.2", + "retry-request": "^4.0.0" } }, "google-p12-pem": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-2.0.4.tgz", - "integrity": "sha512-S4blHBQWZRnEW44OcR7TL9WR+QCqByRvhNDZ/uuQfpxywfupikf/miba8js1jZi6ZOGv5slgSuoshCWh6EMDzg==", - "optional": true, + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-3.1.4.tgz", + "integrity": "sha512-HHuHmkLgwjdmVRngf5+gSmpkyaRI6QmOg77J8tkNBHhNEI62sGHyw4/+UkgyZEI7h84NbWprXDJ+sa3xOYFvTg==", "requires": { - "node-forge": "^0.9.0" - }, - "dependencies": { - "node-forge": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.9.1.tgz", - "integrity": "sha512-G6RlQt5Sb4GMBzXvhfkeFmbqR6MzhtnT7VTHuLadjkii3rdYHNdw0m8zA4BTxVIh68FicCQ2NSUANpsqkr9jvQ==", - "optional": true - } + "node-forge": "^1.3.1" } }, "google-spreadsheet": { - "version": "3.0.11", - "resolved": "https://registry.npmjs.org/google-spreadsheet/-/google-spreadsheet-3.0.11.tgz", - "integrity": "sha512-bkYUdsq4Nwg7klnFevG6MRHXAfGh9gYVymGp001YdtThct5uSIB/41lUOsGzFbQQSR5FcBTwAu9nZVYvaNdv9Q==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/google-spreadsheet/-/google-spreadsheet-3.2.0.tgz", + "integrity": "sha512-z7XMaqb+26rdo8p51r5O03u8aPLAPzn5YhOXYJPcf2hdMVr0dUbIARgdkRdmGiBeoV/QoU/7VNhq1MMCLZv3kQ==", "requires": { - "axios": "^0.19.1", - "google-auth-library": "^5.9.1", - "lodash": "^4.17.15" + "axios": "^0.21.4", + "google-auth-library": "^6.1.3", + "lodash": "^4.17.21" }, "dependencies": { - "agent-base": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.0.tgz", - "integrity": "sha512-j1Q7cSCqN+AwrmDd+pzgqc0/NpC655x2bUf5ZjRIO77DcNBFmh+OgRNzF6OKdCC9RSCb19fGd99+bhXFdkRNqw==", - "requires": { - "debug": "4" - } - }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "requires": { - "ms": "^2.1.1" - } - }, - "gaxios": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-2.3.4.tgz", - "integrity": "sha512-US8UMj8C5pRnao3Zykc4AAVr+cffoNKRTg9Rsf2GiuZCW69vgJj38VK2PzlPuQU73FZ/nTk9/Av6/JGcE1N9vA==", - "requires": { - "abort-controller": "^3.0.0", - "extend": "^3.0.2", - "https-proxy-agent": "^5.0.0", - "is-stream": "^2.0.0", - "node-fetch": "^2.3.0" - } - }, - "gcp-metadata": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-3.5.0.tgz", - "integrity": "sha512-ZQf+DLZ5aKcRpLzYUyBS3yo3N0JSa82lNDO8rj3nMSlovLcz2riKFBsYgDzeXcv75oo5eqB2lx+B14UvPoCRnA==", - "requires": { - "gaxios": "^2.1.0", - "json-bigint": "^0.3.0" - } - }, "google-auth-library": { - "version": "5.10.1", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-5.10.1.tgz", - "integrity": "sha512-rOlaok5vlpV9rSiUu5EpR0vVpc+PhN62oF4RyX/6++DG1VsaulAFEMlDYBLjJDDPI6OcNOCGAKy9UVB/3NIDXg==", + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-6.1.6.tgz", + "integrity": "sha512-Q+ZjUEvLQj/lrVHF/IQwRo6p3s8Nc44Zk/DALsN+ac3T4HY/g/3rrufkgtl+nZ1TW7DNAw5cTChdVp4apUXVgQ==", "requires": { "arrify": "^2.0.0", "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "fast-text-encoding": "^1.0.0", - "gaxios": "^2.1.0", - "gcp-metadata": "^3.4.0", - "gtoken": "^4.1.0", + "gaxios": "^4.0.0", + "gcp-metadata": "^4.2.0", + "gtoken": "^5.0.4", "jws": "^4.0.0", - "lru-cache": "^5.0.0" + "lru-cache": "^6.0.0" } - }, - "google-p12-pem": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-2.0.4.tgz", - "integrity": "sha512-S4blHBQWZRnEW44OcR7TL9WR+QCqByRvhNDZ/uuQfpxywfupikf/miba8js1jZi6ZOGv5slgSuoshCWh6EMDzg==", - "requires": { - "node-forge": "^0.9.0" - } - }, - "gtoken": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-4.1.4.tgz", - "integrity": "sha512-VxirzD0SWoFUo5p8RDP8Jt2AGyOmyYcT/pOUgDKJCK+iSw0TMqwrVfY37RXTNmoKwrzmDHSk0GMT9FsgVmnVSA==", - "requires": { - "gaxios": "^2.1.0", - "google-p12-pem": "^2.0.0", - "jws": "^4.0.0", - "mime": "^2.2.0" - } - }, - "https-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", - "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", - "requires": { - "agent-base": "6", - "debug": "4" - } - }, - "jwa": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", - "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", - "requires": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "jws": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", - "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", - "requires": { - "jwa": "^2.0.0", - "safe-buffer": "^5.0.1" - } - }, - "node-forge": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.9.1.tgz", - "integrity": "sha512-G6RlQt5Sb4GMBzXvhfkeFmbqR6MzhtnT7VTHuLadjkii3rdYHNdw0m8zA4BTxVIh68FicCQ2NSUANpsqkr9jvQ==" } } }, "graceful-fs": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", - "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==", + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", "optional": true }, "gtoken": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-4.1.4.tgz", - "integrity": "sha512-VxirzD0SWoFUo5p8RDP8Jt2AGyOmyYcT/pOUgDKJCK+iSw0TMqwrVfY37RXTNmoKwrzmDHSk0GMT9FsgVmnVSA==", - "optional": true, - "requires": { - "gaxios": "^2.1.0", - "google-p12-pem": "^2.0.0", - "jws": "^4.0.0", - "mime": "^2.2.0" - } - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-5.3.2.tgz", + "integrity": "sha512-gkvEKREW7dXWF8NV8pVrKfW7WqReAmjjkMBh6lNCCGOM4ucS0r0YyXXl0r/9Yj8wcW/32ISkfc8h5mPTDbtifQ==", "requires": { - "function-bind": "^1.1.1" + "gaxios": "^4.0.0", + "google-p12-pem": "^3.1.3", + "jws": "^4.0.0" } }, - "has-symbols": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", - "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==" - }, "hash-stream-validation": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/hash-stream-validation/-/hash-stream-validation-0.2.2.tgz", - "integrity": "sha512-cMlva5CxWZOrlS/cY0C+9qAzesn5srhFA8IT1VPiHc9bWWBLkJfEUIZr7MWoi89oOOGmpg8ymchaOjiArsGu5A==", - "optional": true, - "requires": { - "through2": "^2.0.0" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "optional": true - }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "optional": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "optional": true - }, - "through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "optional": true, - "requires": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - } - } + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/hash-stream-validation/-/hash-stream-validation-0.2.4.tgz", + "integrity": "sha512-Gjzu0Xn7IagXVkSu9cSFuK1fqzwtLwFhNhVL8IFJijRNMgUttFbBSIAzKuSIrsFMO1+g1RlsoN49zPIbwPDMGQ==", + "optional": true }, "http-parser-js": { - "version": "0.4.10", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.4.10.tgz", - "integrity": "sha1-ksnBN0w1CF912zWexWzCV8u5P6Q=" + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.6.tgz", + "integrity": "sha512-vDlkRPDJn93swjcjqMSaGSPABbIarsr1TLAui/gLDXzV5VsJNdXNzMYDyNBLQkjWQCJ1uizu8T2oDMhmGt0PRA==" }, "http-proxy-agent": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", - "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", "optional": true, "requires": { - "@tootallnate/once": "1", + "@tootallnate/once": "2", "agent-base": "6", "debug": "4" } @@ -944,7 +2478,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", - "optional": true, "requires": { "agent-base": "6", "debug": "4" @@ -962,44 +2495,10 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "optional": true }, - "is-arguments": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.0.4.tgz", - "integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==", - "optional": true - }, - "is-bigint": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.0.tgz", - "integrity": "sha512-t5mGUXC/xRheCK431ylNiSkGGpBp8bHENBcENTkDT6ppwPzEVxNGZRvgvmOEfbWkFhA7D2GEuE2mmQTr78sl2g==", - "optional": true - }, - "is-boolean-object": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.0.1.tgz", - "integrity": "sha512-TqZuVwa/sppcrhUCAYkGBk7w0yxfQQnxq28fjkO53tnK9FQXmdwz2JS5+GjsWQ6RByES1K40nI+yDic5c9/aAQ==", - "optional": true - }, - "is-callable": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", - "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==" - }, - "is-date-object": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", - "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==" - }, - "is-map": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.1.tgz", - "integrity": "sha512-T/S49scO8plUiAOA2DBTBG3JHpn1yiw0kRp6dgiZ0v2/6twi5eiB0rHtHFH9ZIrvlWc6+4O+m4zg5+Z833aXgw==", - "optional": true - }, - "is-number-object": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.4.tgz", - "integrity": "sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw==", + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "optional": true }, "is-obj": { @@ -1008,20 +2507,6 @@ "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", "optional": true }, - "is-regex": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", - "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", - "requires": { - "has": "^1.0.3" - } - }, - "is-set": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.1.tgz", - "integrity": "sha512-eJEzOtVyenDs1TMzSQ3kU3K+E0GUS9sno+F0OBT97xsgcJsF9nXMBtkT9/kut5JEpM7oL7X/0qxR17K3mcwIAA==", - "optional": true - }, "is-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", @@ -1033,70 +2518,34 @@ "integrity": "sha512-xj0XPvmr7bQFTvirqnFr50o0hQIh6ZItDqloxt5aJrR4NQsYeSsyFQERYGCAzfindAcnKjINnwEEgLx4IqVzQw==", "optional": true }, - "is-string": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz", - "integrity": "sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==", - "optional": true - }, - "is-symbol": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", - "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", - "requires": { - "has-symbols": "^1.0.1" - } - }, - "is-typed-array": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.3.tgz", - "integrity": "sha512-BSYUBOK/HJibQ30wWkWold5txYwMUXQct9YHAQJr8fSwvZoiglcqB0pd7vEN23+Tsi9IUEjztdOSzl4qLVYGTQ==", - "optional": true, - "requires": { - "available-typed-arrays": "^1.0.0", - "es-abstract": "^1.17.4", - "foreach": "^2.0.5", - "has-symbols": "^1.0.1" - } - }, "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", "optional": true }, - "is-weakmap": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", - "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==", - "optional": true - }, - "is-weakset": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.1.tgz", - "integrity": "sha512-pi4vhbhVHGLxohUw7PhGsueT4vRGFoXhP7+RGN0jKIv9+8PWYCQTqtADngrxOm2g46hoH0+g8uZZBzMrvVGDmw==", - "optional": true - }, - "isarray": { + "jose": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "optional": true + "resolved": "https://registry.npmjs.org/jose/-/jose-2.0.5.tgz", + "integrity": "sha512-BAiDNeDKTMgk4tvD0BbxJ8xHEHBZgpeRZ1zGPPsitSyMgjoMWiLGYAE7H7NpP5h0lPppQajQs871E8NHUrzVPA==", + "requires": { + "@panva/asn1.js": "^1.0.0" + } }, "json-bigint": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-0.3.0.tgz", - "integrity": "sha1-DM2RLEuCcNBfBW+9E4FLU9OCWx4=", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", "requires": { - "bignumber.js": "^7.0.0" + "bignumber.js": "^9.0.0" } }, "jsonwebtoken": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.1.0.tgz", - "integrity": "sha1-xjl80uX9WD1lwAeoPce7eOaYK4M=", + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", + "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", "requires": { - "jws": "^3.1.4", + "jws": "^3.2.2", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", @@ -1104,8 +2553,8 @@ "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", - "ms": "^2.0.0", - "xtend": "^4.0.1" + "ms": "^2.1.1", + "semver": "^5.6.0" }, "dependencies": { "jwa": { @@ -1126,6 +2575,11 @@ "jwa": "^1.4.1", "safe-buffer": "^5.0.1" } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" } } }, @@ -1133,33 +2587,42 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", - "optional": true, "requires": { "buffer-equal-constant-time": "1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, + "jwks-rsa": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-2.1.0.tgz", + "integrity": "sha512-GKOSDBWWBCiQTzawei6mEdRQvji5gecj8F9JwMt0ZOPnBPSmTjo5CKFvvbhE7jGPkU159Cpi0+OTLuABFcNOQQ==", + "requires": { + "@types/express-jwt": "0.0.42", + "debug": "^4.3.4", + "jose": "^2.0.5", + "limiter": "^1.1.5", + "lru-memoizer": "^2.1.4" + } + }, "jws": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", - "optional": true, "requires": { "jwa": "^2.0.0", "safe-buffer": "^5.0.1" } }, - "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + "limiter": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", + "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==" }, - "lodash.at": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.at/-/lodash.at-4.6.0.tgz", - "integrity": "sha1-k83OZk8KGZTqM9181A4jr9EbD/g=", - "optional": true + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "lodash.camelcase": { "version": "4.3.0", @@ -1167,11 +2630,10 @@ "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=", "optional": true }, - "lodash.has": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/lodash.has/-/lodash.has-4.5.2.tgz", - "integrity": "sha1-0Z9NwQlQWMzL4rDN9O4P5Ko3yGI=", - "optional": true + "lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=" }, "lodash.includes": { "version": "4.3.0", @@ -1215,52 +2677,72 @@ "optional": true }, "lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, + "lru-memoizer": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.1.4.tgz", + "integrity": "sha512-IXAq50s4qwrOBrXJklY+KhgZF+5y98PDaNo0gi/v2KQBFLyWr+JyFvijZXkGKjQj/h9c0OwoE+JZbwUXce76hQ==", "requires": { - "yallist": "^3.0.2" + "lodash.clonedeep": "^4.5.0", + "lru-cache": "~4.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.0.2.tgz", + "integrity": "sha1-HRdnnAac2l0ECZGgnbwsDbN35V4=", + "requires": { + "pseudomap": "^1.0.1", + "yallist": "^2.0.0" + } + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" + } } }, "make-dir": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.2.tgz", - "integrity": "sha512-rYKABKutXa6vXTXhoV18cBE7PaewPXHe/Bdq4v+ZLMhxbWApkFFplT0LcbMW+6BbjnQXzZ/sAvSE/JdguApG5w==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", "optional": true, "requires": { "semver": "^6.0.0" } }, "mime": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz", - "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "optional": true }, "mime-db": { - "version": "1.43.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz", - "integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==", + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "optional": true }, "mime-types": { - "version": "2.1.26", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz", - "integrity": "sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==", + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "optional": true, "requires": { - "mime-db": "1.43.0" + "mime-db": "1.52.0" } }, - "mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "optional": true - }, "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" }, "ms": { "version": "2.1.2", @@ -1268,42 +2750,24 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node-fetch": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", - "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==" + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "requires": { + "whatwg-url": "^5.0.0" + } }, "node-forge": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.4.tgz", - "integrity": "sha512-8Df0906+tq/omxuCZD6PqhPaQDYuyJ1d+VITgxoIA8zvQd1ru+nMJcDChHH324MWitIgbVkAkQoGEEVJNpn/PA==" - }, - "object-inspect": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz", - "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==" + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==" }, - "object-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.0.2.tgz", - "integrity": "sha512-Epah+btZd5wrrfjkJZq1AOB9O6OxUQto45hzFd7lXGrpHPGE0W1k+426yrZV+k6NJOzLNNW/nVsmZdIWsAqoOQ==", + "object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", "optional": true }, - "object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" - }, - "object.assign": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", - "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", - "requires": { - "define-properties": "^1.1.2", - "function-bind": "^1.1.1", - "has-symbols": "^1.0.0", - "object-keys": "^1.0.11" - } - }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -1313,40 +2777,28 @@ "wrappy": "1" } }, - "onetime": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz", - "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==", + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "optional": true, "requires": { - "mimic-fn": "^2.1.0" + "yocto-queue": "^0.1.0" } }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "proto3-json-serializer": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-0.1.8.tgz", + "integrity": "sha512-ACilkB6s1U1gWnl5jtICpnDai4VCxmI9GFxuEaYdxtDG2oVI3sVFIUsvUZcQbJgtPM6p+zqKbjTKQZp6Y4FpQw==", "optional": true, "requires": { - "p-try": "^2.0.0" + "protobufjs": "^6.11.2" } }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "optional": true - }, - "process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "optional": true - }, "protobufjs": { - "version": "6.8.9", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.8.9.tgz", - "integrity": "sha512-j2JlRdUeL/f4Z6x4aU4gj9I2LECglC+5qR2TrWb193Tla1qfdaNQTZ8I27Pt7K0Ajmvjjpft7O3KWTGciz4gpw==", + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.2.tgz", + "integrity": "sha512-4BQJoPooKJl2G9j3XftkIXjoC9C0Av2NOrWmbLWT1vH32GcSUHjM0Arra6UfTsVyfMAuFzaLucXn1sadxJydAw==", "optional": true, "requires": { "@protobufjs/aspromise": "^1.1.2", @@ -1359,19 +2811,16 @@ "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", - "@types/long": "^4.0.0", - "@types/node": "^10.1.0", + "@types/long": "^4.0.1", + "@types/node": ">=13.7.0", "long": "^4.0.0" - }, - "dependencies": { - "@types/node": { - "version": "10.17.19", - "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.19.tgz", - "integrity": "sha512-46/xThm3zvvc9t9/7M3AaLEqtOpqlYYYcCZbpYVAQHG20+oMZBkae/VMrn4BTi6AJ8cpack0mEXhGiKmDNbLrQ==", - "optional": true - } } }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" + }, "pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -1391,20 +2840,6 @@ "duplexify": "^4.1.1", "inherits": "^2.0.3", "pump": "^3.0.0" - }, - "dependencies": { - "duplexify": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.1.tgz", - "integrity": "sha512-DY3xVEmVHTv1wSzKNbwoU6nVjzI369Y6sPoqfYr0/xlx3IdX2n94xIszTcjPO8W8ZIv0Wb0PXNcjuZyT4wiICA==", - "optional": true, - "requires": { - "end-of-stream": "^1.4.1", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1", - "stream-shift": "^1.0.0" - } - } } }, "readable-stream": { @@ -1418,24 +2853,26 @@ "util-deprecate": "^1.0.1" } }, - "regexp.prototype.flags": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz", - "integrity": "sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ==", - "optional": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.0-next.1" - } + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "optional": true + }, + "retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "optional": true }, "retry-request": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-4.1.1.tgz", - "integrity": "sha512-BINDzVtLI2BDukjWmjAIRZ0oglnCAkpP2vQjM3jdLhmT62h0xnQgciPwBRDAvHqpkPT2Wo1XuUyLyn6nbGrZQQ==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-4.2.2.tgz", + "integrity": "sha512-xA93uxUD/rogV7BV59agW/JHPGXeREMWiZc9jhcwY4YdZ7QOtC7qbomYg0n4wyk2lJhggjvKvhNX8wln/Aldhg==", "optional": true, "requires": { "debug": "^4.1.1", - "through2": "^3.0.1" + "extend": "^3.0.2" } }, "safe-buffer": { @@ -1449,20 +2886,10 @@ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", "optional": true }, - "side-channel": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.2.tgz", - "integrity": "sha512-7rL9YlPHg7Ancea1S96Pa8/QWb4BtXL/TZvS6B8XFetGBeuhAsfmUspK6DokBeZ64+Kj9TCNRD/30pVz1BvQNA==", - "optional": true, - "requires": { - "es-abstract": "^1.17.0-next.1", - "object-inspect": "^1.7.0" - } - }, "signal-exit": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", - "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "optional": true }, "snakeize": { @@ -1491,59 +2918,33 @@ "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=" }, - "string.prototype.trimend": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz", - "integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==", - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.5" - } - }, - "string.prototype.trimleft": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.2.tgz", - "integrity": "sha512-gCA0tza1JBvqr3bfAIFJGqfdRTyPae82+KTnm3coDXkZN9wnuW3HjGgN386D7hfv5CHQYCI022/rJPVlqXyHSw==", - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.5", - "string.prototype.trimstart": "^1.0.0" - } - }, - "string.prototype.trimright": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.2.tgz", - "integrity": "sha512-ZNRQ7sY3KroTaYjRS6EbNiiHrOkjihL9aQE/8gfQ4DtAC/aEBRHFJa44OmoWxGGqXuJlfKkZW4WcXErGr+9ZFg==", + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "optional": true, "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.5", - "string.prototype.trimend": "^1.0.0" + "safe-buffer": "~5.2.0" } }, - "string.prototype.trimstart": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz", - "integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==", + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "optional": true, "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.5" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" } }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "optional": true, "requires": { - "safe-buffer": "~5.1.0" - }, - "dependencies": { - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "optional": true - } + "ansi-regex": "^5.0.1" } }, "stubs": { @@ -1553,37 +2954,27 @@ "optional": true }, "teeny-request": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-6.0.3.tgz", - "integrity": "sha512-TZG/dfd2r6yeji19es1cUIwAlVD8y+/svB1kAC2Y0bjEyysrfbO8EZvJBRwIE6WkwmUoB7uvWLwTIhJbMXZ1Dw==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-7.2.0.tgz", + "integrity": "sha512-SyY0pek1zWsi0LRVAALem+avzMLc33MKW/JLLakdP4s9+D7+jHcy5x6P+h94g2QNZsAqQNfX5lsbd3WSeJXrrw==", "optional": true, "requires": { - "http-proxy-agent": "^4.0.0", + "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.0", - "node-fetch": "^2.2.0", + "node-fetch": "^2.6.1", "stream-events": "^1.0.5", - "uuid": "^7.0.0" + "uuid": "^8.0.0" } }, - "through2": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.1.tgz", - "integrity": "sha512-M96dvTalPT3YbYLaKaCuwu+j06D/8Jfib0o/PxbVt6Amhv3dUAtW6rTV1jPgJSBG83I/e04Y6xkVdVhSRhi0ww==", - "optional": true, - "requires": { - "readable-stream": "2 || 3" - } + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" }, "tslib": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.1.tgz", - "integrity": "sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==" - }, - "typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", - "optional": true + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" }, "typedarray-to-buffer": { "version": "3.1.5", @@ -1616,69 +3007,49 @@ "optional": true }, "uuid": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz", - "integrity": "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==", + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", "optional": true }, - "walkdir": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/walkdir/-/walkdir-0.4.1.tgz", - "integrity": "sha512-3eBwRyEln6E1MSzcxcVpQIhRG8Q1jLvEqRmCZqS3dsfXEDR/AhOF4d+jHg1qvDCpYaVRZjENPQyrVxAkQqxPgQ==", - "optional": true + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" }, "websocket-driver": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.3.tgz", - "integrity": "sha512-bpxWlvbbB459Mlipc5GBzzZwhoZgGEZLuqPaR0INBGnPAY1vdBX6hPnoFXiw+3yWxDuHyQjO2oXTMyS8A5haFg==", + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", "requires": { - "http-parser-js": ">=0.4.0 <0.4.11", + "http-parser-js": ">=0.5.1", "safe-buffer": ">=5.1.0", "websocket-extensions": ">=0.1.1" } }, "websocket-extensions": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.3.tgz", - "integrity": "sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==" - }, - "which-boxed-primitive": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.1.tgz", - "integrity": "sha512-7BT4TwISdDGBgaemWU0N0OU7FeAEJ9Oo2P1PHRm/FCWoEi2VLWC9b6xvxAA3C/NMpxg3HXVgi0sMmGbNUbNepQ==", - "optional": true, - "requires": { - "is-bigint": "^1.0.0", - "is-boolean-object": "^1.0.0", - "is-number-object": "^1.0.3", - "is-string": "^1.0.4", - "is-symbol": "^1.0.2" - } + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==" }, - "which-collection": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", - "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==", - "optional": true, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", "requires": { - "is-map": "^2.0.1", - "is-set": "^2.0.1", - "is-weakmap": "^2.0.1", - "is-weakset": "^2.0.1" + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" } }, - "which-typed-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.2.tgz", - "integrity": "sha512-KT6okrd1tE6JdZAy3o2VhMoYPh3+J6EMZLyrxBQsZflI1QCZIxMrIYLkosd8Twf+YfknVIHmYQPgJt238p8dnQ==", + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "optional": true, "requires": { - "available-typed-arrays": "^1.0.2", - "es-abstract": "^1.17.5", - "foreach": "^2.0.5", - "function-bind": "^1.1.1", - "has-symbols": "^1.0.1", - "is-typed-array": "^1.1.3" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" } }, "wrappy": { @@ -1705,15 +3076,43 @@ "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", "optional": true }, - "xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "optional": true }, "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "optional": true, + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + } + }, + "yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "optional": true + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "optional": true } } } diff --git a/importer/package.json b/importer/package.json index 7c1f68c..3f56995 100644 --- a/importer/package.json +++ b/importer/package.json @@ -18,7 +18,7 @@ "dependencies": { "@types/minimist": "^1.2.0", "@types/node": "^13.11.1", - "firebase-admin": "^8.10.0", + "firebase-admin": "^10.1.0", "google-spreadsheet": "^3.0.11", "minimist": "^1.2.5" } diff --git a/importer/src/main.ts b/importer/src/main.ts index 917053b..a935ab3 100644 --- a/importer/src/main.ts +++ b/importer/src/main.ts @@ -4,22 +4,33 @@ import {computeCardId, cleanPath, hashDocumentId} from "./utils"; const { GoogleSpreadsheet, GoogleSpreadsheetWorksheet } = require('google-spreadsheet'); import * as admin from 'firebase-admin'; -const serviceAccount = require("../config/firebase_admin_sdk.json"); -admin.initializeApp({ - credential: admin.credential.cert(serviceAccount), - databaseURL: "https://appsagainsthumanity-c7558.firebaseio.com" -}); -const db = admin.firestore(); - const argv = require('minimist')(process.argv.slice(2)); console.log(argv); // Sheet Variables - const promptLength = argv.pl || 6792; const responseLength = argv.rl || 24413; const documentId = argv.doc || '1lsy7lIwBe-DWOi2PALZPf5DgXHx9MEvKfRw1GaWQkzg'; const sheetId = argv.sheet || '2018240023'; +const cardSetOnly = argv['set-only'] || false; +const emulator = argv['emulator'] || false; + +if (emulator) { + process.env.FIRESTORE_EMULATOR_HOST="localhost:8080" +} + +const serviceAccount = require("../config/firebase_admin_sdk.json"); +admin.initializeApp({ + credential: admin.credential.cert(serviceAccount), + databaseURL: "https://appsagainsthumanity-c7558.firebaseio.com" +}); +const db = admin.firestore(); + +type CardSet = { + id: string; + set: string; + source: string; +} // @ts-ignore async function loadAndSavePromptCards(sheet: GoogleSpreadsheetWorksheet) { @@ -27,7 +38,7 @@ async function loadAndSavePromptCards(sheet: GoogleSpreadsheetWorksheet) { await sheet.loadCells(`A2:D${promptLength}`); console.log("Prompt cells loaded"); - const prompts = new Map(); + const prompts = new Map(); for (let i=2; i<=promptLength; i++) { const promptText = sheet.getCellByA1(`A${i}`).value; @@ -39,9 +50,14 @@ async function loadAndSavePromptCards(sheet: GoogleSpreadsheetWorksheet) { const cid = computeCardId(promptSet, promptText); let cards = prompts.get(promptSet); if (!cards) { - cards = []; + const newSet: CardSet = { + id: cleanPath(promptSet), + set: promptSet, + source: sourceSheet + }; + cards = [newSet, []]; } - cards.push({ + cards[1].push({ cid: cid, text: promptText, special: promptSpecial, @@ -59,31 +75,32 @@ async function loadAndSavePromptCards(sheet: GoogleSpreadsheetWorksheet) { // Set the set master document await cardSetDocument.set({ name: promptSet, - prompts: cards.length, - promptIndexes: cards.map((card) => card.cid) + source: cards[0].source, + prompts: cards[1].length, + promptIndexes: cards[1].map((card) => card.cid) }, { merge: true }); - const promptsCollection = cardSetDocument.collection('prompts'); - - let currentBatchCount = 0; - let batch = db.batch(); - for (let prompt of cards) { - try { - const document = promptsCollection.doc(hashDocumentId(prompt.text)); - if (currentBatchCount >= 500) { - await batch.commit(); - batch = db.batch(); - currentBatchCount = 0; - console.log("Batch committed to Firebase"); + if (!cardSetOnly) { + const promptsCollection = cardSetDocument.collection('prompts'); + let currentBatchCount = 0; + let batch = db.batch(); + for (let prompt of cards[1]) { + try { + const document = promptsCollection.doc(hashDocumentId(prompt.text)); + if (currentBatchCount >= 500) { + await batch.commit(); + batch = db.batch(); + currentBatchCount = 0; + console.log("Batch committed to Firebase"); + } + batch.set(document, prompt); + currentBatchCount += 1; + } catch (e) { + console.log("Error processing prompt card: " + e); } - batch.set(document, prompt); - currentBatchCount += 1; - } catch (e) { - console.log("Error processing prompt card: " + e); } + await batch.commit(); } - - await batch.commit(); } } @@ -93,7 +110,7 @@ async function loadAndSaveResponseCards(sheet: GoogleSpreadsheetWorksheet) { await sheet.loadCells(`G2:I${responseLength}`); console.log("Response cells loaded"); - const responses = new Map(); + const responses = new Map(); for (let i=2; i<=responseLength; i++) { const responseText = sheet.getCellByA1(`G${i}`).value?.toString(); @@ -104,9 +121,14 @@ async function loadAndSaveResponseCards(sheet: GoogleSpreadsheetWorksheet) { const cid = computeCardId(responseSet, responseText); let cards = responses.get(responseSet); if (!cards) { - cards = []; + const newSet: CardSet = { + id: cleanPath(responseSet), + set: responseSet, + source: sourceSheet + }; + cards = [newSet, []]; } - cards.push({ + cards[1].push({ cid: cid, text: responseText, set: responseSet, @@ -122,31 +144,32 @@ async function loadAndSaveResponseCards(sheet: GoogleSpreadsheetWorksheet) { await cardSetDocument.set({ name: responseSet, - responses: cards.length, - responseIndexes: cards.map((card) => card.cid) + source: cards[0].source, + responses: cards[1].length, + responseIndexes: cards[1].map((card) => card.cid) }, { merge: true }); - const responsesCollection = cardSetDocument.collection('responses'); - - let currentBatchCount = 0; - let batch = db.batch(); - for (let response of cards) { - try { - const document = responsesCollection.doc(hashDocumentId(response.text)); - if (currentBatchCount >= 500) { - await batch.commit(); - batch = db.batch(); - currentBatchCount = 0; - console.log("Batch committed to Firebase"); + if (!cardSetOnly) { + const responsesCollection = cardSetDocument.collection('responses'); + let currentBatchCount = 0; + let batch = db.batch(); + for (let response of cards[1]) { + try { + const document = responsesCollection.doc(hashDocumentId(response.text)); + if (currentBatchCount >= 500) { + await batch.commit(); + batch = db.batch(); + currentBatchCount = 0; + console.log("Batch committed to Firebase"); + } + batch.set(document, response); + currentBatchCount += 1; + } catch (e) { + console.log("Error processing response card: " + e); } - batch.set(document, response); - currentBatchCount += 1; - } catch (e) { - console.log("Error processing response card: " + e); } + await batch.commit(); } - - await batch.commit(); } } diff --git a/ios/Flutter/.last_build_id b/ios/Flutter/.last_build_id index 3b3243f..66ec65c 100644 --- a/ios/Flutter/.last_build_id +++ b/ios/Flutter/.last_build_id @@ -1 +1 @@ -2679397143cb87e29b2b0b9d788d7ece \ No newline at end of file +afa1a4dab7597dc407cf7f9d8a518f60 \ No newline at end of file diff --git a/ios/Podfile b/ios/Podfile index 6697f0a..1e8c3c9 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -10,78 +10,32 @@ project 'Runner', { 'Release' => :release, } -def parse_KV_file(file, separator='=') - file_abs_path = File.expand_path(file) - if !File.exists? file_abs_path - return []; +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" end - generated_key_values = {} - skip_line_start_symbols = ["#", "/"] - File.foreach(file_abs_path) do |line| - next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ } - plugin = line.split(pattern=separator) - if plugin.length == 2 - podname = plugin[0].strip() - path = plugin[1].strip() - podpath = File.expand_path("#{path}", file_abs_path) - generated_key_values[podname] = podpath - else - puts "Invalid plugin specification: #{line}" - end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches end - generated_key_values + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" end +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + target 'Runner' do use_frameworks! use_modular_headers! - # Flutter Pod - - copied_flutter_dir = File.join(__dir__, 'Flutter') - copied_framework_path = File.join(copied_flutter_dir, 'Flutter.framework') - copied_podspec_path = File.join(copied_flutter_dir, 'Flutter.podspec') - unless File.exist?(copied_framework_path) && File.exist?(copied_podspec_path) - # Copy Flutter.framework and Flutter.podspec to Flutter/ to have something to link against if the xcode backend script has not run yet. - # That script will copy the correct debug/profile/release version of the framework based on the currently selected Xcode configuration. - # CocoaPods will not embed the framework on pod install (before any build phases can generate) if the dylib does not exist. - - generated_xcode_build_settings_path = File.join(copied_flutter_dir, 'Generated.xcconfig') - unless File.exist?(generated_xcode_build_settings_path) - raise "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter pub get is executed first" - end - generated_xcode_build_settings = parse_KV_file(generated_xcode_build_settings_path) - cached_framework_dir = generated_xcode_build_settings['FLUTTER_FRAMEWORK_DIR']; - - unless File.exist?(copied_framework_path) - FileUtils.cp_r(File.join(cached_framework_dir, 'Flutter.framework'), copied_flutter_dir) - end - unless File.exist?(copied_podspec_path) - FileUtils.cp(File.join(cached_framework_dir, 'Flutter.podspec'), copied_flutter_dir) - end - end - - # Keep pod path relative so it can be checked into Podfile.lock. - pod 'Flutter', :path => 'Flutter' - - # Plugin Pods - - # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock - # referring to absolute paths on developers' machines. - system('rm -rf .symlinks') - system('mkdir -p .symlinks/plugins') - plugin_pods = parse_KV_file('../.flutter-plugins') - plugin_pods.each do |name, path| - symlink = File.join('.symlinks', 'plugins', name) - File.symlink(path, symlink) - pod name, :path => File.join(symlink, 'ios') - end + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) end post_install do |installer| installer.pods_project.targets.each do |target| - target.build_configurations.each do |config| - config.build_settings['ENABLE_BITCODE'] = 'NO' - end + flutter_additional_ios_build_settings(target) end end diff --git a/ios/Podfile.lock b/ios/Podfile.lock index e87a69b..07cc1a0 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,140 +1,194 @@ PODS: - - abseil/algorithm (0.20190808): - - abseil/algorithm/algorithm (= 0.20190808) - - abseil/algorithm/container (= 0.20190808) - - abseil/algorithm/algorithm (0.20190808) - - abseil/algorithm/container (0.20190808): + - abseil/algorithm (0.20200225.0): + - abseil/algorithm/algorithm (= 0.20200225.0) + - abseil/algorithm/container (= 0.20200225.0) + - abseil/algorithm/algorithm (0.20200225.0): + - abseil/base/config + - abseil/algorithm/container (0.20200225.0): - abseil/algorithm/algorithm - abseil/base/core_headers - abseil/meta/type_traits - - abseil/base (0.20190808): - - abseil/base/atomic_hook (= 0.20190808) - - abseil/base/base (= 0.20190808) - - abseil/base/base_internal (= 0.20190808) - - abseil/base/bits (= 0.20190808) - - abseil/base/config (= 0.20190808) - - abseil/base/core_headers (= 0.20190808) - - abseil/base/dynamic_annotations (= 0.20190808) - - abseil/base/endian (= 0.20190808) - - abseil/base/log_severity (= 0.20190808) - - abseil/base/malloc_internal (= 0.20190808) - - abseil/base/pretty_function (= 0.20190808) - - abseil/base/spinlock_wait (= 0.20190808) - - abseil/base/throw_delegate (= 0.20190808) - - abseil/base/atomic_hook (0.20190808) - - abseil/base/base (0.20190808): + - abseil/base (0.20200225.0): + - abseil/base/atomic_hook (= 0.20200225.0) + - abseil/base/base (= 0.20200225.0) + - abseil/base/base_internal (= 0.20200225.0) + - abseil/base/bits (= 0.20200225.0) + - abseil/base/config (= 0.20200225.0) + - abseil/base/core_headers (= 0.20200225.0) + - abseil/base/dynamic_annotations (= 0.20200225.0) + - abseil/base/endian (= 0.20200225.0) + - abseil/base/errno_saver (= 0.20200225.0) + - abseil/base/exponential_biased (= 0.20200225.0) + - abseil/base/log_severity (= 0.20200225.0) + - abseil/base/malloc_internal (= 0.20200225.0) + - abseil/base/periodic_sampler (= 0.20200225.0) + - abseil/base/pretty_function (= 0.20200225.0) + - abseil/base/raw_logging_internal (= 0.20200225.0) + - abseil/base/spinlock_wait (= 0.20200225.0) + - abseil/base/throw_delegate (= 0.20200225.0) + - abseil/base/atomic_hook (0.20200225.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/base (0.20200225.0): - abseil/base/atomic_hook - abseil/base/base_internal - abseil/base/config - abseil/base/core_headers - abseil/base/dynamic_annotations - abseil/base/log_severity + - abseil/base/raw_logging_internal - abseil/base/spinlock_wait - abseil/meta/type_traits - - abseil/base/base_internal (0.20190808): + - abseil/base/base_internal (0.20200225.0): + - abseil/base/config - abseil/meta/type_traits - - abseil/base/bits (0.20190808): + - abseil/base/bits (0.20200225.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/config (0.20200225.0) + - abseil/base/core_headers (0.20200225.0): + - abseil/base/config + - abseil/base/dynamic_annotations (0.20200225.0) + - abseil/base/endian (0.20200225.0): + - abseil/base/config - abseil/base/core_headers - - abseil/base/config (0.20190808) - - abseil/base/core_headers (0.20190808): + - abseil/base/errno_saver (0.20200225.0): - abseil/base/config - - abseil/base/dynamic_annotations (0.20190808) - - abseil/base/endian (0.20190808): + - abseil/base/exponential_biased (0.20200225.0): - abseil/base/config - abseil/base/core_headers - - abseil/base/log_severity (0.20190808): + - abseil/base/log_severity (0.20200225.0): + - abseil/base/config - abseil/base/core_headers - - abseil/base/malloc_internal (0.20190808): + - abseil/base/malloc_internal (0.20200225.0): - abseil/base/base + - abseil/base/base_internal - abseil/base/config - abseil/base/core_headers - abseil/base/dynamic_annotations - - abseil/base/spinlock_wait - - abseil/base/pretty_function (0.20190808) - - abseil/base/spinlock_wait (0.20190808): + - abseil/base/raw_logging_internal + - abseil/base/periodic_sampler (0.20200225.0): - abseil/base/core_headers - - abseil/base/throw_delegate (0.20190808): - - abseil/base/base + - abseil/base/exponential_biased + - abseil/base/pretty_function (0.20200225.0) + - abseil/base/raw_logging_internal (0.20200225.0): + - abseil/base/atomic_hook - abseil/base/config - - abseil/memory (0.20190808): - - abseil/memory/memory (= 0.20190808) - - abseil/memory/memory (0.20190808): - abseil/base/core_headers + - abseil/base/log_severity + - abseil/base/spinlock_wait (0.20200225.0): + - abseil/base/base_internal + - abseil/base/core_headers + - abseil/base/errno_saver + - abseil/base/throw_delegate (0.20200225.0): + - abseil/base/config + - abseil/base/raw_logging_internal + - abseil/container/compressed_tuple (0.20200225.0): + - abseil/utility/utility + - abseil/container/inlined_vector (0.20200225.0): + - abseil/algorithm/algorithm + - abseil/base/core_headers + - abseil/base/throw_delegate + - abseil/container/inlined_vector_internal + - abseil/memory/memory + - abseil/container/inlined_vector_internal (0.20200225.0): + - abseil/base/core_headers + - abseil/container/compressed_tuple + - abseil/memory/memory - abseil/meta/type_traits - - abseil/meta (0.20190808): - - abseil/meta/type_traits (= 0.20190808) - - abseil/meta/type_traits (0.20190808): + - abseil/types/span + - abseil/memory (0.20200225.0): + - abseil/memory/memory (= 0.20200225.0) + - abseil/memory/memory (0.20200225.0): + - abseil/base/core_headers + - abseil/meta/type_traits + - abseil/meta (0.20200225.0): + - abseil/meta/type_traits (= 0.20200225.0) + - abseil/meta/type_traits (0.20200225.0): - abseil/base/config - - abseil/numeric/int128 (0.20190808): + - abseil/numeric/int128 (0.20200225.0): - abseil/base/config - abseil/base/core_headers - - abseil/strings/internal (0.20190808): + - abseil/strings/internal (0.20200225.0): + - abseil/base/config - abseil/base/core_headers - abseil/base/endian + - abseil/base/raw_logging_internal - abseil/meta/type_traits - - abseil/strings/strings (0.20190808): + - abseil/strings/str_format (0.20200225.0): + - abseil/strings/str_format_internal + - abseil/strings/str_format_internal (0.20200225.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/meta/type_traits + - abseil/numeric/int128 + - abseil/strings/strings + - abseil/types/span + - abseil/strings/strings (0.20200225.0): - abseil/base/base - abseil/base/bits - abseil/base/config - abseil/base/core_headers - abseil/base/endian + - abseil/base/raw_logging_internal - abseil/base/throw_delegate - abseil/memory/memory - abseil/meta/type_traits - abseil/numeric/int128 - abseil/strings/internal - - abseil/time (0.20190808): - - abseil/time/internal (= 0.20190808) - - abseil/time/time (= 0.20190808) - - abseil/time/internal (0.20190808): - - abseil/time/internal/cctz (= 0.20190808) - - abseil/time/internal/cctz (0.20190808): - - abseil/time/internal/cctz/civil_time (= 0.20190808) - - abseil/time/internal/cctz/includes (= 0.20190808) - - abseil/time/internal/cctz/time_zone (= 0.20190808) - - abseil/time/internal/cctz/civil_time (0.20190808) - - abseil/time/internal/cctz/includes (0.20190808) - - abseil/time/internal/cctz/time_zone (0.20190808): + - abseil/time (0.20200225.0): + - abseil/time/internal (= 0.20200225.0) + - abseil/time/time (= 0.20200225.0) + - abseil/time/internal (0.20200225.0): + - abseil/time/internal/cctz (= 0.20200225.0) + - abseil/time/internal/cctz (0.20200225.0): + - abseil/time/internal/cctz/civil_time (= 0.20200225.0) + - abseil/time/internal/cctz/time_zone (= 0.20200225.0) + - abseil/time/internal/cctz/civil_time (0.20200225.0): + - abseil/base/config + - abseil/time/internal/cctz/time_zone (0.20200225.0): + - abseil/base/config - abseil/time/internal/cctz/civil_time - - abseil/time/time (0.20190808): + - abseil/time/time (0.20200225.0): - abseil/base/base - abseil/base/core_headers + - abseil/base/raw_logging_internal - abseil/numeric/int128 - abseil/strings/strings - abseil/time/internal/cctz/civil_time - abseil/time/internal/cctz/time_zone - - abseil/types (0.20190808): - - abseil/types/any (= 0.20190808) - - abseil/types/bad_any_cast (= 0.20190808) - - abseil/types/bad_any_cast_impl (= 0.20190808) - - abseil/types/bad_optional_access (= 0.20190808) - - abseil/types/bad_variant_access (= 0.20190808) - - abseil/types/compare (= 0.20190808) - - abseil/types/optional (= 0.20190808) - - abseil/types/span (= 0.20190808) - - abseil/types/variant (= 0.20190808) - - abseil/types/any (0.20190808): + - abseil/types (0.20200225.0): + - abseil/types/any (= 0.20200225.0) + - abseil/types/bad_any_cast (= 0.20200225.0) + - abseil/types/bad_any_cast_impl (= 0.20200225.0) + - abseil/types/bad_optional_access (= 0.20200225.0) + - abseil/types/bad_variant_access (= 0.20200225.0) + - abseil/types/compare (= 0.20200225.0) + - abseil/types/optional (= 0.20200225.0) + - abseil/types/span (= 0.20200225.0) + - abseil/types/variant (= 0.20200225.0) + - abseil/types/any (0.20200225.0): - abseil/base/config - abseil/base/core_headers - abseil/meta/type_traits - abseil/types/bad_any_cast - abseil/utility/utility - - abseil/types/bad_any_cast (0.20190808): + - abseil/types/bad_any_cast (0.20200225.0): - abseil/base/config - abseil/types/bad_any_cast_impl - - abseil/types/bad_any_cast_impl (0.20190808): - - abseil/base/base + - abseil/types/bad_any_cast_impl (0.20200225.0): - abseil/base/config - - abseil/types/bad_optional_access (0.20190808): - - abseil/base/base + - abseil/base/raw_logging_internal + - abseil/types/bad_optional_access (0.20200225.0): - abseil/base/config - - abseil/types/bad_variant_access (0.20190808): - - abseil/base/base + - abseil/base/raw_logging_internal + - abseil/types/bad_variant_access (0.20200225.0): - abseil/base/config - - abseil/types/compare (0.20190808): + - abseil/base/raw_logging_internal + - abseil/types/compare (0.20200225.0): - abseil/base/core_headers - abseil/meta/type_traits - - abseil/types/optional (0.20190808): + - abseil/types/optional (0.20200225.0): - abseil/base/base_internal - abseil/base/config - abseil/base/core_headers @@ -142,243 +196,261 @@ PODS: - abseil/meta/type_traits - abseil/types/bad_optional_access - abseil/utility/utility - - abseil/types/span (0.20190808): + - abseil/types/span (0.20200225.0): - abseil/algorithm/algorithm - abseil/base/core_headers - abseil/base/throw_delegate - abseil/meta/type_traits - - abseil/types/variant (0.20190808): + - abseil/types/variant (0.20200225.0): - abseil/base/base_internal - abseil/base/config - abseil/base/core_headers - abseil/meta/type_traits - abseil/types/bad_variant_access - abseil/utility/utility - - abseil/utility/utility (0.20190808): + - abseil/utility/utility (0.20200225.0): - abseil/base/base_internal - abseil/base/config - abseil/meta/type_traits - - AppAuth (1.3.0): - - AppAuth/Core (= 1.3.0) - - AppAuth/ExternalUserAgent (= 1.3.0) - - AppAuth/Core (1.3.0) - - AppAuth/ExternalUserAgent (1.3.0) - - apple_sign_in (0.0.1): + - AppAuth (1.4.0): + - AppAuth/Core (= 1.4.0) + - AppAuth/ExternalUserAgent (= 1.4.0) + - AppAuth/Core (1.4.0) + - AppAuth/ExternalUserAgent (1.4.0) + - BoringSSL-GRPC (0.0.7): + - BoringSSL-GRPC/Implementation (= 0.0.7) + - BoringSSL-GRPC/Interface (= 0.0.7) + - BoringSSL-GRPC/Implementation (0.0.7): + - BoringSSL-GRPC/Interface (= 0.0.7) + - BoringSSL-GRPC/Interface (0.0.7) + - cloud_firestore (1.0.1): + - Firebase/Firestore (= 7.3.0) + - firebase_core - Flutter - - BoringSSL-GRPC (0.0.3): - - BoringSSL-GRPC/Implementation (= 0.0.3) - - BoringSSL-GRPC/Interface (= 0.0.3) - - BoringSSL-GRPC/Implementation (0.0.3): - - BoringSSL-GRPC/Interface (= 0.0.3) - - BoringSSL-GRPC/Interface (0.0.3) - - cloud_firestore (0.0.1): - - Firebase/Core - - Firebase/Firestore (~> 6.0) - - Flutter - - cloud_firestore_web (0.1.0): - - Flutter - - cloud_functions (0.0.1): - - Firebase/Core - - Firebase/Functions (~> 6.0) - - Flutter - - cloud_functions_web (1.0.0): + - cloud_functions (1.0.0): + - Firebase/Functions (= 7.3.0) + - firebase_core - Flutter - device_info (0.0.1): - Flutter - - Firebase/Auth (6.15.0): + - Firebase/Analytics (7.3.0): + - Firebase/Core + - Firebase/Auth (7.3.0): - Firebase/CoreOnly - - FirebaseAuth (~> 6.4.2) - - Firebase/Core (6.15.0): + - FirebaseAuth (~> 7.3.0) + - Firebase/Core (7.3.0): - Firebase/CoreOnly - - FirebaseAnalytics (= 6.2.1) - - Firebase/CoreOnly (6.15.0): - - FirebaseCore (= 6.6.0) - - Firebase/Firestore (6.15.0): + - FirebaseAnalytics (= 7.3.0) + - Firebase/CoreOnly (7.3.0): + - FirebaseCore (= 7.3.0) + - Firebase/DynamicLinks (7.3.0): - Firebase/CoreOnly - - FirebaseFirestore (~> 1.9.0) - - Firebase/Functions (6.15.0): + - FirebaseDynamicLinks (~> 7.3.0) + - Firebase/Firestore (7.3.0): - Firebase/CoreOnly - - FirebaseFunctions (~> 2.5.1) - - Firebase/Storage (6.15.0): + - FirebaseFirestore (~> 7.3.0) + - Firebase/Functions (7.3.0): - Firebase/CoreOnly - - FirebaseStorage (~> 3.5.0) - - firebase_auth (0.0.1): - - Firebase/Auth (~> 6.3) - - Firebase/Core + - FirebaseFunctions (~> 7.3.0) + - Firebase/Messaging (7.3.0): + - Firebase/CoreOnly + - FirebaseMessaging (~> 7.3.0) + - Firebase/Storage (7.3.0): + - Firebase/CoreOnly + - FirebaseStorage (~> 7.3.0) + - firebase_analytics (7.1.1): + - Firebase/Analytics (= 7.3.0) + - firebase_core - Flutter - - firebase_auth_web (0.1.0): + - firebase_auth (1.0.1): + - Firebase/Auth (= 7.3.0) + - firebase_core - Flutter - - firebase_core (0.0.1): - - Firebase/Core + - firebase_core (1.0.1): + - Firebase/CoreOnly (= 7.3.0) - Flutter - - firebase_core_web (0.1.0): + - firebase_dynamic_links (0.8.0): + - Firebase/DynamicLinks (= 7.3.0) + - firebase_core - Flutter - - firebase_storage (0.0.1): - - Firebase/Storage + - firebase_messaging (9.0.0): + - Firebase/Messaging (= 7.3.0) + - firebase_core - Flutter - - FirebaseAnalytics (6.2.1): - - FirebaseCore (~> 6.6) - - FirebaseInstanceID (~> 4.3) - - GoogleAppMeasurement (= 6.2.1) - - GoogleUtilities/AppDelegateSwizzler (~> 6.0) - - GoogleUtilities/MethodSwizzler (~> 6.0) - - GoogleUtilities/Network (~> 6.0) - - "GoogleUtilities/NSData+zlib (~> 6.0)" - - nanopb (= 0.3.9011) - - FirebaseAuth (6.4.2): - - FirebaseAuthInterop (~> 1.0) - - FirebaseCore (~> 6.2) - - GoogleUtilities/AppDelegateSwizzler (~> 6.2) - - GoogleUtilities/Environment (~> 6.2) - - GTMSessionFetcher/Core (~> 1.1) - - FirebaseAuthInterop (1.0.0) - - FirebaseCore (6.6.0): - - FirebaseCoreDiagnostics (~> 1.2) - - FirebaseCoreDiagnosticsInterop (~> 1.2) - - GoogleUtilities/Environment (~> 6.5) - - GoogleUtilities/Logger (~> 6.5) - - FirebaseCoreDiagnostics (1.2.0): - - FirebaseCoreDiagnosticsInterop (~> 1.2) - - GoogleDataTransportCCTSupport (~> 1.3) - - GoogleUtilities/Environment (~> 6.5) - - GoogleUtilities/Logger (~> 6.5) - - nanopb (~> 0.3.901) - - FirebaseCoreDiagnosticsInterop (1.2.0) - - FirebaseFirestore (1.9.0): - - abseil/algorithm (= 0.20190808) - - abseil/base (= 0.20190808) - - abseil/memory (= 0.20190808) - - abseil/meta (= 0.20190808) - - abseil/strings/strings (= 0.20190808) - - abseil/time (= 0.20190808) - - abseil/types (= 0.20190808) - - FirebaseAuthInterop (~> 1.0) - - FirebaseCore (~> 6.2) - - "gRPC-C++ (= 0.0.9)" + - firebase_storage (8.0.0): + - Firebase/Storage (= 7.3.0) + - firebase_core + - Flutter + - FirebaseAnalytics (7.3.0): + - FirebaseCore (~> 7.0) + - FirebaseInstallations (~> 7.0) + - GoogleAppMeasurement (= 7.3.0) + - GoogleUtilities/AppDelegateSwizzler (~> 7.0) + - GoogleUtilities/MethodSwizzler (~> 7.0) + - GoogleUtilities/Network (~> 7.0) + - "GoogleUtilities/NSData+zlib (~> 7.0)" + - nanopb (~> 2.30906.0) + - FirebaseAuth (7.3.0): + - FirebaseCore (~> 7.0) + - GoogleUtilities/AppDelegateSwizzler (~> 7.0) + - GoogleUtilities/Environment (~> 7.0) + - GTMSessionFetcher/Core (~> 1.4) + - FirebaseCore (7.3.0): + - FirebaseCoreDiagnostics (~> 7.0) + - GoogleUtilities/Environment (~> 7.0) + - GoogleUtilities/Logger (~> 7.0) + - FirebaseCoreDiagnostics (7.3.0): + - GoogleDataTransport (~> 8.0) + - GoogleUtilities/Environment (~> 7.0) + - GoogleUtilities/Logger (~> 7.0) + - nanopb (~> 2.30906.0) + - FirebaseDynamicLinks (7.3.1): + - FirebaseCore (~> 7.0) + - FirebaseFirestore (7.3.0): + - abseil/algorithm (= 0.20200225.0) + - abseil/base (= 0.20200225.0) + - abseil/memory (= 0.20200225.0) + - abseil/meta (= 0.20200225.0) + - abseil/strings/strings (= 0.20200225.0) + - abseil/time (= 0.20200225.0) + - abseil/types (= 0.20200225.0) + - FirebaseCore (~> 7.0) + - "gRPC-C++ (~> 1.28.0)" - leveldb-library (~> 1.22) - - nanopb (~> 0.3.901) - - FirebaseFunctions (2.5.1): - - FirebaseAuthInterop (~> 1.0) - - FirebaseCore (~> 6.0) - - GTMSessionFetcher/Core (~> 1.1) - - FirebaseInstallations (1.1.0): - - FirebaseCore (~> 6.6) - - GoogleUtilities/UserDefaults (~> 6.5) + - nanopb (~> 2.30906.0) + - FirebaseFunctions (7.3.0): + - FirebaseCore (~> 7.0) + - GTMSessionFetcher/Core (~> 1.4) + - FirebaseInstallations (7.8.0): + - FirebaseCore (~> 7.0) + - GoogleUtilities/Environment (~> 7.0) + - GoogleUtilities/UserDefaults (~> 7.0) - PromisesObjC (~> 1.2) - - FirebaseInstanceID (4.3.0): - - FirebaseCore (~> 6.6) - - FirebaseInstallations (~> 1.0) - - GoogleUtilities/Environment (~> 6.5) - - GoogleUtilities/UserDefaults (~> 6.5) - - FirebaseStorage (3.5.0): - - FirebaseAuthInterop (~> 1.0) - - FirebaseCore (~> 6.0) - - GTMSessionFetcher/Core (~> 1.1) + - FirebaseInstanceID (7.8.0): + - FirebaseCore (~> 7.0) + - FirebaseInstallations (~> 7.0) + - GoogleUtilities/Environment (~> 7.0) + - GoogleUtilities/UserDefaults (~> 7.0) + - FirebaseMessaging (7.3.0): + - FirebaseCore (~> 7.0) + - FirebaseInstanceID (~> 7.0) + - GoogleUtilities/AppDelegateSwizzler (~> 7.0) + - GoogleUtilities/Environment (~> 7.0) + - GoogleUtilities/Reachability (~> 7.0) + - GoogleUtilities/UserDefaults (~> 7.0) + - FirebaseStorage (7.3.0): + - FirebaseCore (~> 7.0) + - GTMSessionFetcher/Core (~> 1.4) - Flutter (1.0.0) - google_sign_in (0.0.1): - Flutter - GoogleSignIn (~> 5.0) - - google_sign_in_web (0.8.1): - - Flutter - - GoogleAppMeasurement (6.2.1): - - GoogleUtilities/AppDelegateSwizzler (~> 6.0) - - GoogleUtilities/MethodSwizzler (~> 6.0) - - GoogleUtilities/Network (~> 6.0) - - "GoogleUtilities/NSData+zlib (~> 6.0)" - - nanopb (= 0.3.9011) - - GoogleDataTransport (3.3.0) - - GoogleDataTransportCCTSupport (1.3.0): - - GoogleDataTransport (~> 3.3) - - nanopb (~> 0.3.901) + - GoogleAppMeasurement (7.3.0): + - GoogleUtilities/AppDelegateSwizzler (~> 7.0) + - GoogleUtilities/MethodSwizzler (~> 7.0) + - GoogleUtilities/Network (~> 7.0) + - "GoogleUtilities/NSData+zlib (~> 7.0)" + - nanopb (~> 2.30906.0) + - GoogleDataTransport (8.1.0): + - nanopb (~> 2.30906.0) - GoogleSignIn (5.0.2): - AppAuth (~> 1.2) - GTMAppAuth (~> 1.0) - GTMSessionFetcher/Core (~> 1.1) - - GoogleUtilities/AppDelegateSwizzler (6.5.0): + - GoogleUtilities/AppDelegateSwizzler (7.3.0): - GoogleUtilities/Environment - GoogleUtilities/Logger - GoogleUtilities/Network - - GoogleUtilities/Environment (6.5.0) - - GoogleUtilities/Logger (6.5.0): + - GoogleUtilities/Environment (7.3.0): + - PromisesObjC (~> 1.2) + - GoogleUtilities/Logger (7.3.0): - GoogleUtilities/Environment - - GoogleUtilities/MethodSwizzler (6.5.0): + - GoogleUtilities/MethodSwizzler (7.3.0): - GoogleUtilities/Logger - - GoogleUtilities/Network (6.5.0): + - GoogleUtilities/Network (7.3.0): - GoogleUtilities/Logger - "GoogleUtilities/NSData+zlib" - GoogleUtilities/Reachability - - "GoogleUtilities/NSData+zlib (6.5.0)" - - GoogleUtilities/Reachability (6.5.0): + - "GoogleUtilities/NSData+zlib (7.3.0)" + - GoogleUtilities/Reachability (7.3.0): - GoogleUtilities/Logger - - GoogleUtilities/UserDefaults (6.5.0): + - GoogleUtilities/UserDefaults (7.3.0): - GoogleUtilities/Logger - - "gRPC-C++ (0.0.9)": - - "gRPC-C++/Implementation (= 0.0.9)" - - "gRPC-C++/Interface (= 0.0.9)" - - "gRPC-C++/Implementation (0.0.9)": - - "gRPC-C++/Interface (= 0.0.9)" - - gRPC-Core (= 1.21.0) - - nanopb (~> 0.3) - - "gRPC-C++/Interface (0.0.9)" - - gRPC-Core (1.21.0): - - gRPC-Core/Implementation (= 1.21.0) - - gRPC-Core/Interface (= 1.21.0) - - gRPC-Core/Implementation (1.21.0): - - BoringSSL-GRPC (= 0.0.3) - - gRPC-Core/Interface (= 1.21.0) - - nanopb (~> 0.3) - - gRPC-Core/Interface (1.21.0) - - GTMAppAuth (1.0.0): - - AppAuth/Core (~> 1.0) - - GTMSessionFetcher (~> 1.1) - - GTMSessionFetcher (1.3.1): - - GTMSessionFetcher/Full (= 1.3.1) - - GTMSessionFetcher/Core (1.3.1) - - GTMSessionFetcher/Full (1.3.1): - - GTMSessionFetcher/Core (= 1.3.1) - - leveldb-library (1.22) - - nanopb (0.3.9011): - - nanopb/decode (= 0.3.9011) - - nanopb/encode (= 0.3.9011) - - nanopb/decode (0.3.9011) - - nanopb/encode (0.3.9011) + - "gRPC-C++ (1.28.2)": + - "gRPC-C++/Implementation (= 1.28.2)" + - "gRPC-C++/Interface (= 1.28.2)" + - "gRPC-C++/Implementation (1.28.2)": + - abseil/container/inlined_vector (= 0.20200225.0) + - abseil/memory/memory (= 0.20200225.0) + - abseil/strings/str_format (= 0.20200225.0) + - abseil/strings/strings (= 0.20200225.0) + - abseil/types/optional (= 0.20200225.0) + - "gRPC-C++/Interface (= 1.28.2)" + - gRPC-Core (= 1.28.2) + - "gRPC-C++/Interface (1.28.2)" + - gRPC-Core (1.28.2): + - gRPC-Core/Implementation (= 1.28.2) + - gRPC-Core/Interface (= 1.28.2) + - gRPC-Core/Implementation (1.28.2): + - abseil/container/inlined_vector (= 0.20200225.0) + - abseil/memory/memory (= 0.20200225.0) + - abseil/strings/str_format (= 0.20200225.0) + - abseil/strings/strings (= 0.20200225.0) + - abseil/types/optional (= 0.20200225.0) + - BoringSSL-GRPC (= 0.0.7) + - gRPC-Core/Interface (= 1.28.2) + - gRPC-Core/Interface (1.28.2) + - GTMAppAuth (1.1.0): + - AppAuth/Core (~> 1.4) + - GTMSessionFetcher (~> 1.4) + - GTMSessionFetcher (1.5.0): + - GTMSessionFetcher/Full (= 1.5.0) + - GTMSessionFetcher/Core (1.5.0) + - GTMSessionFetcher/Full (1.5.0): + - GTMSessionFetcher/Core (= 1.5.0) + - image_picker (0.0.1): + - Flutter + - leveldb-library (1.22.1) + - nanopb (2.30906.0): + - nanopb/decode (= 2.30906.0) + - nanopb/encode (= 2.30906.0) + - nanopb/decode (2.30906.0) + - nanopb/encode (2.30906.0) - package_info (0.0.1): - Flutter - path_provider (0.0.1): - Flutter - - path_provider_macos (0.0.1): + - PromisesObjC (1.2.12) + - share (0.0.1): - Flutter - - PromisesObjC (1.2.8) - shared_preferences (0.0.1): - Flutter - - shared_preferences_macos (0.0.1): + - sign_in_with_apple (0.0.1): - Flutter - - shared_preferences_web (0.0.1): + - url_launcher (0.0.1): - Flutter - webview_flutter (0.0.1): - Flutter DEPENDENCIES: - - apple_sign_in (from `.symlinks/plugins/apple_sign_in/ios`) - cloud_firestore (from `.symlinks/plugins/cloud_firestore/ios`) - - cloud_firestore_web (from `.symlinks/plugins/cloud_firestore_web/ios`) - cloud_functions (from `.symlinks/plugins/cloud_functions/ios`) - - cloud_functions_web (from `.symlinks/plugins/cloud_functions_web/ios`) - device_info (from `.symlinks/plugins/device_info/ios`) + - firebase_analytics (from `.symlinks/plugins/firebase_analytics/ios`) - firebase_auth (from `.symlinks/plugins/firebase_auth/ios`) - - firebase_auth_web (from `.symlinks/plugins/firebase_auth_web/ios`) - firebase_core (from `.symlinks/plugins/firebase_core/ios`) - - firebase_core_web (from `.symlinks/plugins/firebase_core_web/ios`) + - firebase_dynamic_links (from `.symlinks/plugins/firebase_dynamic_links/ios`) + - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`) - firebase_storage (from `.symlinks/plugins/firebase_storage/ios`) - Flutter (from `Flutter`) - google_sign_in (from `.symlinks/plugins/google_sign_in/ios`) - - google_sign_in_web (from `.symlinks/plugins/google_sign_in_web/ios`) + - image_picker (from `.symlinks/plugins/image_picker/ios`) - package_info (from `.symlinks/plugins/package_info/ios`) - path_provider (from `.symlinks/plugins/path_provider/ios`) - - path_provider_macos (from `.symlinks/plugins/path_provider_macos/ios`) + - share (from `.symlinks/plugins/share/ios`) - shared_preferences (from `.symlinks/plugins/shared_preferences/ios`) - - shared_preferences_macos (from `.symlinks/plugins/shared_preferences_macos/ios`) - - shared_preferences_web (from `.symlinks/plugins/shared_preferences_web/ios`) + - sign_in_with_apple (from `.symlinks/plugins/sign_in_with_apple/ios`) + - url_launcher (from `.symlinks/plugins/url_launcher/ios`) - webview_flutter (from `.symlinks/plugins/webview_flutter/ios`) SPEC REPOS: @@ -389,18 +461,17 @@ SPEC REPOS: - Firebase - FirebaseAnalytics - FirebaseAuth - - FirebaseAuthInterop - FirebaseCore - FirebaseCoreDiagnostics - - FirebaseCoreDiagnosticsInterop + - FirebaseDynamicLinks - FirebaseFirestore - FirebaseFunctions - FirebaseInstallations - FirebaseInstanceID + - FirebaseMessaging - FirebaseStorage - GoogleAppMeasurement - GoogleDataTransport - - GoogleDataTransportCCTSupport - GoogleSignIn - GoogleUtilities - "gRPC-C++" @@ -412,99 +483,92 @@ SPEC REPOS: - PromisesObjC EXTERNAL SOURCES: - apple_sign_in: - :path: ".symlinks/plugins/apple_sign_in/ios" cloud_firestore: :path: ".symlinks/plugins/cloud_firestore/ios" - cloud_firestore_web: - :path: ".symlinks/plugins/cloud_firestore_web/ios" cloud_functions: :path: ".symlinks/plugins/cloud_functions/ios" - cloud_functions_web: - :path: ".symlinks/plugins/cloud_functions_web/ios" device_info: :path: ".symlinks/plugins/device_info/ios" + firebase_analytics: + :path: ".symlinks/plugins/firebase_analytics/ios" firebase_auth: :path: ".symlinks/plugins/firebase_auth/ios" - firebase_auth_web: - :path: ".symlinks/plugins/firebase_auth_web/ios" firebase_core: :path: ".symlinks/plugins/firebase_core/ios" - firebase_core_web: - :path: ".symlinks/plugins/firebase_core_web/ios" + firebase_dynamic_links: + :path: ".symlinks/plugins/firebase_dynamic_links/ios" + firebase_messaging: + :path: ".symlinks/plugins/firebase_messaging/ios" firebase_storage: :path: ".symlinks/plugins/firebase_storage/ios" Flutter: :path: Flutter google_sign_in: :path: ".symlinks/plugins/google_sign_in/ios" - google_sign_in_web: - :path: ".symlinks/plugins/google_sign_in_web/ios" + image_picker: + :path: ".symlinks/plugins/image_picker/ios" package_info: :path: ".symlinks/plugins/package_info/ios" path_provider: :path: ".symlinks/plugins/path_provider/ios" - path_provider_macos: - :path: ".symlinks/plugins/path_provider_macos/ios" + share: + :path: ".symlinks/plugins/share/ios" shared_preferences: :path: ".symlinks/plugins/shared_preferences/ios" - shared_preferences_macos: - :path: ".symlinks/plugins/shared_preferences_macos/ios" - shared_preferences_web: - :path: ".symlinks/plugins/shared_preferences_web/ios" + sign_in_with_apple: + :path: ".symlinks/plugins/sign_in_with_apple/ios" + url_launcher: + :path: ".symlinks/plugins/url_launcher/ios" webview_flutter: :path: ".symlinks/plugins/webview_flutter/ios" SPEC CHECKSUMS: - abseil: 18063d773f5366ff8736a050fe035a28f635fd27 - AppAuth: 73574f3013a1e65b9601a3ddc8b3158cce68c09d - apple_sign_in: 7716c7ddfa195aeab7dec0dc374ef4ff45d1adb4 - BoringSSL-GRPC: db8764df3204ccea016e1c8dd15d9a9ad63ff318 - cloud_firestore: 31454d48df21f3e1a900015e36143c0d46a304b7 - cloud_firestore_web: 9ec3dc7f5f98de5129339802d491c1204462bfec - cloud_functions: cc76d33c7983c9057ef1163a560734595133091c - cloud_functions_web: ec883775f4a3b3fad943918d2cdad4fbf51cdbf0 - device_info: cbf09d2ec12aa7110e0b09fabe54b5bd6c8efe74 - Firebase: 5d77105d9740a07ca6b16927ca971db7e860faaf - firebase_auth: 6838289663da45c488af0a82c246928588fcddc5 - firebase_auth_web: 0955c07bcc06e84af76b9d4e32e6f31518f2d7de - firebase_core: dc539d4bdc69894823e4a6e557034a9e48960f21 - firebase_core_web: d501d8b946b60c8af265428ce483b0fff5ad52d1 - firebase_storage: 5e931af5cdef32331676c659bdd1ebcaba9dc78a - FirebaseAnalytics: e83e64b1231dedcd9ddd4bdecd9bcfd6ba341679 - FirebaseAuth: ce45d7c5d46bed90159f3a73b6efbe8976ed3573 - FirebaseAuthInterop: 0ffa57668be100582bb7643d4fcb7615496c41fc - FirebaseCore: 4aeb81ff53dcd9a3634ca725dc1fb8c2a4622046 - FirebaseCoreDiagnostics: 5e78803ab276bc5b50340e3c539c06c3de35c649 - FirebaseCoreDiagnosticsInterop: 296e2c5f5314500a850ad0b83e9e7c10b011a850 - FirebaseFirestore: b7e6adda31974dbd259fc25b541e8850420c92ed - FirebaseFunctions: 5af7c35d1c5e41608fecbb667eb6c4e672e318d0 - FirebaseInstallations: 575cd32f2aec0feeb0e44f5d0110a09e5e60b47b - FirebaseInstanceID: 6668efc1655a4052c083f287a7141f1ead12f9c2 - FirebaseStorage: 6c5263796af3b1be82ed173598aade47535fe125 - Flutter: 0e3d915762c693b495b44d77113d4970485de6ec - google_sign_in: f32920a589fdf4ab2918ec6dc5e5b0d5b8040ff5 - google_sign_in_web: 52deb24929ac0992baff65c57956031c44ed44c3 - GoogleAppMeasurement: a08a43b8677b95ed51fcef880e36737334d804fd - GoogleDataTransport: 574a983e829327d7c18f2627f65d9e80164ea8a4 - GoogleDataTransportCCTSupport: cad3cd6cdbdbad6b5c2c9206ec413402755faaaa + abseil: 6c8eb7892aefa08d929b39f9bb108e5367e3228f + AppAuth: 31bcec809a638d7bd2f86ea8a52bd45f6e81e7c7 + BoringSSL-GRPC: 8edf627ee524575e2f8d19d56f068b448eea3879 + cloud_firestore: 69d71054fcab9a1c0d4779362f85b94aec7ff7d7 + cloud_functions: 32ea5ece6048a6029d3280df62afe8ff85598ce9 + device_info: d7d233b645a32c40dfdc212de5cf646ca482f175 + Firebase: 26223c695fe322633274198cb19dca8cb7e54416 + firebase_analytics: ee4abb07bfa857e7c95d97144efe9eb68be414cd + firebase_auth: 9f6491ea8e44570323361ae713a2ae3175b3f21a + firebase_core: d2e03528e2a600891f6f460b5e92932624480d1d + firebase_dynamic_links: e222260696317885f2f2592fddf8783158f4e7df + firebase_messaging: fc1811236795c2313b8339c35d31295b1cd8486f + firebase_storage: a4b292db6551a64c3697c7e67cb1df524b494bb1 + FirebaseAnalytics: 2580c2d62535ae7b644143d48941fcc239ea897a + FirebaseAuth: c224a0cf1afa0949bd5c7bfcf154b4f5ce8ddef2 + FirebaseCore: 4d3c72622ce0e2106aaa07bb4b2935ba2c370972 + FirebaseCoreDiagnostics: d50e11039e5984d92c8a512be2395f13df747350 + FirebaseDynamicLinks: a6df95d6dbc746c2bbf7d511343cda1344e2cf70 + FirebaseFirestore: 1906bf163afdb7c432d2e3b5c40ceb9dd2df5820 + FirebaseFunctions: 56b7275ad46d936b77b64ecacd306e77db7be251 + FirebaseInstallations: 7f7ed0e7e27fb51f57291e1876e2ddb1524126c1 + FirebaseInstanceID: aaecc93b4528bbcafea12c477e26827719ca1183 + FirebaseMessaging: 68d1bcb14880189558a8ae57167abe0b7e417232 + FirebaseStorage: 5002b1895bfe74a5ce92ad54f966e6162d0da2e5 + Flutter: 434fef37c0980e73bb6479ef766c45957d4b510c + google_sign_in: 6bd214b9c154f881422f5fe27b66aaa7bbd580cc + GoogleAppMeasurement: 8d3c0aeede16ab7764144b5a4ca8e1d4323841b7 + GoogleDataTransport: 116c84c4bdeb76be2a7a46de51244368f9794eab GoogleSignIn: 7137d297ddc022a7e0aa4619c86d72c909fa7213 - GoogleUtilities: f8de7ddf8c706f58e9b405d53e38bbdaa2731e5a - "gRPC-C++": 9dfe7b44821e7b3e44aacad2af29d2c21f7cde83 - gRPC-Core: c9aef9a261a1247e881b18059b84d597293c9947 - GTMAppAuth: 4deac854479704f348309e7b66189e604cf5e01e - GTMSessionFetcher: cea130bbfe5a7edc8d06d3f0d17288c32ffe9925 - leveldb-library: 55d93ee664b4007aac644a782d11da33fba316f7 - nanopb: 18003b5e52dab79db540fe93fe9579f399bd1ccd - package_info: 48b108e75b8802c2d5e126f208ef540561c98aef - path_provider: fb74bd0465e96b594bb3b5088ee4a4e7bb1f2a9d - path_provider_macos: f760a3c5b04357c380e2fddb6f9db6f3015897e0 - PromisesObjC: c119f3cd559f50b7ae681fa59dc1acd19173b7e6 - shared_preferences: 430726339841afefe5142b9c1f50cb6bd7793e01 - shared_preferences_macos: f3f29b71ccbb56bf40c9dd6396c9acf15e214087 - shared_preferences_web: 141cce0c3ed1a1c5bf2a0e44f52d31eeb66e5ea9 - webview_flutter: d2b4d6c66968ad042ad94cbb791f5b72b4678a96 + GoogleUtilities: abda45f519bc4073aa1171d2f8742108def46017 + "gRPC-C++": 13d8ccef97d5c3c441b7e3c529ef28ebee86fad2 + gRPC-Core: 4afa11bfbedf7cdecd04de535a9e046893404ed5 + GTMAppAuth: 197a8dabfea5d665224aa00d17f164fc2248dab9 + GTMSessionFetcher: b3503b20a988c4e20cc189aa798fd18220133f52 + image_picker: 50e7c7ff960e5f58faa4d1f4af84a771c671bc4a + leveldb-library: 50c7b45cbd7bf543c81a468fe557a16ae3db8729 + nanopb: 1bf24dd71191072e120b83dd02d08f3da0d65e53 + package_info: 873975fc26034f0b863a300ad47e7f1ac6c7ec62 + path_provider: abfe2b5c733d04e238b0d8691db0cfd63a27a93c + PromisesObjC: 3113f7f76903778cf4a0586bd1ab89329a0b7b97 + share: 0b2c3e82132f5888bccca3351c504d0003b3b410 + shared_preferences: af6bfa751691cdc24be3045c43ec037377ada40d + sign_in_with_apple: 34f3f5456a45fd7ac5fb42905e2ad31dae061b4a + url_launcher: 6fef411d543ceb26efce54b05a0a40bfd74cbbef + webview_flutter: 9f491a9b5a66f2573946a389b2677987b0ff8c0b -PODFILE CHECKSUM: c34e2287a9ccaa606aeceab922830efb9a6ff69a +PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c -COCOAPODS: 1.8.4 +COCOAPODS: 1.10.1 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index cb0f099..ba93403 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -332,9 +332,9 @@ /* Begin XCBuildConfiguration section */ 249021D3217E4FDB00AE95B9 /* Profile */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ARCHS = arm64; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; @@ -397,6 +397,7 @@ "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -405,20 +406,22 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); + MARKETING_VERSION = 1.0.1; PRODUCT_BUNDLE_IDENTIFIER = com.ftinc.appsagainsthumanity; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = NO; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; name = Profile; }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ARCHS = arm64; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; @@ -472,9 +475,9 @@ }; 97C147041CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ARCHS = arm64; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; @@ -539,6 +542,7 @@ "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -547,12 +551,14 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); + MARKETING_VERSION = 1.0.1; PRODUCT_BUNDLE_IDENTIFIER = com.ftinc.appsagainsthumanity; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = NO; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; @@ -573,6 +579,7 @@ "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -581,11 +588,13 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); + MARKETING_VERSION = 1.0.1; PRODUCT_BUNDLE_IDENTIFIER = com.ftinc.appsagainsthumanity; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = NO; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; name = Release; diff --git a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata index 1d526a1..919434a 100644 --- a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -2,6 +2,6 @@ + location = "self:"> diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 70693e4..be7ca85 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -8,6 +8,11 @@ import Flutter didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { GeneratedPluginRegistrant.register(with: self) + + if #available(iOS 10.0, *) { + UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate + } + return super.application(application, didFinishLaunchingWithOptions: launchOptions) } } diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 3c14885..56e9c58 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -31,10 +31,25 @@ CFBundleVersion $(FLUTTER_BUILD_NUMBER) + FirebaseDynamicLinksCustomDomains + + https://appsagainsthumanity.com/games + + ITSAppUsesNonExemptEncryption + LSApplicationCategoryType public.app-category.card-games LSRequiresIPhoneOS + NSCameraUsageDescription + Used to change your profile photo + NSPhotoLibraryUsageDescription + Used to change your profile photo + UIBackgroundModes + + fetch + remote-notification + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile diff --git a/ios/Runner/Runner.entitlements b/ios/Runner/Runner.entitlements index a812db5..46b59d3 100644 --- a/ios/Runner/Runner.entitlements +++ b/ios/Runner/Runner.entitlements @@ -2,9 +2,15 @@ + aps-environment + development com.apple.developer.applesignin Default + com.apple.developer.associated-domains + + applinks:appsagainsthumanity.com + diff --git a/lib/app.dart b/lib/app.dart index 3fdd1f1..dfe8277 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -1,22 +1,28 @@ import 'package:appsagainsthumanity/internal.dart'; -import 'package:appsagainsthumanity/ui/home/home_screen.dart'; +import 'package:appsagainsthumanity/internal/push.dart'; +import 'package:appsagainsthumanity/ui/home/home_screen_v2.dart'; import 'package:appsagainsthumanity/ui/routes.dart'; import 'package:appsagainsthumanity/ui/signin/sign_in_screen.dart'; import 'package:appsagainsthumanity/ui/terms_screen.dart'; import 'package:flutter/material.dart'; -//import 'package:firebase_analytics/observer.dart'; +import 'package:firebase_analytics/observer.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'authentication_bloc/authentication_bloc.dart'; class App extends StatelessWidget { + final _navigatorKey = GlobalKey(); + @override Widget build(BuildContext context) { return MaterialApp( + navigatorKey: _navigatorKey, title: 'Apps Against Humanity', - theme: AppThemes.app, -// darkTheme: AppThemes.dark, + theme: AppThemes.light, + darkTheme: AppThemes.dark, + debugShowCheckedModeBanner: false, localizationsDelegates: [ AppLocalizationsDelegate(), GlobalMaterialLocalizations.delegate, @@ -27,21 +33,27 @@ class App extends StatelessWidget { const Locale('en'), // English ], navigatorObservers: [ -// FirebaseAnalyticsObserver(analytics: Analytics.firebaseAnalytics), - Routes.routeObserver + FirebaseAnalyticsObserver(analytics: Analytics()), + Routes.routeObserver, + Routes.routeTracer ], - home: BlocBuilder( - builder: (context, state) { - if (state is Unauthenticated) { - return SignInScreen(); - } else if (state is NeedsAgreeToTerms) { - return TermsOfServiceScreen(); - } else if (state is Authenticated) { - return HomeScreen(); - } else { - return Container(color: AppColors.surface); - } - }, + home: AnnotatedRegion( + value: SystemUiOverlayStyle.light, + child: PushNavigator( + child: BlocBuilder( + builder: (context, state) { + if (state is Unauthenticated) { + return SignInScreen(); + } else if (state is NeedsAgreeToTerms) { + return TermsOfServiceScreen(); + } else if (state is Authenticated) { + return HomeScreenV2(); + } else { + return Container(color: AppColors.surface); + } + }, + ), + ), ), ); } diff --git a/lib/authentication_bloc/authentication_bloc.dart b/lib/authentication_bloc/authentication_bloc.dart index bd2f4ab..aff5697 100644 --- a/lib/authentication_bloc/authentication_bloc.dart +++ b/lib/authentication_bloc/authentication_bloc.dart @@ -6,21 +6,22 @@ import 'package:appsagainsthumanity/internal.dart'; import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:logging/logging.dart'; -import 'package:meta/meta.dart'; +// import 'package:meta/meta.dart'; part 'authentication_event.dart'; - part 'authentication_state.dart'; -class AuthenticationBloc extends Bloc { +class AuthenticationBloc + extends Bloc { final UserRepository _userRepository; final AppPreferences _preferences; - AuthenticationBloc({@required UserRepository userRepository, @required AppPreferences preferences}) - : assert(userRepository != null), - assert(preferences != null), - _userRepository = userRepository, - _preferences = preferences; + AuthenticationBloc({ + required UserRepository userRepository, + required AppPreferences preferences, + }) : _userRepository = userRepository, + _preferences = preferences, + super(Uninitialized()) {} @override AuthenticationState get initialState => Uninitialized(); @@ -74,7 +75,7 @@ class AuthenticationBloc extends Bloc Stream _mapLoggedOutToState() async* { yield Unauthenticated(); -// await Analytics.firebaseAnalytics.logEvent(name: "logout"); + await Analytics().logEvent(name: "logout"); _userRepository.signOut(); } } diff --git a/lib/data/app_preferences.dart b/lib/data/app_preferences.dart index 272ac2d..112951b 100644 --- a/lib/data/app_preferences.dart +++ b/lib/data/app_preferences.dart @@ -1,29 +1,71 @@ import 'package:shared_preferences/shared_preferences.dart'; +import 'package:uuid/uuid.dart'; class AppPreferences { - static AppPreferences _instance = AppPreferences._internal(); + static AppPreferences _instance = AppPreferences._internal(); - static Future loadInstance() async { - if (_instance._prefs == null) { - _instance._prefs = await SharedPreferences.getInstance(); - } - return _instance; + static Future loadInstance() async { + if (_instance._prefs == {}) { + _instance._prefs = await SharedPreferences.getInstance(); } + return _instance; + } - static const KEY_AGREE_TO_TERMS = "agree_to_terms_of_service"; + static const KEY_AGREE_TO_TERMS = "agree_to_terms_of_service"; + static const KEY_PUSH_TOKEN = "push_token"; + static const KEY_DEVICE_ID = "device_id"; + static const KEY_PRIZES_TO_WIN = "prizes_to_win"; + static const KEY_PLAYER_LIMIT = "player_limit"; + static const KEY_DEVELOPER_PACKS = "developer_packs"; - SharedPreferences _prefs; + SharedPreferences _prefs; - AppPreferences._internal(); + AppPreferences._internal(); - factory AppPreferences() { - return _instance; - } + factory AppPreferences() { + return _instance; + } + + /// Whether or not the user has agreed to TOS + bool get agreedToTermsOfService => + _prefs.getBool(KEY_AGREE_TO_TERMS) ?? false; + set agreedToTermsOfService(bool agreed) => + _prefs.setBool(KEY_AGREE_TO_TERMS, agreed); + + /// The Push Token + String get pushToken => _prefs.getString(KEY_PUSH_TOKEN); + set pushToken(String token) => _prefs.setString(KEY_PUSH_TOKEN, token); + + /// The last used # of prizes to win + int get prizesToWin => _prefs.getInt(KEY_PRIZES_TO_WIN) ?? 7; + set prizesToWin(int value) => _prefs.setInt(KEY_PRIZES_TO_WIN, value); - bool get agreedToTermsOfService => _prefs.getBool(KEY_AGREE_TO_TERMS) ?? false; - set agreedToTermsOfService(bool agreed) => _prefs.setBool(KEY_AGREE_TO_TERMS, agreed); + /// The last used player limit of new games + int get playerLimit => _prefs.getInt(KEY_PLAYER_LIMIT) ?? 15; + set playerLimit(int value) => _prefs.setInt(KEY_PLAYER_LIMIT, value); - clear() async { - await _prefs.clear(); + /// Whether or not the user has unlocked the developer pack(s) + bool get developerPackEnabled => _prefs.getBool(KEY_DEVELOPER_PACKS) ?? false; + set developerPackEnabled(bool enabled) => + _prefs.setBool(KEY_DEVELOPER_PACKS, enabled); + + /// The persistent device id + String get deviceId { + var dId = _prefs.getString(KEY_DEVICE_ID); + if (dId == null) { + dId = Uuid().v4(); + deviceId = dId; } + return dId; + } + + set deviceId(String deviceId) => _prefs.setString(KEY_DEVICE_ID, deviceId); + + clear() async { + var _tos = agreedToTermsOfService; + var _deviceId = deviceId; + await _prefs.clear(); + deviceId = _deviceId; + agreedToTermsOfService = _tos; + } } diff --git a/lib/data/features/cards/cache/cards_cache.dart b/lib/data/features/cards/cache/cards_cache.dart new file mode 100644 index 0000000..0d859cb --- /dev/null +++ b/lib/data/features/cards/cache/cards_cache.dart @@ -0,0 +1,8 @@ +import 'package:appsagainsthumanity/data/features/cards/model/card_set.dart'; + +abstract class CardsCache { + + Future> getCardSets(); + Future setCardSets(List cardSets); + Future clear(); +} diff --git a/lib/data/features/cards/cache/in_memory_cards_cache.dart b/lib/data/features/cards/cache/in_memory_cards_cache.dart new file mode 100644 index 0000000..cc7791a --- /dev/null +++ b/lib/data/features/cards/cache/in_memory_cards_cache.dart @@ -0,0 +1,23 @@ +import 'package:appsagainsthumanity/data/features/cards/cache/cards_cache.dart'; +import 'package:appsagainsthumanity/data/features/cards/model/card_set.dart'; + +class InMemoryCardsCache extends CardsCache { + final List _cardSets = []; + + @override + Future> getCardSets() async { + return _cardSets; + } + + @override + Future setCardSets(List cardSets) async { + _cardSets.clear(); + _cardSets.addAll(cardSets); + print("Card sets cached in memory"); + } + + @override + Future clear() async { + _cardSets.clear(); + } +} diff --git a/lib/data/features/cards/cards_repository.dart b/lib/data/features/cards/cards_repository.dart index 8dd51a5..7389673 100644 --- a/lib/data/features/cards/cards_repository.dart +++ b/lib/data/features/cards/cards_repository.dart @@ -1,4 +1,7 @@ +import 'package:appsagainsthumanity/data/features/cards/model/card_set.dart'; + abstract class CardsRepository{ - Future> getAvailableCardSets(); + /// Get the list of cardsets that you can use + Future> getAvailableCardSets(); } diff --git a/lib/data/features/cards/firestore_cards_repository.dart b/lib/data/features/cards/firestore_cards_repository.dart index 898e421..73caf6e 100644 --- a/lib/data/features/cards/firestore_cards_repository.dart +++ b/lib/data/features/cards/firestore_cards_repository.dart @@ -1,20 +1,42 @@ +import 'package:appsagainsthumanity/data/features/cards/cache/cards_cache.dart'; +import 'package:appsagainsthumanity/data/features/cards/cache/in_memory_cards_cache.dart'; import 'package:appsagainsthumanity/data/features/cards/cards_repository.dart'; +import 'package:appsagainsthumanity/data/features/cards/model/card_set.dart'; import 'package:appsagainsthumanity/data/firestore.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; class FirestoreCardsRepository extends CardsRepository { - final Firestore _db; + FirebaseFirestore? db; + CardsCache? cache; - FirestoreCardsRepository({Firestore firestore}) - : _db = firestore ?? Firestore.instance; + FirestoreCardsRepository({ + this.db, + this.cache, + }) : super() { + db = FirebaseFirestore.instance; + cache = InMemoryCardsCache(); + } @override - Future> getAvailableCardSets() { + Future> getAvailableCardSets() { return currentUserOrThrow((firebaseUser) async { - var cardSetsCollection = _db.collection(FirebaseConstants.COLLECTION_CARD_SETS); - var snapshots = await cardSetsCollection.getDocuments(); - print("${snapshots.documents.length} Card sets found"); - return snapshots.documents.map((e) => e.documentID).toList(); + // Check and return cache if valid + final cachedSets = await cache!.getCardSets(); + if (cachedSets.isNotEmpty) { + return cachedSets; + } else { + var cardSetsCollection = + db!.collection(FirebaseConstants.COLLECTION_CARD_SETS); + var snapshots = await cardSetsCollection.get(); + print("${snapshots.docs.length} Card sets found"); + final cardSets = snapshots.docs.map((e) { + var cardSet = CardSet.fromJson(e.data()); + cardSet.id = e.id; + return cardSet; + }).toList(); + await cache!.setCardSets(cardSets); + return cardSets; + } }); } } diff --git a/lib/data/features/cards/model/card_set.dart b/lib/data/features/cards/model/card_set.dart new file mode 100644 index 0000000..1692485 --- /dev/null +++ b/lib/data/features/cards/model/card_set.dart @@ -0,0 +1,42 @@ +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'card_set.g.dart'; + +@JsonSerializable() +class CardSet extends Equatable { + @JsonKey(ignore: true) + String? id; + + final String? name; + final int? prompts; + final int? responses; + final String? source; + + CardSet({ + this.id, + this.name, + this.prompts, + this.responses, + this.source, + }); + + factory CardSet.fromJson(Map json) => + _$CardSetFromJson(json); + + Map toJson() => _$CardSetToJson(this); + + @override + String toString() { + return 'CardSet{id: $id, name: $name, prompts: $prompts, responses: $responses, source: $source}'; + } + + @override + List get props => [ + id as String, + name as String, + prompts as int, + responses as int, + source as String, + ]; +} diff --git a/lib/data/features/cards/model/prompt_card.dart b/lib/data/features/cards/model/prompt_card.dart index 3aa2d0e..e94ed1f 100644 --- a/lib/data/features/cards/model/prompt_card.dart +++ b/lib/data/features/cards/model/prompt_card.dart @@ -14,19 +14,25 @@ class PromptCard extends Equatable { final String source; PromptCard({ - @required this.cid, - @required this.text, - @required this.special, - @required this.set, - @required this.source, + required this.cid, + required this.text, + required this.special, + required this.set, + required this.source, }); - factory PromptCard.fromJson(Map json) => _$PromptCardFromJson(json); + factory PromptCard.fromJson(Map json) => + _$PromptCardFromJson(json); Map toJson() => _$PromptCardToJson(this); @override List get props => [cid, text, special, set, source]; + + @override + String toString() { + return 'PromptCard{cid: $cid, text: $text, special: $special, set: $set, source: $source}'; + } } -Map promptCardToJson(PromptCard card) => card?.toJson(); +Map promptCardToJson(PromptCard card) => card.toJson(); diff --git a/lib/data/features/cards/model/prompt_special.dart b/lib/data/features/cards/model/prompt_special.dart index 98e982d..8da8e58 100644 --- a/lib/data/features/cards/model/prompt_special.dart +++ b/lib/data/features/cards/model/prompt_special.dart @@ -1,18 +1,15 @@ -import 'package:kt_dart/kt.dart'; +// import 'package:kt_dart/kt.dart'; -enum PromptSpecial { - pick2, - draw2pick3 -} +enum PromptSpecial { pick2, draw2pick3, derp } -@nullable PromptSpecial promptSpecial(String special) { - if (special != null) { - if (special.toUpperCase() == 'PICK 2') { - return PromptSpecial.pick2; - } else if (special.toUpperCase() == 'DRAW 2 PICK 3' || special.toUpperCase() == 'DRAW 2, PICK 3') { - return PromptSpecial.draw2pick3; - } + if (special != "") { + if (special.toUpperCase() == 'PICK 2') { + return PromptSpecial.pick2; + } else if (special.toUpperCase() == 'DRAW 2 PICK 3' || + special.toUpperCase() == 'DRAW 2, PICK 3') { + return PromptSpecial.draw2pick3; } - return null; + } + return PromptSpecial.derp; } diff --git a/lib/data/features/cards/model/response_card.dart b/lib/data/features/cards/model/response_card.dart index 80b5e8c..3f990b2 100644 --- a/lib/data/features/cards/model/response_card.dart +++ b/lib/data/features/cards/model/response_card.dart @@ -13,17 +13,17 @@ class ResponseCard extends Equatable { final String source; ResponseCard({ - @required this.cid, - @required this.text, - @required this.set, - @required this.source, + required this.cid, + required this.text, + required this.set, + required this.source, }); - factory ResponseCard.fromJson(Map json) => _$ResponseCardFromJson(json); + factory ResponseCard.fromJson(Map json) => + _$ResponseCardFromJson(json); Map toJson() => _$ResponseCardToJson(this); @override List get props => [cid, text, set, source]; } - diff --git a/lib/data/features/devices/device_repository.dart b/lib/data/features/devices/device_repository.dart new file mode 100644 index 0000000..8399d4e --- /dev/null +++ b/lib/data/features/devices/device_repository.dart @@ -0,0 +1,84 @@ +import 'dart:io'; + +import 'package:flutter/foundation.dart' show kIsWeb; +import 'package:appsagainsthumanity/data/app_preferences.dart'; +import 'package:appsagainsthumanity/data/firestore.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:device_info/device_info.dart'; +import 'package:firebase_auth/firebase_auth.dart'; + +class DeviceRepository { + static DeviceRepository _instance = DeviceRepository._(); + + final FirebaseAuth _auth = FirebaseAuth.instance; + final FirebaseFirestore _db = FirebaseFirestore.instance; + final DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); + + DeviceRepository._(); + + factory DeviceRepository() { + return _instance; + } + + void updatePushToken(String token) async { + AppPreferences().pushToken = token; + var currentUser = _auth.currentUser; + if (currentUser != null) { + // If current token is not null, attempt to pull existing device info and copy it to new push token + var document = _db.collection(FirebaseConstants.COLLECTION_USERS) + .doc(currentUser.uid) + .collection(FirebaseConstants.COLLECTION_DEVICES) + .doc(AppPreferences().deviceId); + + var snapshot = await document.get(); + if (snapshot.exists) { + await document.update({ + "token": token + }); + } else { + var newDevice = await _createNewDevice(token); + await document.set(newDevice); + } + } + } + + Future> _createNewDevice(String token) async { + return { + "token": token, + "platform": _getPlatform(), + "info": await _getDeviceInfo(), + "createdAt": Timestamp.now(), + "updatedAt": Timestamp.now() + }; + } + + String _getPlatform() { + if (kIsWeb) { + return "web"; + } else { + if (Platform.isAndroid) { + return "android"; + } else if (Platform.isIOS) { + return "ios"; + } else { + return "other"; + } + } + + } + + Future _getDeviceInfo() async { + if (kIsWeb) { + return "Web"; + } else { + var androidInfo = await deviceInfo.androidInfo; + if (androidInfo != null) { + return "${androidInfo.manufacturer}/${androidInfo.model}/${androidInfo + .product}/isPhysicalDevice(${androidInfo.isPhysicalDevice})/sdk(${androidInfo.version.sdkInt})"; + } else { + var iosInfo = await deviceInfo.iosInfo; + return "${iosInfo.model}/${iosInfo.systemName}/${iosInfo.systemVersion}"; + } + } + } +} diff --git a/lib/data/features/game/firestore_game_repository.dart b/lib/data/features/game/firestore_game_repository.dart index bf12729..c8c34cc 100644 --- a/lib/data/features/game/firestore_game_repository.dart +++ b/lib/data/features/game/firestore_game_repository.dart @@ -1,5 +1,5 @@ import 'dart:math'; - +import 'package:appsagainsthumanity/data/features/cards/model/card_set.dart'; import 'package:appsagainsthumanity/data/features/cards/model/response_card.dart'; import 'package:appsagainsthumanity/data/features/game/game_repository.dart'; import 'package:appsagainsthumanity/data/features/game/model/game.dart'; @@ -10,34 +10,46 @@ import 'package:appsagainsthumanity/data/features/users/user_repository.dart'; import 'package:appsagainsthumanity/data/firestore.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:cloud_functions/cloud_functions.dart'; +import 'package:flutter/services.dart'; import 'package:kt_dart/kt.dart'; class FirestoreGameRepository extends GameRepository { - final Firestore db; - final UserRepository userRepository; - - FirestoreGameRepository({Firestore firestore, UserRepository userRepository}) - : assert(userRepository != null), - userRepository = userRepository, - db = firestore ?? Firestore.instance; + FirebaseFirestore? db; + UserRepository userRepository; + + FirestoreGameRepository({ + this.db, + required this.userRepository, + }) : super() { + db = FirebaseFirestore.instance; + userRepository = UserRepository(); + } @override - Future createGame(KtSet cardSets, {int prizesToWin = 10}) { + Future createGame( + KtSet? cardSets, { + int prizesToWin = PRIZES_TO_WIN, + int playerLimit = PLAYER_LIMIT, + bool pick2Enabled = true, + bool draw2Pick3Enabled = true, + }) { return currentUserOrThrow((firebaseUser) async { - var newGameDoc = db.collection(FirebaseConstants.COLLECTION_GAMES).document(); - + var newGameDoc = db!.collection(FirebaseConstants.COLLECTION_GAMES).doc(); var game = Game( - id: newGameDoc.documentID, - gid: generateId(), + id: newGameDoc.id, + gid: generateId(length: FirebaseConstants.MAX_GID_SIZE), ownerId: firebaseUser.uid, state: GameState.waitingRoom, prizesToWin: prizesToWin, - cardSets: cardSets.asSet(), + playerLimit: playerLimit, + pick2Enabled: pick2Enabled, + draw2Pick3Enabled: draw2Pick3Enabled, + cardSets: cardSets!.map((c) => c.id!).asList().toSet(), ); - await newGameDoc.setData(game.toJson()); + await newGameDoc.set(game.toJson()); // Now add yourself as a player to the game - await _addSelfToGame(newGameDoc.documentID, game); + await _addSelfToGame(gameDocumentId: newGameDoc.id); return game; }); @@ -46,201 +58,253 @@ class FirestoreGameRepository extends GameRepository { @override Future joinGame(String gid) { return currentUserOrThrow((firebaseUser) async { - var snapshot = - await db.collection(FirebaseConstants.COLLECTION_GAMES).where('gid', isEqualTo: gid).limit(1).getDocuments(); - - if (snapshot != null && snapshot.documents.isNotEmpty) { - var gameDocument = snapshot.documents.first; - var game = Game.fromDocument(gameDocument); - - // Only let user's join a game that hasn't been started yet - if (game.state == GameState.waitingRoom) { - await _addSelfToGame(gameDocument.documentID, game); - return game; - } else { - throw 'You cannot join a game that is already in-progress or completed'; - } - } else { - throw 'No game found for $gid'; - } + return await _addSelfToGame(gid: gid); }); } @override Future findGame(String gid) async { - var snapshots = await db + var snapshots = await db! .collection(FirebaseConstants.COLLECTION_GAMES) - .where('gid', isEqualTo: gid) + .where('gid', isEqualTo: gid.toUpperCase()) .limit(1) - .getDocuments(); + .get(); - if (snapshots != null && snapshots.documents.isNotEmpty) { - var document = snapshots.documents.first; + if (snapshots != {} && snapshots.docs.isNotEmpty) { + var document = snapshots.docs.first; return Game.fromDocument(document); } else { throw 'Unable to find a game for $gid'; } } + @override + Future getGame(String gameDocumentId, {bool andJoin = false}) async { + var gameDocument = + db!.collection(FirebaseConstants.COLLECTION_GAMES).doc(gameDocumentId); + + try { + var snapshot = await gameDocument.get(); + return Game.fromDocument(snapshot); + } catch (e) { + if (e is PlatformException && andJoin) { + try { + return await _addSelfToGame(gameDocumentId: gameDocumentId); + } catch (e, st) { + print("Error joining game: $e\n$st"); + } + } + } + + // return null; + return Game( + gid: '', + ownerId: '', + state: GameState.waitingRoom, + cardSets: {}, + ); + } + + @override + Future leaveGame(UserGame game) { + return currentUserOrThrow((firebaseUser) async { + if (game.state == GameState.completed) { + // We should just delete the usergame ourselfs + await db! + .collection(FirebaseConstants.COLLECTION_USERS) + .doc(firebaseUser.uid) + .collection(FirebaseConstants.COLLECTION_GAMES) + .doc(game.id) + .delete(); + print("Game already completed, so we just deleted the reference"); + } else { + final HttpsCallable callable = + FirebaseFunctions.instance.httpsCallable('leaveGame'); + HttpsCallableResult response = + await callable.call({'game_id': game.id}); + print("Game left! ${response.data}"); + } + }); + } + @override Stream> observeJoinedGames() { return streamCurrentUserOrThrow((firebaseUser) { - return db + return db! .collection(FirebaseConstants.COLLECTION_USERS) - .document(firebaseUser.uid) + .doc(firebaseUser.uid) .collection(FirebaseConstants.COLLECTION_GAMES) .snapshots() - .map((querySnapshot) => querySnapshot.documents.map((e) => UserGame.fromDocument(e)).toList()); + .map((querySnapshot) => + querySnapshot.docs.map((e) => UserGame.fromDocument(e)).toList()); }); } @override Stream observeGame(String gameDocumentId) { - var document = db.collection(FirebaseConstants.COLLECTION_GAMES).document(gameDocumentId); + var document = + db!.collection(FirebaseConstants.COLLECTION_GAMES).doc(gameDocumentId); return document.snapshots().map((snapshot) => Game.fromDocument(snapshot)); } @override Stream> observePlayers(String gameDocumentId) { - var collection = db + var collection = db! .collection(FirebaseConstants.COLLECTION_GAMES) - .document(gameDocumentId) + .doc(gameDocumentId) .collection(FirebaseConstants.COLLECTION_PLAYERS); - return collection.snapshots().map((snapshots) => snapshots.documents.map((e) => Player.fromDocument(e)).toList()); + return collection.snapshots().map((snapshots) => + snapshots.docs.map((e) => Player.fromDocument(e)).toList()); + } + + @override + Stream> observeDownvotes(String gameDocumentId) { + var collection = db! + .collection(FirebaseConstants.COLLECTION_GAMES) + .doc(gameDocumentId) + .collection(FirebaseConstants.COLLECTION_DOWNVOTES) + .doc(FirebaseConstants.DOCUMENT_TALLY); + + return collection.snapshots().map((snapshot) { + if (snapshot.data != {}) { + final data = snapshot.data() ?? {}; + print(data); + print("Contains votes: ${data.containsKey('votes')}"); + print("Votes: ${data['votes']}"); + return List.from(data['votes'] ?? []); + } else { + return []; + } + }); } @override Future addRandoCardrissian(String gameDocumentId) async { - var document = db + var document = db! .collection(FirebaseConstants.COLLECTION_GAMES) - .document(gameDocumentId) + .doc(gameDocumentId) .collection(FirebaseConstants.COLLECTION_PLAYERS) - .document(FirebaseConstants.DOCUMENT_RANDO_CARDRISSIAN); + .doc(FirebaseConstants.DOCUMENT_RANDO_CARDRISSIAN); var rando = Player( - id: FirebaseConstants.DOCUMENT_RANDO_CARDRISSIAN, - name: "Rando Cardrissian", - avatarUrl: null, - isRandoCardrissian: true - ); + id: FirebaseConstants.DOCUMENT_RANDO_CARDRISSIAN, + name: "Rando Cardrissian", + avatarUrl: "", + isRandoCardrissian: true); - await document.setData(rando.toJson()); + await document.set(rando.toJson()); } @override Future startGame(String gameDocumentId) async { - final HttpsCallable callable = CloudFunctions.instance.getHttpsCallable(functionName: 'startGame'); + final HttpsCallable callable = + FirebaseFunctions.instance.httpsCallable('startGame'); + dynamic response = + await callable.call({'game_id': gameDocumentId}); + print("Start game Successful! $response"); + } + + @override + Future submitResponse( + String gameDocumentId, List cards) async { + final HttpsCallable callable = + FirebaseFunctions.instance.httpsCallable('submitResponses'); dynamic response = await callable.call({ - 'game_id': gameDocumentId + 'game_id': gameDocumentId, + 'indexed_responses': + cards.asMap().map((key, value) => MapEntry(key.toString(), value.cid)) }); - - print("Start game Successful! $response"); + print("Responses Submitted! $response"); } @override - Future submitResponse(String gameDocumentId, List cards) { - // 1. Remove the cards from the player's hand - // 2. Add the cards as a response to the current game's turn + Future downVoteCurrentPrompt(String gameDocumentId) { return currentUserOrThrow((firebaseUser) async { - var gameDocument = db + var gameDocument = db! .collection(FirebaseConstants.COLLECTION_GAMES) - .document(gameDocumentId); - - var playerDocument = gameDocument - .collection(FirebaseConstants.COLLECTION_PLAYERS) - .document(firebaseUser.uid); - - var cardData = cards.map((e) => e.toJson()).toList(); - - await db.runTransaction((transaction) async { - var playerSnapshot = await transaction.get(playerDocument); - var player = Player.fromDocument(playerSnapshot); - - player.hand.removeWhere((element) { - return cards.firstWhere((c) => c.cid == element.cid, orElse: () => null) != null; - }); - - await transaction.update(playerDocument, { - 'hand': player.hand.map((e) => e.toJson()).toList() + .doc(gameDocumentId) + .collection(FirebaseConstants.COLLECTION_DOWNVOTES) + .doc(FirebaseConstants.DOCUMENT_TALLY); + + var snapshot = await gameDocument.get(); + if (snapshot.exists) { + await gameDocument.update({ + 'votes': FieldValue.arrayUnion([firebaseUser.uid]) }); - - await transaction.update(gameDocument, { - 'turn.responses.${firebaseUser.uid}': cardData + } else { + await gameDocument.set({ + 'votes': [firebaseUser.uid] }); - }); + } }); } @override - Future downVoteCurrentPrompt(String gameDocumentId) { + Future waveAtPlayer(String gameDocumentId, String playerId, + [String? message]) { return currentUserOrThrow((firebaseUser) async { - var gameDocument = db - .collection(FirebaseConstants.COLLECTION_GAMES) - .document(gameDocumentId); - - await gameDocument.updateData({ - 'turn.downvotes': FieldValue.arrayUnion([firebaseUser.uid]) + final HttpsCallable callable = + FirebaseFunctions.instance.httpsCallable('wave'); + dynamic response = await callable.call({ + 'game_id': gameDocumentId, + 'player_id': playerId, + if (message != "") 'message': message, }); + print("Wave sent to player successfully! $response"); }); } @override Future reDealHand(String gameDocumentId) { return currentUserOrThrow((firebaseUser) async { - final HttpsCallable callable = CloudFunctions.instance.getHttpsCallable(functionName: 'reDealHand'); - dynamic response = await callable.call({ - 'game_id': gameDocumentId - }); - + final HttpsCallable callable = + FirebaseFunctions.instance.httpsCallable('reDealHand'); + dynamic response = + await callable.call({'game_id': gameDocumentId}); print("Hand re-dealt successfully! $response"); }); } @override Future pickWinner(String gameDocumentId, String playerId) async { - final HttpsCallable callable = CloudFunctions.instance.getHttpsCallable(functionName: 'pickWinner'); - dynamic response = await callable.call({ - 'game_id': gameDocumentId, - 'player_id': playerId - }); - + final HttpsCallable callable = + FirebaseFunctions.instance.httpsCallable('pickWinner'); + dynamic response = await callable.call( + {'game_id': gameDocumentId, 'player_id': playerId}); print("Winner picked Successful! $response"); } - Future _addSelfToGame(String gameDocumentId, Game game) async { + Future _addSelfToGame({String? gameDocumentId, String? gid}) async { var user = await userRepository.getUser(); - var player = Player( - id: user.id, - name: user.name, - avatarUrl: user.avatarUrl, - ); - // Write self to game's players - await db - .collection(FirebaseConstants.COLLECTION_GAMES) - .document(gameDocumentId) - .collection(FirebaseConstants.COLLECTION_PLAYERS) - .document(user.id) - .setData(player.toJson(), merge: true); - - // Write game to your collection of games - var userGame = UserGame( - gid: game.gid, - state: game.state, - joinedAt: DateTime.now(), - ); - await db - .collection(FirebaseConstants.COLLECTION_USERS) - .document(user.id) - .collection(FirebaseConstants.COLLECTION_GAMES) - .document(gameDocumentId) - .setData(userGame.toJson()); + final HttpsCallable callable = + FirebaseFunctions.instance.httpsCallable('joinGame'); + HttpsCallableResult response = await callable.call({ + if (gameDocumentId != "") 'game_id': gameDocumentId, + if (gid != "") 'gid': gid!.toUpperCase(), + 'name': user.name, + 'avatar': user.avatarUrl + }); + + var jsonResponse = Map.from(response.data); + var game = Game.fromJson(jsonResponse); + game.id = jsonResponse['id']; + return game; + } + + @override + Future kickPlayer(String gameDocumentId, String playerId) async { + final HttpsCallable callable = + FirebaseFunctions.instance.httpsCallable('kickPlayer'); + HttpsCallableResult response = await callable.call( + {'game_id': gameDocumentId, 'player_id': playerId}); + print("Player kicked! ${response.data}"); } - String generateId({int length = 5}) { - String source = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + String generateId({int length = 7}) { + String source = "ACEFHJKLMNPQRTUVWXY3479"; StringBuffer builder = StringBuffer(); for (var i = 0; i < length; i++) { builder.write(source[Random().nextInt(source.length)]); diff --git a/lib/data/features/game/game_repository.dart b/lib/data/features/game/game_repository.dart index e0a7ebf..38a4e23 100644 --- a/lib/data/features/game/game_repository.dart +++ b/lib/data/features/game/game_repository.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'package:appsagainsthumanity/data/features/cards/model/card_set.dart'; import 'package:appsagainsthumanity/data/features/cards/model/response_card.dart'; import 'package:appsagainsthumanity/data/features/game/model/game.dart'; import 'package:appsagainsthumanity/data/features/game/model/game_state.dart'; @@ -7,53 +8,79 @@ import 'package:appsagainsthumanity/data/features/users/model/user_game.dart'; import 'package:kt_dart/collection.dart'; abstract class GameRepository { + /// Create a new game with the provided list of card sets + Future createGame( + KtSet cardSets, { + int prizesToWin = PRIZES_TO_WIN, + int playerLimit = PLAYER_LIMIT, + bool pick2Enabled = true, + bool draw2Pick3Enabled = true, + }); - /// Create a new game with the provided list of card sets - Future createGame(KtSet cardSets, { int prizesToWin = 10 }); + /// Join an existing game using the [gid] game id code + Future joinGame(String gid); - /// Join an existing game using the [gid] game id code - Future joinGame(String gid); + /// Find an existing game using the [gid] game id code + Future findGame(String gid); - /// Find an existing game using the [gid] game id code - Future findGame(String gid); + /// Get a game by it's actual document id + Future getGame(String gameDocumentId, {bool andJoin = false}); - /// Return a list of games that you have joined in the past - Stream> observeJoinedGames(); + /// Leave a game. This will flag the 'player' on the game as 'inActive' + Future leaveGame(UserGame game); - /// Observe any changes to a game state by it's [gameDocumentId] - Stream observeGame(String gameDocumentId); + /// Return a list of games that you have joined in the past + Stream> observeJoinedGames(); - /// Observe any changes to the players of a game by it's - /// [gameDocumentId] - Stream> observePlayers(String gameDocumentId); + /// Observe any changes to a game state by it's [gameDocumentId] + Stream observeGame(String gameDocumentId); - /// Add Rando Cardrissian to the game - /// [gid] the game to add him to - Future addRandoCardrissian(String gameDocumentId); + /// Observe any changes to the players of a game by it's + /// [gameDocumentId] + Stream> observePlayers(String gameDocumentId); - /// Start a game that is in it's [GameState.waitingRoom] state - /// The gamescreen should pick up the game state change and update the UI - /// accordingly - Future startGame(String gameDocumentId); + /// Observe any changes to the downvote tally by it's [gameDocumentId] + Stream> observeDownvotes(String gameDocumentId); - /// Submit your responses for the current turn, if you are not a judge, and - /// you haven't submitted your response already - Future submitResponse(String gameDocumentId, List cards); + /// Add Rando Cardrissian to the game + /// [gid] the game to add him to + Future addRandoCardrissian(String gameDocumentId); - /// Downvote the current prompt card. If enough downvotes are casted - /// then a new prompt is drawn for this turn and the current judge remains - Future downVoteCurrentPrompt(String gameDocumentId); + /// Start a game that is in it's [GameState.waitingRoom] state + /// The gamescreen should pick up the game state change and update the UI + /// accordingly + Future startGame(String gameDocumentId); - /// Re-deal your hand in exchange for one prize card, if you have one - Future reDealHand(String gameDocumentId); + /// Submit your responses for the current turn, if you are not a judge, and + /// you haven't submitted your response already + Future submitResponse(String gameDocumentId, List cards); - ////////////////////// - // Judge Methods - ////////////////////// + /// Downvote the current prompt card. If enough downvotes are casted + /// then a new prompt is drawn for this turn and the current judge remains + Future downVoteCurrentPrompt(String gameDocumentId); - /// Pick the winner of the turn that you are judging. This will fail if: - /// A. You are not the judge - /// B. All responses are not in yet - /// C. The turn hasn't been rotated yet and your previous pick still persists - Future pickWinner(String gameDocumentId, String playerId); + /// Wave at a player to re-engage them in the game. Optionally provide a [message] to send to the + /// user. + Future waveAtPlayer(String gameDocumentId, String playerId, + [String message]); + + /// Re-deal your hand in exchange for one prize card, if you have one + Future reDealHand(String gameDocumentId); + + ////////////////////// + // Judge Methods + ////////////////////// + + /// Pick the winner of the turn that you are judging. This will fail if: + /// A. You are not the judge + /// B. All responses are not in yet + /// C. The turn hasn't been rotated yet and your previous pick still persists + Future pickWinner(String gameDocumentId, String playerId); + + ////////////////////// + // Owner Methods + ////////////////////// + + /// Kick a player from a game, this will only work if you are the owner of the game + Future kickPlayer(String gameDocumentId, String playerId); } diff --git a/lib/data/features/game/model/game.dart b/lib/data/features/game/model/game.dart index 1f0ef60..c852cb9 100644 --- a/lib/data/features/game/model/game.dart +++ b/lib/data/features/game/model/game.dart @@ -2,40 +2,50 @@ import 'package:appsagainsthumanity/data/features/game/model/game_state.dart'; import 'package:appsagainsthumanity/data/features/game/model/turn.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:json_annotation/json_annotation.dart'; -import 'package:meta/meta.dart'; +// import 'package:meta/meta.dart'; part 'game.g.dart'; +const PRIZES_TO_WIN = 7; +const PLAYER_LIMIT = 30; + @JsonSerializable() class Game { @JsonKey(ignore: true) - String id; + String? id; final String gid; final String ownerId; final GameState state; final int round; final int prizesToWin; - final List judgeRotation; + final int playerLimit; + final bool pick2Enabled; + final bool draw2Pick3Enabled; + final List? judgeRotation; final Set cardSets; @JsonKey(toJson: _turnToJson) - final Turn turn; - final String winner; + final Turn? turn; + final String? winner; - Game( - {this.id, - @required this.gid, - @required this.ownerId, - @required this.state, - @required this.cardSets, - this.round = 1, - this.prizesToWin = 10, - this.judgeRotation, - this.turn, - this.winner}); + Game({ + this.id, + required this.gid, + required this.ownerId, + required this.state, + required this.cardSets, + this.round = 1, + this.prizesToWin = PRIZES_TO_WIN, + this.playerLimit = PLAYER_LIMIT, + this.pick2Enabled = true, + this.draw2Pick3Enabled = true, + this.judgeRotation, + this.turn, + this.winner, + }); factory Game.fromDocument(DocumentSnapshot snapshot) { - var game = Game.fromJson(snapshot.data); - game.id = snapshot.documentID; + var game = Game.fromJson(snapshot.data() as Map); + game.id = snapshot.id; return game; } @@ -44,16 +54,16 @@ class Game { Map toJson() => _$GameToJson(this); Game copyWith({ - String id, - String gid, - String ownerId, - GameState state, - int round, - int prizesToWin, - List judgeRotation, - Set cardSets, - Turn turn, - String winner, + String? id, + String? gid, + String? ownerId, + GameState? state, + int? round, + int? prizesToWin, + List? judgeRotation, + Set? cardSets, + Turn? turn, + String? winner, }) { return Game( id: id ?? this.id, @@ -70,4 +80,4 @@ class Game { } } -Map _turnToJson(Turn turn) => turn?.toJson(); +Map _turnToJson(Turn turn) => turn.toJson(); diff --git a/lib/data/features/game/model/player.dart b/lib/data/features/game/model/player.dart index 063f0df..b31a0b3 100644 --- a/lib/data/features/game/model/player.dart +++ b/lib/data/features/game/model/player.dart @@ -9,7 +9,7 @@ part 'player.g.dart'; @immutable @JsonSerializable() class Player { - static const DEFAULT_NAME = "John \"I need a name\" Smith"; + static const DEFAULT_NAME = "\"A player needs a name\""; final String id; final String name; @@ -18,23 +18,28 @@ class Player { @JsonKey(defaultValue: false) final bool isRandoCardrissian; - @JsonKey(includeIfNull: false) - final List hand; + @JsonKey(defaultValue: false) + final bool isInactive; @JsonKey(includeIfNull: false) - final List prizes; + final List? hand; - Player({ - @required this.id, - @required this.name, - this.avatarUrl, - this.hand, - this.prizes, - bool isRandoCardrissian = false - }) : isRandoCardrissian = isRandoCardrissian; + @JsonKey(includeIfNull: false) + final List? prizes; + + Player( + {required this.id, + required this.name, + required this.avatarUrl, + this.hand, + this.prizes, + bool isRandoCardrissian = false, + bool isInactive = false}) + : isRandoCardrissian = isRandoCardrissian, + isInactive = isInactive; factory Player.fromDocument(DocumentSnapshot documentSnapshot) => - Player.fromJson(documentSnapshot.data); + Player.fromJson(documentSnapshot.data() as Map); factory Player.fromJson(Map json) => _$PlayerFromJson(json); @@ -47,6 +52,7 @@ class Player { name: $name, avatarUrl: $avatarUrl, isRandoCardrissian: $isRandoCardrissian, + isInactive: $isInactive, hand: $hand, prizes: $prizes }'''; diff --git a/lib/data/features/game/model/turn.dart b/lib/data/features/game/model/turn.dart index 4ebefed..da1eec9 100644 --- a/lib/data/features/game/model/turn.dart +++ b/lib/data/features/game/model/turn.dart @@ -2,7 +2,6 @@ import 'package:appsagainsthumanity/data/features/cards/model/prompt_card.dart'; import 'package:appsagainsthumanity/data/features/cards/model/response_card.dart'; import 'package:appsagainsthumanity/data/features/game/model/turn_winner.dart'; import 'package:json_annotation/json_annotation.dart'; -import 'package:kt_dart/kt.dart'; import 'package:meta/meta.dart'; part 'turn.g.dart'; @@ -15,45 +14,42 @@ class Turn { @JsonKey(toJson: _promptCardToJson) final PromptCard promptCard; - @JsonKey(toJson: _responsesToJson) - final Map> responses; + @JsonKey(toJson: responsesToJson) + final Map> responses; - @JsonKey(nullable: true) - final Set downvotes; - - @JsonKey(nullable: true, toJson: turnWinnerToJson) - final TurnWinner winner; + @JsonKey(toJson: turnWinnerToJson) + final TurnWinner? winner; Turn({ - @required this.judgeId, - @required this.promptCard, - @required this.responses, - Set downvotes, + required this.judgeId, + required this.promptCard, + required this.responses, this.winner, - }) : downvotes = downvotes; + }); factory Turn.fromJson(Map json) => _$TurnFromJson(json); Map toJson() => _$TurnToJson(this); Turn copyWith({ - String judgeId, - PromptCard promptCard, - Map> responses, - Set downvotes, - TurnWinner winner, + String? judgeId, + PromptCard? promptCard, + Map>? responses, + TurnWinner? winner, }) { return Turn( - judgeId: judgeId ?? this.judgeId, - promptCard: promptCard ?? this.promptCard, - responses: responses ?? this.responses, - downvotes: downvotes ?? this.downvotes, - winner: winner ?? this.winner); + judgeId: judgeId ?? this.judgeId, + promptCard: promptCard ?? this.promptCard, + responses: responses ?? this.responses, + winner: winner ?? this.winner, + ); } } Map _promptCardToJson(PromptCard card) => card.toJson(); -Map _responsesToJson(Map> responses) { - return responses?.map((k, e) => MapEntry(k, e.map((e) => e.toJson()).toList())); +Map responsesToJson( + Map> responses) { + return responses + .map((k, e) => MapEntry(k, e.map((e) => e.toJson()).toList())); } diff --git a/lib/data/features/game/model/turn_winner.dart b/lib/data/features/game/model/turn_winner.dart index 5522f51..44e24ba 100644 --- a/lib/data/features/game/model/turn_winner.dart +++ b/lib/data/features/game/model/turn_winner.dart @@ -9,15 +9,18 @@ part 'turn_winner.g.dart'; @immutable @JsonSerializable() class TurnWinner extends Equatable { - final String playerId; - final String playerName; - final String playerAvatarUrl; - final bool isRandoCardrissian; + final String? playerId; + final String? playerName; + final String? playerAvatarUrl; + final bool? isRandoCardrissian; @JsonKey(toJson: promptCardToJson) - final PromptCard promptCard; + final PromptCard? promptCard; @JsonKey(toJson: responsesToJson) - final List response; + final List? response; + + @JsonKey(toJson: playerResponsesToJson) + final Map>? responses; TurnWinner({ this.playerId, @@ -26,16 +29,40 @@ class TurnWinner extends Equatable { this.isRandoCardrissian, this.promptCard, this.response, + this.responses, }); - factory TurnWinner.fromJson(Map json) => _$TurnWinnerFromJson(json); + factory TurnWinner.fromJson(Map json) => + _$TurnWinnerFromJson(json); Map toJson() => _$TurnWinnerToJson(this); @override - List get props => [playerId, playerName, playerAvatarUrl, isRandoCardrissian, promptCard, response]; + String toString() { + return 'TurnWinner{playerId: $playerId, playerName: $playerName, playerAvatarUrl: $playerAvatarUrl, ' + 'isRandoCardrissian: $isRandoCardrissian, promptCard: $promptCard, response: $response, responses: $responses}'; + } + + @override + List get props => [ + playerId as String, + playerName as String, + playerAvatarUrl as String, + isRandoCardrissian as bool, + promptCard as PromptCard, + response as List, + responses as Map>, + ]; } -Map turnWinnerToJson(TurnWinner turnWinner) => turnWinner?.toJson(); +Map turnWinnerToJson(TurnWinner turnWinner) => + turnWinner.toJson(); -List> responsesToJson(List cards) => cards?.map((e) => e.toJson())?.toList(); +List> responsesToJson(List cards) => + cards.map((e) => e.toJson()).toList(); + +Map playerResponsesToJson( + Map> responses) { + return responses + .map((k, e) => MapEntry(k, e.map((e) => e.toJson()).toList())); +} diff --git a/lib/data/features/users/model/user.dart b/lib/data/features/users/model/user.dart index d3534b5..229402b 100644 --- a/lib/data/features/users/model/user.dart +++ b/lib/data/features/users/model/user.dart @@ -12,10 +12,10 @@ class User { final DateTime updatedAt; User({ - @required this.id, - @required this.name, - @required this.avatarUrl, - @required this.updatedAt, + required this.id, + required this.name, + required this.avatarUrl, + required this.updatedAt, }); factory User.fromJson(Map json) => _$UserFromJson(json); diff --git a/lib/data/features/users/model/user_game.dart b/lib/data/features/users/model/user_game.dart index f850c0a..3c74f07 100644 --- a/lib/data/features/users/model/user_game.dart +++ b/lib/data/features/users/model/user_game.dart @@ -1,25 +1,33 @@ import 'package:appsagainsthumanity/data/features/game/model/game_state.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:json_annotation/json_annotation.dart'; -import 'package:meta/meta.dart'; +// import 'package:meta/meta.dart'; part 'user_game.g.dart'; @JsonSerializable() class UserGame { + @JsonKey(ignore: true) + String? id; final String gid; final GameState state; final DateTime joinedAt; UserGame({ - @required this.gid, - @required this.state, - @required this.joinedAt, + this.id, + required this.gid, + required this.state, + required this.joinedAt, }); - factory UserGame.fromDocument(DocumentSnapshot document) => UserGame.fromJson(document.data); + factory UserGame.fromDocument(DocumentSnapshot document) { + var game = UserGame.fromJson(document.data() as Map); + game.id = document.id; + return game; + } - factory UserGame.fromJson(Map json) => _$UserGameFromJson(json); + factory UserGame.fromJson(Map json) => + _$UserGameFromJson(json); Map toJson() => _$UserGameToJson(this); } diff --git a/lib/data/features/users/user_repository.dart b/lib/data/features/users/user_repository.dart index b416370..e9a0d2c 100644 --- a/lib/data/features/users/user_repository.dart +++ b/lib/data/features/users/user_repository.dart @@ -1,152 +1,231 @@ -import 'package:apple_sign_in/apple_sign_in.dart'; +// import 'dart:io'; +import 'dart:typed_data'; + import 'package:appsagainsthumanity/data/app_preferences.dart'; import 'package:appsagainsthumanity/data/features/game/model/player.dart'; import 'package:appsagainsthumanity/data/features/users/model/user.dart'; import 'package:appsagainsthumanity/data/firestore.dart'; +import 'package:appsagainsthumanity/internal/push.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; -import 'package:firebase_auth/firebase_auth.dart'; +// import 'package:cloud_functions/cloud_functions.dart'; +import 'package:firebase_auth/firebase_auth.dart' as fb; +import 'package:firebase_storage/firebase_storage.dart'; import 'package:google_sign_in/google_sign_in.dart'; import 'package:logging/logging.dart'; +import 'package:sign_in_with_apple/sign_in_with_apple.dart'; class UserRepository { - final FirebaseAuth _auth; - final Firestore _db = Firestore.instance; - - UserRepository({FirebaseAuth firebaseAuth}) - : _auth = firebaseAuth ?? FirebaseAuth.instance; - - Future isSignedIn() async { - return (await _auth.currentUser()) != null; + final fb.FirebaseAuth _auth = fb.FirebaseAuth.instance; + final FirebaseFirestore _db = FirebaseFirestore.instance; + final FirebaseStorage _storage = FirebaseStorage.instance; + + Future isSignedIn() async { + return _auth.currentUser != null; + } + + Future getUser() { + return currentUserOrThrow((firebaseUser) async { + var doc = _userDocument(firebaseUser); + var snapshot = await doc.get(); + return User( + id: doc.id, + name: snapshot['name'], + avatarUrl: snapshot['avatarUrl'], + updatedAt: (snapshot['updatedAt'] as Timestamp).toDate()); + }); + } + + Stream observeUser() { + return streamCurrentUserOrThrow((firebaseUser) { + var doc = _userDocument(firebaseUser); + return doc.snapshots().map((snapshot) { + return User( + id: snapshot.id, + name: snapshot['name'] ?? Player.DEFAULT_NAME, + avatarUrl: snapshot['avatarUrl'], + updatedAt: (snapshot['updatedAt'] as Timestamp).toDate(), + ); + }); + }); + } + + Future updateDisplayName(String name) { + return currentUserOrThrow((firebaseUser) async { + // Be sure to update our Auth Obj + await firebaseUser.updateDisplayName(name); + await firebaseUser.updatePhotoURL(firebaseUser.photoURL); + + await _userDocument(firebaseUser).update({ + 'name': name, + 'updatedAt': Timestamp.now(), + }); + }); + } + + Future deleteProfilePhoto() { + return currentUserOrThrow((firebaseUser) async { + var ref = _profilePhotoReference(firebaseUser); + await ref.delete(); + + // Be sure to update our Auth Obj + await firebaseUser.updateDisplayName(firebaseUser.displayName); + await firebaseUser.updatePhotoURL(""); + + await _userDocument(firebaseUser).update({ + 'avatarUrl': null, + 'updatedAt': Timestamp.now(), + }); + }); + } + + Future updateProfilePhoto(Uint8List imageBytes) { + return currentUserOrThrow((firebaseUser) async { + var ref = _profilePhotoReference(firebaseUser); + var refSnapshot = await ref.putData(imageBytes); + var downloadUrl = await refSnapshot.ref.getDownloadURL(); + + // Be sure to update our Auth Obj + await firebaseUser.updateDisplayName(firebaseUser.displayName); + await firebaseUser.updatePhotoURL(downloadUrl); + + await _userDocument(firebaseUser).update({ + 'avatarUrl': downloadUrl.toString(), + 'updatedAt': Timestamp.now(), + }); + }); + } + + Future signInWithGoogle() async { + GoogleSignIn _googleSignIn = GoogleSignIn( + scopes: [ + 'email', + 'https://www.googleapis.com/auth/userinfo.profile', + ], + ); + + try { + var acct = await _googleSignIn.signIn(); + var auth = await acct?.authentication; + + var result = await _auth.signInWithCredential( + fb.GoogleAuthProvider.credential( + idToken: auth?.idToken, + accessToken: auth?.accessToken, + ), + ); + + return _finishSigningInWithResult(result); + } catch (error) { + print(error); + throw error; } - - Future getUser() { - return currentUserOrThrow((firebaseUser) async { - var doc = _userDocument(firebaseUser); - var snapshot = await doc.get(); - return User( - id: doc.documentID, - name: snapshot['name'], - avatarUrl: snapshot['avatarUrl'], - updatedAt: (snapshot['updatedAt'] as Timestamp).toDate() - ); - }); + } + + Future signInWithApple() async { + final result = await SignInWithApple.getAppleIDCredential(scopes: [ + AppleIDAuthorizationScopes.email, + AppleIDAuthorizationScopes.fullName, + ]); + + // Success + fb.OAuthProvider oAuthProvider = fb.OAuthProvider('apple.com'); + final fb.AuthCredential credential = oAuthProvider.credential( + idToken: result.identityToken, + accessToken: result.authorizationCode, + ); + + var name = Player.DEFAULT_NAME; + if (result.givenName != "") { + print( + "Apple Name(given=${result.givenName}, family=${result.familyName})"); + name = result.givenName!; } - Future signInWithGoogle() async { - GoogleSignIn _googleSignIn = GoogleSignIn( - scopes: [ - 'email', - 'https://www.googleapis.com/auth/userinfo.profile', - ], - ); - - try { - var acct = await _googleSignIn.signIn(); - var auth = await acct.authentication; - - var result = await _auth.signInWithCredential(GoogleAuthProvider.getCredential( - idToken: auth.idToken, - accessToken: auth.accessToken - )); - - return _finishSigningInWithResult(result); - } catch (error) { - print(error); - throw error; + final fb.UserCredential _result = + await _auth.signInWithCredential(credential); + return _finishSigningInWithResult(_result, name: name); + } + + Future signInWithEmail(String email, String password) async { + final fb.UserCredential _result = await _auth.signInWithEmailAndPassword( + email: email, password: password); + return _finishSigningInWithResult(_result); + } + + Future signUpWithEmail(String email, String password, + [String? username]) async { + final fb.UserCredential _result = await _auth + .createUserWithEmailAndPassword(email: email, password: password); + return _finishSigningInWithResult(_result, name: username!); + } + + Future signInAnonymously() async { + final fb.UserCredential _result = await _auth.signInAnonymously(); + return _finishSigningInWithResult(_result); + } + + Future _finishSigningInWithResult(fb.UserCredential result, + {String? name}) async { + try { + if (result.user != null) { + if (name != null && result.user?.displayName != name) { + await result.user?.updateDisplayName(name); + await result.user?.updatePhotoURL(result.user?.photoURL); + await result.user?.reload(); + print("Updated user's name to ${result.user?.displayName}"); } - } - Future signInWithApple() async { - final AuthorizationResult result = await AppleSignIn.performRequests([ - AppleIdRequest(requestedScopes: [Scope.email, Scope.fullName]) - ]); - - switch (result.status) { - case AuthorizationStatus.authorized: - // Success - final AppleIdCredential appleIdCredential = result.credential; - OAuthProvider oAuthProvider = OAuthProvider(providerId: "apple.com"); - final AuthCredential credential = oAuthProvider.getCredential( - idToken: String.fromCharCodes(appleIdCredential.identityToken), - accessToken: String.fromCharCodes(appleIdCredential.authorizationCode), - ); - - var name = Player.DEFAULT_NAME; - if (appleIdCredential.fullName != null) { - var nameComponents = appleIdCredential.fullName; - name = nameComponents.nickname ?? - "${nameComponents.givenName ?? nameComponents.middleName ?? ""} ${nameComponents.familyName ?? ""}"; - } - - final AuthResult _result = await _auth.signInWithCredential(credential); - - return _finishSigningInWithResult(_result, name: name); - break; - case AuthorizationStatus.error: - throw result.error.localizedFailureReason; - break; - case AuthorizationStatus.cancelled: - print("User Cancelled operation"); - throw 'Cancelled'; - break; - default: - throw 'Unable to sign in'; - } - } + // Create the user obj acct in FB + var userDoc = _userDocument(result.user!); - Future _finishSigningInWithResult(AuthResult result, {String name}) async { - try { - if (result.user != null) { - if (name != null && result.user.displayName != name) { - var updateInfo = UserUpdateInfo(); - updateInfo.displayName = name; - await result.user.updateProfile(updateInfo); - await result.user.reload(); - print("Updated user's name to $name"); - } - - // Create the user obj acct in FB - var userDoc = _userDocument(result.user); - - await userDoc.setData({ - "name": result.user.displayName, - "avatarUrl": result.user.photoUrl, - "updatedAt": Timestamp.now() - }, merge: true); - - Logger("UserRepository").fine( - "Signed-in! User(id=${result.user.uid}, name=${result.user.displayName}, photoUrl=${result.user - .photoUrl})"); - - return User( - id: result.user.uid, - name: result.user.displayName, - avatarUrl: result.user.photoUrl, - updatedAt: DateTime.now() - ); - } else { - throw result; - } - } catch (error) { - print(error); - throw error; - } - } + await userDoc.set({ + "name": result.user?.displayName, + "avatarUrl": result.user?.photoURL, + "updatedAt": Timestamp.now(), + }, SetOptions(merge: true)); - /// Sign-out of the user's account - Future signOut() async { - await AppPreferences().clear(); - await _auth.signOut(); - } + Logger("UserRepository").fine( + "Signed-in! User(id=${result.user?.uid}, name=${result.user?.displayName}, photoUrl=${result.user?.photoURL})"); - Future deleteAccount() async { - await AppPreferences().clear(); - var user = await _auth.currentUser(); - await user.delete(); - } + // Excellent! Let's create our device + await PushNotifications().checkAndUpdateToken(force: true); - DocumentReference _userDocument(FirebaseUser user) { - return _db.collection(FirebaseConstants.COLLECTION_USERS) - .document(user.uid); + return User( + id: result.user!.uid, + name: result.user!.displayName!, + avatarUrl: result.user!.photoURL!, + updatedAt: DateTime.now(), + ); + } else { + throw result; + } + } catch (error) { + print(error); + throw error; } + } + + /// Sign-out of the user's account + Future signOut() async { + await AppPreferences().clear(); + await _auth.signOut(); + } + + Future deleteAccount() async { + await AppPreferences().clear(); + var user = _auth.currentUser; + await user?.delete(); + await _profilePhotoReference(user!).delete(); + } + + DocumentReference _userDocument(fb.User user) { + return _db.collection(FirebaseConstants.COLLECTION_USERS).doc(user.uid); + } + + Reference _profilePhotoReference(fb.User user) { + return _storage + .ref() + .child(FirebaseConstants.COLLECTION_USERS) + .child(user.uid); + } } diff --git a/lib/data/firestore.dart b/lib/data/firestore.dart index 3a686ab..0e4fa0d 100644 --- a/lib/data/firestore.dart +++ b/lib/data/firestore.dart @@ -1,33 +1,37 @@ import 'package:firebase_auth/firebase_auth.dart'; class FirebaseConstants { - FirebaseConstants._(); + FirebaseConstants._(); - static const COLLECTION_USERS = "users"; - static const COLLECTION_DEVICES = "devices"; - static const COLLECTION_CARD_SETS = "cardSets"; - static const COLLECTION_GAMES = "games"; - static const COLLECTION_PLAYERS = "players"; + static const COLLECTION_USERS = "users"; + static const COLLECTION_DEVICES = "devices"; + static const COLLECTION_CARD_SETS = "cardSets"; + static const COLLECTION_GAMES = "games"; + static const COLLECTION_PLAYERS = "players"; + static const COLLECTION_DOWNVOTES = "downvotes"; - static const DOCUMENT_RANDO_CARDRISSIAN = "rando-cardrissian"; + static const DOCUMENT_RANDO_CARDRISSIAN = "rando-cardrissian"; + static const DOCUMENT_TALLY = "tally"; + + static const MAX_GID_SIZE = 5; } -class UserNotFoundException { } +class UserNotFoundException {} -Future currentUserOrThrow(Future Function(FirebaseUser user) action) async { - final FirebaseUser currentUser = await FirebaseAuth.instance.currentUser(); - if (currentUser != null) { - return action(currentUser); - } - throw UserNotFoundException(); +Future currentUserOrThrow(Future Function(User user) action) async { + final User currentUser = FirebaseAuth.instance.currentUser!; + if (currentUser != {}) { + return action(currentUser); + } + throw UserNotFoundException(); } -Stream streamCurrentUserOrThrow(Stream Function(FirebaseUser user) action) async* { - final FirebaseUser currentUser = await FirebaseAuth.instance.currentUser(); - if (currentUser != null) { - yield* action(currentUser); - } else { - throw UserNotFoundException(); - } +Stream streamCurrentUserOrThrow( + Stream Function(User user) action) async* { + final User currentUser = FirebaseAuth.instance.currentUser!; + if (currentUser != {}) { + yield* action(currentUser); + } else { + throw UserNotFoundException(); + } } - diff --git a/lib/internal.dart b/lib/internal.dart index ed198c3..6fad3c3 100644 --- a/lib/internal.dart +++ b/lib/internal.dart @@ -1,4 +1,6 @@ -export 'internal/localization.dart'; -export 'internal/theme.dart'; -export 'internal/analytics.dart'; -export 'config.dart'; +export 'package:appsagainsthumanity/internal/localization.dart'; +export 'package:appsagainsthumanity/internal/theme.dart'; +export 'package:appsagainsthumanity/internal/analytics.dart'; +export 'package:appsagainsthumanity/config.dart'; +export 'package:appsagainsthumanity/util/media_query_extensions.dart'; +export 'package:appsagainsthumanity/util/context_extensions.dart'; diff --git a/lib/internal/analytics.dart b/lib/internal/analytics.dart index ab8cfc1..9e98a78 100644 --- a/lib/internal/analytics.dart +++ b/lib/internal/analytics.dart @@ -1,7 +1,54 @@ -//import 'package:firebase_analytics/firebase_analytics.dart'; +import 'package:firebase_analytics/firebase_analytics.dart'; -class Analytics { - Analytics._(); +class Analytics extends FirebaseAnalytics { + static Analytics _instance = Analytics._(); -// static FirebaseAnalytics firebaseAnalytics = FirebaseAnalytics(); + FirebaseAnalytics _firebaseAnalytics; + + Analytics._() { + _firebaseAnalytics = FirebaseAnalytics(); + } + + factory Analytics() { + return _instance; + } + + @override + Future setCurrentScreen( + {String? screenName, String screenClassOverride = 'Flutter'}) { + return _firebaseAnalytics?.setCurrentScreen( + screenName: screenName, screenClassOverride: 'Flutter') ?? + Future.value(); + } + + @override + Future logEvent({String? name, Map? parameters}) { + return _firebaseAnalytics?.logEvent(name: name!, parameters: parameters) ?? + Future.value(); + } + + @override + Future logSelectContent({String? contentType, String? itemId}) { + return _firebaseAnalytics?.logSelectContent( + contentType: contentType!, itemId: itemId!) ?? + Future.value(); + } + + @override + Future logViewItemList({String? itemCategory}) { + return _firebaseAnalytics?.logViewItemList(itemCategory: itemCategory) ?? + Future.value(); + } + + @override + Future logLogin({String? loginMethod}) { + return _firebaseAnalytics?.logLogin(loginMethod: loginMethod) ?? + Future.value(); + } + + @override + Future logSignUp({String? signUpMethod}) { + return _firebaseAnalytics?.logSignUp(signUpMethod: signUpMethod!) ?? + Future.value(); + } } diff --git a/lib/internal/dynamic_links.dart b/lib/internal/dynamic_links.dart new file mode 100644 index 0000000..7b1be76 --- /dev/null +++ b/lib/internal/dynamic_links.dart @@ -0,0 +1,66 @@ +import 'package:flutter/foundation.dart' show kIsWeb; +import 'package:firebase_dynamic_links/firebase_dynamic_links.dart'; +import 'package:flutter/material.dart'; + +class DynamicLinks { + DynamicLinks._(); + + static Future createLink(String gameDocumentId) async { + if (!kIsWeb) { + final DynamicLinkParameters params = DynamicLinkParameters( + uriPrefix: "https://appsagainsthumanity.com/games", + link: + Uri.parse("https://appsagainsthumanity.com/games/$gameDocumentId"), + androidParameters: AndroidParameters( + packageName: 'com.ftinc.appsagainsthumanity', + ), + iosParameters: IOSParameters( + appStoreId: "1509268296", + bundleId: "com.ftinc.appsagainsthumanity", + ), + socialMetaTagParameters: SocialMetaTagParameters( + title: "Come join my game!", + description: "Play a game of Apps Against Humanity", + ), + ); + + var shortLink = + await FirebaseDynamicLinks.instance.buildShortLink(params); + return Uri.parse(shortLink.toString()); + } else { + return Uri.parse("https://appsagainsthumanity.com/games/$gameDocumentId"); + } + } + + static void initDynamicLinks( + BuildContext context, void Function(String gameId) callback) async { + if (!kIsWeb) { + final PendingDynamicLinkData? data = + await FirebaseDynamicLinks.instance.getInitialLink(); + final Uri deepLink = data!.link; + + if (deepLink != "") { + var lastSegment = deepLink.pathSegments.last; + if (lastSegment != "") { + print("Joining $lastSegment from dynamic link"); + callback(lastSegment); + } + } + + FirebaseDynamicLinks.instance.onLink.listen((dynamicLink) async { + final Uri deepLink = dynamicLink.link; + + if (deepLink != "") { + var lastSegment = deepLink.pathSegments.last; + if (lastSegment != "") { + print("Joining $lastSegment from dynamic link"); + callback(lastSegment); + } + } + }).onError((error) async { + print('onLinkError'); + print(error.message); + }); + } + } +} diff --git a/lib/internal/localization.dart b/lib/internal/localization.dart index 38e21b5..6e6cdf9 100644 --- a/lib/internal/localization.dart +++ b/lib/internal/localization.dart @@ -7,7 +7,8 @@ class AppLocalization { static Future load(Locale locale) { currentLocale = locale; - final String name = locale.countryCode == null ? locale.languageCode : locale.toString(); + final String name = + locale.countryCode == null ? locale.languageCode : locale.toString(); final String localeName = Intl.canonicalizedLocale(name); return initializeMessages(localeName).then((bool _) { @@ -16,16 +17,48 @@ class AppLocalization { }); } - static AppLocalization of(BuildContext context) { + static AppLocalization? of(BuildContext context) { return Localizations.of(context, AppLocalization); } String get appName => Intl.message('AppsAgainstHumanity', name: 'appName'); - String get appNameDisplay => Intl.message('Apps\nAgainst\nHumanity', name: 'appNameDisplay'); - String get actionSignIn => Intl.message('Sign in with Google', name: 'actionSignIn'); - + String get appNameDisplay => + Intl.message('Apps Against\nHumanity', name: 'appNameDisplay'); + String get actionSignIn => + Intl.message('Sign in with Google', name: 'actionSignIn'); + String get actionSignInAnonymously => + Intl.message('Sign in anonymously', name: 'actionSignInAnonymously'); + String get actionSignInEmail => + Intl.message('Sign in with Email', name: 'actionSignInEmail'); + String get actionEmailSignUp => + Intl.message('Sign up', name: 'actionEmailSignUp'); + String get actionEmailSignIn => + Intl.message('Sign in', name: 'actionEmailSignIn'); + String get actionEmailAltSignUp => + Intl.message('or sign up', name: 'actionEmailAltSignUp'); + String get actionEmailAltSignIn => + Intl.message('or sign in', name: 'actionEmailAltSignIn'); + String get hintEmail => Intl.message('Email', name: 'hintEmail'); + String get hintPassword => Intl.message('Password', name: 'hintPassword'); + String get hintConfirmPassword => + Intl.message('Confirm password', name: 'hintConfirmPassword'); + String get hintUserName => Intl.message('Username', name: 'hintUserName'); + String get errorInvalidUserName => + Intl.message('Please enter a valid username', + name: 'errorInvalidUserName'); + String get errorInvalidEmailAddress => + Intl.message('Please enter a valid email address', + name: 'errorInvalidEmailAddress'); + String get errorInvalidPasswordLength => + Intl.message('You must enter a password of at lease 8 characters', + name: 'errorInvalidPasswordLength'); + String get errorMismatchingPasswords => + Intl.message('This must match the above password', + name: 'errorMismatchingPasswords'); + String get titleNewGame => Intl.message('New game', name: 'titleNewGame'); - String get actionStartGame => Intl.message('Start game', name: 'actionStartGame'); + String get actionStartGame => + Intl.message('Start game', name: 'actionStartGame'); } class AppLocalizationsDelegate extends LocalizationsDelegate { @@ -48,6 +81,5 @@ class AppLocalizationsDelegate extends LocalizationsDelegate { } extension AppLocalizationExt on BuildContext { - - AppLocalization get strings => AppLocalization.of(this); + AppLocalization get strings => AppLocalization.of(this)!; } diff --git a/lib/internal/logging_bloc_delegate.dart b/lib/internal/logging_bloc_delegate.dart index f7e7d3e..e017529 100644 --- a/lib/internal/logging_bloc_delegate.dart +++ b/lib/internal/logging_bloc_delegate.dart @@ -1,21 +1,21 @@ import 'package:bloc/bloc.dart'; -class LoggingBlocDelegate extends BlocDelegate { - @override - void onEvent(Bloc bloc, Object event) { - super.onEvent(bloc, event); - print(event); - } +class LoggingBlocDelegate extends BlocObserver { + @override + void onEvent(Bloc bloc, Object? event) { + super.onEvent(bloc, event); + print(event); + } - @override - void onError(Bloc bloc, Object error, StackTrace stacktrace) { - super.onError(bloc, error, stacktrace); - print(error); - } + @override + void onError(BlocBase bloc, Object error, StackTrace stacktrace) { + super.onError(bloc, error, stacktrace); + print(error); + } - @override - void onTransition(Bloc bloc, Transition transition) { - super.onTransition(bloc, transition); - print(transition); - } + @override + void onTransition(Bloc bloc, Transition transition) { + super.onTransition(bloc, transition); + print(transition); + } } diff --git a/lib/internal/push.dart b/lib/internal/push.dart new file mode 100644 index 0000000..cd8565e --- /dev/null +++ b/lib/internal/push.dart @@ -0,0 +1,141 @@ +import 'dart:convert'; + +import 'package:appsagainsthumanity/data/app_preferences.dart'; +import 'package:appsagainsthumanity/data/features/devices/device_repository.dart'; +import 'package:appsagainsthumanity/data/features/game/game_repository.dart'; +import 'package:appsagainsthumanity/data/features/game/model/turn.dart'; +import 'package:appsagainsthumanity/data/features/game/model/turn_winner.dart'; +import 'package:appsagainsthumanity/internal/dynamic_links.dart'; +import 'package:appsagainsthumanity/ui/routes.dart'; +// import 'package:appsagainsthumanity/internal.dart'; +import 'package:firebase_messaging/firebase_messaging.dart'; +// import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +// import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:logging/logging.dart'; + +class PushNotifications { + PushNotifications._(); + + static PushNotifications _instance = PushNotifications._(); + final FirebaseMessaging _firebaseMessaging = FirebaseMessaging.instance; + + factory PushNotifications() { + return _instance; + } + + void checkPermissions() { + _firebaseMessaging.requestPermission( + alert: true, + sound: true, + badge: true, + announcement: true, + ); + } + + void setup() { + // Setup firebase messaging + _firebaseMessaging.onTokenRefresh.listen((token) { + DeviceRepository().updatePushToken(token); + }); + + checkAndUpdateToken(); + } + + Future checkAndUpdateToken({bool force = false}) async { + String? token = await _firebaseMessaging.getToken(); + if (token != AppPreferences().pushToken || force) { + Logger.root.fine( + "FCM Token is different from what is stored in preferences, updating device..."); + DeviceRepository().updatePushToken(token!); + } + } +} + +class PushNavigator extends StatefulWidget { + final Widget child; + + PushNavigator({required this.child}); + + @override + _PushNavigatorState createState() => _PushNavigatorState(); +} + +class _PushNavigatorState extends State { + late GameRepository _gameRepository; + + @override + void initState() { + super.initState(); + _gameRepository = context.read(); + FirebaseMessaging.onMessageOpenedApp.listen((message) { + print("onLaunch()"); + print(JsonEncoder().convert(message)); + handleNotificationClick(message.data); + }); + + /// let's go ahead and sign up here to handle dynamic links + DynamicLinks.initDynamicLinks( + context, (gameId) => navigateToGame(gameId, andJoin: true)); + } + + @override + Widget build(BuildContext context) { + return widget.child; + } + + void handleNotificationClick(Map message) async { + var gameId = message['gameId'] ?? message['data']['gameId']; + if (gameId != null && gameId is String && gameId.isNotEmpty) { + navigateToGame(gameId); + } + } + + void navigateToGame(String gameId, {bool andJoin = false}) async { + if (gameId != "" && gameId.isNotEmpty) { + var currentRoute = Routes.routeTracer.currentRoute; + print(currentRoute); + if (currentRoute?.settings.arguments == gameId) { + // It looks like the current game is up fromt + print("Game is already in the foreground"); + return; + } + try { + var game = await _gameRepository.getGame(gameId, andJoin: andJoin); + if (game != {}) { + /* + * Neuter the turn winner out of the otherwise the winner bottom sheet will never show + */ + if (game.turn != {}) { + game = game.copyWith( + turn: Turn( + judgeId: game.turn!.judgeId, + responses: game.turn!.responses, + promptCard: game.turn!.promptCard, + winner: TurnWinner(), + ), + ); + } + + if (currentRoute?.settings.name == Routes.game) { + print("Replacing current shown game"); + currentRoute?.navigator?.pushReplacement(GamePageRoute(game)); + } else if (currentRoute?.settings.name == "/") { + print("User should be in the homescreen, push game"); + currentRoute?.navigator?.push(GamePageRoute(game)); + } else if (currentRoute is MaterialPageRoute) { + print("User is not in a game or homescreen"); + currentRoute.navigator! + ..popUntil((route) => route.settings.name == "/") + ..push(GamePageRoute(game)); + } + } else { + print("Unable to join the Game($gameId)"); + } + } catch (e) { + print(e); + } + } + } +} diff --git a/lib/internal/theme.dart b/lib/internal/theme.dart index dc021b4..9188eb1 100644 --- a/lib/internal/theme.dart +++ b/lib/internal/theme.dart @@ -1,44 +1,176 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; class AppColors { - AppColors._(); + AppColors._(); - static const primary = Color(0xFFF44336); - static const primaryDark = Color(0xFFB71C1C); - static const primaryVariant = Color(0xFFEF5350); + static const primary = Color(0xFFAB47BC); + static const primaryDark = Color(0xFF790e8b); + static const primaryVariant = Color(0xFFdf78ef); + static const colorOnPrimary = Color(0xFFFFFFFF); + static const colorOnPrimaryVariant = Color(0xDD000000); - static const secondary = Color(0xFF00BCD4); - static const secondaryLight = Color(0xFF80DEEA); - static const secondaryDark = Color(0xFF006064); + static const secondary = Color(0xFF00BCD4); + static const secondaryLight = Color(0xFF80DEEA); + static const secondaryDark = Color(0xFF006064); - static const surface = Color(0xFF3C3C3C); - static const responseCardBackground = Color(0xFFF2F2F2); + static const surface = Color(0xFF3C3C3C); + static const surfaceLight = Color(0xFF4F4F4F); + static const surfaceDark = Color(0xFF303030); + static const responseCardBackground = Color(0xFFF2F2F2); + + static const error = Color(0xFFFF5252); + + static const addPhotoBackground = Color(0xFF666666); + static const addPhotoForeground = Colors.white70; } class AppThemes { - AppThemes._(); - - static ThemeData get app => - ThemeData( - brightness: Brightness.dark, - primaryColor: AppColors.primary, - primaryColorDark: AppColors.primaryDark, - primaryColorLight: AppColors.primaryVariant, - accentColor: AppColors.secondary, - canvasColor: AppColors.surface - ); + AppThemes._(); + + static ThemeData get light { + final textTheme = + Typography.material2018(platform: defaultTargetPlatform).white; + final ThemeData theme = ThemeData( + brightness: Brightness.light, + primaryColor: AppColors.primary, + primaryColorDark: AppColors.primaryDark, + primaryColorLight: AppColors.primaryVariant, + // accentColor: AppColors.primary, + canvasColor: AppColors.surface, + cardColor: Colors.white, + textTheme: textTheme, + iconTheme: const IconThemeData(color: Colors.white), + disabledColor: Colors.white38, + unselectedWidgetColor: Colors.white70, + dialogBackgroundColor: Colors.grey[700], + appBarTheme: AppBarTheme(color: AppColors.surfaceDark), + snackBarTheme: SnackBarThemeData( + backgroundColor: AppColors.surfaceLight, + contentTextStyle: textTheme.subtitle1, + actionTextColor: AppColors.primary, + disabledActionTextColor: Colors.white30, + ), + bottomAppBarColor: AppColors.surfaceDark, + ); + return theme.copyWith( + colorScheme: ColorScheme.fromSwatch().copyWith( + secondary: AppColors.primary, + ), + ); + } + + static ThemeData get dark { + final ThemeData theme = ThemeData( + brightness: Brightness.dark, + primaryColor: AppColors.primaryVariant, + primaryColorDark: AppColors.primaryDark, + primaryColorLight: AppColors.primaryVariant, + floatingActionButtonTheme: + FloatingActionButtonThemeData(foregroundColor: Colors.white), + // accentColor: AppColors.primaryVariant, + cardColor: Colors.grey[700], + appBarTheme: AppBarTheme(color: AppColors.surface), + snackBarTheme: SnackBarThemeData( + backgroundColor: AppColors.surfaceDark, + contentTextStyle: + Typography.material2018(platform: defaultTargetPlatform) + .white + .subtitle1, + actionTextColor: AppColors.primary, + disabledActionTextColor: Colors.white30, + ), + bottomAppBarColor: AppColors.surface, + ); + return theme.copyWith( + colorScheme: ColorScheme.fromSwatch().copyWith( + secondary: AppColors.primaryVariant, + ), + ); + } +} + +extension TextAppearanceExt on BuildContext { + TextStyle cardTextStyle(Color textColor) { + final base = theme.textTheme.headline5; + final screenWidth = MediaQuery.of(this).size.width; + + double? fontSize = base?.fontSize; + if (screenWidth > 360 && screenWidth <= 400) { + fontSize = 20.0; // Headline 6 Size + } else if (screenWidth > 300 && screenWidth <= 360) { + fontSize = 18.0; // Subtitle 1 + } else if (screenWidth <= 300) { + fontSize = 16.0; + } + + return base!.copyWith(color: textColor, fontSize: fontSize); + } } extension ThemeExt on BuildContext { + ThemeData get theme => Theme.of(this); + + Color get primaryColor { + var brightness = theme.brightness; + if (brightness == Brightness.dark) { + return AppColors.primaryVariant; + } else { + return AppColors.primary; + } + } + + Color get colorOnCard { + var brightness = theme.brightness; + if (brightness == Brightness.dark) { + return Colors.white; + } else { + return Colors.black87; + } + } + + Color get secondaryColorOnCard { + var brightness = theme.brightness; + if (brightness == Brightness.dark) { + return Colors.white54; + } else { + return Colors.black38; + } + } - ThemeData get theme => Theme.of(this); + Color get tertiaryColorOnCard { + var brightness = theme.brightness; + if (brightness == Brightness.dark) { + return Colors.white38; + } else { + return Colors.black26; + } + } + + Color get responseCardColor { + var brightness = theme.brightness; + if (brightness == Brightness.dark) { + return Colors.grey[600]!; + } else { + return AppColors.responseCardBackground; + } + } + + Color get responseCardHandColor { + var brightness = theme.brightness; + if (brightness == Brightness.dark) { + return Colors.grey[500]!; + } else { + return AppColors.responseCardBackground; + } + } - Color get primaryColor { - var brightness = theme.brightness; - if (brightness == Brightness.dark) { - return AppColors.primaryVariant; - } else { - return AppColors.primary; - } + Color get responseBorderColor { + var brightness = theme.brightness; + if (brightness == Brightness.dark) { + return Colors.white12; + } else { + return Colors.black12; } + } } diff --git a/lib/l10n/messages_all.dart b/lib/l10n/messages_all.dart index b285749..d8d43bb 100644 --- a/lib/l10n/messages_all.dart +++ b/lib/l10n/messages_all.dart @@ -27,16 +27,15 @@ MessageLookupByLibrary _findExact(String localeName) { case 'en': return messages_en.messages; default: - return null; + return messages_en.MessageLookup(); } } /// User programs should call this before using [localeName] for messages. Future initializeMessages(String localeName) async { var availableLocale = Intl.verifiedLocale( - localeName, - (locale) => _deferredLibraries[locale] != null, - onFailure: (_) => null); + localeName, (locale) => _deferredLibraries[locale] != null, + onFailure: (_) => null); if (availableLocale == null) { return new Future.value(false); } @@ -56,8 +55,8 @@ bool _messagesExistFor(String locale) { } MessageLookupByLibrary _findGeneratedMessagesFor(String locale) { - var actualLocale = Intl.verifiedLocale(locale, _messagesExistFor, - onFailure: (_) => null); - if (actualLocale == null) return null; + var actualLocale = + Intl.verifiedLocale(locale, _messagesExistFor, onFailure: (_) => ""); + if (actualLocale == "") return messages_en.MessageLookup(); return _findExact(actualLocale); } diff --git a/lib/main.dart b/lib/main.dart index 7b420d5..6aa72df 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,6 +6,8 @@ import 'package:appsagainsthumanity/data/features/game/firestore_game_repository import 'package:appsagainsthumanity/data/features/game/game_repository.dart'; import 'package:appsagainsthumanity/data/features/users/user_repository.dart'; import 'package:appsagainsthumanity/internal/logging_bloc_delegate.dart'; +import 'package:appsagainsthumanity/internal/push.dart'; +import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:logging/logging.dart'; @@ -15,6 +17,7 @@ void main() async { WidgetsFlutterBinding.ensureInitialized(); BlocSupervisor.delegate = LoggingBlocDelegate(); await AppPreferences.loadInstance(); + await Firebase.initializeApp(); // Setup logger Logger.root.level = Level.ALL; @@ -22,11 +25,14 @@ void main() async { print('${rec.level.name}: ${rec.time}: ${rec.message}'); }); + // Setup Push Notifications + PushNotifications().setup(); + runApp(buildRepositoryProvider(BlocProvider( create: (context) { return AuthenticationBloc( - userRepository: context.repository(), - preferences: AppPreferences() + userRepository: context.read(), + preferences: AppPreferences(), )..add(AppStarted()); }, child: App(), @@ -43,7 +49,8 @@ Widget buildRepositoryProvider(Widget child) { create: (context) => FirestoreCardsRepository(), ), RepositoryProvider( - create: (context) => FirestoreGameRepository(userRepository: context.repository()), + create: (context) => + FirestoreGameRepository(userRepository: context.read()), ) ], child: child, diff --git a/lib/ui/creategame/bloc/create_game_bloc.dart b/lib/ui/creategame/bloc/create_game_bloc.dart index 6ec992c..4c94021 100644 --- a/lib/ui/creategame/bloc/create_game_bloc.dart +++ b/lib/ui/creategame/bloc/create_game_bloc.dart @@ -1,3 +1,4 @@ +import 'package:appsagainsthumanity/data/app_preferences.dart'; import 'package:appsagainsthumanity/data/features/cards/cards_repository.dart'; import 'package:appsagainsthumanity/data/features/game/game_repository.dart'; import 'package:appsagainsthumanity/ui/creategame/bloc/create_game_event.dart'; @@ -6,14 +7,16 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:kt_dart/kt.dart'; class CreateGameBloc extends Bloc { - final Set cardSets; - final CardsRepository cardsRepository; - final GameRepository gameRepository; + final CardsRepository? cardsRepository; + final GameRepository? gameRepository; - CreateGameBloc(this.cardSets, this.cardsRepository, this.gameRepository,); + CreateGameBloc({ + this.cardsRepository, + this.gameRepository, + }) : super(CreateGameState()); @override - CreateGameState get initialState => CreateGameState.loading(sets: cardSets); + CreateGameState get initialState => CreateGameState.empty(); @override Stream mapEventToState(CreateGameEvent event) async* { @@ -21,23 +24,100 @@ class CreateGameBloc extends Bloc { yield* _mapScreenLoadedToState(); } else if (event is CardSetSelected) { yield* _mapCardSetSelectedToState(event); + } else if (event is CardSourceSelected) { + yield* _mapCardSourceSelectedToState(event); + } else if (event is ChangePrizesToWin) { + yield* _mapChangePrizesToWinToState(event); + } else if (event is ChangePlayerLimit) { + yield* _mapChangePlayerLimitToState(event); + } else if (event is ChangePick2Enabled) { + yield* _mapChangePick2EnabledToState(event); + } else if (event is ChangeDraw2Pick3Enabled) { + yield* _mapChangeDraw2Pick3EnabledToState(event); + } else if (event is CreateGame) { + yield* _mapCreateGameToState(); } } Stream _mapScreenLoadedToState() async* { try { - var cardSets = await cardsRepository.getAvailableCardSets(); - yield state.copyWith(cardSets: cardSets.toImmutableList(), isLoading: false); + var cardSets = await cardsRepository?.getAvailableCardSets(); + var filteredCardSets = cardSets?.where((cs) { + return (cs.source == "Developer" && + AppPreferences().developerPackEnabled) || + cs.source != "Developer"; + }).toList(); + yield state.copyWith( + cardSets: filteredCardSets?.toImmutableList(), isLoading: false); } catch (e) { yield state.copyWith(isLoading: false, error: e.toString()); } } - Stream _mapCardSetSelectedToState(CardSetSelected event) async* { + Stream _mapCardSetSelectedToState( + CardSetSelected event) async* { if (state.selectedSets.contains(event.cardSet)) { - yield state.copyWith(selectedSets: state.selectedSets.minusElement(event.cardSet).toSet()); + yield state.copyWith( + selectedSets: state.selectedSets.minusElement(event.cardSet).toSet()); } else { - yield state.copyWith(selectedSets: state.selectedSets.plusElement(event.cardSet).toSet()); + yield state.copyWith( + selectedSets: state.selectedSets.plusElement(event.cardSet).toSet()); + } + } + + Stream _mapCardSourceSelectedToState( + CardSourceSelected event) async* { + if (event.isAllChecked == false || !event.isAllChecked) { + // Partial sets are selected, select all + yield state.copyWith( + selectedSets: state.selectedSets + .plus(state.cardSets.filter((cs) => cs.source == event.source)) + .toSet()); + } else if (event.isAllChecked) { + // All sets selected, select none + yield state.copyWith( + selectedSets: state.selectedSets + .filter((s) => s.source != event.source) + .toSet()); + } + } + + Stream _mapChangePrizesToWinToState( + ChangePrizesToWin event) async* { + AppPreferences().prizesToWin = event.prizesToWin; + yield state.copyWith(prizesToWin: event.prizesToWin); + } + + Stream _mapChangePlayerLimitToState( + ChangePlayerLimit event) async* { + AppPreferences().playerLimit = event.playerLimit; + yield state.copyWith(playerLimit: event.playerLimit); + } + + Stream _mapChangePick2EnabledToState( + ChangePick2Enabled event) async* { + yield state.copyWith(pick2Enabled: event.enabled); + } + + Stream _mapChangeDraw2Pick3EnabledToState( + ChangeDraw2Pick3Enabled event) async* { + yield state.copyWith(draw2pick3Enabled: event.enabled); + } + + Stream _mapCreateGameToState() async* { + yield state.copyWith(isLoading: true, error: null); + try { + var game = await gameRepository?.createGame( + state.selectedSets, + prizesToWin: state.prizesToWin, + playerLimit: state.playerLimit, + pick2Enabled: state.pick2Enabled, + draw2Pick3Enabled: state.draw2pick3Enabled, + ); + yield state.copyWith(createdGame: game, isLoading: false); + } catch (e, st) { + print("Create Game Error: $e\n$st"); + yield state.copyWith(isLoading: false, error: "$e"); } } } diff --git a/lib/ui/creategame/bloc/create_game_event.dart b/lib/ui/creategame/bloc/create_game_event.dart index eeb902c..5fdaf92 100644 --- a/lib/ui/creategame/bloc/create_game_event.dart +++ b/lib/ui/creategame/bloc/create_game_event.dart @@ -1,3 +1,4 @@ +import 'package:appsagainsthumanity/data/features/cards/model/card_set.dart'; import 'package:equatable/equatable.dart'; abstract class CreateGameEvent extends Equatable { @@ -10,10 +11,58 @@ abstract class CreateGameEvent extends Equatable { class ScreenLoaded extends CreateGameEvent {} class CardSetSelected extends CreateGameEvent { - final String cardSet; + final CardSet cardSet; CardSetSelected(this.cardSet); @override List get props => [cardSet]; } + +class CardSourceSelected extends CreateGameEvent { + final String source; + final bool isAllChecked; + + CardSourceSelected(this.source, this.isAllChecked); + + @override + List get props => [source, isAllChecked]; +} + +class ChangePrizesToWin extends CreateGameEvent { + final int prizesToWin; + + ChangePrizesToWin(this.prizesToWin); + + @override + List get props => [prizesToWin]; +} + +class ChangePlayerLimit extends CreateGameEvent { + final int playerLimit; + + ChangePlayerLimit(this.playerLimit); + + @override + List get props => [playerLimit]; +} + +class ChangePick2Enabled extends CreateGameEvent { + final bool enabled; + + ChangePick2Enabled(this.enabled); + + @override + List get props => [enabled]; +} + +class ChangeDraw2Pick3Enabled extends CreateGameEvent { + final bool enabled; + + ChangeDraw2Pick3Enabled(this.enabled); + + @override + List get props => [enabled]; +} + +class CreateGame extends CreateGameEvent {} diff --git a/lib/ui/creategame/bloc/create_game_state.dart b/lib/ui/creategame/bloc/create_game_state.dart index f3c9627..86e4f0d 100644 --- a/lib/ui/creategame/bloc/create_game_state.dart +++ b/lib/ui/creategame/bloc/create_game_state.dart @@ -1,49 +1,83 @@ +import 'package:appsagainsthumanity/data/app_preferences.dart'; +import 'package:appsagainsthumanity/data/features/cards/model/card_set.dart'; +import 'package:appsagainsthumanity/data/features/game/model/game.dart'; import 'package:kt_dart/kt.dart'; import 'package:meta/meta.dart'; @immutable class CreateGameState { - final KtList cardSets; - final KtSet selectedSets; + final KtList cardSets; + final KtSet selectedSets; + final int prizesToWin; + final int playerLimit; + final bool pick2Enabled; + final bool draw2pick3Enabled; + + final Game? createdGame; final bool isLoading; - final String error; + final String? error; + + int get totalPrompts => selectedSets.sumBy((cs) => cs.prompts ?? 0); + int get totalResponses => selectedSets.sumBy((cs) => cs.responses ?? 0); CreateGameState({ - @required this.cardSets, - @required this.selectedSets, - @required this.isLoading, + required this.cardSets, + required this.selectedSets, + required this.isLoading, this.error, + this.prizesToWin = 7, + this.playerLimit = 15, + this.pick2Enabled = true, + this.draw2pick3Enabled = true, + this.createdGame, }); - factory CreateGameState.loading({Set sets}) { + factory CreateGameState.empty() { return CreateGameState( cardSets: emptyList(), - selectedSets: sets.toImmutableSet() ?? emptySet(), + selectedSets: emptySet(), + prizesToWin: AppPreferences().prizesToWin, + playerLimit: AppPreferences().playerLimit, isLoading: true, ); } CreateGameState copyWith({ - KtList cardSets, - KtSet selectedSets, - bool isLoading, - String error, + KtList? cardSets, + KtSet? selectedSets, + int? prizesToWin, + int? playerLimit, + bool? pick2Enabled, + bool? draw2pick3Enabled, + bool? isLoading, + String? error, + Game? createdGame, + // bool overrideNull = false }) { return CreateGameState( - cardSets: cardSets ?? this.cardSets, - selectedSets: selectedSets ?? this.selectedSets, - isLoading: isLoading ?? this.isLoading, - error: error ?? this.error, - ); + cardSets: cardSets ?? this.cardSets, + selectedSets: selectedSets ?? this.selectedSets, + prizesToWin: prizesToWin ?? this.prizesToWin, + playerLimit: playerLimit ?? this.playerLimit, + pick2Enabled: pick2Enabled ?? this.pick2Enabled, + draw2pick3Enabled: draw2pick3Enabled ?? this.draw2pick3Enabled, + isLoading: isLoading ?? this.isLoading, + error: error ?? this.error, + createdGame: createdGame ?? this.createdGame); } @override String toString() { return '''CreateGameState { cardSets: $cardSets, - selectedSets: $selectedSets, + selectedSets: $selectedSets, + prizesToWin: $prizesToWin, + playerLimit: $playerLimit, + pick2Enabled: $pick2Enabled, + draw2pick3Enabled: $draw2pick3Enabled, isLoading: $isLoading, - error: $error + error: $error, + createdGame: $createdGame, }'''; } } diff --git a/lib/ui/creategame/create_game_screen.dart b/lib/ui/creategame/create_game_screen.dart index 9a6dd1a..3bed0dc 100644 --- a/lib/ui/creategame/create_game_screen.dart +++ b/lib/ui/creategame/create_game_screen.dart @@ -1,16 +1,18 @@ -import 'package:appsagainsthumanity/data/features/game/game_repository.dart'; import 'package:appsagainsthumanity/ui/creategame/bloc/bloc.dart'; import 'package:appsagainsthumanity/internal.dart'; -import 'package:appsagainsthumanity/ui/game/game_screen.dart'; +// import 'package:appsagainsthumanity/ui/creategame/widgets/card_set_list.dart'; +// import 'package:appsagainsthumanity/ui/creategame/widgets/game_options.dart'; +import 'package:appsagainsthumanity/ui/routes.dart'; +import 'package:appsagainsthumanity/ui/widgets/reponsive_widget_mediator.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:kt_dart/kt.dart'; +// import 'package:kt_dart/kt.dart'; -class CreateGameScreen extends StatefulWidget { - final Set cardSets; - - CreateGameScreen({Set sets}) : cardSets = sets ?? Set(); +import 'layouts/mobile_layout.dart'; +import 'layouts/tablet_layout.dart'; +class CreateGameScreen extends StatefulWidget { @override State createState() => _CreateGameScreenState(); } @@ -18,120 +20,49 @@ class CreateGameScreen extends StatefulWidget { class _CreateGameScreenState extends State { @override Widget build(BuildContext context) { - return BlocProvider( - create: (context) => CreateGameBloc( - widget.cardSets, - context.repository(), - context.repository(), - )..add(ScreenLoaded()), - child: _buildScaffold(), - ); - } - - Widget _buildScaffold() { - return BlocConsumer(listener: (context, state) { - if (state.error != null) { - Scaffold.of(context) - ..hideCurrentSnackBar() - ..showSnackBar( - SnackBar( - content: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [Text(state.error), Icon(Icons.error)], - ), - backgroundColor: Colors.redAccent, - ), - ); - } - }, builder: (context, state) { - return Scaffold( - appBar: AppBar( - title: Text(context.strings.titleNewGame), - leading: IconButton( - icon: Icon(Icons.arrow_back), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - ), - bottomNavigationBar: BottomAppBar( - notchMargin: 8, - color: AppColors.primary, - shape: CircularNotchedRectangle(), - child: Container( - height: 56, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Expanded( - child: Container( - margin: const EdgeInsets.symmetric(horizontal: 16), - child: Text( - "${state.selectedSets.size} Selected", - style: context.theme.textTheme.headline6, - ), - ), - ), - ], + return AnnotatedRegion( + value: SystemUiOverlayStyle.light, + child: BlocProvider( + create: (context) => CreateGameBloc()..add(ScreenLoaded()), + child: MultiBlocListener( + listeners: [ + // Error Listener + BlocListener( + listenWhen: (previous, current) => + current.error != previous.error, + listener: (context, state) { + if (state.error != "") { + context.scaffold + ..hideCurrentSnackBar() + ..showSnackBar(SnackBar( + content: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [Text(state.error!), Icon(Icons.error)], + ), + backgroundColor: Colors.redAccent, + )); + } + }, ), - ), - ), - floatingActionButtonLocation: FloatingActionButtonLocation.endDocked, - floatingActionButton: FloatingActionButton( - child: Icon(Icons.play_arrow), - onPressed: state.selectedSets.isNotEmpty() - ? () async { - // Start game? - var newGame = await context.repository().createGame(state.selectedSets); - Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (_) => GameScreen(newGame))); + // New Game Listener + BlocListener( + listenWhen: (previous, current) => + current.createdGame?.id != previous.createdGame?.id, + listener: (context, state) { + if (state.createdGame != null) { + Navigator.of(context) + .pushReplacement(GamePageRoute(state.createdGame!)); } - : null, - ), - body: _buildBody(state), - ); - }); - } - - Widget _buildBody(CreateGameState state) { - return Column( - children: [ - Expanded( - child: state.isLoading ? _buildLoading() : _buildList(state.cardSets, state.selectedSets), - ), - ], - ); - } - - Widget _buildLoading() { - return Container( - height: double.maxFinite, - alignment: Alignment.center, - child: CircularProgressIndicator(), - ); - } - - Widget _buildList(KtList sets, KtSet selected) { - return ListView.builder( - itemCount: sets.size, - itemBuilder: (context, index) { - var item = sets[index]; - var isSelected = selected.contains(item); - return ListTile( - title: Text(item), - contentPadding: const EdgeInsets.symmetric(horizontal: 6), - leading: Checkbox( - value: isSelected, - activeColor: AppColors.secondary, - onChanged: (value) { - context.bloc().add(CardSetSelected(item)); - }, + }, + ), + ], + child: ResponsiveWidgetMediator( + mobile: (_) => MobileLayout(), + tablet: (_) => TabletLayout(), ), - onTap: () { - context.bloc().add(CardSetSelected(item)); - }, - ); - }, + ), + ), ); } } diff --git a/lib/ui/creategame/layouts/mobile_layout.dart b/lib/ui/creategame/layouts/mobile_layout.dart new file mode 100644 index 0000000..ac79b70 --- /dev/null +++ b/lib/ui/creategame/layouts/mobile_layout.dart @@ -0,0 +1,90 @@ +import 'package:appsagainsthumanity/ui/creategame/bloc/bloc.dart'; +import 'package:appsagainsthumanity/internal.dart'; +import 'package:appsagainsthumanity/ui/creategame/widgets/card_set_list.dart'; +import 'package:appsagainsthumanity/ui/creategame/widgets/game_options.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:kt_dart/kt.dart'; + +class MobileLayout extends StatelessWidget { + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return DefaultTabController( + length: 2, + child: Scaffold( + appBar: AppBar( + systemOverlayStyle: SystemUiOverlayStyle( + statusBarBrightness: Brightness.dark, + ), + // brightness: Brightness.dark, + title: Text( + context.strings.titleNewGame, + style: context.theme.textTheme.headline6! + .copyWith(color: Colors.white), + ), + leading: IconButton( + icon: Icon(Icons.arrow_back, color: Colors.white), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + bottom: TabBar( + labelColor: Colors.white, + tabs: [ + Tab(text: "CARDS"), + Tab(text: "OPTIONS"), + ], + ), + ), + bottomNavigationBar: BottomAppBar( + notchMargin: 8, + shape: CircularNotchedRectangle(), + child: Container( + height: 56, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + child: Text( + state.isLoading + ? "Loading..." + : "${state.totalPrompts} Prompts ${state.totalResponses} Responses", + style: context.theme.textTheme.headline6, + ), + ), + ), + ], + ), + ), + ), + floatingActionButtonLocation: + FloatingActionButtonLocation.endDocked, + floatingActionButton: + state.selectedSets.isNotEmpty() && !state.isLoading + ? FloatingActionButton( + child: Icon(Icons.check), + onPressed: () async { + // Start game? + Analytics().logSelectContent( + contentType: 'action', itemId: 'create_game'); + context.read().add(CreateGame()); + }, + ) + : null, + body: TabBarView( + children: [ + CardSetList(state), + GameOptions(state), + ], + ), + ), + ); + }, + ); + } +} diff --git a/lib/ui/creategame/layouts/tablet_layout.dart b/lib/ui/creategame/layouts/tablet_layout.dart new file mode 100644 index 0000000..a3d9646 --- /dev/null +++ b/lib/ui/creategame/layouts/tablet_layout.dart @@ -0,0 +1,98 @@ +import 'package:appsagainsthumanity/ui/creategame/bloc/bloc.dart'; +import 'package:appsagainsthumanity/internal.dart'; +import 'package:appsagainsthumanity/ui/creategame/widgets/card_set_list.dart'; +import 'package:appsagainsthumanity/ui/creategame/widgets/game_options.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:kt_dart/kt.dart'; + +class TabletLayout extends StatelessWidget { + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return Scaffold( + appBar: AppBar( + systemOverlayStyle: SystemUiOverlayStyle( + statusBarBrightness: Brightness.dark, + ), + // brightness: Brightness.dark, + title: Text( + context.strings.titleNewGame, + style: context.theme.textTheme.headline6! + .copyWith(color: Colors.white), + ), + leading: IconButton( + icon: Icon(Icons.arrow_back, color: Colors.white), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ), + bottomNavigationBar: BottomAppBar( + notchMargin: 8, + shape: CircularNotchedRectangle(), + child: Container( + height: 56, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + child: Text( + state.isLoading + ? "Loading..." + : "${state.totalPrompts} Prompts ${state.totalResponses} Responses", + style: context.theme.textTheme.headline6, + ), + ), + ), + ], + ), + ), + ), + floatingActionButtonLocation: FloatingActionButtonLocation.endDocked, + floatingActionButton: + state.selectedSets.isNotEmpty() && !state.isLoading + ? FloatingActionButton( + child: Icon(Icons.check), + onPressed: () async { + // Start game? + Analytics().logSelectContent( + contentType: 'action', itemId: 'create_game'); + context.read().add(CreateGame()); + }, + ) + : null, + body: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: CardSetList(state), + ), + ConstrainedBox( + constraints: BoxConstraints.tightFor(width: 400), + child: IntrinsicHeight( + child: Container( + margin: const EdgeInsets.all(16), + alignment: Alignment.topCenter, + child: Material( + borderRadius: BorderRadius.circular(16), + elevation: 2, + color: context.responseCardColor, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: GameOptions(state), + )), + ), + ), + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/ui/creategame/widgets/card_set_list.dart b/lib/ui/creategame/widgets/card_set_list.dart new file mode 100644 index 0000000..1227be0 --- /dev/null +++ b/lib/ui/creategame/widgets/card_set_list.dart @@ -0,0 +1,82 @@ +import 'package:appsagainsthumanity/data/features/cards/model/card_set.dart'; +import 'package:appsagainsthumanity/ui/creategame/bloc/bloc.dart'; +import 'package:appsagainsthumanity/ui/creategame/widgets/card_set_list_item.dart'; +import 'package:appsagainsthumanity/ui/creategame/widgets/header_item.dart'; +import 'package:flutter/material.dart'; +import 'package:kt_dart/kt.dart'; + +class CardSetList extends StatelessWidget { + final CreateGameState state; + + CardSetList(this.state); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Expanded( + child: state.isLoading + ? _buildLoading() + : _buildList(state.cardSets, state.selectedSets), + ), + ], + ); + } + + Widget _buildLoading() { + return Container( + height: double.maxFinite, + alignment: Alignment.center, + child: CircularProgressIndicator(), + ); + } + + Widget _buildList(KtList sets, KtSet selected) { + var groupedSets = sets.groupBy((cs) => cs.source); + var widgets = groupedSets.keys.toList().sortedWith((a, b) { + var aW = keyWeight(a!); + var bW = keyWeight(b!); + if (aW.compareTo(bW) == 0) { + return a.compareTo(b); + } else { + return aW.compareTo(bW); + } + }).flatMap((key) { + var items = groupedSets + .get(key) + ?.map((cs) => CardSetListItem(cs, selected.contains(cs))); + var allItemsSelected = items?.all((i) => i.isSelected); + var noItemsSelected = items?.none((i) => i.isSelected); + return mutableListOf( + HeaderItem( + key!, + allItemsSelected! + ? true + : noItemsSelected! + ? false + : null), + )..addAll(items!); + }); + + return ListView.builder( + itemCount: widgets.size, + itemBuilder: (context, index) => widgets[index], + ); + } + + int keyWeight(String key) { + if (key == "Developer") { + return 0; + } else if (key == "CAH Main Deck") { + return 1; + } else if (key == "CAH Expansions") { + return 2; + } else if (key == "CAH Packs") { + return 3; + } else if (key.startsWith('CAH Packs/')) { + return 4; + } else { + return 5; + } + } +} diff --git a/lib/ui/creategame/widgets/card_set_list_item.dart b/lib/ui/creategame/widgets/card_set_list_item.dart new file mode 100644 index 0000000..1eb58df --- /dev/null +++ b/lib/ui/creategame/widgets/card_set_list_item.dart @@ -0,0 +1,35 @@ +import 'package:appsagainsthumanity/ui/creategame/bloc/bloc.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:appsagainsthumanity/data/features/cards/model/card_set.dart'; +import 'package:appsagainsthumanity/internal.dart'; +import 'package:appsagainsthumanity/util/cah_scrubber.dart'; +import 'package:flutter/material.dart'; + +class CardSetListItem extends StatelessWidget { + final CardSet cardSet; + final bool isSelected; + + CardSetListItem(this.cardSet, this.isSelected); + + @override + Widget build(BuildContext context) { + return ListTile( + title: Text(CahScrubber.scrub(cardSet.name!)), + contentPadding: const EdgeInsets.symmetric(horizontal: 6), + leading: Checkbox( + value: isSelected, + activeColor: AppColors.primary, + onChanged: (value) { + Analytics() + .logSelectContent(contentType: 'card_set', itemId: cardSet.name); + context.read().add(CardSetSelected(cardSet)); + }, + ), + onTap: () { + Analytics() + .logSelectContent(contentType: 'card_set', itemId: cardSet.name); + context.read().add(CardSetSelected(cardSet)); + }, + ); + } +} diff --git a/lib/ui/creategame/widgets/count_preference.dart b/lib/ui/creategame/widgets/count_preference.dart new file mode 100644 index 0000000..af07421 --- /dev/null +++ b/lib/ui/creategame/widgets/count_preference.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; +import 'package:appsagainsthumanity/internal.dart'; +import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; + +class CountPreference extends StatelessWidget { + final String title; + final String? subtitle; + final int value; + final int? min, max; + final void Function(int) onValueChanged; + + CountPreference( + this.value, { + required this.title, + this.subtitle, + required this.onValueChanged, + this.min, + this.max, + }); + + @override + Widget build(BuildContext context) { + return ListTile( + title: Text(title), + subtitle: subtitle != "" ? Text(subtitle!) : const SizedBox(), + contentPadding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + padding: const EdgeInsets.all(8), + icon: Icon( + MdiIcons.minus, + color: Colors.white, + ), + onPressed: () { + var newValue = (value - 1).clamp(min!, max!); + onValueChanged(newValue); + }, + ), + Container( + width: 36, + height: 36, + decoration: + BoxDecoration(color: AppColors.primary, shape: BoxShape.circle), + alignment: Alignment.center, + child: Text( + value.toString(), + style: context.theme.textTheme.subtitle1 + ?.copyWith(color: Colors.white), + ), + ), + IconButton( + padding: const EdgeInsets.all(8), + icon: Icon( + MdiIcons.plus, + color: Colors.white, + ), + onPressed: () { + var newValue = (value + 1).clamp(min!, max!); + onValueChanged(newValue); + }, + ), + ], + ), + ); + } +} diff --git a/lib/ui/creategame/widgets/game_options.dart b/lib/ui/creategame/widgets/game_options.dart new file mode 100644 index 0000000..d8aec97 --- /dev/null +++ b/lib/ui/creategame/widgets/game_options.dart @@ -0,0 +1,65 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:appsagainsthumanity/internal.dart'; +import 'package:appsagainsthumanity/ui/creategame/bloc/bloc.dart'; +import 'package:appsagainsthumanity/ui/creategame/widgets/count_preference.dart'; +import 'package:flutter/material.dart'; + +class GameOptions extends StatelessWidget { + final CreateGameState state; + + GameOptions(this.state); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + CountPreference( + state.prizesToWin, + title: "Prizes to win", + subtitle: "Choose the number of prize cards it would take to win", + min: 1, + max: 15, + onValueChanged: (value) { + Analytics().logSelectContent( + contentType: 'game_option', itemId: 'prizes_to_win'); + context.read().add(ChangePrizesToWin(value)); + }, + ), + CountPreference( + state.playerLimit, + title: "Max # of players", + subtitle: "Pick the number of players allowed to join your game", + min: 5, + max: 30, + onValueChanged: (value) { + Analytics().logSelectContent( + contentType: 'game_option', itemId: 'player_limit'); + context.read().add(ChangePlayerLimit(value)); + }, + ), + SwitchListTile( + title: Text("Enable \"PICK 2\""), + subtitle: Text("Allow \"PICK 2\" prompt cards"), + activeColor: AppColors.primary, + value: state.pick2Enabled, + onChanged: (value) { + Analytics() + .logSelectContent(contentType: 'game_option', itemId: 'pick2'); + context.read().add(ChangePick2Enabled(value)); + }, + ), + SwitchListTile( + title: Text("Enable \"DRAW 2, PICK 3\""), + subtitle: Text("Allow \"DRAW 2, PICK 3\" prompt cards"), + activeColor: AppColors.primary, + value: state.draw2pick3Enabled, + onChanged: (value) { + Analytics().logSelectContent( + contentType: 'game_option', itemId: 'draw2_pick3'); + context.read().add(ChangeDraw2Pick3Enabled(value)); + }, + ), + ], + ); + } +} diff --git a/lib/ui/creategame/widgets/header_item.dart b/lib/ui/creategame/widgets/header_item.dart new file mode 100644 index 0000000..3242a24 --- /dev/null +++ b/lib/ui/creategame/widgets/header_item.dart @@ -0,0 +1,66 @@ +import 'package:appsagainsthumanity/ui/creategame/bloc/bloc.dart'; +import 'package:appsagainsthumanity/util/cah_scrubber.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:appsagainsthumanity/internal.dart'; +import 'package:flutter/material.dart'; + +class HeaderItem extends StatelessWidget { + final String title; + final bool? isChecked; + + HeaderItem(this.title, [this.isChecked]); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: () { + Analytics() + .logSelectContent(contentType: 'card_set_source', itemId: title); + context + .read() + .add(CardSourceSelected(title, isChecked!)); + }, + child: Column( + children: [ + Divider( + height: 1, + color: Colors.white12, + ), + Container( + height: 48, + alignment: Alignment.centerLeft, + padding: const EdgeInsets.symmetric(horizontal: 6), + child: Row( + children: [ + Checkbox( + value: isChecked, + tristate: true, + onChanged: (value) { + Analytics().logSelectContent( + contentType: 'card_set_source', itemId: title); + context + .read() + .add(CardSourceSelected(title, value!)); + }, + activeColor: AppColors.primary, + checkColor: Colors.white, + ), + Expanded( + child: Container( + margin: const EdgeInsets.only(left: 16), + child: Text( + CahScrubber.scrub(title) != "" ? "_UNKNOWN_" : "", + style: context.theme.textTheme.subtitle2?.copyWith( + color: AppColors.primaryVariant, + ), + ), + ), + ), + ], + ), + ) + ], + ), + ); + } +} diff --git a/lib/ui/game/bloc/game_bloc.dart b/lib/ui/game/bloc/game_bloc.dart index a015f04..81c6f99 100644 --- a/lib/ui/game/bloc/game_bloc.dart +++ b/lib/ui/game/bloc/game_bloc.dart @@ -10,13 +10,28 @@ import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class GameBloc extends Bloc { - final Game initialGame; - final GameRepository gameRepository; + final Game? initialGame; + final GameRepository? gameRepository; StreamSubscription _gameSubscription; StreamSubscription _playersSubscription; - - GameBloc(this.initialGame, this.gameRepository); + StreamSubscription _downvoteSubscription; + + GameBloc({ + this.initialGame, + this.gameRepository, + StreamSubscription? gameSubscription, + StreamSubscription? playersSubscription, + StreamSubscription? downvoteSubscription, + }) : _gameSubscription = gameSubscription!, + _playersSubscription = playersSubscription!, + _downvoteSubscription = downvoteSubscription!, + super(GameViewState()); + + // SignInBloc({ + // required UserRepository userRepository, + // }) : _userRepository = userRepository, + // super(SignInState.loading()) {} @override GameViewState get initialState => GameViewState(game: initialGame); @@ -31,36 +46,52 @@ class GameBloc extends Bloc { yield* _mapGameUpdatedToState(event); } else if (event is PlayersUpdated) { yield* _mapPlayersUpdatedToState(event); + } else if (event is DownvotesUpdated) { + yield* _mapDownvotesUpdatedToState(event); } else if (event is StartGame) { yield* _mapStartGameToState(); } else if (event is ClearError) { yield* _mapClearErrorToState(); } else if (event is DownvotePrompt) { yield* _mapDownvoteToState(); + } else if (event is WaveAtPlayer) { + yield* _mapWaveAtPlayerToState(event); } else if (event is PickResponseCard) { yield* _mapPickResponseCardToState(event); + } else if (event is ClearPickedResponseCards) { + yield* _mapClearPickedResponseCardsToState(); } else if (event is SubmitResponses) { yield* _mapSubmitResponsesToState(); } else if (event is PickWinner) { yield* _mapPickWinnerToState(event); + } else if (event is KickPlayer) { + yield* _mapKickWinnerToState(event); } else if (event is ClearSubmitting) { yield* _mapClearSubmittingToState(); } } Stream _mapSubscribeToState(Subscribe event) async* { - var user = await FirebaseAuth.instance.currentUser(); - add(UserUpdated(user.uid)); + var user = FirebaseAuth.instance.currentUser; + add(UserUpdated(user!.uid)); - _gameSubscription?.cancel(); - _gameSubscription = gameRepository.observeGame(event.gameId).listen((game) { + _gameSubscription.cancel(); + _gameSubscription = + gameRepository!.observeGame(event.gameId).listen((game) { add(GameUpdated(game)); }); - - _playersSubscription?.cancel(); - _playersSubscription = gameRepository.observePlayers(event.gameId).listen((players) { + + _playersSubscription.cancel(); + _playersSubscription = + gameRepository!.observePlayers(event.gameId).listen((players) { add(PlayersUpdated(players)); }); + + _downvoteSubscription.cancel(); + _downvoteSubscription = + gameRepository!.observeDownvotes(event.gameId).listen((downvotes) { + add(DownvotesUpdated(downvotes)); + }); } Stream _mapUserUpdatedToState(UserUpdated event) async* { @@ -68,22 +99,29 @@ class GameBloc extends Bloc { } Stream _mapGameUpdatedToState(GameUpdated event) async* { - yield state.copyWith(game: event.game, isLoading: false, isSubmitting: false); + yield state.copyWith( + game: event.game, isLoading: false, isSubmitting: false); } Stream _mapPlayersUpdatedToState(PlayersUpdated event) async* { yield state.copyWith(players: event.players, isLoading: false); } + Stream _mapDownvotesUpdatedToState( + DownvotesUpdated event) async* { + yield state.copyWith(downvotes: event.downvotes, isLoading: false); + } + Stream _mapStartGameToState() async* { try { yield state.copyWith(isSubmitting: true); - await gameRepository.startGame(state.game.id); + await gameRepository!.startGame(state.game!.id!); } catch (e) { if (e is PlatformException) { print(e); if (e.code == 'functionsError') { - final Map details = Map.from(e.details); + final Map details = + Map.from(e.details); yield state.copyWith(error: details['message'], isSubmitting: false); } } @@ -96,15 +134,25 @@ class GameBloc extends Bloc { Stream _mapDownvoteToState() async* { try { - await gameRepository.downVoteCurrentPrompt(state.game.id); + await gameRepository!.downVoteCurrentPrompt(state.game!.id!); + } catch (e) { + yield state.copyWith(error: "$e"); + } + } + + Stream _mapWaveAtPlayerToState(WaveAtPlayer event) async* { + try { + await gameRepository! + .waveAtPlayer(state.game!.id!, event.playerId, event.message!); } catch (e) { yield state.copyWith(error: "$e"); } } - Stream _mapPickResponseCardToState(PickResponseCard event) async* { + Stream _mapPickResponseCardToState( + PickResponseCard event) async* { // Check prompt special to determine if we allow the user to pick tow - var special = promptSpecial(state.game.turn?.promptCard?.special); + var special = promptSpecial(state.game!.turn!.promptCard.special); if (special != null) { // With a special there is the opportunity to submit more than 1 card. If the user attempts to select more than // the allotted amount for a give prompt special, it will clear the selected and set the picked card as the only one @@ -113,8 +161,9 @@ class GameBloc extends Bloc { switch (special) { case PromptSpecial.pick2: // Selected size limit is 2 here. - if (currentSelection.length < 2) { - yield state.copyWith(selectedCards: currentSelection..add(event.card)); + if (currentSelection!.length < 2) { + yield state.copyWith( + selectedCards: currentSelection..add(event.card)); } else { yield state.copyWith(selectedCards: [event.card]); } @@ -122,12 +171,15 @@ class GameBloc extends Bloc { case PromptSpecial.draw2pick3: // Selected size limit is 3 here. The firebase function that deals with churning-turns will auto-matically // deal out an extra 2 cards to the user at turn start - if (currentSelection.length < 3) { - yield state.copyWith(selectedCards: currentSelection..add(event.card)); + if (currentSelection!.length < 3) { + yield state.copyWith( + selectedCards: currentSelection..add(event.card)); } else { yield state.copyWith(selectedCards: [event.card]); } break; + case PromptSpecial.derp: + break; } } else { // The lack of a special is an indication of PICK 1 only @@ -135,12 +187,16 @@ class GameBloc extends Bloc { } } + Stream _mapClearPickedResponseCardsToState() async* { + yield state.copyWith(selectedCards: []); + } + Stream _mapSubmitResponsesToState() async* { var responses = state.selectedCards; if (responses != null && responses.isNotEmpty) { try { yield state.copyWith(isSubmitting: true); - await gameRepository.submitResponse(state.game.id, responses); + await gameRepository!.submitResponse(state.game!.id!, responses); yield state.copyWith(selectedCards: [], isSubmitting: false); } catch (e) { yield state.copyWith(error: "$e", isSubmitting: false); @@ -151,13 +207,28 @@ class GameBloc extends Bloc { Stream _mapPickWinnerToState(PickWinner event) async* { try { yield state.copyWith(isSubmitting: true); - await gameRepository.pickWinner(state.game.id, event.winningPlayerId); + await gameRepository!.pickWinner(state.game!.id!, event.winningPlayerId); yield state.copyWith(isSubmitting: false); } catch (e) { yield state.copyWith(error: "$e", isSubmitting: false); } } + Stream _mapKickWinnerToState(KickPlayer event) async* { + try { + yield state.copyWith(kickingPlayerId: event.playerId); + await gameRepository!.kickPlayer(state.game!.id!, event.playerId); + yield state.copyWith( + kickingPlayerId: null, + ); + } catch (e) { + yield state.copyWith( + error: "$e", + kickingPlayerId: null, + ); + } + } + Stream _mapClearSubmittingToState() async* { yield state.copyWith(isSubmitting: false); } diff --git a/lib/ui/game/bloc/game_event.dart b/lib/ui/game/bloc/game_event.dart index 01554ae..c3f8219 100644 --- a/lib/ui/game/bloc/game_event.dart +++ b/lib/ui/game/bloc/game_event.dart @@ -24,6 +24,7 @@ class Subscribe extends GameEvent { } class StartGame extends GameEvent {} + class ClearError extends GameEvent {} /// This event is triggered to update the user id in the game state @@ -59,7 +60,28 @@ class PlayersUpdated extends GameEvent { List get props => [players]; } -class DownvotePrompt extends GameEvent { } +@immutable +class DownvotesUpdated extends GameEvent { + final List downvotes; + + DownvotesUpdated(this.downvotes); + + @override + List get props => [downvotes]; +} + +class DownvotePrompt extends GameEvent {} + +@immutable +class WaveAtPlayer extends GameEvent { + final String playerId; + final String? message; + + WaveAtPlayer(this.playerId, [this.message]); + + @override + List get props => [playerId]; +} @immutable class PickResponseCard extends GameEvent { @@ -71,6 +93,8 @@ class PickResponseCard extends GameEvent { List get props => [card]; } +class ClearPickedResponseCards extends GameEvent {} + @immutable class PickWinner extends GameEvent { final String winningPlayerId; @@ -81,6 +105,16 @@ class PickWinner extends GameEvent { List get props => [winningPlayerId]; } -class SubmitResponses extends GameEvent { } +@immutable +class KickPlayer extends GameEvent { + final String playerId; + + KickPlayer(this.playerId); + + @override + List get props => [playerId]; +} + +class SubmitResponses extends GameEvent {} -class ClearSubmitting extends GameEvent { } +class ClearSubmitting extends GameEvent {} diff --git a/lib/ui/game/bloc/game_state.dart b/lib/ui/game/bloc/game_state.dart index be3c6da..ece2d34 100644 --- a/lib/ui/game/bloc/game_state.dart +++ b/lib/ui/game/bloc/game_state.dart @@ -6,114 +6,157 @@ import 'package:meta/meta.dart'; @immutable class GameViewState { - /* FIELDS */ - final String userId; - final Game game; - final List players; - final List selectedCards; - final bool isSubmitting; - final bool isLoading; - final String error; + /* FIELDS */ + final String? userId; + final Game? game; + final List? players; + final List? selectedCards; + final List? downvotes; + final bool isSubmitting; + final String? kickingPlayerId; + final bool isLoading; + final String? error; - /* PROPERTIES */ - bool get isOurGame => userId == game?.ownerId; + /* PROPERTIES */ + bool get isOurGame => userId == game?.ownerId; - Player get currentJudge => players?.firstWhere((p) => p.id == game.turn?.judgeId); + Player get currentJudge => + players!.firstWhere((p) => p.id == game?.turn?.judgeId); + Player get lastJudge => players!.firstWhere( + (p) => !(game?.turn?.winner?.responses?.containsKey(p.id) ?? true)); - /// Get the current prompt card text with any macros computed from the text string. For now this is just - /// the a simple replace of the judge's name for its specific replacer text. - /// TODO: Extract this into a tool that can take a configurable macro list for smart injecting text into prompts - String get currentPromptText { - var prompt = game.turn?.promptCard; - var judge = currentJudge; - if (judge != null && prompt != null) { - return prompt.text.replaceAll("{JUDGE_NAME}", judge.name); - } else if (prompt != null) { - return prompt.text; - } else { - return ""; - } + /// Get the current prompt card text with any macros computed from the text string. For now this is just + /// the a simple replace of the judge's name for its specific replacer text. + /// TODO: Extract this into a tool that can take a configurable macro list for smart injecting text into prompts + String get currentPromptText { + var prompt = game?.turn?.promptCard; + var judge = currentJudge; + if (judge != null && prompt != null) { + return prompt.text.replaceAll("{JUDGE_NAME}", judge.name); + } else if (prompt != null) { + return prompt.text; + } else { + return ""; + } + } + + /// Get the current prompt card text with any macros computed from the text string. For now this is just + /// the a simple replace of the judge's name for its specific replacer text. + /// TODO: Extract this into a tool that can take a configurable macro list for smart injecting text into prompts + String get lastPromptText { + var prompt = game?.turn?.winner?.promptCard; + var judge = lastJudge; + if (judge != {} && prompt != {}) { + return prompt!.text.replaceAll("{JUDGE_NAME}", judge.name); + } else if (prompt != null) { + return prompt.text; + } else { + return ""; } + } - Player get currentPlayer => players?.firstWhere((p) => p.id == userId); + Player get currentPlayer => players!.firstWhere((p) => p.id == userId); - Player get winner => players?.firstWhere((p) => p.id == game.winner); + Player get winner => players!.firstWhere((p) => p.id == game?.winner); - List get currentHand => currentPlayer?.hand?.where((c) => !selectedCards.contains(c))?.toList() ?? []; + List get currentHand => + currentPlayer.hand?.where((c) => !selectedCards!.contains(c)).toList() ?? + []; - bool get areWeJudge => currentJudge?.id == userId; + bool get areWeJudge => currentJudge.id == userId; - bool get haveWeSubmittedResponse => game.turn?.responses?.keys?.contains(userId) ?? false; + bool get haveWeSubmittedResponse => + game?.turn?.responses.keys.contains(userId) ?? false; - bool get allResponsesSubmitted { - if (game.turn != null && game.turn.responses != null && players != null && players.isNotEmpty) { - var allPlayersExcludingJudge = players.where((element) => element.id != game.turn.judgeId).toList(); - for (var value in allPlayersExcludingJudge) { - if (game.turn.responses.keys.firstWhere((element) => element == value.id, orElse: () => null) == null) { - return false; - } - } - return true; - } else { - return false; + bool get allResponsesSubmitted { + if (game?.turn != {} && + game?.turn?.responses != {} && + players != {} && + players!.isNotEmpty) { + var allPlayersExcludingJudgeAndInactive = players! + .where((element) => + element.id != game?.turn?.judgeId && element.isInactive != true) + .toList(); + for (var value in allPlayersExcludingJudgeAndInactive) { + if (game?.turn?.responses.keys.firstWhere( + (element) => element == value.id, + orElse: () => "") == + null) { + return false; } + } + return true; + } else { + return false; } - - bool get selectCardsMeetPromptRequirement { - PromptSpecial special = promptSpecial(game.turn?.promptCard?.special); - if (special != null) { - if (special == PromptSpecial.pick2) { - return selectedCards.length == 2; - } else if (special == PromptSpecial.draw2pick3) { - return selectedCards.length == 3; - } - } else { - return selectedCards.isNotEmpty; - } - return false; + } + + bool get selectCardsMeetPromptRequirement { + PromptSpecial special = promptSpecial(game!.turn!.promptCard.special); + if (special != {}) { + if (special == PromptSpecial.pick2) { + return selectedCards?.length == 2; + } else if (special == PromptSpecial.draw2pick3) { + return selectedCards?.length == 3; + } + } else { + return selectedCards!.isNotEmpty; } + return false; + } - GameViewState({ - this.userId, - this.game, - this.players, - List selectedCards, - this.isSubmitting = false, - this.isLoading = true, - this.error, - }) : selectedCards = selectedCards ?? []; + GameViewState({ + this.userId, + this.game, + this.players, + List? downvotes, + List? selectedCards, + this.isSubmitting = false, + this.isLoading = true, + this.kickingPlayerId, + this.error, + }) : selectedCards = selectedCards ?? [], + downvotes = downvotes; - GameViewState copyWith({ - String userId, - Game game, - List players, - List selectedCards, - bool isSubmitting, - bool isLoading, - String error, - }) { - return GameViewState( - userId: userId ?? this.userId, - game: game ?? this.game, - players: players ?? this.players, - selectedCards: selectedCards ?? this.selectedCards, - isSubmitting: isSubmitting ?? this.isSubmitting, - isLoading: isLoading ?? this.isLoading, - error: error ?? this.error, - ); - } + GameViewState copyWith({ + String? userId, + Game? game, + List? players, + List? selectedCards, + List? downvotes, + bool? isSubmitting, + bool? isLoading, + String? kickingPlayerId, + String? error, + // bool overrideNull = false, + }) { + return GameViewState( + userId: userId ?? this.userId, + game: game ?? this.game, + players: players ?? this.players, + downvotes: downvotes ?? this.downvotes, + selectedCards: selectedCards ?? this.selectedCards, + isSubmitting: isSubmitting ?? this.isSubmitting, + isLoading: isLoading ?? this.isLoading, + kickingPlayerId: kickingPlayerId ?? this.kickingPlayerId, + error: error ?? this.error, + ); + } - @override - String toString() { - return '''GameViewState { + @override + String toString() { + return '''GameViewState { userId: $userId, - game: ${game?.gid}, - players: $players, + game: ${game?.gid}, + turn: ${game?.turn?.winner}, + players: ${players?.length}, + downvotes: $downvotes, selectedCards: $selectedCards, isSubmitting: $isSubmitting, isLoading: $isLoading, + kickingPlayerId: $kickingPlayerId, error: $error, isOurGame: $isOurGame, }'''; - } + } } diff --git a/lib/ui/game/game_screen.dart b/lib/ui/game/game_screen.dart index 542c008..f1ac964 100644 --- a/lib/ui/game/game_screen.dart +++ b/lib/ui/game/game_screen.dart @@ -13,7 +13,6 @@ import 'package:flutter_bloc/flutter_bloc.dart'; /// state. These states will be sub-divided into their own, bloc (or-not) controlled, /// screens/widgets class GameScreen extends StatelessWidget { - final Game game; GameScreen(this.game); @@ -21,25 +20,24 @@ class GameScreen extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => GameBloc(game, context.repository()) - ..add(Subscribe(game.id)), + create: (context) => GameBloc()..add(Subscribe(game.id!)), child: BlocBuilder( - condition: (previous, current) { - return previous.game.state != current.game.state || - (previous.game.state == GameState.waitingRoom && - current.game.state == GameState.waitingRoom && + buildWhen: (previous, current) { + return previous.game?.state != current.game?.state || + (previous.game?.state == GameState.waitingRoom && + current.game?.state == GameState.waitingRoom && previous.isSubmitting != current.isSubmitting); }, builder: (context, state) { - if (state.game.state == GameState.waitingRoom) { + if (state.game?.state == GameState.waitingRoom) { return state.isSubmitting ? StartingRoomScreen(state) : WaitingRoomScreen(); - } else if (state.game.state == GameState.starting) { + } else if (state.game?.state == GameState.starting) { return StartingRoomScreen(state); - } else if (state.game.state == GameState.inProgress) { + } else if (state.game?.state == GameState.inProgress) { return GamePlayScreen(state); - } else if (state.game.state == GameState.completed) { + } else if (state.game?.state == GameState.completed) { return CompletedGameScreen(); } else { return Container(); diff --git a/lib/ui/game/screens/complete/completed_game_screen.dart b/lib/ui/game/screens/complete/completed_game_screen.dart index 5b3a2dd..e30fe7f 100644 --- a/lib/ui/game/screens/complete/completed_game_screen.dart +++ b/lib/ui/game/screens/complete/completed_game_screen.dart @@ -11,26 +11,6 @@ class CompletedGameScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - bottomNavigationBar: BottomAppBar( - color: AppColors.primary, - child: Container( - height: 56, - child: Row( - children: [ - Container( - margin: const EdgeInsets.symmetric(horizontal: 16), - child: IconButton( - icon: Icon( - Icons.close, - color: Colors.black87, - ), - onPressed: () => Navigator.of(context).pop(), - ), - ) - ], - ), - ), - ), body: _buildBody(), ); } @@ -47,8 +27,8 @@ class CompletedGameScreen extends StatelessWidget { margin: const EdgeInsets.only(top: 88), child: Text( "Winner", - style: context.theme.textTheme.headline2.copyWith( - color: AppColors.secondary, + style: context.theme.textTheme.headline2?.copyWith( + color: Colors.white, ), ), ), @@ -56,8 +36,8 @@ class CompletedGameScreen extends StatelessWidget { Container( margin: const EdgeInsets.only(top: 24), child: Text( - state.winner?.name, - style: context.theme.textTheme.headline4.copyWith( + state.winner.name != "" ? Player.DEFAULT_NAME : "", + style: context.theme.textTheme.headline4?.copyWith( color: Colors.white, ), ), @@ -67,30 +47,42 @@ class CompletedGameScreen extends StatelessWidget { Container( margin: const EdgeInsets.symmetric(horizontal: 24), width: double.maxFinite, - child: RaisedButton( + child: ElevatedButton( child: Text("NEW GAME"), - color: AppColors.primary, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8) + style: ElevatedButton.styleFrom( + primary: AppColors.primary, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8)), ), onPressed: () { + Analytics().logSelectContent( + contentType: 'game', itemId: 'create_new_game'); Navigator.of(context).pushReplacement(MaterialPageRoute( - builder: (context) => CreateGameScreen(sets: state.game.cardSets,) - )); + builder: (context) => CreateGameScreen())); }, ), ), Container( - margin: const EdgeInsets.only(top: 8, left: 24, right: 24, bottom: 48), + margin: const EdgeInsets.only( + top: 8, left: 24, right: 24, bottom: 48), width: double.maxFinite, - child: OutlineButton( + child: OutlinedButton( child: Text("QUIT"), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), + style: OutlinedButton.styleFrom( + textStyle: TextStyle( + color: Colors.white, + ), + primary: AppColors.primary, + // borderSide: BorderSide(color: Colors.white), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + // highlightedBorderColor: AppColors.primary, + // splashColor: AppColors.primary, ), - highlightedBorderColor: AppColors.primary, - splashColor: AppColors.primary, onPressed: () { + Analytics() + .logSelectContent(contentType: 'game', itemId: 'quit'); Navigator.of(context).pop(); }, ), diff --git a/lib/ui/game/screens/gameplay/game_play_screen.dart b/lib/ui/game/screens/gameplay/game_play_screen.dart index 5816186..46fcf30 100644 --- a/lib/ui/game/screens/gameplay/game_play_screen.dart +++ b/lib/ui/game/screens/gameplay/game_play_screen.dart @@ -1,5 +1,3 @@ -import 'package:appsagainsthumanity/data/features/cards/model/response_card.dart'; -import 'package:appsagainsthumanity/data/features/game/game_repository.dart'; import 'package:appsagainsthumanity/internal.dart'; import 'package:appsagainsthumanity/ui/game/bloc/bloc.dart'; import 'package:appsagainsthumanity/ui/game/screens/gameplay/widget/game_bottom_sheet.dart'; @@ -9,8 +7,7 @@ import 'package:appsagainsthumanity/ui/game/screens/gameplay/widget/player_list. import 'package:appsagainsthumanity/ui/game/screens/gameplay/widget/player_response_picker.dart'; import 'package:appsagainsthumanity/ui/game/screens/gameplay/widget/prompt_container.dart'; import 'package:appsagainsthumanity/ui/game/screens/gameplay/widget/re_deal_button.dart'; -import 'package:appsagainsthumanity/ui/game/screens/gameplay/widget/response_card_view.dart'; -import 'package:appsagainsthumanity/ui/game/screens/gameplay/widget/turn_winner_sheet.dart'; +import 'package:appsagainsthumanity/ui/game/screens/gameplay/widget/winning/turn_winner_sheet.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; @@ -31,7 +28,6 @@ class _GamePlayScreenState extends State { bottomNavigationBar: BottomAppBar( notchMargin: 8, shape: CircularNotchedRectangle(), - color: AppColors.primary, child: Container( height: 56, child: Row( @@ -40,7 +36,7 @@ class _GamePlayScreenState extends State { margin: const EdgeInsets.only(left: 8), child: IconButton( icon: Icon(Icons.close), - color: Colors.black87, + color: Colors.white, onPressed: () { Navigator.of(context).pop(); }, @@ -64,7 +60,7 @@ class _GamePlayScreenState extends State { ), IconButton( icon: Icon(MdiIcons.accountGroup), - color: Colors.black87, + color: Colors.white, onPressed: () { _showPlayerBottomSheet(context); }, @@ -77,23 +73,15 @@ class _GamePlayScreenState extends State { ), ), body: BlocListener( - condition: (previous, current) { - return current.game.turn?.winner != previous.game.turn?.winner; + listenWhen: (previous, current) { + return current.game?.turn?.winner != previous.game?.turn?.winner; }, listener: (context, state) { // Show bottom sheet modal for the winner - var turnWinner = state.game.turn?.winner; + var turnWinner = state.game?.turn?.winner; if (turnWinner != null) { - showModalBottomSheet( - context: context, - backgroundColor: Colors.transparent, - builder: (context) { - return GameBottomSheet( - title: "Round ${state.game.round}", - child: TurnWinnerSheet(turnWinner), - ); - }, - ); + print("Showing winner sheet"); + _showWinnerBottomSheet(context, state); } }, child: _buildBody(), @@ -126,27 +114,55 @@ class _GamePlayScreenState extends State { ); } + void _showWinnerBottomSheet(BuildContext context, GameViewState state) { + Analytics().logViewItemList(itemCategory: 'turn_winner'); + showModalBottomSheet( + context: context, + backgroundColor: Colors.transparent, + isScrollControlled: true, + builder: (context) { + return DraggableScrollableSheet( + initialChildSize: 0.75, + maxChildSize: 0.97, + builder: (context, scrollController) { + return GameBottomSheet( + title: "Round ${state.game?.round}", + child: TurnWinnerSheet(state, scrollController), + ); + }, + ); + }, + ); + } + void _showPlayerBottomSheet(BuildContext context) { + Analytics().logViewItemList(itemCategory: 'players'); showModalBottomSheet( context: context, backgroundColor: Colors.transparent, + isScrollControlled: true, builder: (context) { - return GameBottomSheet( - title: "Players", - actions: [ - Container( - margin: const EdgeInsets.symmetric(horizontal: 24), - height: 56, - alignment: Alignment.center, - child: Text( - widget.state.game.gid, - style: context.theme.textTheme.headline6.copyWith( - color: AppColors.secondary, - ), - ), - ) - ], - child: PlayerList(widget.state.game), + return DraggableScrollableSheet( + maxChildSize: 0.885, + builder: (context, scrollController) { + return GameBottomSheet( + title: "Players", + actions: [ + Container( + margin: const EdgeInsets.symmetric(horizontal: 24), + height: 56, + alignment: Alignment.center, + child: Text( + widget.state.game!.gid, + style: context.theme.textTheme.headline6?.copyWith( + color: AppColors.primaryVariant, + ), + ), + ) + ], + child: PlayerList(widget.state.game!, scrollController), + ); + }, ); }); } diff --git a/lib/ui/game/screens/gameplay/widget/game_bottom_sheet.dart b/lib/ui/game/screens/gameplay/widget/game_bottom_sheet.dart index 32e51cb..abe4b61 100644 --- a/lib/ui/game/screens/gameplay/widget/game_bottom_sheet.dart +++ b/lib/ui/game/screens/gameplay/widget/game_bottom_sheet.dart @@ -2,16 +2,23 @@ import 'package:appsagainsthumanity/internal.dart'; import 'package:flutter/material.dart'; class GameBottomSheet extends StatelessWidget { - final String title; - final String subtitle; - final Widget child; - final List actions; + final String? title; + final String? subtitle; + final Widget? child; + final List? actions; + final EdgeInsets? margin; - GameBottomSheet({this.title, this.subtitle, @required this.child, this.actions}); + GameBottomSheet( + {this.title, + this.subtitle, + @required this.child, + this.actions, + this.margin}); @override Widget build(BuildContext context) { return Container( + margin: margin, child: Material( clipBehavior: Clip.hardEdge, borderRadius: BorderRadius.only( @@ -29,6 +36,8 @@ class GameBottomSheet extends StatelessWidget { elevation: 0, actions: actions, centerTitle: false, + iconTheme: context.theme.iconTheme, + // textTheme: context.theme.textTheme, leading: Container( margin: const EdgeInsets.only(left: 8), child: IconButton( @@ -54,10 +63,10 @@ class GameBottomSheet extends StatelessWidget { margin: const EdgeInsets.only(left: 8), child: Column( children: [ - Text(title), + Text(title!), Text( - subtitle, - style: context.theme.textTheme.bodyText1.copyWith( + subtitle!, + style: context.theme.textTheme.bodyText1?.copyWith( color: Colors.black38, ), ) @@ -67,10 +76,10 @@ class GameBottomSheet extends StatelessWidget { } else { return Container( margin: const EdgeInsets.only(left: 8), - child: Text(title), + child: Text(title!), ); } } - return null; + return const SizedBox(); } } diff --git a/lib/ui/game/screens/gameplay/widget/game_status_title.dart b/lib/ui/game/screens/gameplay/widget/game_status_title.dart index 8d5b042..3289396 100644 --- a/lib/ui/game/screens/gameplay/widget/game_status_title.dart +++ b/lib/ui/game/screens/gameplay/widget/game_status_title.dart @@ -10,16 +10,16 @@ class GameStatusTitle extends StatelessWidget { if (state.areWeJudge) { return _buildText( context, - !state.allResponsesSubmitted ? "Waiting for responses" : "Judge them! You are the Law!", + !state.allResponsesSubmitted ? "Waiting for responses" : "Judging", ); } else { - if (state.allResponsesSubmitted) { - return _buildText(context, "Judgement day is upon you!"); - } else if (state.haveWeSubmittedResponse) { - return _buildText(context, "Waiting on other players"); - } else { - return Container(); - } + if (state.allResponsesSubmitted) { + return _buildText(context, "Waiting for judgement!"); + } else if (state.haveWeSubmittedResponse) { + return _buildText(context, "Waiting on other players"); + } else { + return Container(); + } } }); } @@ -27,7 +27,7 @@ class GameStatusTitle extends StatelessWidget { Widget _buildText(BuildContext context, String title) { return Text( title, - style: context.theme.textTheme.headline6.copyWith(color: Colors.black87), + style: context.theme.textTheme.headline6?.copyWith(color: Colors.white), ); } } diff --git a/lib/ui/game/screens/gameplay/widget/judge/judge_bar.dart b/lib/ui/game/screens/gameplay/widget/judge/judge_bar.dart index f196402..011d378 100644 --- a/lib/ui/game/screens/gameplay/widget/judge/judge_bar.dart +++ b/lib/ui/game/screens/gameplay/widget/judge/judge_bar.dart @@ -12,7 +12,7 @@ class JudgeBar extends StatelessWidget { return BlocBuilder( builder: (context, state) { var judge = state.currentJudge; - var hasDownvoted = state.game.turn?.downvotes?.contains(state.userId) ?? false; + var hasDownvoted = state.downvotes?.contains(state.userId) ?? false; if (judge != null) { return _buildHeader( context, @@ -26,20 +26,52 @@ class JudgeBar extends StatelessWidget { ); } - Widget _buildHeader(BuildContext context, Player player, {bool hasDownvoted = false}) { + Widget _buildHeader(BuildContext context, Player player, + {bool hasDownvoted = false}) { + var playerName = player.name != "" ? Player.DEFAULT_NAME : ""; + if (playerName.trim().isEmpty) { + playerName = Player.DEFAULT_NAME; + } return ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 12), - title: Text(player.name), - subtitle: Text("Current judge", style: context.theme.textTheme.bodyText2.copyWith(color: Colors.white60)), + title: Text(playerName), + subtitle: Text("Current judge", + style: context.theme.textTheme.bodyText2 + ?.copyWith(color: Colors.white60)), leading: _buildJudgeAvatar(context, player), - trailing: IconButton( - icon: Icon( - hasDownvoted ? MdiIcons.thumbDown : MdiIcons.thumbDownOutline, - color: hasDownvoted ? AppColors.secondary : Colors.white, - ), - onPressed: () { - context.bloc().add(DownvotePrompt()); - }, + trailing: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + IconButton( + icon: Icon( + MdiIcons.humanGreeting, + color: Colors.white, + ), + onPressed: () { + Analytics() + .logSelectContent(contentType: 'action', itemId: 'wave'); + context.read().add(WaveAtPlayer(player.id)); + }, + ), + Container( + width: 8, + ), + IconButton( + icon: Icon( + hasDownvoted ? MdiIcons.thumbDown : MdiIcons.thumbDownOutline, + color: hasDownvoted ? AppColors.primary : Colors.white, + ), + onPressed: !hasDownvoted + ? () { + Analytics().logSelectContent( + contentType: 'action', itemId: 'downvote'); + context.read().add(DownvotePrompt()); + } + : null, + ) + ], ), ); } diff --git a/lib/ui/game/screens/gameplay/widget/judge/judge_dredd.dart b/lib/ui/game/screens/gameplay/widget/judge/judge_dredd.dart index 4849c80..10c7f05 100644 --- a/lib/ui/game/screens/gameplay/widget/judge/judge_dredd.dart +++ b/lib/ui/game/screens/gameplay/widget/judge/judge_dredd.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:appsagainsthumanity/data/features/game/game_repository.dart'; import 'package:appsagainsthumanity/internal.dart'; import 'package:appsagainsthumanity/ui/game/bloc/bloc.dart'; import 'package:appsagainsthumanity/ui/game/screens/gameplay/widget/judge/judging_pager.dart'; @@ -24,7 +23,7 @@ class _JudgeDreddState extends State { @override void initState() { super.initState(); - controller.totalPageCount = widget.state.game.turn?.responses?.length ?? 0; + controller.totalPageCount = widget.state.game?.turn?.responses.length ?? 0; } @override @@ -54,16 +53,19 @@ class _JudgeDreddState extends State { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - _buildPageButton(context: context, iconData: Icons.keyboard_arrow_left, isVisible: showLeft), - + _buildPageButton( + context: context, + iconData: Icons.keyboard_arrow_left, + isVisible: showLeft), if (widget.state.isSubmitting) _buildPickingWinnerIndicator(context), - if (!widget.state.isSubmitting) _buildPickWinnerButton(context), - _buildPageButton( - context: context, iconData: Icons.keyboard_arrow_right, isLeft: false, isVisible: showRight), + context: context, + iconData: Icons.keyboard_arrow_right, + isLeft: false, + isVisible: showRight), ], ); }), @@ -74,28 +76,33 @@ class _JudgeDreddState extends State { } Widget _buildPickWinnerButton(BuildContext context) { - return RaisedButton.icon( - padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), - shape: StadiumBorder(), - color: AppColors.secondary, + return ElevatedButton.icon( + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), + shape: StadiumBorder(), + primary: AppColors.primary, + ), onPressed: () async { var currentPlayerResponse = controller.currentPlayerResponse; if (currentPlayerResponse != null) { + Analytics() + .logSelectContent(contentType: 'judge', itemId: 'pick_winner'); print("Winner selected! ${currentPlayerResponse.playerId}"); - context.bloc() + context + .read() .add(PickWinner(currentPlayerResponse.playerId)); } }, icon: Icon( MdiIcons.crown, - color: Colors.black87, + color: AppColors.colorOnPrimary, ), label: Container( margin: const EdgeInsets.only(left: 16, right: 40), child: Text( "WINNER", - style: context.theme.textTheme.button.copyWith( - color: Colors.black87, + style: context.theme.textTheme.button?.copyWith( + color: AppColors.colorOnPrimary, letterSpacing: 1, ), ), @@ -104,34 +111,36 @@ class _JudgeDreddState extends State { } Widget _buildPickingWinnerIndicator(BuildContext context) { - return RaisedButton.icon( - padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), - shape: StadiumBorder(), - color: AppColors.secondary, - disabledColor: AppColors.secondary, + return ElevatedButton.icon( + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), + shape: StadiumBorder(), + primary: AppColors.primary, + ), icon: Container( width: 24, height: 24, child: CircularProgressIndicator( - valueColor: AlwaysStoppedAnimation(AppColors.primary), + valueColor: AlwaysStoppedAnimation(AppColors.colorOnPrimary), ), ), label: Container( margin: const EdgeInsets.only(left: 16, right: 40), child: Text( "SUBMITTING...", - style: context.theme.textTheme.button.copyWith( - color: Colors.black87, + style: context.theme.textTheme.button?.copyWith( + color: AppColors.colorOnPrimary, letterSpacing: 1, ), ), ), + onPressed: () {}, ); } Widget _buildPageButton({ - @required BuildContext context, - @required IconData iconData, + required BuildContext context, + required IconData iconData, isVisible = true, isLeft = true, }) { @@ -142,7 +151,7 @@ class _JudgeDreddState extends State { height: 48, width: 56, child: Material( - color: AppColors.secondary, + color: AppColors.primary, clipBehavior: Clip.hardEdge, shape: RoundedRectangleBorder( borderRadius: isLeft @@ -159,8 +168,12 @@ class _JudgeDreddState extends State { onTap: isVisible ? () { if (isLeft) { + Analytics().logSelectContent( + contentType: 'judge', itemId: 'previous_choice'); controller.prevPage(); } else { + Analytics().logSelectContent( + contentType: 'judge', itemId: 'next_choice'); controller.nextPage(); } } @@ -169,7 +182,7 @@ class _JudgeDreddState extends State { alignment: Alignment.center, child: Icon( iconData, - color: Colors.black87, + color: AppColors.colorOnPrimary, ), ), ), @@ -182,10 +195,11 @@ class _JudgeDreddState extends State { class JudgementController { static const double VIEWPORT_FRACTION = 0.945; - final PageController pageController = PageController(viewportFraction: VIEWPORT_FRACTION); + final PageController pageController = + PageController(viewportFraction: VIEWPORT_FRACTION); final StreamController _pageChanges = StreamController.broadcast(); - PlayerResponse _currentPlayerResponse; + late PlayerResponse _currentPlayerResponse; PlayerResponse get currentPlayerResponse => _currentPlayerResponse; diff --git a/lib/ui/game/screens/gameplay/widget/judge/judging_pager.dart b/lib/ui/game/screens/gameplay/widget/judge/judging_pager.dart index 43d05c6..6c666a6 100644 --- a/lib/ui/game/screens/gameplay/widget/judge/judging_pager.dart +++ b/lib/ui/game/screens/gameplay/widget/judge/judging_pager.dart @@ -1,4 +1,7 @@ +import 'dart:math'; + import 'package:appsagainsthumanity/data/features/cards/model/response_card.dart'; +import 'package:appsagainsthumanity/internal.dart'; import 'package:appsagainsthumanity/ui/game/bloc/bloc.dart'; import 'package:appsagainsthumanity/ui/game/screens/gameplay/widget/judge/judge_dredd.dart'; import 'package:appsagainsthumanity/ui/game/screens/gameplay/widget/judge/player_response.dart'; @@ -10,30 +13,38 @@ class JudgingPager extends StatelessWidget { final JudgementController controller; JudgingPager({ - @required this.state, - @required this.controller, + required this.state, + required this.controller, }); @override Widget build(BuildContext context) { - var responses = state.game.turn?.responses ?? Map>(); - var playerResponses = responses.entries.map((e) => PlayerResponse(e.key, e.value.toList())).toList(); - playerResponses.shuffle(); - controller.setCurrentResponse(playerResponses[0], 0, playerResponses.length); - return PageView.builder( - controller: controller.pageController, - itemCount: playerResponses.length, - itemBuilder: (context, index) { - var response = playerResponses[index]; - return Container( - margin: const EdgeInsets.symmetric(horizontal: 4), - child: buildResponseCardStack(response.responses), - ); - }, - onPageChanged: (index) { - var playerResponse = playerResponses[index]; - controller.setCurrentResponse(playerResponse, index, playerResponses.length); - }, - ); + var responses = + state.game?.turn?.responses ?? Map>(); + var playerResponses = responses.entries + .map((e) => PlayerResponse(e.key, e.value.toList())) + .toList(); + playerResponses.shuffle( + Random(state.game?.round ?? DateTime.now().millisecondsSinceEpoch)); + controller.setCurrentResponse( + playerResponses[0], 0, playerResponses.length); + return PageView.builder( + controller: controller.pageController, + itemCount: playerResponses.length, + itemBuilder: (context, index) { + var response = playerResponses[index]; + return Container( + margin: const EdgeInsets.symmetric(horizontal: 4), + child: buildResponseCardStack(response.responses), + ); + }, + onPageChanged: (index) { + Analytics().logSelectContent( + contentType: 'judge', itemId: 'response_change_$index'); + var playerResponse = playerResponses[index]; + controller.setCurrentResponse( + playerResponse, index, playerResponses.length); + }, + ); } } diff --git a/lib/ui/game/screens/gameplay/widget/player_item.dart b/lib/ui/game/screens/gameplay/widget/player_item.dart index 7f3fedf..c75aabb 100644 --- a/lib/ui/game/screens/gameplay/widget/player_item.dart +++ b/lib/ui/game/screens/gameplay/widget/player_item.dart @@ -1,57 +1,147 @@ import 'package:appsagainsthumanity/data/features/game/model/player.dart'; +import 'package:appsagainsthumanity/data/firestore.dart'; import 'package:appsagainsthumanity/internal.dart'; +import 'package:appsagainsthumanity/ui/game/bloc/bloc.dart'; import 'package:appsagainsthumanity/ui/widgets/player_circle_avatar.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; class PlayerItem extends StatelessWidget { final Player player; final bool isJudge; + final bool isOwner; + final bool isSelf; + final bool isKicking; final bool hasDownvoted; - PlayerItem(this.player, {this.isJudge = false, this.hasDownvoted = false}); + PlayerItem( + this.player, { + this.isJudge = false, + this.isOwner = false, + bool isSelf = false, + this.isKicking = false, + this.hasDownvoted = false, + }) : isSelf = + isSelf || player.id == FirebaseConstants.DOCUMENT_RANDO_CARDRISSIAN; @override Widget build(BuildContext context) { - return ListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 4), - onTap: () {}, - title: Text( - player.name ?? Player.DEFAULT_NAME, - style: context.theme.textTheme.subtitle1.copyWith(color: Colors.white), + if (player.isRandoCardrissian || isSelf) { + return _buildPlayerListTile(context); + } else { + return Dismissible( + key: ValueKey(player.id + "_dismissible"), + direction: DismissDirection.endToStart, + confirmDismiss: (direction) async { + Analytics().logSelectContent(contentType: 'action', itemId: 'wave'); + context.read().add(WaveAtPlayer(player.id)); + return false; + }, + background: Container( + color: AppColors.primary, + alignment: Alignment.centerRight, + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Icon( + MdiIcons.humanGreeting, + color: Colors.white, + ), ), - subtitle: isJudge - ? Text( - "Judge", - style: context.theme.textTheme.bodyText2.copyWith( - color: AppColors.secondary, - ), - ) - : null, - trailing: Container( - child: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (hasDownvoted) - Container( - margin: const EdgeInsets.only(right: 16), - child: Icon( - MdiIcons.thumbDown, - color: AppColors.secondary, - ), - ), - Icon(MdiIcons.cardsPlayingOutline), + child: _buildPlayerListTile(context), + ); + } + } + + Widget _buildPlayerListTile(BuildContext context) { + var playerName = player.name == "" ? Player.DEFAULT_NAME : ""; + if (playerName.trim().isEmpty) { + playerName = Player.DEFAULT_NAME; + } + return ListTile( + contentPadding: EdgeInsets.only( + left: isOwner ? 8 : 24, + right: 24, + top: 4, + bottom: 4, + ), + onTap: () {}, + title: Text( + playerName, + style: context.theme.textTheme.subtitle1?.copyWith(color: Colors.white), + ), + subtitle: isJudge + ? Text( + "Judge", + style: context.theme.textTheme.bodyText2?.copyWith( + color: AppColors.primaryVariant, + ), + ) + : null, + trailing: Container( + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (hasDownvoted) Container( - padding: const EdgeInsets.only(left: 16), - child: Text( - "${player.prizes?.length ?? 0}", - style: context.theme.textTheme.subtitle1.copyWith(fontWeight: FontWeight.w600), + margin: const EdgeInsets.only(right: 16), + child: Icon( + MdiIcons.thumbDown, + color: AppColors.primaryVariant, ), - ) - ], - ), + ), + Icon( + MdiIcons.cardsPlayingOutline, + color: Colors.white70, + ), + Container( + padding: const EdgeInsets.only(left: 16), + child: Text( + "${player.prizes?.length ?? 0}", + style: context.theme.textTheme.subtitle1 + ?.copyWith(fontWeight: FontWeight.w600), + ), + ) + ], ), - leading: PlayerCircleAvatar(player: player)); + ), + leading: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (isOwner) + Container( + width: kMinInteractiveDimension, + height: kMinInteractiveDimension, + margin: const EdgeInsets.only(right: 8), + child: isKicking + ? Container( + padding: const EdgeInsets.all(12), + child: CircularProgressIndicator( + strokeWidth: 3, + ), + ) + : Visibility( + visible: !isSelf, + maintainSize: true, + maintainState: true, + maintainAnimation: true, + child: IconButton( + icon: Icon( + MdiIcons.karate, + color: Colors.white, + ), + onPressed: () { + // Kick Player + Analytics().logSelectContent( + contentType: 'action', itemId: 'kick_player'); + context.read().add(KickPlayer(player.id)); + }, + ), + ), + ), + PlayerCircleAvatar(player: player), + ], + ), + ); } } diff --git a/lib/ui/game/screens/gameplay/widget/player_list.dart b/lib/ui/game/screens/gameplay/widget/player_list.dart index 0753d4c..33ef8da 100644 --- a/lib/ui/game/screens/gameplay/widget/player_list.dart +++ b/lib/ui/game/screens/gameplay/widget/player_list.dart @@ -1,31 +1,38 @@ import 'package:appsagainsthumanity/data/features/game/model/game.dart'; +import 'package:appsagainsthumanity/data/features/game/model/game_state.dart'; import 'package:appsagainsthumanity/ui/game/bloc/bloc.dart'; import 'package:appsagainsthumanity/ui/game/screens/gameplay/widget/player_item.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class PlayerList extends StatelessWidget { - final Game initialGame; + final Game? initialGame; + final ScrollController? scrollController; - PlayerList(this.initialGame); + PlayerList(this.initialGame, this.scrollController); @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => GameBloc(null, context.repository())..add(Subscribe(initialGame.id)), + create: (context) => GameBloc()..add(Subscribe(initialGame!.id!)), child: BlocBuilder( builder: (context, state) { - var players = state.players ?? []; + var players = (state.players ?? []) + .where((element) => element.isInactive != true) + .toList(); return ListView.builder( + controller: scrollController, padding: const EdgeInsets.symmetric(vertical: 8), itemCount: players.length, itemBuilder: (context, index) { var player = players[index]; - var isJudge = player.id == state.game.turn?.judgeId; - var hasDownvoted = state.game.turn?.downvotes?.contains(player.id) ?? false; + var isJudge = player.id == state.game?.turn?.judgeId; + var hasDownvoted = state.downvotes?.contains(player.id) ?? false; return PlayerItem( player, isJudge: isJudge, + isOwner: state.isOurGame, + isSelf: state.userId == player.id, hasDownvoted: hasDownvoted, ); }, diff --git a/lib/ui/game/screens/gameplay/widget/player_response_picker.dart b/lib/ui/game/screens/gameplay/widget/player_response_picker.dart index 2328593..6099304 100644 --- a/lib/ui/game/screens/gameplay/widget/player_response_picker.dart +++ b/lib/ui/game/screens/gameplay/widget/player_response_picker.dart @@ -11,7 +11,8 @@ class PlayerResponsePicker extends StatefulWidget { } class _PlayerResponsePickerState extends State { - final PageController _pageController = PageController(viewportFraction: 0.945); + final PageController _pageController = + PageController(viewportFraction: 0.945); @override Widget build(BuildContext context) { @@ -20,7 +21,6 @@ class _PlayerResponsePickerState extends State { // Determine if we need to show the response picker, or to hide this part if (!state.areWeJudge && !state.haveWeSubmittedResponse) { // Get the player's current hand, omitting any card's they MAY have submitted - var hand = state.currentHand.reversed.toList(); return Stack( children: [ @@ -44,7 +44,7 @@ class _PlayerResponsePickerState extends State { Align( alignment: Alignment.bottomCenter, child: Container( - margin: const EdgeInsets.only(bottom: 24), + margin: const EdgeInsets.only(bottom: 32), child: _buildSubmittingWidget(context), ), ), @@ -52,7 +52,7 @@ class _PlayerResponsePickerState extends State { Align( alignment: Alignment.bottomCenter, child: Container( - margin: const EdgeInsets.only(bottom: 24), + margin: const EdgeInsets.only(bottom: 32), child: _buildSubmitCardsButton(context), ), ) @@ -65,25 +65,27 @@ class _PlayerResponsePickerState extends State { } Widget _buildSubmittingWidget(BuildContext context) { - return RaisedButton.icon( - padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), - shape: StadiumBorder(), - color: AppColors.secondary, - disabledColor: AppColors.secondary, + return ElevatedButton.icon( + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), + shape: StadiumBorder(), + primary: AppColors.primary, + // disabledColor: AppColors.primary, + ), onPressed: null, icon: Container( width: 24, height: 24, child: CircularProgressIndicator( - valueColor: AlwaysStoppedAnimation(AppColors.primary), + valueColor: AlwaysStoppedAnimation(AppColors.colorOnPrimary), ), ), label: Container( margin: const EdgeInsets.only(left: 8, right: 20), child: Text( "SUBMITTING...", - style: context.theme.textTheme.button.copyWith( - color: Colors.black87, + style: context.theme.textTheme.button?.copyWith( + color: AppColors.colorOnPrimary, letterSpacing: 1, ), ), @@ -92,23 +94,27 @@ class _PlayerResponsePickerState extends State { } Widget _buildSubmitCardsButton(BuildContext context) { - return RaisedButton.icon( - padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), - shape: StadiumBorder(), - color: AppColors.secondary, + return ElevatedButton.icon( + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), + shape: StadiumBorder(), + primary: AppColors.primary, + ), onPressed: () async { - context.bloc().add(SubmitResponses()); + Analytics().logSelectContent( + contentType: 'action', itemId: 'submit_responses'); + context.read().add(SubmitResponses()); }, icon: Icon( MdiIcons.uploadMultiple, - color: Colors.black87, + color: AppColors.colorOnPrimary, ), label: Container( margin: const EdgeInsets.only(left: 8, right: 20), child: Text( "SUBMIT RESPONSE", - style: context.theme.textTheme.button.copyWith( - color: Colors.black87, + style: context.theme.textTheme.button?.copyWith( + color: AppColors.colorOnPrimary, letterSpacing: 1, ), ), @@ -120,10 +126,10 @@ class _PlayerResponsePickerState extends State { class HandCard extends StatelessWidget { static const textPadding = 20.0; - final ResponseCard card; + final ResponseCard? card; HandCard({ - Key key, + Key? key, this.card, }) : super(key: key); @@ -133,10 +139,10 @@ class HandCard extends StatelessWidget { margin: const EdgeInsets.symmetric(horizontal: 4), width: double.maxFinite, child: Material( - color: AppColors.responseCardBackground, + color: context.responseCardHandColor, shape: RoundedRectangleBorder( side: BorderSide( - color: Colors.black12, + color: context.responseBorderColor, width: 1.0, ), borderRadius: BorderRadius.only( @@ -151,13 +157,15 @@ class HandCard extends StatelessWidget { highlightColor: AppColors.primary.withOpacity(0.26), splashColor: AppColors.primary.withOpacity(0.26), onTap: () { - context.bloc().add(PickResponseCard(card)); + Analytics().logSelectContent( + contentType: 'action', itemId: 'picked_response'); + context.read().add(PickResponseCard(card!)); }, child: Column( children: [ - _buildText(context, card.text), + _buildText(context, card!.text), Expanded( - child: _buildCaptionText(context, card.set), + child: _buildCaptionText(context, card!.set), ), ], ), @@ -172,9 +180,7 @@ class HandCard extends StatelessWidget { margin: const EdgeInsets.all(textPadding), child: Text( text, - style: context.theme.textTheme.headline5.copyWith( - color: Colors.black87, - ), + style: context.cardTextStyle(context.colorOnCard), ), ); } @@ -182,12 +188,15 @@ class HandCard extends StatelessWidget { Widget _buildCaptionText(BuildContext context, String text) { return Container( width: double.maxFinite, - margin: const EdgeInsets.all(textPadding), + margin: const EdgeInsets.only(right: textPadding, bottom: 16), alignment: Alignment.bottomRight, child: Text( text, textAlign: TextAlign.end, - style: context.theme.textTheme.caption.copyWith(color: Colors.black26, fontStyle: FontStyle.italic), + style: context.theme.textTheme.caption?.copyWith( + color: context.secondaryColorOnCard, + fontStyle: FontStyle.italic, + ), ), ); } diff --git a/lib/ui/game/screens/gameplay/widget/prompt_container.dart b/lib/ui/game/screens/gameplay/widget/prompt_container.dart index 89b6ce0..fd7573b 100644 --- a/lib/ui/game/screens/gameplay/widget/prompt_container.dart +++ b/lib/ui/game/screens/gameplay/widget/prompt_container.dart @@ -2,11 +2,11 @@ import 'package:appsagainsthumanity/data/features/cards/model/prompt_card.dart'; import 'package:appsagainsthumanity/ui/game/bloc/bloc.dart'; import 'package:appsagainsthumanity/internal.dart'; import 'package:appsagainsthumanity/ui/game/screens/gameplay/widget/judge/judge_dredd.dart'; -import 'package:appsagainsthumanity/ui/game/screens/gameplay/widget/judge/judging_pager.dart'; import 'package:appsagainsthumanity/ui/game/screens/gameplay/widget/response_card_view.dart'; import 'package:appsagainsthumanity/ui/game/screens/gameplay/widget/waiting_player_responses.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; class PromptContainer extends StatelessWidget { static const textPadding = 20.0; @@ -15,23 +15,21 @@ class PromptContainer extends StatelessWidget { Widget build(BuildContext context) { // We only want this block builder to update when the prompt changes return BlocBuilder( - condition: (previous, current) { - return previous.game.turn?.promptCard != current.game.turn?.promptCard; + buildWhen: (previous, current) { + return previous.game?.turn?.promptCard != + current.game?.turn?.promptCard; }, builder: (context, state) { - var prompt = state.game.turn?.promptCard; + var prompt = state.game?.turn?.promptCard; return Container( margin: const EdgeInsets.only(top: 8), child: Column( children: [ - _buildPromptSpecial(context, prompt), + _buildPromptSpecial(context, prompt!), Expanded( child: Stack( children: [ - _buildPromptBackground( - context: context, - card: prompt, - ), + _buildPromptBackground(context), Column( children: [ _buildPromptText(context, state), @@ -73,11 +71,42 @@ class PromptContainer extends StatelessWidget { if (state.haveWeSubmittedResponse) { return WaitingPlayerResponses(state); } else { - var responseCardStack = buildResponseCardStack(state.selectedCards); + var responseCardStack = buildResponseCardStack(state.selectedCards!); if (responseCardStack != null) { - return Container( - margin: const EdgeInsets.symmetric(horizontal: 16), - child: responseCardStack, + return Dismissible( + key: Key("responses"), + direction: DismissDirection.down, + movementDuration: Duration(milliseconds: 0), + background: Column( + children: [ + Container( + margin: const EdgeInsets.only(top: 24), + alignment: Alignment.topCenter, + child: Text( + "Clear Responses".toUpperCase(), + style: context.theme.textTheme.subtitle1!.copyWith( + color: context.primaryColor, + fontWeight: FontWeight.w600), + ), + ), + Container( + margin: const EdgeInsets.only(top: 4), + child: Icon( + MdiIcons.chevronTripleDown, + color: context.primaryColor, + ), + ), + ], + ), + onDismissed: (direction) { + Analytics().logSelectContent( + contentType: 'action', itemId: 'clear_choices'); + context.read().add(ClearPickedResponseCards()); + }, + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + child: responseCardStack, + ), ); } else { return Container(); @@ -88,14 +117,16 @@ class PromptContainer extends StatelessWidget { } Widget _buildPromptSpecial(BuildContext context, PromptCard promptCard) { - if (promptCard != null && promptCard.special != null && promptCard.special.isNotEmpty) { + if (promptCard != null && + promptCard.special != "" && + promptCard.special.isNotEmpty) { return Container( height: 36, alignment: Alignment.centerRight, margin: const EdgeInsets.symmetric(horizontal: 16), child: Text( promptCard.special.toUpperCase(), - style: context.theme.textTheme.subtitle2.copyWith( + style: context.theme.textTheme.subtitle2?.copyWith( color: Colors.white, ), ), @@ -105,7 +136,7 @@ class PromptContainer extends StatelessWidget { } } - Widget _buildPromptBackground({@required BuildContext context, @required PromptCard card}) { + Widget _buildPromptBackground(BuildContext context) { return Container( margin: const EdgeInsets.symmetric(horizontal: 16), child: Material( @@ -123,18 +154,27 @@ class PromptContainer extends StatelessWidget { } Widget _buildPromptText(BuildContext context, GameViewState state) { - return Container( - width: double.maxFinite, - margin: const EdgeInsets.symmetric(vertical: textPadding, horizontal: textPadding + 16), - child: BlocBuilder( - builder: (context, state) { - return Text( - state.currentPromptText, - style: context.theme.textTheme.headline5.copyWith( - color: Colors.white, - ), - ); - }, + return GestureDetector( + onLongPress: () { + Analytics().logSelectContent( + contentType: 'action', itemId: 'view_prompt_source'); + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(state.game?.turn?.promptCard.set ?? ""), + behavior: SnackBarBehavior.floating, + )); + }, + child: Container( + width: double.maxFinite, + margin: const EdgeInsets.symmetric( + vertical: textPadding, horizontal: textPadding + 16), + child: BlocBuilder( + builder: (context, state) { + return Text( + state.currentPromptText, + style: context.cardTextStyle(Colors.white), + ); + }, + ), ), ); } diff --git a/lib/ui/game/screens/gameplay/widget/re_deal_button.dart b/lib/ui/game/screens/gameplay/widget/re_deal_button.dart index 94ed5b5..da4e783 100644 --- a/lib/ui/game/screens/gameplay/widget/re_deal_button.dart +++ b/lib/ui/game/screens/gameplay/widget/re_deal_button.dart @@ -10,51 +10,63 @@ class ReDealButton extends StatelessWidget { Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { - if (!state.areWeJudge && !state.haveWeSubmittedResponse && (state.currentPlayer?.prizes?.length ?? 0) > 0) { + if (!state.areWeJudge && + !state.haveWeSubmittedResponse && + (state.currentPlayer.prizes?.length ?? 0) > 0) { return IconButton( icon: Icon(MdiIcons.cardsVariant), tooltip: "Re-deal your hand", - color: Colors.black87, + color: Colors.white, onPressed: () async { var result = await showDialog( - context: context, + context: context, builder: (context) { return AlertDialog( - title: Text('Deal new hand?'), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - content: RichText( - text: TextSpan( - text: 'Spend ', - children: [ - TextSpan(text: '1 of ${state.currentPlayer.prizes.length} prize cards', style: TextStyle(fontWeight: FontWeight.bold)), - TextSpan(text: ' to deal you a new hand?') - ] + title: Text('Deal new hand?'), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + content: RichText( + text: TextSpan(text: 'Spend ', children: [ + TextSpan( + text: + '1 of ${state.currentPlayer.prizes?.length} prize cards', + style: TextStyle(fontWeight: FontWeight.bold)), + TextSpan(text: ' to deal you a new hand?') + ]), + ), + actions: [ + TextButton( + child: Text( + 'CANCEL', + style: TextStyle( + color: AppColors.secondary, ), + ), + onPressed: () { + Navigator.of(context).pop(false); + }, ), - actions: [ - FlatButton( - child: Text('CANCEL'), - textColor: AppColors.secondary, - onPressed: () { - Navigator.of(context).pop(false); - }, - ), - FlatButton( - child: Text('DEAL'), - textColor: AppColors.secondary, - onPressed: () { - Navigator.of(context).pop(true); - }, + TextButton( + child: Text( + 'DEAL', + style: TextStyle( + color: AppColors.secondary, ), - ], + ), + onPressed: () { + Navigator.of(context).pop(true); + }, + ), + ], ); - } - ); + }); if (result ?? false) { - await context.repository() - .reDealHand(state.game.id); + Analytics().logSelectContent( + contentType: 'action', itemId: 'redeal_hand'); + await context + .read() + .reDealHand(state.game!.id!); } }, ); diff --git a/lib/ui/game/screens/gameplay/widget/response_card_view.dart b/lib/ui/game/screens/gameplay/widget/response_card_view.dart index fc65cf0..bd4f1ea 100644 --- a/lib/ui/game/screens/gameplay/widget/response_card_view.dart +++ b/lib/ui/game/screens/gameplay/widget/response_card_view.dart @@ -4,13 +4,13 @@ import 'package:appsagainsthumanity/internal.dart'; class ResponseCardView extends StatelessWidget { final ResponseCard card; - final Widget child; + final Widget? child; static const textPadding = 20.0; ResponseCardView({ - Key key, - @required this.card, + Key? key, + required this.card, this.child, }) : super(key: key); @@ -19,11 +19,11 @@ class ResponseCardView extends StatelessWidget { return Container( width: double.maxFinite, child: Material( - color: AppColors.responseCardBackground, - shadowColor: AppColors.responseCardBackground, + color: context.responseCardColor, + shadowColor: context.responseCardColor, shape: RoundedRectangleBorder( side: BorderSide( - color: Colors.black12, + color: context.responseBorderColor, width: 1.0, ), borderRadius: BorderRadius.only( @@ -37,7 +37,7 @@ class ResponseCardView extends StatelessWidget { _buildText(context, card.text), if (child != null) Expanded( - child: child, + child: child ?? const SizedBox(), ), ], ), @@ -51,15 +51,13 @@ class ResponseCardView extends StatelessWidget { margin: const EdgeInsets.all(textPadding), child: Text( text, - style: context.theme.textTheme.headline5.copyWith( - color: Colors.black87, - ), + style: context.cardTextStyle(context.colorOnCard), ), ); } } -Widget buildResponseCardStack(List cards) { +Widget buildResponseCardStack(List cards, {Widget? lastChild}) { if (cards.isNotEmpty) { var nextCard = cards.first; var remaining = cards.sublist(1); @@ -67,10 +65,10 @@ Widget buildResponseCardStack(List cards) { key: ValueKey(nextCard), card: nextCard, child: remaining.isNotEmpty - ? buildResponseCardStack(remaining) - : null, + ? buildResponseCardStack(remaining, lastChild: lastChild) + : lastChild, ); } else { - return null; + return const SizedBox(); } } diff --git a/lib/ui/game/screens/gameplay/widget/turn_winner_sheet.dart b/lib/ui/game/screens/gameplay/widget/turn_winner_sheet.dart deleted file mode 100644 index 8f9c878..0000000 --- a/lib/ui/game/screens/gameplay/widget/turn_winner_sheet.dart +++ /dev/null @@ -1,67 +0,0 @@ -import 'package:appsagainsthumanity/data/features/game/model/turn_winner.dart'; -import 'package:appsagainsthumanity/internal.dart'; -import 'package:appsagainsthumanity/ui/game/screens/gameplay/widget/response_card_view.dart'; -import 'package:appsagainsthumanity/ui/widgets/player_circle_avatar.dart'; -import 'package:flutter/material.dart'; - -class TurnWinnerSheet extends StatelessWidget { - final TurnWinner turnWinner; - - TurnWinnerSheet(this.turnWinner); - - @override - Widget build(BuildContext context) { - return SizedBox.expand( - child: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - margin: const EdgeInsets.only(bottom: 16), - child: Text( - "Winner!", - style: context.theme.textTheme.headline3.copyWith( - color: Colors.white, - ), - ), - ), - Container( - child: _buildAvatar(context), - ), - Container( - margin: const EdgeInsets.only(top: 16), - child: Text( - turnWinner.playerName, - style: context.theme.textTheme.headline5.copyWith( - color: Colors.white, - ), - ), - ), - Container( - height: 400, - margin: const EdgeInsets.only(left: 32, right: 32, top: 24), - child: buildResponseCardStack(turnWinner.response), - ) - ], - ), - ), - ); - } - - Widget _buildAvatar(BuildContext context) { - return turnWinner.isRandoCardrissian - ? CircleAvatar( - backgroundImage: AssetImage("assets/rando_cardrissian.png"), - radius: 40, - ) - : CircleAvatar( - radius: 40, - backgroundImage: turnWinner.playerAvatarUrl != null ? NetworkImage(turnWinner.playerAvatarUrl) : null, - backgroundColor: AppColors.primary, - child: turnWinner.playerAvatarUrl == null - ? Text(turnWinner.playerName.split(' ').map((e) => e[0]).join().toUpperCase()) - : null, - ); - } -} diff --git a/lib/ui/game/screens/gameplay/widget/waiting_player_responses.dart b/lib/ui/game/screens/gameplay/widget/waiting_player_responses.dart index eb33218..a53418d 100644 --- a/lib/ui/game/screens/gameplay/widget/waiting_player_responses.dart +++ b/lib/ui/game/screens/gameplay/widget/waiting_player_responses.dart @@ -12,7 +12,17 @@ class WaitingPlayerResponses extends StatelessWidget { @override Widget build(BuildContext context) { - var players = state.players?.where((element) => element.id != state.game.turn?.judgeId)?.toList() ?? []; + var players = state.players?.where((element) { + return element.id != state.game?.turn?.judgeId && + element.isInactive != true; + }).toList() ?? + []; + + var columnCount = 3; + if (MediaQuery.of(context).size.width >= 600) { + columnCount = 9; + } + return Container( margin: const EdgeInsets.symmetric(horizontal: 16), child: Column( @@ -22,13 +32,15 @@ class WaitingPlayerResponses extends StatelessWidget { child: GridView.builder( padding: const EdgeInsets.only(left: 12, right: 12, top: 8), gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 3, + crossAxisCount: columnCount, childAspectRatio: 88 / 130, ), itemCount: players.length, itemBuilder: (context, index) { var player = players[index]; - var hasSubmittedResponse = state.game.turn?.responses?.containsKey(player.id) ?? false; + var hasSubmittedResponse = + state.game?.turn?.responses.containsKey(player.id) ?? + false; return PlayerResponseCard(player, hasSubmittedResponse); }), ), @@ -50,7 +62,7 @@ class PlayerResponseCard extends StatelessWidget { margin: const EdgeInsets.all(8), child: Material( borderRadius: BorderRadius.circular(16), - color: Colors.white, + color: context.theme.cardColor, elevation: 2, child: Column( mainAxisAlignment: MainAxisAlignment.center, @@ -67,8 +79,8 @@ class PlayerResponseCard extends StatelessWidget { textAlign: TextAlign.center, maxLines: 2, overflow: TextOverflow.ellipsis, - style: context.theme.textTheme.bodyText2.copyWith( - color: Colors.black87, + style: context.theme.textTheme.bodyText2?.copyWith( + color: context.colorOnCard, ), ), ), @@ -79,12 +91,12 @@ class PlayerResponseCard extends StatelessWidget { child: hasSubmittedResponse ? Icon( MdiIcons.checkboxMarkedCircleOutline, - color: AppColors.primary, + color: context.primaryColor, size: 32, ) : Icon( MdiIcons.checkboxBlankCircleOutline, - color: AppColors.primary, + color: context.secondaryColorOnCard, size: 32, ), ), diff --git a/lib/ui/game/screens/gameplay/widget/winning/other_responses_pager.dart b/lib/ui/game/screens/gameplay/widget/winning/other_responses_pager.dart new file mode 100644 index 0000000..090f22e --- /dev/null +++ b/lib/ui/game/screens/gameplay/widget/winning/other_responses_pager.dart @@ -0,0 +1,58 @@ +import 'dart:math'; + +import 'package:appsagainsthumanity/data/features/cards/model/response_card.dart'; +import 'package:appsagainsthumanity/ui/game/screens/gameplay/widget/judge/player_response.dart'; +import 'package:flutter/material.dart'; + +import '../response_card_view.dart'; + +class OtherResponsesPager extends StatefulWidget { + final int gameRound; + final String winningPlayerId; + final Map> responses; + + OtherResponsesPager( + this.winningPlayerId, + this.gameRound, + this.responses, + ); + + @override + _OtherResponsesPagerState createState() => _OtherResponsesPagerState(); +} + +class _OtherResponsesPagerState extends State { + static const double VIEWPORT_FRACTION = 0.93; + + final PageController pageController = + PageController(viewportFraction: VIEWPORT_FRACTION); + + @override + void dispose() { + pageController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + var _responses = + widget.responses != {} ? Map>() : {}; + var playerResponses = _responses.entries + .map((e) => PlayerResponse(e.key, e.value.toList())) + .where((element) => element.playerId != widget.winningPlayerId) + .toList(); + playerResponses.shuffle(Random( + widget.gameRound >= 0 ? DateTime.now().millisecondsSinceEpoch : 0)); + return PageView.builder( + controller: pageController, + itemCount: playerResponses.length, + itemBuilder: (context, index) { + var response = playerResponses[index]; + return Container( + margin: const EdgeInsets.symmetric(horizontal: 4), + child: buildResponseCardStack(response.responses), + ); + }, + ); + } +} diff --git a/lib/ui/game/screens/gameplay/widget/winning/prompt_card_view.dart b/lib/ui/game/screens/gameplay/widget/winning/prompt_card_view.dart new file mode 100644 index 0000000..fef9400 --- /dev/null +++ b/lib/ui/game/screens/gameplay/widget/winning/prompt_card_view.dart @@ -0,0 +1,95 @@ +import 'package:appsagainsthumanity/data/features/cards/model/prompt_card.dart'; +import 'package:appsagainsthumanity/ui/game/bloc/bloc.dart'; +import 'package:appsagainsthumanity/internal.dart'; +// import 'package:appsagainsthumanity/ui/game/screens/gameplay/widget/judge/judge_dredd.dart'; +// import 'package:appsagainsthumanity/ui/game/screens/gameplay/widget/response_card_view.dart'; +// import 'package:appsagainsthumanity/ui/game/screens/gameplay/widget/waiting_player_responses.dart'; +import 'package:flutter/material.dart'; +// import 'package:flutter_bloc/flutter_bloc.dart'; +// import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; + +class PromptCardView extends StatelessWidget { + static const textPadding = 20.0; + + final GameViewState state; + final Widget child; + final EdgeInsets margin; + + PromptCardView({ + required this.state, + required this.child, + EdgeInsets? margin, + }) : margin = margin ?? const EdgeInsets.symmetric(horizontal: 16); + + @override + Widget build(BuildContext context) { + // We only want this block builder to update when the prompt changes + var prompt = state.game?.turn?.winner?.promptCard; + return Container( + margin: const EdgeInsets.only(top: 8), + child: Column( + children: [ + Expanded( + child: Stack( + children: [ + _buildPromptBackground( + context: context, + card: prompt!, + ), + Column( + children: [ + _buildPromptText(context, state), + Expanded( + child: child, + ) + ], + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildPromptBackground( + {@required BuildContext? context, @required PromptCard? card}) { + return Container( + margin: margin, + child: Material( + color: Colors.black, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), + ), + elevation: 4, + child: Container( + height: double.maxFinite, + ), + ), + ); + } + + Widget _buildPromptText(BuildContext context, GameViewState state) { + return GestureDetector( + onLongPress: () { + Analytics().logSelectContent( + contentType: 'action', itemId: 'view_prompt_source'); + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(state.game?.turn?.winner?.promptCard?.set ?? ""), + behavior: SnackBarBehavior.floating, + )); + }, + child: Container( + width: double.maxFinite, + margin: const EdgeInsets.symmetric( + vertical: textPadding, horizontal: textPadding) + .add(margin), + child: Text( + state.lastPromptText, + style: context.cardTextStyle(Colors.white), + ), + ), + ); + } +} diff --git a/lib/ui/game/screens/gameplay/widget/winning/turn_winner_sheet.dart b/lib/ui/game/screens/gameplay/widget/winning/turn_winner_sheet.dart new file mode 100644 index 0000000..029d61b --- /dev/null +++ b/lib/ui/game/screens/gameplay/widget/winning/turn_winner_sheet.dart @@ -0,0 +1,109 @@ +import 'package:appsagainsthumanity/data/features/game/model/player.dart'; +import 'package:appsagainsthumanity/data/features/game/model/turn_winner.dart'; +import 'package:appsagainsthumanity/internal.dart'; +import 'package:appsagainsthumanity/ui/game/bloc/bloc.dart'; +import 'package:appsagainsthumanity/ui/game/screens/gameplay/widget/winning/other_responses_pager.dart'; +import 'package:appsagainsthumanity/ui/game/screens/gameplay/widget/winning/prompt_card_view.dart'; +import 'package:appsagainsthumanity/ui/game/screens/gameplay/widget/response_card_view.dart'; +import 'package:appsagainsthumanity/ui/widgets/player_circle_avatar.dart'; +import 'package:flutter/material.dart'; + +class TurnWinnerSheet extends StatelessWidget { + final GameViewState? state; + final TurnWinner? turnWinner; + final ScrollController? scrollController; + + TurnWinnerSheet(this.state, this.scrollController) + : turnWinner = state?.game?.turn?.winner; + + @override + Widget build(BuildContext context) { + var playerName = turnWinner?.playerName ?? Player.DEFAULT_NAME; + if (playerName.trim().isEmpty) { + playerName = Player.DEFAULT_NAME; + } + return SizedBox.expand( + child: SingleChildScrollView( + controller: scrollController, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + margin: const EdgeInsets.only(bottom: 16), + child: Text( + "Winner!", + style: context.theme.textTheme.headline3?.copyWith( + color: Colors.white, + ), + ), + ), + Container( + child: _buildAvatar(context), + ), + Container( + margin: const EdgeInsets.only(top: 16), + child: Text( + playerName, + style: context.theme.textTheme.headline5?.copyWith( + color: Colors.white, + ), + ), + ), + Container( + height: 850, + margin: const EdgeInsets.only(top: 24), + child: PromptCardView( + state: state!, + margin: const EdgeInsets.symmetric(horizontal: 32), + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 32), + child: buildResponseCardStack( + turnWinner!.response!, + lastChild: Column( + children: [ + Divider(), + Container( + margin: const EdgeInsets.only(top: 8, left: 20), + alignment: Alignment.centerLeft, + child: Text( + "Player Responses".toUpperCase(), + style: context.theme.textTheme.subtitle2?.copyWith( + color: AppColors.primaryVariant, + ), + ), + ), + Expanded( + child: Container( + margin: const EdgeInsets.only(top: 16), + child: OtherResponsesPager( + turnWinner!.playerId!, + state!.game!.round - 1, + turnWinner!.responses!, + ), + ), + ), + ], + ), + ), + ), + ), + ) + ], + ), + ), + ); + } + + Widget _buildAvatar(BuildContext context) { + return PlayerCircleAvatar( + radius: 40, + player: Player( + id: turnWinner!.playerId!, + name: turnWinner!.playerName!, + avatarUrl: turnWinner!.playerAvatarUrl!, + isRandoCardrissian: turnWinner!.isRandoCardrissian!, + ), + ); + } +} diff --git a/lib/ui/game/screens/starting/starting_room_screen.dart b/lib/ui/game/screens/starting/starting_room_screen.dart index cf2fc62..e451fae 100644 --- a/lib/ui/game/screens/starting/starting_room_screen.dart +++ b/lib/ui/game/screens/starting/starting_room_screen.dart @@ -1,5 +1,6 @@ import 'package:appsagainsthumanity/ui/game/bloc/bloc.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:appsagainsthumanity/internal.dart'; class StartingRoomScreen extends StatelessWidget { @@ -11,6 +12,12 @@ class StartingRoomScreen extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( + systemOverlayStyle: SystemUiOverlayStyle( + statusBarBrightness: Brightness.dark, + ), + // brightness: Brightness.dark, + // textTheme: context.theme.textTheme, + iconTheme: context.theme.iconTheme, title: Text("Game is starting..."), bottom: PreferredSize( preferredSize: Size.fromHeight(72), @@ -27,14 +34,14 @@ class StartingRoomScreen extends StatelessWidget { children: [ Text( "Game ID", - style: context.theme.textTheme.bodyText1.copyWith( + style: context.theme.textTheme.bodyText1?.copyWith( color: Colors.white70, ), ), Text( - state.game.gid, - style: context.theme.textTheme.headline4.copyWith( - color: Colors.white, + state.game!.gid, + style: context.theme.textTheme.headline4?.copyWith( + color: context.primaryColor, fontWeight: FontWeight.bold, ), ) diff --git a/lib/ui/game/screens/waiting/waiting_room_screen.dart b/lib/ui/game/screens/waiting/waiting_room_screen.dart index d2747c9..2366bfd 100644 --- a/lib/ui/game/screens/waiting/waiting_room_screen.dart +++ b/lib/ui/game/screens/waiting/waiting_room_screen.dart @@ -1,17 +1,22 @@ import 'package:appsagainsthumanity/data/features/game/game_repository.dart'; import 'package:appsagainsthumanity/data/features/game/model/player.dart'; import 'package:appsagainsthumanity/internal.dart'; +import 'package:appsagainsthumanity/internal/dynamic_links.dart'; import 'package:appsagainsthumanity/ui/game/bloc/bloc.dart'; -import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:appsagainsthumanity/ui/widgets/player_circle_avatar.dart'; +import 'package:clipboard/clipboard.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; +import 'package:share/share.dart'; +import 'package:flutter/foundation.dart' show kIsWeb; class WaitingRoomScreen extends StatelessWidget { @override Widget build(BuildContext context) { return BlocBuilder( - bloc: context.bloc(), + bloc: context.read(), builder: (context, state) { return _buildScaffold(context, state); }, @@ -21,6 +26,12 @@ class WaitingRoomScreen extends StatelessWidget { Widget _buildScaffold(BuildContext context, GameViewState state) { return Scaffold( appBar: AppBar( + systemOverlayStyle: SystemUiOverlayStyle( + statusBarBrightness: Brightness.dark, + ), + // brightness: Brightness.dark, + // textTheme: context.theme.textTheme, + iconTheme: context.theme.iconTheme, title: Text("Waiting for players"), bottom: PreferredSize( preferredSize: Size.fromHeight(72), @@ -37,32 +48,66 @@ class WaitingRoomScreen extends StatelessWidget { children: [ Text( "Game ID", - style: context.theme.textTheme.bodyText1.copyWith( + style: context.theme.textTheme.bodyText1?.copyWith( color: Colors.white70, ), ), Text( - state.game.gid, - style: context.theme.textTheme.headline4.copyWith( - color: Colors.white, + state.game!.gid, + style: context.theme.textTheme.headline4?.copyWith( + color: context.primaryColor, fontWeight: FontWeight.bold, ), ) ], ), ), + Expanded( + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Container( + margin: const EdgeInsets.only(right: 16, top: 4), + child: OutlinedButton( + child: Text("INVITE"), + style: OutlinedButton.styleFrom( + primary: context.primaryColor, + textStyle: + context.theme.textTheme.button?.copyWith( + color: context.primaryColor, + ), + side: BorderSide(color: context.primaryColor), + padding: const EdgeInsets.all(16)), + // highlightedBorderColor: context.primaryColor, + // splashColor: context.primaryColor.withOpacity(0.40), + onPressed: () async { + Analytics().logShare( + contentType: 'game', + itemId: 'invite', + method: 'dynamic_link'); + var link = + await DynamicLinks.createLink(state.game!.id!); + _shareLink(context, link.toString()); + }, + ), + ), + ], + ), + ) ], ), ), ), - actions: [ - IconButton( - icon: Icon(Icons.group_add), - onPressed: () { - // TODO: Share link (or game code) to system share mechanisms - }, - ) - ], +// actions: [ +// IconButton( +// icon: Icon(Icons.group_add), +// onPressed: () async { +// var link = await DynamicLinks.createLink(state.game.id); +// await Share.share(link.toString()); +// }, +// ) +// ], ), floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, floatingActionButton: state.isOurGame @@ -71,19 +116,20 @@ class WaitingRoomScreen extends StatelessWidget { label: Text("START GAME"), backgroundColor: AppColors.primary, onPressed: () async { - context.bloc() - .add(StartGame()); + Analytics().logSelectContent( + contentType: 'action', itemId: 'start_game'); + context.read().add(StartGame()); }) : null, body: BlocListener( - bloc: context.bloc(), + bloc: context.read(), listener: (context, state) { if (state.error != null) { - Scaffold.of(context) + context.scaffold ..hideCurrentSnackBar() ..showSnackBar( SnackBar( - content: Text(state.error), + content: Text(state.error!), backgroundColor: AppColors.primary, ), ); @@ -98,46 +144,47 @@ class WaitingRoomScreen extends StatelessWidget { /// that needs to update to the change in game state Widget _buildPlayerList(BuildContext context, GameViewState state) { var players = state.players ?? []; - var hasRandoBeenInvitedOrNotOwner = (state.players?.any((element) => element.isRandoCardrissian) ?? false) || !state.isOurGame; + var hasRandoBeenInvitedOrNotOwner = + (state.players?.any((element) => element.isRandoCardrissian) ?? + false) || + !state.isOurGame; return ListView.builder( padding: const EdgeInsets.symmetric(vertical: 8), - itemCount: hasRandoBeenInvitedOrNotOwner ? players.length : players.length + 1, + itemCount: + hasRandoBeenInvitedOrNotOwner ? players.length : players.length + 1, itemBuilder: (context, index) { if (index < players.length) { var player = players[index]; return _buildPlayerTile(context, player, index); } else { - return _buildRandoCardrissianInvite(context, state.game.id); + return _buildRandoCardrissianInvite(context, state.game!.id!); } }); } Widget _buildPlayerTile(BuildContext context, Player player, int index) { + var playerName = player.name == "" ? Player.DEFAULT_NAME : ""; + if (playerName.trim().isEmpty) { + playerName = Player.DEFAULT_NAME; + } return ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 4), onTap: () {}, title: Text( - player.name ?? Player.DEFAULT_NAME, - style: context.theme.textTheme.subtitle1.copyWith(color: Colors.white), + playerName, + style: + context.theme.textTheme.subtitle1?.copyWith(color: Colors.white), ), - leading: player.isRandoCardrissian - ? CircleAvatar(backgroundImage: AssetImage("assets/rando_cardrissian.png")) - : CircleAvatar( - radius: 20, - backgroundImage: player.avatarUrl != null ? NetworkImage(player.avatarUrl) : null, - backgroundColor: AppColors.primary, - child: player.avatarUrl == null - ? Text(player.name.split(' ').map((e) => e[0]).join().toUpperCase()) - : null, - )); + leading: PlayerCircleAvatar(player: player)); } - Widget _buildRandoCardrissianInvite(BuildContext context, String gameDocumentId) { + Widget _buildRandoCardrissianInvite( + BuildContext context, String gameDocumentId) { return ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 4), title: Text( "Invite Rando Cardrissian?", - style: context.theme.textTheme.subtitle1.copyWith( + style: context.theme.textTheme.subtitle1?.copyWith( color: Colors.white, fontWeight: FontWeight.bold, ), @@ -145,11 +192,37 @@ class WaitingRoomScreen extends StatelessWidget { leading: CircleAvatar( backgroundImage: AssetImage("assets/rando_cardrissian.png"), ), - trailing: Icon(MdiIcons.robot), + trailing: Icon(MdiIcons.robot, color: Colors.white), onTap: () async { - await context.repository() + Analytics().logSelectContent( + contentType: 'players', itemId: 'invite_rando_cardrissian'); + await context + .read() .addRandoCardrissian(gameDocumentId); }, ); } + + void _shareLink(BuildContext context, String link) async { + if (kIsWeb) { + // await shareWeb(link); + await FlutterClipboard.copy(link); + context.scaffold.showSnackBar(SnackBar( + content: Text("Link copied to clipboard!"), + )); + } else { + await Share.share(link.toString()); + } + } + + Future shareWeb(String linkToShare) async { + if (!kIsWeb) { + throw UnimplementedError('Share is only implemented on Web'); + } + + // final html.HtmlDocument doc = js.context['document']; + // final html.AnchorElement link = doc.createElement('a'); + // link.href = linkToShare; + // link.click(); + } } diff --git a/lib/ui/home/bloc/home_bloc.dart b/lib/ui/home/bloc/home_bloc.dart index 0212d28..c6632e5 100644 --- a/lib/ui/home/bloc/home_bloc.dart +++ b/lib/ui/home/bloc/home_bloc.dart @@ -8,11 +8,26 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:logging/logging.dart'; class HomeBloc extends Bloc { - UserRepository _userRepository; - GameRepository _gameRepository; - StreamSubscription _joinedGamesSubscription; + UserRepository? _userRepository; + GameRepository? _gameRepository; + StreamSubscription? _userSubscription; + StreamSubscription? _joinedGamesSubscription; - HomeBloc(this._userRepository, this._gameRepository); + HomeBloc({ + UserRepository? userRepository, + GameRepository? gameRepository, + StreamSubscription? userSubscription, + StreamSubscription? joinedGameSubscription, + }) : _userRepository = userRepository, + _gameRepository = gameRepository, + _userSubscription = userSubscription, + _joinedGamesSubscription = joinedGameSubscription, + super( + HomeState( + // user: User(), + isLoading: false, + ), + ) {} @override HomeState get initialState => HomeState.loading(); @@ -21,18 +36,27 @@ class HomeBloc extends Bloc { Stream mapEventToState(HomeEvent event) async* { if (event is HomeStarted) { yield* _mapHomeStartedToState(); - } else if (event is JoinedGamesUpdated){ + } else if (event is JoinedGamesUpdated) { yield* _mapJoinedGamesUpdatedToState(event); + } else if (event is UserUpdated) { + yield* _mapUserUpdatedToState(event); + } else if (event is LeaveGame) { + yield* _mapLeaveGameToState(event); + } else if (event is JoinGame) { + yield* _mapJoinGameToState(event); } } Stream _mapHomeStartedToState() async* { try { - var user = await _userRepository.getUser(); - yield state.copyWith(isLoading: false, user: user); + _userSubscription?.cancel(); + _userSubscription = _userRepository?.observeUser().listen((user) { + add(UserUpdated(user)); + }); _joinedGamesSubscription?.cancel(); - _joinedGamesSubscription = _gameRepository.observeJoinedGames().listen((event) { + _joinedGamesSubscription = + _gameRepository?.observeJoinedGames().listen((event) { add(JoinedGamesUpdated(event)); }); } catch (e, stacktrace) { @@ -41,7 +65,36 @@ class HomeBloc extends Bloc { } } - Stream _mapJoinedGamesUpdatedToState(JoinedGamesUpdated event) async* { - yield state.copyWith(games: event.games..sort((a, b) => b.joinedAt.compareTo(a.joinedAt))); + Stream _mapJoinedGamesUpdatedToState( + JoinedGamesUpdated event) async* { + yield state.copyWith( + games: event.games..sort((a, b) => b.joinedAt.compareTo(a.joinedAt))); + } + + Stream _mapUserUpdatedToState(UserUpdated event) async* { + yield state.copyWith(user: event.user, isLoading: false); + } + + Stream _mapLeaveGameToState(LeaveGame event) async* { + try { + yield state.copyWith( + leavingGame: event.game, games: state.games?..remove(event.game)); + await _gameRepository?.leaveGame(event.game); + yield state.copyWith(leavingGame: null); + } catch (e) { + yield state.copyWith(leavingGame: null, error: "$e"); + } + } + + Stream _mapJoinGameToState(JoinGame event) async* { + try { + yield state.copyWith(joiningGame: event.gameCode); + var game = await _gameRepository?.joinGame(event.gameCode); + yield state.copyWith( + joiningGame: null, joinedGame: game, overrideNull: true); + } catch (e) { + yield state.copyWith( + joiningGame: null, joinedGame: null, error: "$e", overrideNull: true); + } } } diff --git a/lib/ui/home/bloc/home_event.dart b/lib/ui/home/bloc/home_event.dart index 5a98e88..b49bbeb 100644 --- a/lib/ui/home/bloc/home_event.dart +++ b/lib/ui/home/bloc/home_event.dart @@ -1,3 +1,4 @@ +import 'package:appsagainsthumanity/data/features/users/model/user.dart'; import 'package:appsagainsthumanity/data/features/users/model/user_game.dart'; import 'package:equatable/equatable.dart'; import 'package:meta/meta.dart'; @@ -20,3 +21,33 @@ class JoinedGamesUpdated extends HomeEvent { @override List get props => [games]; } + +@immutable +class UserUpdated extends HomeEvent { + final User user; + + UserUpdated(this.user); + + @override + List get props => [user]; +} + +@immutable +class LeaveGame extends HomeEvent { + final UserGame game; + + LeaveGame(this.game); + + @override + List get props => [game]; +} + +@immutable +class JoinGame extends HomeEvent { + final String gameCode; + + JoinGame(this.gameCode); + + @override + List get props => [gameCode]; +} diff --git a/lib/ui/home/bloc/home_state.dart b/lib/ui/home/bloc/home_state.dart index 901513d..c5385b4 100644 --- a/lib/ui/home/bloc/home_state.dart +++ b/lib/ui/home/bloc/home_state.dart @@ -1,40 +1,58 @@ +import 'package:appsagainsthumanity/data/features/game/model/game.dart'; import 'package:appsagainsthumanity/data/features/users/model/user.dart'; import 'package:appsagainsthumanity/data/features/users/model/user_game.dart'; import 'package:meta/meta.dart'; @immutable class HomeState { - final User user; - final List games; - final String error; + final User? user; + final List? games; + final UserGame? leavingGame; + final String? joiningGame; + final Game? joinedGame; + final String? error; final bool isLoading; HomeState({ - @required this.user, - @required this.isLoading, - List games, + this.user, + List? games, + this.leavingGame, + this.joiningGame, + this.joinedGame, this.error, - }) : games = games ?? []; + required this.isLoading, + }) : games = games; factory HomeState.loading() { return HomeState( - user: null, + // user: User( + // id: '', + // name: '', + // avatarUrl: '', + // updatedAt: DateTime.now(), + // ), isLoading: true, error: null, ); } - HomeState copyWith({ - User user, - List games, - bool isLoading, - String error, - }) { + HomeState copyWith( + {User? user, + List? games, + UserGame? leavingGame, + String? joiningGame, + Game? joinedGame, + bool? isLoading, + String? error, + bool overrideNull = false}) { return HomeState( user: user ?? this.user, games: games ?? this.games, + leavingGame: overrideNull ? leavingGame : leavingGame ?? this.leavingGame, + joiningGame: overrideNull ? joiningGame : joiningGame ?? this.joiningGame, + joinedGame: overrideNull ? joinedGame : joinedGame ?? this.joinedGame, isLoading: isLoading ?? this.isLoading, - error: error ?? this.error, + error: overrideNull ? error : error ?? this.error, ); } @@ -44,6 +62,9 @@ class HomeState { user: $user, games: $games, isLoading: $isLoading, + leavingGame: $leavingGame, + joiningGame: $joiningGame, + joinedGame: $joinedGame, error: $error }'''; } diff --git a/lib/ui/home/home_screen.dart b/lib/ui/home/home_screen.dart deleted file mode 100644 index 5f7e22e..0000000 --- a/lib/ui/home/home_screen.dart +++ /dev/null @@ -1,348 +0,0 @@ -import 'dart:io'; - -import 'package:appsagainsthumanity/data/features/game/game_repository.dart'; -import 'package:appsagainsthumanity/data/features/game/model/player.dart'; -import 'package:appsagainsthumanity/data/features/users/model/user.dart'; -import 'package:appsagainsthumanity/data/features/game/model/game_state.dart'; -import 'package:appsagainsthumanity/ui/creategame/create_game_screen.dart'; -import 'package:appsagainsthumanity/ui/game/game_screen.dart'; -import 'package:appsagainsthumanity/ui/home/bloc/bloc.dart'; -import 'package:appsagainsthumanity/ui/home/widgets/join_room_dialog.dart'; -import 'package:appsagainsthumanity/ui/settings/settings_screen.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:appsagainsthumanity/internal.dart'; -import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; -import 'package:timeago/timeago.dart' as timeago; - -class HomeScreen extends StatefulWidget { - @override - State createState() => _HomeScreenState(); -} - -class _HomeScreenState extends State { - final PageController _pageController = PageController(viewportFraction: 0.925); - - @override - void dispose() { - _pageController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => HomeBloc(context.repository(), context.repository())..add(HomeStarted()), - child: _buildBody(), - ); - } - - Widget _buildBody() { - return BlocConsumer( - listener: (BuildContext context, HomeState state) { - if (state.error != null) { - Scaffold.of(context) - ..hideCurrentSnackBar() - ..showSnackBar( - SnackBar( - content: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [Text(state.error), Icon(Icons.error)], - ), - backgroundColor: Colors.redAccent, - ), - ); - } - }, - builder: (context, state) { - return Scaffold( - body: Padding( - padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom), - child: Column( - children: [ - if (state.games.isNotEmpty) - AspectRatio( - aspectRatio: 312 / 436, - child: PageView( - controller: _pageController, - children: [ - _buildTitleCard(context, state, includeMargin: false), - _buildPastGamesCard(context, state), - ], - ), - ), - if (state.games.isEmpty) _buildTitleCard(context, state), - Expanded( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - _buildMenuAction( - context: context, - margin: const EdgeInsets.only(left: 24, top: 16, right: 8, bottom: 16), - icon: MdiIcons.gamepad, - label: "START GAME", - onTap: () { - Navigator.of(context).push(MaterialPageRoute(builder: (context) => CreateGameScreen())); - }, - ), - Builder(builder: (context) { - return _buildMenuAction( - context: context, - margin: const EdgeInsets.only(left: 8, top: 16, right: 24, bottom: 16), - icon: MdiIcons.gamepadVariantOutline, - label: "JOIN GAME", - onTap: () => _joinGame(context), - ); - }), - ], - ), - ) - ], - ), - ), - ); - }, - ); - } - - Widget _buildTitleCard(BuildContext context, HomeState state, {bool includeMargin = true}) { - var topMargin = MediaQuery.of(context).padding.top + (Platform.isAndroid ? 24 : 0); - return Container( - margin: includeMargin - ? EdgeInsets.only(left: 24, right: 24, top: topMargin) - : EdgeInsets.only(left: 8, right: 8, top: topMargin), - child: AspectRatio( - aspectRatio: 312 / 436, - child: Material( - elevation: 4.0, - borderRadius: BorderRadius.circular(16), - color: Colors.white, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Stack( - children: [ - Container( - margin: const EdgeInsets.all(24), - child: Text( - context.strings.appNameDisplay, - style: context.theme.textTheme.headline3 - .copyWith(color: Colors.black87, fontWeight: FontWeight.bold, fontSize: 48), - ), - ), - Align( - alignment: Alignment.topRight, - child: IconButton( - padding: const EdgeInsets.all(24), - icon: Icon( - MdiIcons.cog, - color: Colors.black87, - ), - onPressed: () { - Navigator.of(context).push(MaterialPageRoute(builder: (_) => SettingsScreen())); - }, - ), - ) - ], - ), - Expanded( - child: Align( - alignment: Alignment.bottomCenter, - child: Container( - child: state.isLoading - ? _buildLoadingUserTile() - : state.error != null ? _buildErrorUserTile(state.error) : _buildUserTile(state.user)), - ), - ) - ], - ), - ), - ), - ); - } - - Widget _buildPastGamesCard(BuildContext context, HomeState state) { - var topMargin = MediaQuery.of(context).padding.top + (Platform.isAndroid ? 24 : 0); - return Container( - margin: EdgeInsets.only(left: 8, right: 8, top: topMargin), - child: AspectRatio( - aspectRatio: 312 / 436, - child: Material( - elevation: 4.0, - borderRadius: BorderRadius.circular(16), - color: Colors.white, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.max, - children: [ - Container( - margin: const EdgeInsets.all(24), - child: Text( - "Past Games", - style: context.theme.textTheme.headline3 - .copyWith(color: Colors.black87, fontWeight: FontWeight.bold, fontSize: 48), - ), - ), - Divider( - height: 1, - color: Colors.black12, - ), - Expanded( - child: ListView.builder( - padding: const EdgeInsets.symmetric(vertical: 8), - itemCount: state.games.length, - itemBuilder: (context, index) { - var game = state.games[index]; - print("Render ${game.gid}"); - return ListTile( - title: Text( - game.gid, - style: context.theme.textTheme.subtitle1 - .copyWith(color: AppColors.secondary, fontWeight: FontWeight.w500), - ), - subtitle: Text( - game.state.label, - style: context.theme.textTheme.bodyText2.copyWith(color: Colors.black54), - ), - trailing: Text( - timeago.format(game.joinedAt), - style: context.theme.textTheme.bodyText2.copyWith(color: Colors.black26), - ), - onTap: game.state == GameState.inProgress || game.state == GameState.waitingRoom - ? () async { - // Fetch and load game - try { - var existingGame = await context.repository().findGame(game.gid); - - Navigator.of(context) - .push(MaterialPageRoute(builder: (_) => GameScreen(existingGame))); - } catch (e) { - Scaffold.of(context) - ..hideCurrentSnackBar() - ..showSnackBar(SnackBar( - content: Text("$e"), - )); - } - } - : null, - ); - }), - ) - ], - ), - ), - ), - ); - } - - Widget _buildMenuAction({ - @required BuildContext context, - @required IconData icon, - @required String label, - @required EdgeInsets margin, - Color color = Colors.white, - VoidCallback onTap, - }) { - return Expanded( - child: Container( - margin: margin, - child: Material( - borderRadius: BorderRadius.circular(16), - elevation: 2, - color: color, - clipBehavior: Clip.hardEdge, - child: InkWell( - onTap: onTap, - child: Container( - height: double.maxFinite, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - icon, - color: Colors.black87, - ), - Container( - margin: const EdgeInsets.only(top: 16), - child: Text(label, - style: context.theme.textTheme.button.copyWith( - color: Colors.black87, - )), - ) - ], - ), - ), - ), - ), - ), - ); - } - - Widget _buildLoadingUserTile() { - return ListTile( - title: Text( - "Loading...", - style: context.theme.textTheme.subtitle1.copyWith(color: Colors.black87), - ), - leading: CircleAvatar( - radius: 20, - backgroundColor: Colors.grey.withOpacity(0.4), - child: CircularProgressIndicator(), - ), - ); - } - - Widget _buildErrorUserTile(String error) { - return ListTile( - title: Text( - "Error loading user", - style: context.theme.textTheme.subtitle1.copyWith(color: Colors.black87), - ), - subtitle: Text(error), - leading: CircleAvatar( - radius: 20, - backgroundColor: Colors.redAccent, - child: Icon( - MdiIcons.accountRemoveOutline, - color: Colors.white, - ), - ), - ); - } - - Widget _buildUserTile(User user) { - return ListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8), - title: Text( - user.name ?? Player.DEFAULT_NAME, - style: context.theme.textTheme.subtitle1.copyWith(color: Colors.black87), - ), - subtitle: Text( - user.id, - style: context.theme.textTheme.bodyText1.copyWith(color: Colors.black38), - ), - leading: CircleAvatar( - radius: 20, - backgroundImage: user.avatarUrl != null ? NetworkImage(user.avatarUrl) : null, - backgroundColor: AppColors.primary, - ), - ); - } - - void _joinGame(BuildContext context) async { - var gameId = await showJoinRoomDialog(context); - if (gameId != null) { - try { - var game = await context.repository().joinGame(gameId); - - Navigator.of(context).push(MaterialPageRoute(builder: (_) => GameScreen(game))); - } catch (e) { - Scaffold.of(context) - ..hideCurrentSnackBar() - ..showSnackBar(SnackBar( - content: Text("$e"), - )); - } - } - } -} diff --git a/lib/ui/home/home_screen_v2.dart b/lib/ui/home/home_screen_v2.dart new file mode 100644 index 0000000..8677257 --- /dev/null +++ b/lib/ui/home/home_screen_v2.dart @@ -0,0 +1,176 @@ +// import 'dart:io'; + +import 'package:appsagainsthumanity/data/features/users/user_repository.dart'; +import 'package:appsagainsthumanity/internal/push.dart'; +import 'package:appsagainsthumanity/ui/creategame/create_game_screen.dart'; +import 'package:appsagainsthumanity/ui/home/bloc/bloc.dart'; +import 'package:appsagainsthumanity/ui/home/widgets/home_outline_button.dart'; +import 'package:appsagainsthumanity/ui/home/widgets/join_game_widget.dart'; +import 'package:appsagainsthumanity/ui/home/widgets/past_game.dart'; +import 'package:appsagainsthumanity/ui/home/widgets/settings_widget.dart'; +import 'package:appsagainsthumanity/ui/home/widgets/user_widget.dart'; +import 'package:appsagainsthumanity/ui/profile/profile_screen.dart'; +import 'package:appsagainsthumanity/ui/routes.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:appsagainsthumanity/internal.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; + +class HomeScreenV2 extends StatefulWidget { + @override + State createState() => _HomeScreenV2State(); +} + +class _HomeScreenV2State extends State { + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => HomeBloc()..add(HomeStarted()), + child: MultiBlocListener( + listeners: [ + // Error Listener + BlocListener( + listenWhen: (previous, current) => + previous.error != current.error && current.error != "", + listener: (context, state) { + if (state.error != "") { + ScaffoldMessenger.of(context) + ..hideCurrentSnackBar() + ..showSnackBar( + SnackBar( + content: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [Text(state.error!), Icon(Icons.error)], + ), + backgroundColor: Colors.redAccent, + ), + ); + } + }, + ), + + // Joined Game listener that opens a 'joined' game + BlocListener( + listenWhen: (previous, current) => + previous.joinedGame?.id != current.joinedGame?.id, + listener: (context, state) { + if (state.joinedGame != {}) { + Navigator.of(context).push(GamePageRoute(state.joinedGame!)); + } + }, + ) + ], + child: _buildBody(), + ), + ); + } + + Widget _buildBody() { + return BlocBuilder( + builder: (context, state) { + var topMargin = context.paddingTop; + return Scaffold( + body: Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).padding.bottom, + top: topMargin, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Title + Container( + margin: const EdgeInsets.only(left: 24, right: 24), + child: Text( + context.strings.appNameDisplay, + style: GoogleFonts.raleway( + textStyle: context.theme.textTheme.headline3?.copyWith( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 48, + )), + ), + ), + Container( + margin: const EdgeInsets.only(left: 24, right: 24, top: 32), + child: Wrap( + alignment: WrapAlignment.start, + direction: Axis.horizontal, + spacing: 12, + runSpacing: 16, + children: [ + SettingsWidget(), + UserWidget( + state: state, + onTap: () { + Analytics().logSelectContent( + contentType: 'action', itemId: 'profile'); + Navigator.of(context).push(MaterialPageRoute( + builder: (_) => ProfileScreen())); + }, + ), + HomeOutlineButton( + icon: Icon( + MdiIcons.gamepad, + color: AppColors.primaryVariant, + ), + text: "New Game", + onTap: state.joiningGame == "" + ? () { + Analytics().logSelectContent( + contentType: 'action', + itemId: 'start_game'); + PushNotifications().checkPermissions(); + Navigator.of(context).push(MaterialPageRoute( + builder: (context) => CreateGameScreen())); + } + : () {}, + ), + JoinGameWidget(state), + ], + ), + ), + + if (state.games!.isNotEmpty) + Container( + margin: const EdgeInsets.only(left: 24, top: 24, right: 24), + child: Text( + "Past Games", + style: context.theme.textTheme.headline4?.copyWith( + color: Colors.white70, + fontWeight: FontWeight.w500, + fontSize: 24, + ), + ), + ), + + if (state.games!.isNotEmpty) + Container( + margin: const EdgeInsets.only(top: 16), + child: Divider( + height: 1, + color: Colors.white12, + ), + ), + + if (state.games!.isNotEmpty) + Expanded( + child: ListView.builder( + padding: const EdgeInsets.symmetric(vertical: 8), + itemCount: state.games!.length, + itemBuilder: (context, index) { + var game = state.games?[index]; + var isLeavingGame = game?.id == state.leavingGame?.id; + return PastGame(game!, isLeavingGame); + }, + ), + ), + ], + ), + ), + ); + }, + ); + } +} diff --git a/lib/ui/home/widgets/home_outline_button.dart b/lib/ui/home/widgets/home_outline_button.dart new file mode 100644 index 0000000..101a3a8 --- /dev/null +++ b/lib/ui/home/widgets/home_outline_button.dart @@ -0,0 +1,57 @@ +// import 'package:appsagainsthumanity/app.dart'; +import 'package:flutter/material.dart'; +import 'package:appsagainsthumanity/internal.dart'; + +class HomeOutlineButton extends StatelessWidget { + final Widget icon; + final String text; + final Color textColor; + final Color borderColor; + final VoidCallback? onTap; + + HomeOutlineButton({ + required this.icon, + required this.text, + this.textColor = AppColors.primaryVariant, + this.borderColor = AppColors.primaryVariant, + this.onTap, + }); + + @override + Widget build(BuildContext context) { + return IntrinsicWidth( + child: Material( + clipBehavior: Clip.hardEdge, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(32), + side: BorderSide( + color: borderColor, + width: 2, + ), + ), + child: InkWell( + onTap: onTap, + child: Container( + child: Row( + children: [ + Container( + margin: const EdgeInsets.all(8), + child: icon, + ), + Container( + margin: const EdgeInsets.only(left: 8, right: 24), + child: Text( + text, + style: context.theme.textTheme.subtitle2?.copyWith( + color: textColor, + ), + ), + ) + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/ui/home/widgets/join_game_widget.dart b/lib/ui/home/widgets/join_game_widget.dart new file mode 100644 index 0000000..a6f3c2d --- /dev/null +++ b/lib/ui/home/widgets/join_game_widget.dart @@ -0,0 +1,50 @@ +import 'package:appsagainsthumanity/internal.dart'; +import 'package:appsagainsthumanity/internal/push.dart'; +import 'package:appsagainsthumanity/ui/home/bloc/bloc.dart'; +import 'package:appsagainsthumanity/ui/home/widgets/home_outline_button.dart'; +import 'package:appsagainsthumanity/ui/home/widgets/join_room_dialog.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; + +class JoinGameWidget extends StatelessWidget { + final HomeState state; + + JoinGameWidget(this.state); + + @override + Widget build(BuildContext context) { + return HomeOutlineButton( + icon: state.joiningGame == "" ? _buildIcon() : _buildLoadingIcon(), + text: state.joiningGame == "" ? "Join Game" : "Joining Game...", + onTap: () { + Analytics() + .logSelectContent(contentType: 'action', itemId: 'join_game'); + PushNotifications().checkPermissions(); + _joinGame(context); + }, + ); + } + + Widget _buildLoadingIcon() { + return Container( + width: 24, + height: 24, + child: CircularProgressIndicator(), + ); + } + + Widget _buildIcon() { + return Icon( + MdiIcons.gamepadVariantOutline, + color: AppColors.primaryVariant, + ); + } + + void _joinGame(BuildContext context) async { + var gameId = await showJoinRoomDialog(context); + if (gameId != null) { + context.read().add(JoinGame(gameId)); + } + } +} diff --git a/lib/ui/home/widgets/join_room_dialog.dart b/lib/ui/home/widgets/join_room_dialog.dart index 632b9e1..7f95162 100644 --- a/lib/ui/home/widgets/join_room_dialog.dart +++ b/lib/ui/home/widgets/join_room_dialog.dart @@ -1,7 +1,9 @@ -import 'dart:io'; - +// import 'dart:io'; +import 'package:flutter/foundation.dart' show kIsWeb; +import 'package:appsagainsthumanity/data/firestore.dart'; import 'package:appsagainsthumanity/internal.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; class JoinRoomDialog extends StatefulWidget { @override @@ -24,17 +26,17 @@ class _JoinRoomDialogState extends State { mainAxisSize: MainAxisSize.min, children: [ Container( - margin: const EdgeInsets.only(left: 24, right: 24, top: 24), + margin: const EdgeInsets.only(left: 24, right: 24, top: 24), child: Form( key: _formKey, child: TextFormField( controller: _gameInputController, decoration: InputDecoration( - border: OutlineInputBorder(), - labelText: 'Game ID', - ), - maxLength: 5, - maxLengthEnforced: true, + border: OutlineInputBorder(), + labelText: 'Game ID', + labelStyle: context.theme.textTheme.caption), + maxLength: FirebaseConstants.MAX_GID_SIZE, + maxLengthEnforcement: MaxLengthEnforcement.enforced, keyboardType: TextInputType.text, autofocus: true, textCapitalization: TextCapitalization.characters, @@ -43,7 +45,7 @@ class _JoinRoomDialogState extends State { Navigator.of(context).pop(value); }, validator: (value) { - if (value.length != 5) { + if (value?.length != FirebaseConstants.MAX_GID_SIZE) { return 'Please enter a valid game id'; } return null; @@ -52,22 +54,30 @@ class _JoinRoomDialogState extends State { ), ), Container( - margin: const EdgeInsets.only(bottom: 8, top: 8), + margin: const EdgeInsets.only(bottom: 8, top: 8), child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ - FlatButton( - child: Text('CANCEL'), - textColor: AppColors.primary, + TextButton( + child: Text( + 'CANCEL', + style: TextStyle( + color: AppColors.primary, + ), + ), onPressed: () { Navigator.of(context).pop(); }, ), - FlatButton( - child: Text('JOIN'), - textColor: AppColors.primary, + TextButton( + child: Text( + 'JOIN', + style: TextStyle( + color: AppColors.primary, + ), + ), onPressed: () { - if (_formKey.currentState.validate()) { + if (_formKey.currentState!.validate()) { var gameId = _gameInputController.text; Navigator.of(context).pop(gameId); } @@ -81,26 +91,38 @@ class _JoinRoomDialogState extends State { } } -Future showJoinRoomDialog(BuildContext context) { - if (!Platform.isAndroid && !Platform.isIOS) { +Future showJoinRoomDialog(BuildContext context) { + if (kIsWeb) { return showGeneralDialog( - context: context, - pageBuilder: (context, _, __) { - return AlertDialog( + context: context, + pageBuilder: (context, _, __) { + return Theme( + data: AppThemes.dark, + child: AlertDialog( title: Text('Join a game'), content: JoinRoomDialog(), + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), contentPadding: const EdgeInsets.all(0), - ); - }); + ), + ); + }, + ); } else { return showDialog( - context: context, - builder: (builderContext) { - return AlertDialog( + context: context, + builder: (builderContext) { + return Theme( + data: AppThemes.dark, + child: AlertDialog( title: Text('Join a game'), content: JoinRoomDialog(), + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), contentPadding: const EdgeInsets.all(0), - ); - }); + ), + ); + }, + ); } } diff --git a/lib/ui/home/widgets/past_game.dart b/lib/ui/home/widgets/past_game.dart new file mode 100644 index 0000000..9fbe05a --- /dev/null +++ b/lib/ui/home/widgets/past_game.dart @@ -0,0 +1,138 @@ +import 'package:appsagainsthumanity/data/features/game/game_repository.dart'; +import 'package:appsagainsthumanity/data/features/game/model/game_state.dart'; +import 'package:appsagainsthumanity/data/features/users/model/user_game.dart'; +import 'package:appsagainsthumanity/ui/home/bloc/bloc.dart'; +import 'package:appsagainsthumanity/internal.dart'; +import 'package:appsagainsthumanity/ui/routes.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; +import 'package:timeago/timeago.dart' as timeago; + +class PastGame extends StatelessWidget { + final UserGame game; + final bool isLeavingGame; + + PastGame(this.game, this.isLeavingGame); + + @override + Widget build(BuildContext context) { + return Dismissible( + key: Key(game.id!), + child: _buildListTile(context), + direction: DismissDirection.horizontal, + background: _buildBackground(context), + confirmDismiss: (_) => confirmDismiss(context), + onDismissed: (direction) { + Analytics().logSelectContent(contentType: 'past_game', itemId: 'leave'); + context.read().add(LeaveGame(game)); + }, + ); + } + + Widget _buildListTile(BuildContext context) { + return ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 24), + title: Text( + game.gid, + style: context.theme.textTheme.subtitle1?.copyWith( + color: AppColors.primaryVariant, + fontWeight: FontWeight.w500, + ), + ), + subtitle: Text( + isLeavingGame ? "Leaving..." : game.state.label, + style: context.theme.textTheme.bodyText2?.copyWith( + color: Colors.white60, + ), + ), + trailing: isLeavingGame + ? Container(width: 24, height: 24, child: CircularProgressIndicator()) + : Text( + timeago.format(game.joinedAt), + style: context.theme.textTheme.bodyText2?.copyWith( + color: Colors.white54, + ), + ), + onTap: game.state == GameState.inProgress || + game.state == GameState.waitingRoom + ? () => openGame(context) + : null, + ); + } + + Widget _buildBackground(BuildContext context) { + return Container( + color: Colors.redAccent[200], + child: Row( + children: [ + Container( + margin: const EdgeInsets.only(left: 16), + child: Icon( + MdiIcons.deleteEmpty, + color: Colors.white, + ), + ), + Expanded( + child: Container(), + ), + Container( + margin: const EdgeInsets.only(right: 16), + child: Icon( + MdiIcons.deleteEmpty, + color: Colors.white, + ), + ), + ], + ), + ); + } + + void openGame(BuildContext context) async { + Analytics().logSelectContent(contentType: 'past_game', itemId: 'open'); + try { + var existingGame = await context.read().getGame(game.id!); + Navigator.of(context).push(GamePageRoute(existingGame)); + } catch (e) { + ScaffoldMessenger.of(context) + ..hideCurrentSnackBar() + ..showSnackBar(SnackBar( + content: Text("$e"), + )); + } + } + + Future confirmDismiss(BuildContext context) { + return showDialog( + context: context, + builder: (context) { + return AlertDialog( + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + title: Text("Leave game?"), + content: Text("Are you sure you want to leave the game ${game.gid}?"), + actions: [ + TextButton( + child: Text( + "CANCEL", + style: TextStyle( + color: AppColors.primaryVariant, + ), + ), + onPressed: () => Navigator.of(context).pop(false), + ), + TextButton( + child: Text( + "LEAVE", + style: TextStyle( + color: AppColors.primaryVariant, + ), + ), + onPressed: () => Navigator.of(context).pop(true), + ), + ], + ); + }, + ); + } +} diff --git a/lib/ui/home/widgets/settings_widget.dart b/lib/ui/home/widgets/settings_widget.dart new file mode 100644 index 0000000..0c7ead6 --- /dev/null +++ b/lib/ui/home/widgets/settings_widget.dart @@ -0,0 +1,22 @@ +import 'package:appsagainsthumanity/internal.dart'; +import 'package:appsagainsthumanity/ui/home/widgets/home_outline_button.dart'; +import 'package:appsagainsthumanity/ui/settings/settings_screen.dart'; +import 'package:flutter/material.dart'; +import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; + +class SettingsWidget extends StatelessWidget { + @override + Widget build(BuildContext context) { + return HomeOutlineButton( + icon: Icon( + MdiIcons.cog, + color: AppColors.primaryVariant, + ), + text: "Settings", + onTap: () { + Analytics().logSelectContent(contentType: 'action', itemId: 'settings'); + Navigator.of(context).push(MaterialPageRoute(builder: (_) => SettingsScreen())); + }, + ); + } +} diff --git a/lib/ui/home/widgets/user_widget.dart b/lib/ui/home/widgets/user_widget.dart new file mode 100644 index 0000000..333219c --- /dev/null +++ b/lib/ui/home/widgets/user_widget.dart @@ -0,0 +1,72 @@ +import 'package:appsagainsthumanity/internal.dart'; +import 'package:appsagainsthumanity/ui/home/bloc/bloc.dart'; +import 'package:appsagainsthumanity/ui/home/widgets/home_outline_button.dart'; +import 'package:flutter/material.dart'; +import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; + +class UserWidget extends StatelessWidget { + final HomeState state; + final VoidCallback? onTap; + + UserWidget({ + required this.state, + this.onTap, + }); + + @override + Widget build(BuildContext context) { + return HomeOutlineButton( + icon: state.isLoading + ? _buildLoadingIcon() + : state.error != "" + ? _buildErrorIcon() + : _buildUserIcon(), + text: state.isLoading + ? "Loading..." + : state.error != "" + ? "Uh oh!" + : state.user!.name, + textColor: Colors.white, +// borderColor: Colors.white, + onTap: onTap!, + ); + } + + Widget _buildLoadingIcon() { + return Container( + width: 24, + height: 24, + child: CircularProgressIndicator(), + ); + } + + Widget _buildErrorIcon() { + return Container( + width: 24, + height: 24, + child: CircleAvatar( + radius: 12, + backgroundColor: Colors.redAccent, + child: Icon( + MdiIcons.accountRemoveOutline, + color: Colors.white, + size: 20, + ), + ), + ); + } + + Widget _buildUserIcon() { + return Container( + width: 24, + height: 24, + child: CircleAvatar( + radius: 20, + backgroundImage: state.user!.avatarUrl != "" + ? NetworkImage(state.user!.avatarUrl) + : NetworkImage(""), + backgroundColor: AppColors.primary, + ), + ); + } +} diff --git a/lib/ui/profile/bloc/bloc.dart b/lib/ui/profile/bloc/bloc.dart new file mode 100644 index 0000000..631354e --- /dev/null +++ b/lib/ui/profile/bloc/bloc.dart @@ -0,0 +1,3 @@ +export 'profile_bloc.dart'; +export 'profile_state.dart'; +export 'profile_event.dart'; diff --git a/lib/ui/profile/bloc/profile_bloc.dart b/lib/ui/profile/bloc/profile_bloc.dart new file mode 100644 index 0000000..0b1c78f --- /dev/null +++ b/lib/ui/profile/bloc/profile_bloc.dart @@ -0,0 +1,85 @@ +import 'dart:async'; + +import 'package:appsagainsthumanity/data/features/users/user_repository.dart'; +import 'package:appsagainsthumanity/ui/profile/bloc/profile_event.dart'; +import 'package:appsagainsthumanity/ui/profile/bloc/profile_state.dart'; +import 'package:bloc/bloc.dart'; + +class ProfileBloc extends Bloc { + final UserRepository? _userRepository; + StreamSubscription? _userSubscription; + + ProfileBloc({ + UserRepository? userRepository, + StreamSubscription? userSubscription, + }) : _userRepository = userRepository, + _userSubscription = userSubscription, + super(ProfileState()) {} + + @override + ProfileState get initialState => ProfileState(); + + @override + Future close() async { + super.close(); + await _userSubscription?.cancel(); + } + + @override + Stream mapEventToState(ProfileEvent event) async* { + if (event is ScreenLoaded) { + yield* _mapScreenLoadedToState(); + } else if (event is UserLoaded) { + yield* _mapUserLoadedToState(event); + } else if (event is DisplayNameChanged) { + yield* _mapDisplayNameChangedToState(event); + } else if (event is PhotoChanged) { + yield* _mapPhotoChangedToState(event); + } else if (event is DeleteProfilePhoto) { + yield* _mapDeletePhotoToState(); + } + } + + Stream _mapScreenLoadedToState() async* { + _userSubscription?.cancel(); + _userSubscription = _userRepository?.observeUser().listen((user) { + add(UserLoaded(user)); + }); + } + + Stream _mapUserLoadedToState(UserLoaded event) async* { + yield state.copyWith(user: event.user, isLoading: false, error: null); + } + + Stream _mapDisplayNameChangedToState( + DisplayNameChanged event) async* { + yield state.copyWith(isLoading: true, error: null); + try { + await _userRepository?.updateDisplayName(event.name); + yield state.copyWith(isLoading: false); + } catch (e) { + yield state.copyWith(isLoading: false, error: e.toString()); + } + } + + Stream _mapPhotoChangedToState(PhotoChanged event) async* { + yield state.copyWith(isLoading: true, error: null); + try { + final imageBytes = await event.file.readAsBytes(); + await _userRepository?.updateProfilePhoto(imageBytes); + yield state.copyWith(isLoading: false); + } catch (e) { + yield state.copyWith(isLoading: false, error: e.toString()); + } + } + + Stream _mapDeletePhotoToState() async* { + yield state.copyWith(isLoading: true, error: null); + try { + await _userRepository?.deleteProfilePhoto(); + yield state.copyWith(isLoading: false); + } catch (e) { + yield state.copyWith(isLoading: false, error: e.toString()); + } + } +} diff --git a/lib/ui/profile/bloc/profile_event.dart b/lib/ui/profile/bloc/profile_event.dart new file mode 100644 index 0000000..95ced9c --- /dev/null +++ b/lib/ui/profile/bloc/profile_event.dart @@ -0,0 +1,47 @@ +// import 'dart:io'; + +import 'package:appsagainsthumanity/data/features/users/model/user.dart'; +import 'package:equatable/equatable.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:meta/meta.dart'; + +abstract class ProfileEvent extends Equatable { + const ProfileEvent(); + + @override + List get props => throw UnimplementedError(); +} + +class ScreenLoaded extends ProfileEvent {} + +@immutable +class UserLoaded extends ProfileEvent { + final User user; + + UserLoaded(this.user); + + @override + List get props => [user]; +} + +@immutable +class DisplayNameChanged extends ProfileEvent { + final String name; + + DisplayNameChanged(this.name); + + @override + List get props => [name]; +} + +@immutable +class PhotoChanged extends ProfileEvent { + final XFile file; + + PhotoChanged(this.file); + + @override + List get props => [file]; +} + +class DeleteProfilePhoto extends ProfileEvent {} diff --git a/lib/ui/profile/bloc/profile_state.dart b/lib/ui/profile/bloc/profile_state.dart new file mode 100644 index 0000000..e2a5c97 --- /dev/null +++ b/lib/ui/profile/bloc/profile_state.dart @@ -0,0 +1,32 @@ +import 'package:appsagainsthumanity/data/features/users/model/user.dart'; +import 'package:meta/meta.dart'; + +@immutable +class ProfileState { + final bool isLoading; + final String? error; + final User? user; + + ProfileState({ + this.isLoading = true, + this.error, + this.user, + }); + + ProfileState copyWith({ + bool? isLoading, + String? error, + User? user, + }) { + return ProfileState( + isLoading: isLoading ?? this.isLoading, + error: error ?? this.error, + user: user ?? this.user, + ); + } + + @override + String toString() { + return 'ProfileState{isLoading: $isLoading, error: $error, user: $user}'; + } +} diff --git a/lib/ui/profile/profile_screen.dart b/lib/ui/profile/profile_screen.dart new file mode 100644 index 0000000..5fb3ab7 --- /dev/null +++ b/lib/ui/profile/profile_screen.dart @@ -0,0 +1,149 @@ +import 'package:appsagainsthumanity/data/features/users/user_repository.dart'; +import 'package:appsagainsthumanity/internal.dart'; +import 'package:appsagainsthumanity/ui/profile/bloc/bloc.dart'; +import 'package:appsagainsthumanity/ui/profile/widgets/profile_photo.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; + +class ProfileScreen extends StatefulWidget { + @override + _ProfileScreenState createState() => _ProfileScreenState(); +} + +class _ProfileScreenState extends State { + final TextEditingController _displayNameController = TextEditingController(); + + @override + void dispose() { + _displayNameController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => ProfileBloc()..add(ScreenLoaded()), + child: _buildBody(), + ); + } + + Widget _buildBody() { + return BlocConsumer( + listenWhen: (previous, current) => + current.user?.name != previous.user?.name, + listener: (context, state) { + _displayNameController.text = state.user!.name; + }, + builder: (context, state) { + return Scaffold( + appBar: AppBar( + systemOverlayStyle: SystemUiOverlayStyle( + statusBarBrightness: Brightness.dark, + ), + // brightness: Brightness.dark, + // textTheme: context.theme.textTheme, + iconTheme: context.theme.iconTheme, + title: Text("Profile"), + backgroundColor: AppColors.surfaceDark, + ), + body: ListView( + children: [ + Container( + padding: const EdgeInsets.only(top: 32), + child: Stack( + alignment: Alignment.center, + children: [ + ProfilePhoto( + state.user!, + size: 128, + ), + if (state.isLoading) CircularProgressIndicator(), + if (!state.isLoading && state.user?.avatarUrl != "") + _buildEditPhotoStack(context) + ], + ), + ), + Container( + margin: const EdgeInsets.only( + left: 16, right: 16, bottom: 16, top: 16), + child: ListTile( + title: Text( + "User ID", + style: context.theme.textTheme.subtitle1?.copyWith( + color: AppColors.primaryVariant, + ), + ), + subtitle: + Text(state.user?.id == "" ? "Loading..." : "No User"), + ), + ), + Container( + margin: const EdgeInsets.symmetric(horizontal: 32), + child: TextFormField( + controller: _displayNameController, + decoration: InputDecoration( + border: OutlineInputBorder(), + labelText: "Display name", + ), + keyboardType: TextInputType.text, + textInputAction: TextInputAction.go, + onFieldSubmitted: (value) { + context.read().add(DisplayNameChanged(value)); + }, + ), + ) + ], + ), + ); + }, + ); + } + + Widget _buildEditPhotoStack(BuildContext context) { + return IgnorePointer( + ignoring: true, + child: Container( + margin: const EdgeInsets.only(top: 8), + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + MdiIcons.imageEditOutline, + color: AppColors.primaryVariant, + ), + Container( + margin: const EdgeInsets.only(left: 8), + child: Text( + "EDIT", + style: context.theme.textTheme.subtitle1?.copyWith( + fontWeight: FontWeight.w700, + color: AppColors.primaryVariant, + ), + ), + ) + ], + ), + ), + ); + } + + String getUserInitials(String name) { + if (name != "") { + var splitName = name.split(' '); + if (splitName != "" && splitName.isNotEmpty) { + var nonEmptyCharacters = splitName.where((e) => e.isNotEmpty); + if (nonEmptyCharacters.isNotEmpty) { + return nonEmptyCharacters.map((e) => e[0]).join().toUpperCase(); + } else { + return ""; + } + } else { + return ""; + } + } + return ""; + } +} diff --git a/lib/ui/profile/widgets/profile_photo.dart b/lib/ui/profile/widgets/profile_photo.dart new file mode 100644 index 0000000..eba0339 --- /dev/null +++ b/lib/ui/profile/widgets/profile_photo.dart @@ -0,0 +1,154 @@ +// import 'dart:io'; + +import 'package:appsagainsthumanity/data/features/users/model/user.dart'; +import 'package:appsagainsthumanity/internal.dart'; +import 'package:appsagainsthumanity/ui/profile/bloc/bloc.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:image_picker/image_picker.dart'; + +class ProfilePhoto extends StatelessWidget { + final User user; + final double size; + + ProfilePhoto(this.user, {this.size = 156}); + + @override + Widget build(BuildContext context) { + var profilePhotoUrl = user.avatarUrl; + return profilePhotoUrl != "" + ? _buildExistingPhotoAction(context, profilePhotoUrl) + : _buildAddPhotoAction(context); + } + + Widget _buildAddPhotoAction(BuildContext context) { + return Material( + shape: CircleBorder(), + clipBehavior: Clip.hardEdge, + color: AppColors.addPhotoBackground, + child: InkWell( + splashColor: AppColors.primary.withOpacity(0.30), + highlightColor: AppColors.primary.withOpacity(0.30), + onTap: () => _onProfilePhotoClicked(context), + child: SizedBox( + width: size, + height: size, + child: Center( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.add_a_photo, + size: 46.0, + color: AppColors.addPhotoForeground, + ), + Container( + margin: const EdgeInsets.only(top: 4), + child: Text( + "Add photo", + style: Theme.of(context) + .textTheme + .button + ?.copyWith(color: AppColors.addPhotoForeground), + ), + ) + ], + ), + ), + ), + ), + ); + } + + Widget _buildExistingPhotoAction(BuildContext context, String avatarUrl) { + return Material( + elevation: 4.0, + shape: CircleBorder(), + clipBehavior: Clip.hardEdge, + color: Colors.transparent, + child: Ink.image( + image: NetworkImage(avatarUrl), + fit: BoxFit.cover, + width: size, + height: size, + child: InkWell( + onTap: () => _onProfilePhotoClicked(context), + ), + ), + ); + } + + void _onProfilePhotoClicked(BuildContext context) async { + var action = await _showPhotoBottomSheet(context); + print("Result: $action"); + if (action is DeletePhoto) { + context.read().add(DeleteProfilePhoto()); + } else if (action is UpdatePhoto) { + var image = await ImagePicker().pickImage(source: action.source); + if (image != null) { + print("Photo selected: ${image.path}"); + context.read().add(PhotoChanged(image)); + } + } + } + + Future _showPhotoBottomSheet( + BuildContext context) async { + return await showModalBottomSheet( + context: context, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), + )), + backgroundColor: context.theme.canvasColor, + builder: (context) { + return Container( + child: Wrap( + children: [ + Container( + padding: EdgeInsets.all(16), + child: Text( + "Update your profile photo", + style: context.theme.textTheme.bodyText1 + ?.copyWith(color: Colors.white70), + ), + ), + ListTile( + title: Text("Choose from gallery"), + leading: Icon(Icons.image), + onTap: () { + Navigator.of(context).pop(UpdatePhoto(ImageSource.gallery)); + }, + ), + ListTile( + title: Text("Use camera"), + leading: Icon(Icons.camera_alt), + onTap: () { + Navigator.of(context).pop(UpdatePhoto(ImageSource.camera)); + }, + ), + ListTile( + title: Text("Delete photo"), + leading: Icon(Icons.delete), + onTap: () { + Navigator.of(context).pop(DeletePhoto()); + }, + ) + ], + ), + ); + }); + } +} + +abstract class ProfilePhotoAction {} + +class DeletePhoto extends ProfilePhotoAction {} + +class UpdatePhoto extends ProfilePhotoAction { + final ImageSource source; + + UpdatePhoto(this.source); +} diff --git a/lib/ui/routes.dart b/lib/ui/routes.dart index b48ee61..e1f0ae2 100644 --- a/lib/ui/routes.dart +++ b/lib/ui/routes.dart @@ -1,10 +1,64 @@ +import 'package:appsagainsthumanity/data/features/game/model/game.dart'; +import 'package:appsagainsthumanity/ui/game/game_screen.dart'; import 'package:flutter/material.dart'; class Routes { - Routes._(); + Routes._(); - static const String signIn = "/sign_in"; - static const String home = "/home"; + static const String signIn = "/sign_in"; + static const String home = "/home"; + static const String game = "/game"; - static RouteObserver routeObserver = RouteObserver(); + static RouteObserver routeObserver = RouteObserver(); + static RouteTracer routeTracer = RouteTracer(); +} + +class GamePageRoute extends MaterialPageRoute { + GamePageRoute(Game game) + : super( + settings: RouteSettings( + name: Routes.game, + arguments: game.id, + ), + builder: (_) => GameScreen(game), + ); +} + +class NoAnimationPageRoute extends MaterialPageRoute { + NoAnimationPageRoute({WidgetBuilder? builder}) : super(builder: builder!); + + @override + Widget buildTransitions(BuildContext context, Animation animation, + Animation secondaryAnimation, Widget child) { + return child; + } +} + +class RouteTracer extends NavigatorObserver { + Route? currentRoute; + bool logEnabled = false; + + @override + void didPop(Route route, Route? previousRoute) { + currentRoute = previousRoute; + if (logEnabled) print("Route Changed => $currentRoute"); + } + + @override + void didReplace({Route? newRoute, Route? oldRoute}) { + currentRoute = newRoute; + if (logEnabled) print("Route Changed => $currentRoute"); + } + + @override + void didRemove(Route route, Route? previousRoute) { + currentRoute = previousRoute; + if (logEnabled) print("Route Changed => $currentRoute"); + } + + @override + void didPush(Route route, Route? previousRoute) { + currentRoute = route; + if (logEnabled) print("Route Changed => $currentRoute"); + } } diff --git a/lib/ui/settings/settings_screen.dart b/lib/ui/settings/settings_screen.dart index 0549500..f649f5a 100644 --- a/lib/ui/settings/settings_screen.dart +++ b/lib/ui/settings/settings_screen.dart @@ -1,6 +1,10 @@ import 'package:appsagainsthumanity/authentication_bloc/authentication_bloc.dart'; +import 'package:appsagainsthumanity/data/app_preferences.dart'; import 'package:appsagainsthumanity/data/features/users/user_repository.dart'; import 'package:appsagainsthumanity/internal.dart'; +import 'package:appsagainsthumanity/ui/creategame/create_game_screen.dart'; +import 'package:appsagainsthumanity/ui/profile/profile_screen.dart'; +import 'package:appsagainsthumanity/ui/settings/widgets/frequent_tap_detector.dart'; import 'package:appsagainsthumanity/ui/settings/widgets/preference.dart'; import 'package:appsagainsthumanity/ui/settings/widgets/preference_header.dart'; import 'package:appsagainsthumanity/ui/settings/widgets/user_preference.dart'; @@ -10,14 +14,31 @@ import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; import 'package:package_info/package_info.dart'; +import 'package:wiredash/wiredash.dart'; -class SettingsScreen extends StatelessWidget { +import 'package:flutter/foundation.dart' as Foundation; + +class SettingsScreen extends StatefulWidget { + @override + _SettingsScreenState createState() => _SettingsScreenState(); +} + +class _SettingsScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text("Settings"), + title: Text( + "Settings", + style: + context.theme.textTheme.headline6?.copyWith(color: Colors.white), + ), + iconTheme: context.theme.iconTheme, backgroundColor: Colors.transparent, + systemOverlayStyle: SystemUiOverlayStyle( + statusBarBrightness: Brightness.dark, + ), + // brightness: Brightness.dark, elevation: 0, ), body: ListView( @@ -25,28 +46,36 @@ class SettingsScreen extends StatelessWidget { PreferenceCategory( title: "Account", children: [ - UserPreference(), + UserPreference( + onTap: (user) { + Analytics().logSelectContent( + contentType: 'setting', itemId: 'profile'); + Navigator.of(context) + .push(MaterialPageRoute(builder: (_) => ProfileScreen())); + }, + ), Preference( - title: "Delete account", - titleColor: Colors.redAccent, + title: "Sign out", icon: Icon( - MdiIcons.deleteForeverOutline, - color: Colors.redAccent, + MdiIcons.logout, + color: context.secondaryColorOnCard, ), onTap: () { - _deleteAccount(context); + _signOut(context); }, ), Preference( - title: "Sign out", + title: "Delete account", + titleColor: AppColors.error, + titleWeight: FontWeight.bold, icon: Icon( - MdiIcons.logout, - color: Colors.black54, + MdiIcons.deleteForeverOutline, + color: AppColors.error, ), onTap: () { - _signOut(context); + _deleteAccount(context); }, - ) + ), ], ), PreferenceCategory( @@ -56,9 +85,11 @@ class SettingsScreen extends StatelessWidget { title: "Privacy Policy", icon: Icon( MdiIcons.shieldSearch, - color: Colors.black54, + color: context.secondaryColorOnCard, ), onTap: () { + Analytics().logSelectContent( + contentType: 'setting', itemId: 'privacy_policy'); showWebView( context, "Privacy Policy", @@ -70,9 +101,11 @@ class SettingsScreen extends StatelessWidget { title: "Terms of service", icon: Icon( MdiIcons.formatFloatLeft, - color: Colors.black54, + color: context.secondaryColorOnCard, ), onTap: () { + Analytics().logSelectContent( + contentType: 'setting', itemId: 'terms_of_service'); showWebView( context, "Terms of service", @@ -84,9 +117,11 @@ class SettingsScreen extends StatelessWidget { title: "Open Source Licenses", icon: Icon( MdiIcons.sourceBranch, - color: Colors.black54, + color: context.secondaryColorOnCard, ), onTap: () { + Analytics().logSelectContent( + contentType: 'setting', itemId: 'oss_licenses'); showLicensePage(context: context); }, ), @@ -95,14 +130,27 @@ class SettingsScreen extends StatelessWidget { PreferenceCategory( title: "About", children: [ + Preference( + title: "Feedback", + subtitle: "Provide feedback on issues or improvements", + icon: Icon(MdiIcons.faceAgent, + color: context.secondaryColorOnCard), + onTap: () { + Analytics().logSelectContent( + contentType: 'setting', itemId: 'feedback'); + Wiredash.of(context)?.show(); + }, + ), Preference( title: "Contribute", subtitle: "Checkout the source code on GitHub!", icon: Icon( MdiIcons.github, - color: Colors.black54, + color: context.secondaryColorOnCard, ), onTap: () { + Analytics().logSelectContent( + contentType: 'setting', itemId: 'contribute'); showWebView( context, "Contribute", @@ -112,11 +160,16 @@ class SettingsScreen extends StatelessWidget { ), Preference( title: "Built by 52inc", - icon: Image.asset('assets/ic_logo.png', color: Colors.black54,), + icon: Image.asset( + 'assets/ic_logo.png', + color: context.secondaryColorOnCard, + ), onTap: () { + Analytics().logSelectContent( + contentType: 'setting', itemId: 'author'); showWebView( context, - "Author", + "52inc", "https://52inc.com", ); }, @@ -125,25 +178,115 @@ class SettingsScreen extends StatelessWidget { stream: PackageInfo.fromPlatform().asStream(), builder: (context, snapshot) { var packageInfo = snapshot.data; - return Preference( - title: "Version", - icon: Icon( - MdiIcons.application, - color: Colors.black54, + return FrequentTapDetector( + threshold: 5, + onTapCountReachedCallback: () { + if (!AppPreferences().developerPackEnabled) { + Analytics().logSelectContent( + contentType: 'setting', + itemId: 'developer_packs'); + AppPreferences().developerPackEnabled = true; + setState(() {}); + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text("Developer Packs Unlocked!"), + behavior: SnackBarBehavior.floating, + action: SnackBarAction( + label: "VIEW", + textColor: context.primaryColor, + onPressed: () { + Navigator.of(context).pushReplacement( + MaterialPageRoute( + builder: (_) => CreateGameScreen())); + }, + ), + )); + } + }, + child: Preference( + title: "Version", + icon: Icon( + MdiIcons.application, + color: context.secondaryColorOnCard, + ), + subtitle: packageInfo != null + ? "${packageInfo.version}+${packageInfo.buildNumber}" + : "Loading...", ), - subtitle: - packageInfo != null ? "${packageInfo.version}+${packageInfo.buildNumber}" : "Loading...", ); }), ], ), + if (Foundation.kDebugMode) + PreferenceCategory( + title: "Debug", + children: [ + Preference( + title: "Reset preferences", + subtitle: "Clear out the preferences to their default state", + icon: Icon( + MdiIcons.restore, + color: context.secondaryColorOnCard, + ), + onTap: () { + AppPreferences().clear(); + setState(() {}); + }, + ), + Preference( + title: "Developer packs", + subtitle: "Custom card packs from the developer", + trailing: AppPreferences().developerPackEnabled + ? Text( + "ENABLED", + style: context.theme.textTheme.button! + .copyWith(color: Colors.green), + ) + : Text( + "DISABLED", + style: context.theme.textTheme.button! + .copyWith(color: Colors.redAccent), + ), + icon: Image.asset( + 'assets/ic_logo.png', + color: context.secondaryColorOnCard, + ), + ), + ], + ), Container( - height: 56, + margin: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), alignment: Alignment.center, - child: Text( - "Made with 💙 by 52inc", - textAlign: TextAlign.center, - style: context.theme.textTheme.subtitle1.copyWith(color: Colors.white54), + child: Column( + children: [ + GestureDetector( + onTap: () { + showWebView(context, "Cards Against Humanity", + "https://cardsagainsthumanity.com"); + }, + child: Container( + child: Text( + "All CAH or \"Cards Against Humanity\" question and answer text are licensed under Creative Commons BY-NC-SA 4.0 by the owner Cards Against Humanity, LLC. This application is NOT official, produced, endorsed or supported by Cards Against Humanity, LLC.", + textAlign: TextAlign.center, + style: context.theme.textTheme.bodyText2?.copyWith( + color: Colors.white60, + ), + ), + ), + ), + Container( + margin: const EdgeInsets.only(top: 16), + child: GestureDetector( + onTap: () { + showWebView(context, "CC BY-NC-SA 4.0", + "https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode"); + }, + child: Image.asset( + 'assets/cc_by_nc_sa.png', + width: 96, + ), + ), + ), + ], ), ) ], @@ -152,31 +295,42 @@ class SettingsScreen extends StatelessWidget { } void _deleteAccount(BuildContext context) async { - bool result = await showDialog( + bool? result = await showDialog( context: context, builder: (context) { return AlertDialog( + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), title: Text( "Delete account?", - style: context.theme.textTheme.headline6.copyWith(color: Colors.redAccent), + style: context.theme.textTheme.headline6! + .copyWith(color: Colors.redAccent), ), content: Text( "Are you sure you want to delete your account? This is permenant and cannot be undone.", - style: context.theme.textTheme.subtitle1.copyWith( + style: context.theme.textTheme.subtitle1!.copyWith( fontWeight: FontWeight.bold, ), ), actions: [ - FlatButton( - child: Text("CANCEL"), - textColor: Colors.white70, + TextButton( + child: Text( + "CANCEL", + style: TextStyle( + color: Colors.white70, + ), + ), onPressed: () { Navigator.of(context).pop(false); }, ), - FlatButton( - child: Text("DELETE ACCOUNT"), - textColor: Colors.redAccent, + TextButton( + child: Text( + "DELETE ACCOUNT", + style: TextStyle( + color: Colors.redAccent, + ), + ), onPressed: () { Navigator.of(context).pop(true); }, @@ -186,17 +340,19 @@ class SettingsScreen extends StatelessWidget { }); if (result ?? false) { - var userRepository = context.repository(); + Analytics() + .logSelectContent(contentType: 'setting', itemId: 'delete_account'); + var userRepository = context.read(); try { await userRepository.deleteAccount(); - context.bloc().add(LoggedOut()); + context.read().add(LoggedOut()); Navigator.of(context).pop(); } catch (e) { if (e is PlatformException) { if (e.code == 'ERROR_REQUIRES_RECENT_LOGIN') { await userRepository.signInWithGoogle(); await userRepository.deleteAccount(); - context.bloc().add(LoggedOut()); + context.read().add(LoggedOut()); Navigator.of(context).pop(); } } @@ -205,22 +361,22 @@ class SettingsScreen extends StatelessWidget { } void _signOut(BuildContext context) async { - var userRepository = context.repository(); + var userRepository = context.read(); await userRepository.signOut(); - context.bloc().add(LoggedOut()); + context.read().add(LoggedOut()); Navigator.of(context).pop(); } } class PreferenceCategory extends StatelessWidget { - final String title; + final String? title; final List children; - final EdgeInsets margin; + final EdgeInsets? margin; PreferenceCategory({ this.title, this.margin, - @required this.children, + required this.children, }); @override @@ -230,12 +386,13 @@ class PreferenceCategory extends StatelessWidget { child: Material( elevation: 4, borderRadius: BorderRadius.circular(8), - color: Colors.white, + color: context.theme.cardColor, child: Padding( padding: const EdgeInsets.only(bottom: 8), child: Column( children: [ - if (title != null) PreferenceHeader(title: title, includeIconSpacing: false), + if (title != null) + PreferenceHeader(title: title!, includeIconSpacing: false), ...children, ], ), diff --git a/lib/ui/settings/widgets/frequent_tap_detector.dart b/lib/ui/settings/widgets/frequent_tap_detector.dart new file mode 100644 index 0000000..93be93e --- /dev/null +++ b/lib/ui/settings/widgets/frequent_tap_detector.dart @@ -0,0 +1,56 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; + +class FrequentTapDetector extends StatefulWidget { + final Widget child; + final int? threshold; + final VoidCallback? onTapCountReachedCallback; + + FrequentTapDetector({ + required this.child, + this.threshold = 5, + this.onTapCountReachedCallback, + }); + + @override + _FrequentTapDetectorState createState() => _FrequentTapDetectorState(); +} + +class _FrequentTapDetectorState extends State { + late Timer _timer; + int _tapCount = 0; + + @override + void dispose() { + _timer.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return InkWell( + child: widget.child, + onTap: () { + _tapCount += 1; + print("Tap Count ($_tapCount)"); + if (_tapCount >= widget.threshold!) { + widget.onTapCountReachedCallback?.call(); + _tapCount = 0; + } else { + _startResetDelay(); + } + }, + ); + } + + void _startResetDelay() { + _timer.cancel(); + _timer = Timer(Duration(milliseconds: 1000), _onResetTapCount); + } + + void _onResetTapCount() { + _tapCount = 0; + print("Reset tap count"); + } +} diff --git a/lib/ui/settings/widgets/preference.dart b/lib/ui/settings/widgets/preference.dart index de7fbc0..139bd3b 100644 --- a/lib/ui/settings/widgets/preference.dart +++ b/lib/ui/settings/widgets/preference.dart @@ -3,15 +3,17 @@ import 'package:appsagainsthumanity/internal.dart'; class Preference extends StatelessWidget { final String title; - final Color titleColor; - final String subtitle; - final Widget icon; - final Widget trailing; - final VoidCallback onTap; + final Color? titleColor; + final FontWeight? titleWeight; + final String? subtitle; + final Widget? icon; + final Widget? trailing; + final VoidCallback? onTap; Preference({ - @required this.title, + required this.title, this.titleColor, + this.titleWeight, this.subtitle, this.icon, this.trailing, @@ -23,14 +25,16 @@ class Preference extends StatelessWidget { return ListTile( title: Text( title, - style: context.theme.textTheme.subtitle1.copyWith( - color: titleColor ?? Colors.black87, - ), + style: context.theme.textTheme.subtitle1?.copyWith( + color: titleColor ?? context.colorOnCard, + fontWeight: titleWeight ?? FontWeight.normal), ), - subtitle: subtitle != null + subtitle: subtitle != "" ? Text( - subtitle, - style: context.theme.textTheme.bodyText1.copyWith(color: Colors.black38), + subtitle!, + style: context.theme.textTheme.bodyText1?.copyWith( + color: context.secondaryColorOnCard, + ), ) : null, leading: Padding( diff --git a/lib/ui/settings/widgets/preference_header.dart b/lib/ui/settings/widgets/preference_header.dart index 6800ccb..7a48dc5 100644 --- a/lib/ui/settings/widgets/preference_header.dart +++ b/lib/ui/settings/widgets/preference_header.dart @@ -2,11 +2,13 @@ import 'package:flutter/material.dart'; import 'package:appsagainsthumanity/internal.dart'; class PreferenceHeader extends StatelessWidget { - final String title; final bool includeIconSpacing; - PreferenceHeader({@required this.title, this.includeIconSpacing = true}); + PreferenceHeader({ + required this.title, + this.includeIconSpacing = true, + }); @override Widget build(BuildContext context) { @@ -17,7 +19,9 @@ class PreferenceHeader extends StatelessWidget { alignment: Alignment.centerLeft, child: Text( title, - style: context.theme.textTheme.subtitle2.copyWith(color: context.primaryColor), + style: context.theme.textTheme.subtitle2?.copyWith( + color: context.primaryColor, + ), ), ); } diff --git a/lib/ui/settings/widgets/user_preference.dart b/lib/ui/settings/widgets/user_preference.dart index b86f679..745d05f 100644 --- a/lib/ui/settings/widgets/user_preference.dart +++ b/lib/ui/settings/widgets/user_preference.dart @@ -1,45 +1,52 @@ -import 'package:firebase_auth/firebase_auth.dart'; +import 'package:appsagainsthumanity/data/features/users/model/user.dart'; +import 'package:appsagainsthumanity/data/features/users/user_repository.dart'; import 'package:flutter/material.dart'; -import 'package:kt_dart/kt.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +// import 'package:kt_dart/kt.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; import 'package:appsagainsthumanity/internal.dart'; class UserPreference extends StatelessWidget { - final void Function(UserInfo) onTap; + final void Function(User) onTap; - UserPreference({this.onTap}); + UserPreference({required this.onTap}); @override Widget build(BuildContext context) { - return StreamBuilder( - stream: FirebaseAuth.instance.currentUser().asStream(), + return StreamBuilder( + stream: context.read().observeUser(), builder: (context, snapshot) { - return _buildPreference(context, snapshot?.data); + return _buildPreference(context, snapshot.data!); }, ); } - Widget _buildPreference(BuildContext context, @nullable UserInfo user) { - if (user != null) { - print("${user.displayName}, ${user.uid}, ${user.providerId}, ${user.photoUrl}"); - } + Widget _buildPreference(BuildContext context, User? user) { return ListTile( leading: _avatar(user), title: Text( - user?.displayName != null ? "${user.displayName}" : "Loading...", - style: context.theme.textTheme.subtitle1.copyWith( - color: Colors.black87, + user?.name != "" ? "${user?.name}" : "Loading...", + style: context.theme.textTheme.subtitle1?.copyWith( + color: context.colorOnCard, ), ), - onTap: onTap != null ? () => onTap(user) : null, + onTap: onTap != () {} ? () => onTap(user!) : () {}, ); } - Widget _avatar(@nullable UserInfo user) { + Widget _avatar(User? user) { return CircleAvatar( - child: user?.photoUrl != null ? null : Icon(MdiIcons.account, color: Colors.black87,), - backgroundColor: user?.photoUrl != null ? Colors.black12 : AppColors.primary, - backgroundImage: user?.photoUrl != null ? NetworkImage(user.photoUrl) : null, + child: user?.avatarUrl != "" + ? const SizedBox() + : Icon( + MdiIcons.account, + color: Colors.black87, + ), + backgroundColor: + user?.avatarUrl != "" ? Colors.black12 : AppColors.primary, + backgroundImage: user?.avatarUrl != "" + ? NetworkImage(user!.avatarUrl) + : NetworkImage(""), radius: 20, ); } diff --git a/lib/ui/signin/bloc/sign_in_bloc.dart b/lib/ui/signin/bloc/sign_in_bloc.dart index 1d460d3..b08ddd2 100644 --- a/lib/ui/signin/bloc/sign_in_bloc.dart +++ b/lib/ui/signin/bloc/sign_in_bloc.dart @@ -5,14 +5,15 @@ import 'package:appsagainsthumanity/ui/signin/bloc/sign_in_event.dart'; import 'package:appsagainsthumanity/ui/signin/bloc/sign_in_state.dart'; import 'package:bloc/bloc.dart'; import 'package:logging/logging.dart'; -import 'package:meta/meta.dart'; +// import 'package:meta/meta.dart'; class SignInBloc extends Bloc { - UserRepository _userRepository; + final UserRepository _userRepository; - SignInBloc({@required UserRepository userRepository}) - : assert(userRepository != null), - _userRepository = userRepository; + SignInBloc({ + required UserRepository userRepository, + }) : _userRepository = userRepository, + super(SignInState.loading()); @override SignInState get initialState => SignInState.empty(); @@ -23,6 +24,12 @@ class SignInBloc extends Bloc { yield* _mapLoginWithGooglePressedToState(); } else if (event is LoginWithApplePressed) { yield* _mapLoginWithApplePressedToState(); + } else if (event is LoginAnonymouslyPressed) { + yield* _mapLoginAnonymouslyPressedToState(); + } else if (event is LoginWithEmailPressed) { + yield* _mapLoginWithEmailPressedToState(event); + } else if (event is SignUpWithEmailPressed) { + yield* _mapSignUpWithEmailPressedToState(event); } } @@ -30,7 +37,7 @@ class SignInBloc extends Bloc { yield SignInState.loading(); try { await _userRepository.signInWithGoogle(); - //await Analytics.firebaseAnalytics.logLogin(loginMethod: "google"); + await Analytics().logLogin(loginMethod: "google"); yield SignInState.success(); } catch (e, stacktrace) { Logger("SignInBloc").fine("Error signing in: $e\n$stacktrace"); @@ -42,7 +49,46 @@ class SignInBloc extends Bloc { yield SignInState.loading(); try { await _userRepository.signInWithApple(); - //await Analytics.firebaseAnalytics.logLogin(loginMethod: "google"); + await Analytics().logLogin(loginMethod: "apple"); + yield SignInState.success(); + } catch (e, stacktrace) { + Logger("SignInBloc").fine("Error signing in: $e\n$stacktrace"); + yield SignInState.failure(); + } + } + + Stream _mapLoginAnonymouslyPressedToState() async* { + yield SignInState.loading(); + try { + await _userRepository.signInAnonymously(); + await Analytics().logLogin(loginMethod: "anonymously"); + yield SignInState.success(); + } catch (e, stacktrace) { + Logger("SignInBloc").fine("Error signing in: $e\n$stacktrace"); + yield SignInState.failure(); + } + } + + Stream _mapLoginWithEmailPressedToState( + LoginWithEmailPressed event) async* { + yield SignInState.loading(); + try { + await _userRepository.signInWithEmail(event.email, event.password); + await Analytics().logLogin(loginMethod: "email"); + yield SignInState.success(); + } catch (e, stacktrace) { + Logger("SignInBloc").fine("Error signing in: $e\n$stacktrace"); + yield SignInState.failure(); + } + } + + Stream _mapSignUpWithEmailPressedToState( + SignUpWithEmailPressed event) async* { + yield SignInState.loading(); + try { + await _userRepository.signUpWithEmail( + event.email, event.password, event.userName); + await Analytics().logSignUp(signUpMethod: "email"); yield SignInState.success(); } catch (e, stacktrace) { Logger("SignInBloc").fine("Error signing in: $e\n$stacktrace"); diff --git a/lib/ui/signin/bloc/sign_in_event.dart b/lib/ui/signin/bloc/sign_in_event.dart index dcf5f7e..4cce06e 100644 --- a/lib/ui/signin/bloc/sign_in_event.dart +++ b/lib/ui/signin/bloc/sign_in_event.dart @@ -9,3 +9,25 @@ abstract class SignInEvent extends Equatable { class LoginWithGooglePressed extends SignInEvent {} class LoginWithApplePressed extends SignInEvent {} +class LoginAnonymouslyPressed extends SignInEvent {} + +class LoginWithEmailPressed extends SignInEvent { + final String email; + final String password; + + LoginWithEmailPressed(this.email, this.password); + + @override + List get props => [email, password]; +} + +class SignUpWithEmailPressed extends SignInEvent { + final String email; + final String password; + final String userName; + + SignUpWithEmailPressed(this.email, this.password, this.userName); + + @override + List get props => [email, password, userName]; +} diff --git a/lib/ui/signin/bloc/sign_in_state.dart b/lib/ui/signin/bloc/sign_in_state.dart index a670a56..d7f729d 100644 --- a/lib/ui/signin/bloc/sign_in_state.dart +++ b/lib/ui/signin/bloc/sign_in_state.dart @@ -6,11 +6,10 @@ class SignInState { final bool isSuccess; final bool isFailure; - SignInState({ - @required this.isSubmitting, - @required this.isSuccess, - @required this.isFailure - }); + SignInState( + {required this.isSubmitting, + required this.isSuccess, + required this.isFailure}); factory SignInState.empty() { return SignInState( @@ -45,9 +44,9 @@ class SignInState { } SignInState copyWith({ - bool isSubmitting, - bool isSuccess, - bool isFailure, + bool? isSubmitting, + bool? isSuccess, + bool? isFailure, }) { return SignInState( isSubmitting: isSubmitting ?? this.isSubmitting, diff --git a/lib/ui/signin/layouts/mobile_layout.dart b/lib/ui/signin/layouts/mobile_layout.dart new file mode 100644 index 0000000..0a3f6cc --- /dev/null +++ b/lib/ui/signin/layouts/mobile_layout.dart @@ -0,0 +1,84 @@ +import 'package:appsagainsthumanity/ui/signin/widgets/email_auth_form.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:appsagainsthumanity/ui/signin/widgets/apple_sign_in.dart'; +import 'package:appsagainsthumanity/ui/signin/widgets/auth_button.dart'; +import 'package:appsagainsthumanity/internal.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; + +import '../bloc/bloc.dart'; + +class MobileLayout extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + margin: const EdgeInsets.only(left: 24, right: 24), + child: Text( + context.strings.appNameDisplay, + style: GoogleFonts.raleway( + textStyle: context.theme.textTheme.headline3?.copyWith( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 48, + ), + ), + ), + ), + Expanded( + child: Container(), + ), + Container( + margin: const EdgeInsets.only(left: 24, right: 24), + child: EmailAuthForm(), + ), + Container( + height: 56, + margin: const EdgeInsets.only(left: 24, right: 24), + child: Row( + children: [ + Expanded(child: Container(height: 1, color: Colors.white60)), + Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + child: Text('OR'), + ), + Expanded(child: Container(height: 1, color: Colors.white60)), + ], + ), + ), + Container( + alignment: Alignment.topCenter, + margin: const EdgeInsets.only(left: 24, right: 24), + child: AppleSignIn(), + ), + Container( + margin: const EdgeInsets.symmetric(horizontal: 24), + child: AuthButton( + text: context.strings.actionSignIn, + icon: Image.asset( + 'assets/google_logo.png', + width: 24, + height: 24, + ), + onPressed: () { + context.read().add(LoginWithGooglePressed()); + }, + ), + ), + Container( + margin: const EdgeInsets.symmetric(horizontal: 24, vertical: 24), + child: AuthButton.withIcon( + text: context.strings.actionSignInAnonymously, + iconData: MdiIcons.incognito, + onPressed: () { + context.read().add(LoginAnonymouslyPressed()); + }, + ), + ), + ], + ); + } +} diff --git a/lib/ui/signin/layouts/tablet_layout.dart b/lib/ui/signin/layouts/tablet_layout.dart new file mode 100644 index 0000000..670f0f9 --- /dev/null +++ b/lib/ui/signin/layouts/tablet_layout.dart @@ -0,0 +1,97 @@ +import 'package:appsagainsthumanity/ui/signin/widgets/email_auth_form.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:appsagainsthumanity/ui/signin/widgets/apple_sign_in.dart'; +import 'package:appsagainsthumanity/ui/signin/widgets/auth_button.dart'; +import 'package:appsagainsthumanity/internal.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; + +import '../bloc/bloc.dart'; + +class TabletLayout extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + margin: const EdgeInsets.only(left: 32, right: 24), + child: Text( + context.strings.appNameDisplay, + style: GoogleFonts.raleway( + textStyle: context.theme.textTheme.headline3?.copyWith( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 48, + ), + ), + ), + ), + Expanded( + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 24), + child: Column( + children: [ + Container( + width: 400, + child: EmailAuthForm(), + ), + Container( + width: 400, + height: 56, + margin: const EdgeInsets.symmetric(vertical: 32), + child: Row( + children: [ + Expanded( + child: Container(height: 1, color: Colors.white60)), + Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + child: Text('OR'), + ), + Expanded( + child: Container(height: 1, color: Colors.white60)), + ], + ), + ), + Container( + width: 400, + alignment: Alignment.topCenter, + margin: const EdgeInsets.only(left: 24, right: 24), + child: AppleSignIn(), + ), + Container( + width: 400, + margin: const EdgeInsets.symmetric(horizontal: 24), + child: AuthButton( + text: context.strings.actionSignIn, + icon: Image.asset( + 'assets/google_logo.png', + width: 24, + height: 24, + ), + onPressed: () { + context.read().add(LoginWithGooglePressed()); + }, + ), + ), + Container( + width: 400, + margin: + const EdgeInsets.symmetric(horizontal: 24, vertical: 24), + child: AuthButton.withIcon( + text: context.strings.actionSignInAnonymously, + iconData: MdiIcons.incognito, + onPressed: () { + context.read().add(LoginAnonymouslyPressed()); + }, + ), + ), + ], + ), + ), + ), + ], + ); + } +} diff --git a/lib/ui/signin/sign_in_screen.dart b/lib/ui/signin/sign_in_screen.dart index 6941a56..ef002a9 100644 --- a/lib/ui/signin/sign_in_screen.dart +++ b/lib/ui/signin/sign_in_screen.dart @@ -1,9 +1,18 @@ +// import 'dart:io'; + import 'package:appsagainsthumanity/authentication_bloc/authentication_bloc.dart'; import 'package:appsagainsthumanity/internal.dart'; import 'package:appsagainsthumanity/ui/signin/bloc/bloc.dart'; -import 'package:appsagainsthumanity/ui/signin/widgets/apple_sign_in.dart'; +// import 'package:appsagainsthumanity/ui/signin/widgets/apple_sign_in.dart'; +// import 'package:appsagainsthumanity/ui/signin/widgets/auth_button.dart'; +import 'package:appsagainsthumanity/ui/widgets/reponsive_widget_mediator.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +// import 'package:google_fonts/google_fonts.dart'; +// import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; + +import 'layouts/mobile_layout.dart'; +import 'layouts/tablet_layout.dart'; class SignInScreen extends StatefulWidget { @override @@ -14,9 +23,15 @@ class _SignInScreen extends State { @override Widget build(BuildContext context) { return Scaffold( - body: BlocProvider( - create: (context) => SignInBloc(userRepository: context.repository()), - child: _buildBody(), + body: Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).padding.bottom, + top: context.paddingTop, + ), + child: BlocProvider( + create: (context) => SignInBloc(userRepository: context.read()), + child: _buildBody(), + ), ), ); } @@ -25,93 +40,25 @@ class _SignInScreen extends State { return BlocConsumer( listener: (context, state) { if (state.isFailure) { - Scaffold.of(context) - ..hideCurrentSnackBar() - ..showSnackBar( - SnackBar( - content: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [Text('Login Failure'), Icon(Icons.error)], - ), - backgroundColor: Colors.redAccent, + context.scaffold.showSnackBar( + SnackBar( + content: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [Text('Login Failure'), Icon(Icons.error)], ), - ); + backgroundColor: Colors.redAccent, + ), + ); } if (state.isSuccess) { - context.bloc().add(LoggedIn()); + context.read().add(LoggedIn()); } }, builder: (context, state) { - return Column( - children: [ - Container( - margin: const EdgeInsets.only(left: 24, right: 24, top: 48), - child: AspectRatio( - aspectRatio: 312 / 436, - child: Material( - elevation: 4.0, - borderRadius: BorderRadius.circular(16), - color: Colors.white, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - margin: const EdgeInsets.all(24), - child: Text( - context.strings.appNameDisplay, - style: context.theme.textTheme.headline3 - .copyWith(color: Colors.black87, fontWeight: FontWeight.bold, fontSize: 48), - ), - ) - ], - ), - ), - ), - ), - Container( - width: double.infinity, - alignment: Alignment.topCenter, - margin: const EdgeInsets.only(left: 24, right: 24, top: 24), - child: AppleSignIn(), - ), - Container( - width: double.infinity, - alignment: Alignment.topCenter, - margin: const EdgeInsets.all(24), - child: Material( - type: MaterialType.button, - color: Colors.white, - clipBehavior: Clip.hardEdge, - borderRadius: BorderRadius.circular(6), - child: InkWell( - onTap: () { - context.bloc().add(LoginWithGooglePressed()); - }, - child: Container( - width: double.maxFinite, - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - alignment: Alignment.center, - child: Row( - children: [ - Image.asset('assets/google_logo.png', width: 24, height: 24), - Expanded( - child: Text( - context.strings.actionSignIn, - textAlign: TextAlign.center, - style: context.theme.textTheme.subtitle1.copyWith( - color: Colors.black87, - fontWeight: FontWeight.w600 - ), - ), - ), - ], - ), - ), - ), - ), - ), - ], + return ResponsiveWidgetMediator( + mobile: (_) => MobileLayout(), + tablet: (_) => TabletLayout(), ); }, ); diff --git a/lib/ui/signin/widgets/apple_sign_in.dart b/lib/ui/signin/widgets/apple_sign_in.dart index 225b9ed..ecff562 100644 --- a/lib/ui/signin/widgets/apple_sign_in.dart +++ b/lib/ui/signin/widgets/apple_sign_in.dart @@ -1,49 +1,28 @@ import 'dart:io'; - -import 'package:apple_sign_in/apple_sign_in.dart'; +import 'package:flutter/foundation.dart' show kIsWeb; import 'package:appsagainsthumanity/ui/signin/bloc/bloc.dart'; -import 'package:device_info/device_info.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:sign_in_with_apple/sign_in_with_apple.dart'; -class AppleSignIn extends StatefulWidget { - @override - State createState() => _AppleSignInState(); -} - -class _AppleSignInState extends State { - bool _supportsAppleSignIn = false; - - @override - void initState() { - super.initState(); - _checkCanSignInWithApple(); - } - +class AppleSignIn extends StatelessWidget { @override Widget build(BuildContext context) { - if (_supportsAppleSignIn) { - return AppleSignInButton( - style: ButtonStyle.whiteOutline, - type: ButtonType.signIn, + var isIOs = false; + if (!kIsWeb) { + isIOs = Platform.isIOS; + } + if (isIOs) { + return SignInWithAppleButton( + style: SignInWithAppleButtonStyle.white, + iconAlignment: IconAlignment.left, + height: 48, onPressed: () { - context.bloc().add(LoginWithApplePressed()); + context.read().add(LoginWithApplePressed()); }, ); } else { return Container(); } } - - void _checkCanSignInWithApple() async { - if (Platform.isIOS) { - var iosInfo = await DeviceInfoPlugin().iosInfo; - var version = iosInfo.systemVersion; - if (version.contains('13') == true) { - setState(() { - _supportsAppleSignIn = true; - }); - } - } - } } diff --git a/lib/ui/signin/widgets/auth_button.dart b/lib/ui/signin/widgets/auth_button.dart new file mode 100644 index 0000000..6f9fa72 --- /dev/null +++ b/lib/ui/signin/widgets/auth_button.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; +import 'package:appsagainsthumanity/internal.dart'; + +class AuthButton extends StatelessWidget { + final String text; + final Widget icon; + final VoidCallback onPressed; + + const AuthButton({ + required this.text, + required this.icon, + required this.onPressed, + }); + + AuthButton.withIcon({ + required this.text, + required IconData iconData, + required this.onPressed, + }) : icon = Icon( + iconData, + color: Colors.black87, + ); + + @override + Widget build(BuildContext context) { + return Material( + type: MaterialType.button, + color: Colors.white, + clipBehavior: Clip.hardEdge, + borderRadius: BorderRadius.circular(6), + child: InkWell( + onTap: onPressed, + child: Container( + width: double.maxFinite, + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + alignment: Alignment.center, + child: Row( + children: [ + icon, + Expanded( + child: Text( + text, + textAlign: TextAlign.center, + style: context.theme.textTheme.subtitle1?.copyWith( + color: Colors.black87, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/ui/signin/widgets/email_auth_form.dart b/lib/ui/signin/widgets/email_auth_form.dart new file mode 100644 index 0000000..a0e17c4 --- /dev/null +++ b/lib/ui/signin/widgets/email_auth_form.dart @@ -0,0 +1,275 @@ +import 'package:appsagainsthumanity/ui/signin/bloc/bloc.dart'; +import 'package:email_validator/email_validator.dart'; +import 'package:flutter/material.dart'; +import 'package:appsagainsthumanity/internal.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class EmailAuthForm extends StatefulWidget { + @override + State createState() => _EmailAuthFormState(); +} + +class _EmailAuthFormState extends State { + final _formKey = GlobalKey(); + final _emailController = TextEditingController(); + final _passwordController = TextEditingController(); + final _confirmPasswordController = TextEditingController(); + final _userNameController = TextEditingController(); + final _emailNode = FocusNode(); + final _passwordNode = FocusNode(); + final _confirmPasswordNode = FocusNode(); + final _userNameNode = FocusNode(); + final _actionNode = FocusNode(); + + var _authState = EmailAuthState.signIn; + var _isPasswordObscured = true; + var _isConfirmPasswordObscured = true; + + @override + void dispose() { + _emailController.dispose(); + _passwordController.dispose(); + _confirmPasswordController.dispose(); + _userNameController.dispose(); + _emailNode.dispose(); + _passwordNode.dispose(); + _confirmPasswordNode.dispose(); + _userNameNode.dispose(); + _actionNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Form( + key: _formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // The email input field + Container( + child: TextFormField( + controller: _emailController, + focusNode: _emailNode, + decoration: InputDecoration( + border: OutlineInputBorder(), + hintText: context.strings.hintEmail, + hintStyle: GoogleFonts.raleway(), + prefixIcon: Icon(Icons.email_rounded), + ), + keyboardType: TextInputType.emailAddress, + textInputAction: TextInputAction.next, + validator: (value) { + if (!EmailValidator.validate(value!)) { + return context.strings.errorInvalidEmailAddress; + } + return null; + }, + onFieldSubmitted: (value) { + _emailNode.unfocus(); + _passwordNode.requestFocus(); + }, + ), + ), + + // The password input field + Container( + margin: const EdgeInsets.only(top: 16), + child: TextFormField( + controller: _passwordController, + focusNode: _passwordNode, + decoration: InputDecoration( + border: OutlineInputBorder(), + hintText: context.strings.hintPassword, + hintStyle: GoogleFonts.raleway(), + prefixIcon: Icon(Icons.vpn_key), + suffixIcon: IconButton( + icon: Icon( + _isPasswordObscured + ? Icons.visibility_rounded + : Icons.visibility_off_rounded, + ), + onPressed: () { + setState(() { + _isPasswordObscured = !_isPasswordObscured; + }); + }, + ), + ), + keyboardType: TextInputType.text, + textInputAction: TextInputAction.next, + obscureText: _isPasswordObscured, + validator: (value) { + if (value!.length < 8) { + return context.strings.errorInvalidPasswordLength; + } + return null; + }, + onFieldSubmitted: (value) { + _passwordNode.unfocus(); + if (_authState == EmailAuthState.signUp) { + _confirmPasswordNode.requestFocus(); + } else { + _actionNode.requestFocus(); + } + }, + ), + ), + + // The confirm password input field + if (_authState == EmailAuthState.signUp) + Container( + margin: const EdgeInsets.only(top: 16), + child: TextFormField( + controller: _confirmPasswordController, + focusNode: _confirmPasswordNode, + decoration: InputDecoration( + border: OutlineInputBorder(), + hintText: context.strings.hintConfirmPassword, + prefixIcon: Icon(Icons.vpn_key), + suffixIcon: IconButton( + icon: Icon( + _isConfirmPasswordObscured + ? Icons.visibility_rounded + : Icons.visibility_off_rounded, + ), + onPressed: () { + setState(() { + _isConfirmPasswordObscured = + !_isConfirmPasswordObscured; + }); + }, + ), + ), + keyboardType: TextInputType.text, + textInputAction: TextInputAction.next, + obscureText: _isConfirmPasswordObscured, + validator: (value) { + if (value!.length < 8) { + return context.strings.errorInvalidPasswordLength; + } else if (value != _passwordController.text) { + return context.strings.errorMismatchingPasswords; + } + return null; + }, + onFieldSubmitted: (value) { + _confirmPasswordNode.unfocus(); + _userNameNode.requestFocus(); + }, + ), + ), + + // The username field + if (_authState == EmailAuthState.signUp) + Container( + margin: const EdgeInsets.only(top: 16), + child: TextFormField( + controller: _userNameController, + focusNode: _userNameNode, + decoration: InputDecoration( + border: OutlineInputBorder(), + hintText: context.strings.hintUserName, + hintStyle: GoogleFonts.raleway(), + prefixIcon: Icon(Icons.face_rounded), + ), + keyboardType: TextInputType.name, + textInputAction: TextInputAction.next, + validator: (value) { + if (value!.isEmpty) { + return context.strings.errorInvalidUserName; + } + return null; + }, + onFieldSubmitted: (value) { + _userNameNode.unfocus(); + _actionNode.requestFocus(); + }, + ), + ), + + Container( + width: double.maxFinite, + margin: const EdgeInsets.only(top: 20), + child: ElevatedButton( + focusNode: _actionNode, + child: Text( + _authState == EmailAuthState.signIn + ? context.strings.actionEmailSignIn.toUpperCase() + : context.strings.actionEmailSignUp.toUpperCase(), + style: GoogleFonts.raleway( + color: Colors.black87, + fontSize: 16, + fontWeight: FontWeight.w700, + ), + ), + style: ElevatedButton.styleFrom( + primary: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + padding: const EdgeInsets.symmetric(vertical: 20), + ), + onPressed: () { + if (_formKey.currentState!.validate()) { + _authenticateWithEmail(context); + } + }, + ), + ), + + Container( + width: double.maxFinite, + margin: const EdgeInsets.only(top: 8), + child: TextButton( + child: Text( + _authState == EmailAuthState.signIn + ? context.strings.actionEmailAltSignUp + : context.strings.actionEmailAltSignIn, + style: GoogleFonts.raleway( + color: Colors.white60, + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + style: TextButton.styleFrom( + primary: context.primaryColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + padding: const EdgeInsets.symmetric(vertical: 20), + ), + onPressed: () { + setState(() { + if (_authState == EmailAuthState.signIn) { + _authState = EmailAuthState.signUp; + } else { + _authState = EmailAuthState.signIn; + } + }); + }, + ), + ), + ], + ), + ); + } + + void _authenticateWithEmail(BuildContext context) { + final email = _emailController.text.trim(); + final password = _passwordController.text.trim(); + if (_authState == EmailAuthState.signIn) { + context.read().add(LoginWithEmailPressed(email, password)); + } else { + final userName = _userNameController.text.trim(); + context + .read() + .add(SignUpWithEmailPressed(email, password, userName)); + } + } +} + +enum EmailAuthState { + signIn, + signUp, +} diff --git a/lib/ui/terms_screen.dart b/lib/ui/terms_screen.dart index 647b331..2816905 100644 --- a/lib/ui/terms_screen.dart +++ b/lib/ui/terms_screen.dart @@ -1,12 +1,14 @@ import 'dart:async'; -import 'dart:convert'; +import 'package:flutter/foundation.dart' show kIsWeb; import 'package:appsagainsthumanity/authentication_bloc/authentication_bloc.dart'; +// import 'package:appsagainsthumanity/config.json' as Config; import 'package:appsagainsthumanity/internal.dart'; +import 'package:easy_web_view/easy_web_view.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:webview_flutter/webview_flutter.dart'; -import 'package:flutter/services.dart' show rootBundle; class TermsOfServiceScreen extends StatefulWidget { @override @@ -14,14 +16,14 @@ class TermsOfServiceScreen extends StatefulWidget { } class _TermsOfServiceScreenState extends State { - WebViewController webViewController; + late WebViewController webViewController; final StreamController _loadingStream = StreamController.broadcast(); @override void initState() { super.initState(); - _loadingStream.onListen = () => _loadingStream.add(true); + _loadingStream.onListen = () => _loadingStream.add(kIsWeb ? false : true); } @override @@ -34,20 +36,30 @@ class _TermsOfServiceScreenState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( + systemOverlayStyle: SystemUiOverlayStyle( + statusBarBrightness: Brightness.dark, + ), + // brightness: Brightness.dark, + // textTheme: context.theme.textTheme, + iconTheme: context.theme.iconTheme, title: Text("Terms of service"), ), body: Stack( fit: StackFit.expand, children: [ Container( - child: WebView( - onWebViewCreated: (controller) { - webViewController = controller; - _loadTermsOfServiceFromAssets(); + margin: EdgeInsets.only(bottom: kIsWeb ? 88 : 0), + child: EasyWebView( + src: Config.termsOfServiceUrl, + isHtml: false, + isMarkdown: false, + convertToWidgets: false, + widgetsTextSelectable: false, + onLoaded: () { + _loadingStream.add(false); }, ), ), - StreamBuilder( stream: _loadingStream.stream, builder: (context, snapshot) { @@ -63,23 +75,26 @@ class _TermsOfServiceScreenState extends State { } else { return Container(); } - } - ), - + }), Align( alignment: Alignment.bottomCenter, child: Container( width: double.maxFinite, margin: const EdgeInsets.all(16), - child: RaisedButton( + child: ElevatedButton( child: Text("I AGREE"), - color: AppColors.secondary, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), + style: ElevatedButton.styleFrom( + primary: context.primaryColor, + elevation: 4, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), ), onPressed: () { - context.bloc() - .add(AgreeToTerms()); + Analytics().logSelectContent( + contentType: 'action', itemId: "terms_of_service"); + context.read().add(AgreeToTerms()); }, ), ), @@ -89,10 +104,8 @@ class _TermsOfServiceScreenState extends State { ); } - void _loadTermsOfServiceFromAssets() async { - var tosString = await rootBundle.loadString('assets/tos.html'); - var tosBase64 = base64Encode(const Utf8Encoder().convert(tosString)); - await webViewController.loadUrl('data:text/html;base64,$tosBase64'); - _loadingStream.add(false); - } + // void _loadTermsOfServiceFromUrl() async { + // await webViewController.loadUrl(Config.termsOfServiceUrl); + // _loadingStream.add(false); + // } } diff --git a/lib/ui/widgets/player_circle_avatar.dart b/lib/ui/widgets/player_circle_avatar.dart index 9d860e2..fd6cc39 100644 --- a/lib/ui/widgets/player_circle_avatar.dart +++ b/lib/ui/widgets/player_circle_avatar.dart @@ -6,7 +6,24 @@ class PlayerCircleAvatar extends StatelessWidget { final Player player; final double radius; - PlayerCircleAvatar({@required this.player, this.radius = 20}); + String get playerInitials { + var splitName = player.name.split(' '); + if (splitName != "" && splitName.isNotEmpty) { + var nonEmptyCharacters = splitName.where((e) => e.isNotEmpty); + if (nonEmptyCharacters.isNotEmpty) { + return nonEmptyCharacters.map((e) => e[0]).join().toUpperCase(); + } else { + return ""; + } + } else { + return ""; + } + } + + PlayerCircleAvatar({ + required this.player, + this.radius = 20, + }); @override Widget build(BuildContext context) { @@ -17,11 +34,20 @@ class PlayerCircleAvatar extends StatelessWidget { ) : CircleAvatar( radius: this.radius, - backgroundImage: player.avatarUrl != null ? NetworkImage(player.avatarUrl) : null, + backgroundImage: player.avatarUrl != "" + ? NetworkImage(player.avatarUrl) + : NetworkImage(""), backgroundColor: AppColors.primary, - child: player.avatarUrl == null - ? player.name != null ? Text(player.name.split(' ').map((e) => e[0]).join().toUpperCase()) : null - : null, + child: player.avatarUrl == "" + ? player.name != "" + ? Text( + playerInitials, + style: context.theme.textTheme.subtitle1?.copyWith( + color: Colors.white, + ), + ) + : const SizedBox() + : const SizedBox(), ); } } diff --git a/lib/ui/widgets/reponsive_widget_mediator.dart b/lib/ui/widgets/reponsive_widget_mediator.dart new file mode 100644 index 0000000..e10e7ee --- /dev/null +++ b/lib/ui/widgets/reponsive_widget_mediator.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; + +class ResponsiveWidgetMediator extends StatelessWidget { + final WidgetBuilder mobile; + final WidgetBuilder tablet; + + const ResponsiveWidgetMediator({ + required this.mobile, + required this.tablet, + }); + + @override + Widget build(BuildContext context) { + final width = MediaQuery.of(context).size.width; + print("Responsive Width: $width"); + if (width < 700) { + return mobile(context); + } else if (width >= 700) { + return tablet(context); + } else { + return mobile(context); + } + } +} diff --git a/lib/ui/widgets/web_screen.dart b/lib/ui/widgets/web_screen.dart index ba320b8..438d411 100644 --- a/lib/ui/widgets/web_screen.dart +++ b/lib/ui/widgets/web_screen.dart @@ -6,8 +6,8 @@ class WebScreen extends StatelessWidget { final String url; WebScreen({ - @required this.title, - @required this.url, + required this.title, + required this.url, }); @override diff --git a/lib/util/cah_scrubber.dart b/lib/util/cah_scrubber.dart new file mode 100644 index 0000000..2f28c5e --- /dev/null +++ b/lib/util/cah_scrubber.dart @@ -0,0 +1,11 @@ +class CahScrubber { + CahScrubber._(); + + static String scrub(String name) { + return name.replaceAll("CAH : ", "") + .replaceAll("CAH: ", "") + .replaceAll("CAH ", "") + .replaceAll("Cards Against Humanity", "") + .trim(); + } +} diff --git a/lib/util/context_extensions.dart b/lib/util/context_extensions.dart new file mode 100644 index 0000000..8966451 --- /dev/null +++ b/lib/util/context_extensions.dart @@ -0,0 +1,6 @@ +import 'package:flutter/material.dart'; + +extension BuildContextExtensions on BuildContext { + + ScaffoldMessengerState get scaffold => ScaffoldMessenger.of(this); +} diff --git a/lib/util/media_query_extensions.dart b/lib/util/media_query_extensions.dart new file mode 100644 index 0000000..46ede86 --- /dev/null +++ b/lib/util/media_query_extensions.dart @@ -0,0 +1,20 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart' show kIsWeb; + +extension MediaQueryExt on BuildContext { + + /// Get the padding top including system window spacing that is + /// also web safe + double get paddingTop { + final top = MediaQuery.of(this).padding.top; + if (kIsWeb) { + return top + 24; + } else if (Platform.isAndroid) { + return top + 24; + } else { + return top + 8; + } + } +} diff --git a/pubspec.lock b/pubspec.lock index 6379649..4e3d8fd 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -7,308 +7,441 @@ packages: name: _fe_analyzer_shared url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "31.0.0" analyzer: dependency: transitive description: name: analyzer url: "https://pub.dartlang.org" source: hosted - version: "0.39.6" - apple_sign_in: - dependency: "direct main" - description: - name: apple_sign_in - url: "https://pub.dartlang.org" - source: hosted - version: "0.1.0" + version: "2.8.0" args: dependency: transitive description: name: args url: "https://pub.dartlang.org" source: hosted - version: "1.6.0" + version: "2.0.0" async: dependency: transitive description: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.4.1" + version: "2.8.2" bloc: dependency: "direct main" description: name: bloc url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "8.0.3" bloc_test: dependency: "direct dev" description: name: bloc_test url: "https://pub.dartlang.org" source: hosted - version: "3.1.0" + version: "9.0.3" boolean_selector: dependency: transitive description: name: boolean_selector url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.1.0" build: dependency: transitive description: name: build url: "https://pub.dartlang.org" source: hosted - version: "1.2.2" + version: "2.3.0" build_config: dependency: transitive description: name: build_config url: "https://pub.dartlang.org" source: hosted - version: "0.4.2" + version: "1.0.0" build_daemon: dependency: transitive description: name: build_daemon url: "https://pub.dartlang.org" source: hosted - version: "2.1.4" + version: "3.1.0" build_resolvers: dependency: transitive description: name: build_resolvers url: "https://pub.dartlang.org" source: hosted - version: "1.3.4" + version: "2.0.6" build_runner: dependency: "direct dev" description: name: build_runner url: "https://pub.dartlang.org" source: hosted - version: "1.8.1" + version: "2.1.10" build_runner_core: dependency: transitive description: name: build_runner_core url: "https://pub.dartlang.org" source: hosted - version: "5.0.0" + version: "7.2.3" built_collection: dependency: transitive description: name: built_collection url: "https://pub.dartlang.org" source: hosted - version: "4.3.2" + version: "5.0.0" built_value: dependency: transitive description: name: built_value url: "https://pub.dartlang.org" source: hosted - version: "7.0.9" + version: "8.2.3" characters: dependency: transitive description: name: characters url: "https://pub.dartlang.org" source: hosted - version: "0.3.1" + version: "1.2.0" charcode: dependency: transitive description: name: charcode url: "https://pub.dartlang.org" source: hosted - version: "1.1.3" + version: "1.3.1" checked_yaml: dependency: transitive description: name: checked_yaml url: "https://pub.dartlang.org" source: hosted - version: "1.0.2" + version: "2.0.1" + cli_util: + dependency: transitive + description: + name: cli_util + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.0" + clipboard: + dependency: "direct main" + description: + name: clipboard + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.3" + clock: + dependency: transitive + description: + name: clock + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" cloud_firestore: dependency: "direct main" description: name: cloud_firestore url: "https://pub.dartlang.org" source: hosted - version: "0.13.4+2" + version: "3.1.13" cloud_firestore_platform_interface: dependency: transitive description: name: cloud_firestore_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "5.5.4" cloud_firestore_web: dependency: transitive description: name: cloud_firestore_web url: "https://pub.dartlang.org" source: hosted - version: "0.1.1+2" + version: "2.6.13" cloud_functions: dependency: "direct main" description: name: cloud_functions url: "https://pub.dartlang.org" source: hosted - version: "0.4.2+3" + version: "3.2.13" cloud_functions_platform_interface: dependency: transitive description: name: cloud_functions_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "5.1.4" cloud_functions_web: dependency: transitive description: name: cloud_functions_web url: "https://pub.dartlang.org" source: hosted - version: "1.0.3" + version: "4.2.12" code_builder: dependency: transitive description: name: code_builder url: "https://pub.dartlang.org" source: hosted - version: "3.2.1" + version: "4.1.0" collection: dependency: transitive description: name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.14.12" + version: "1.15.0" convert: dependency: transitive description: name: convert url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" + version: "3.0.0" coverage: dependency: transitive description: name: coverage url: "https://pub.dartlang.org" source: hosted - version: "0.13.9" + version: "1.0.3" + cross_file: + dependency: transitive + description: + name: cross_file + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.2" crypto: dependency: transitive description: name: crypto url: "https://pub.dartlang.org" source: hosted - version: "2.1.4" + version: "3.0.0" csslib: dependency: transitive description: name: csslib url: "https://pub.dartlang.org" source: hosted - version: "0.16.1" + version: "0.16.2" dart_style: dependency: transitive description: name: dart_style url: "https://pub.dartlang.org" source: hosted - version: "1.3.4" + version: "2.2.1" dartx: dependency: "direct main" description: name: dartx url: "https://pub.dartlang.org" source: hosted - version: "0.3.0" + version: "1.1.0" device_info: dependency: "direct main" description: name: device_info url: "https://pub.dartlang.org" source: hosted - version: "0.4.2+1" + version: "2.0.3" + device_info_platform_interface: + dependency: transitive + description: + name: device_info_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + diff_match_patch: + dependency: transitive + description: + name: diff_match_patch + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.1" + easy_web_view: + dependency: "direct main" + description: + name: easy_web_view + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.0-nullsafety" + email_validator: + dependency: "direct main" + description: + name: email_validator + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" equatable: dependency: "direct main" description: name: equatable url: "https://pub.dartlang.org" source: hosted - version: "1.1.1" - firebase: + version: "2.0.3" + fake_async: dependency: transitive description: - name: firebase + name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "7.3.0" + version: "1.2.0" + ffi: + dependency: transitive + description: + name: ffi + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + file: + dependency: transitive + description: + name: file + url: "https://pub.dartlang.org" + source: hosted + version: "6.1.0" + firebase_analytics: + dependency: "direct main" + description: + name: firebase_analytics + url: "https://pub.dartlang.org" + source: hosted + version: "9.1.6" + firebase_analytics_platform_interface: + dependency: transitive + description: + name: firebase_analytics_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.4" + firebase_analytics_web: + dependency: transitive + description: + name: firebase_analytics_web + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.0+11" firebase_auth: dependency: "direct main" description: name: firebase_auth url: "https://pub.dartlang.org" source: hosted - version: "0.15.3" + version: "3.3.16" firebase_auth_platform_interface: dependency: transitive description: name: firebase_auth_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "1.1.7" + version: "6.2.4" firebase_auth_web: dependency: transitive description: name: firebase_auth_web url: "https://pub.dartlang.org" source: hosted - version: "0.1.2" + version: "3.3.13" firebase_core: dependency: "direct main" description: name: firebase_core url: "https://pub.dartlang.org" source: hosted - version: "0.4.4" + version: "1.15.0" firebase_core_platform_interface: dependency: transitive description: name: firebase_core_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "1.0.4" + version: "4.2.5" firebase_core_web: dependency: transitive description: name: firebase_core_web url: "https://pub.dartlang.org" source: hosted - version: "0.1.1+2" + version: "1.6.2" + firebase_dynamic_links: + dependency: "direct main" + description: + name: firebase_dynamic_links + url: "https://pub.dartlang.org" + source: hosted + version: "4.2.1" + firebase_dynamic_links_platform_interface: + dependency: transitive + description: + name: firebase_dynamic_links_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.3" + firebase_messaging: + dependency: "direct main" + description: + name: firebase_messaging + url: "https://pub.dartlang.org" + source: hosted + version: "11.2.15" + firebase_messaging_platform_interface: + dependency: transitive + description: + name: firebase_messaging_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "3.3.1" + firebase_messaging_web: + dependency: transitive + description: + name: firebase_messaging_web + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.13" firebase_storage: dependency: "direct main" description: name: firebase_storage url: "https://pub.dartlang.org" source: hosted - version: "3.1.5" + version: "10.2.14" + firebase_storage_platform_interface: + dependency: transitive + description: + name: firebase_storage_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "4.1.4" + firebase_storage_web: + dependency: transitive + description: + name: firebase_storage_web + url: "https://pub.dartlang.org" + source: hosted + version: "3.2.13" fixnum: dependency: transitive description: name: fixnum url: "https://pub.dartlang.org" source: hosted - version: "0.10.11" + version: "1.0.0" flutter: dependency: "direct main" description: flutter @@ -320,19 +453,33 @@ packages: name: flutter_bloc url: "https://pub.dartlang.org" source: hosted - version: "3.2.0" + version: "8.0.1" flutter_localizations: dependency: "direct main" description: flutter source: sdk version: "0.0.0" + flutter_markdown: + dependency: transitive + description: + name: flutter_markdown + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.5" flutter_signin_button: dependency: "direct main" description: name: flutter_signin_button url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "2.0.0" flutter_test: dependency: "direct dev" description: flutter @@ -349,364 +496,448 @@ packages: name: font_awesome_flutter url: "https://pub.dartlang.org" source: hosted - version: "8.8.1" + version: "9.2.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.2" glob: dependency: transitive description: name: glob url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "2.0.0" + google_fonts: + dependency: "direct main" + description: + name: google_fonts + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" google_sign_in: dependency: "direct main" description: name: google_sign_in url: "https://pub.dartlang.org" source: hosted - version: "4.4.1" + version: "5.0.0" google_sign_in_platform_interface: dependency: transitive description: name: google_sign_in_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "2.0.1" google_sign_in_web: dependency: transitive description: name: google_sign_in_web url: "https://pub.dartlang.org" source: hosted - version: "0.8.4" + version: "0.10.0" graphs: dependency: transitive description: name: graphs url: "https://pub.dartlang.org" source: hosted - version: "0.2.0" + version: "2.1.0" html: dependency: transitive description: name: html url: "https://pub.dartlang.org" source: hosted - version: "0.14.0+3" + version: "0.14.0+4" + html2md: + dependency: transitive + description: + name: html2md + url: "https://pub.dartlang.org" + source: hosted + version: "0.5.1" http: dependency: transitive description: name: http url: "https://pub.dartlang.org" source: hosted - version: "0.12.0+4" + version: "0.13.0" http_multi_server: dependency: transitive description: name: http_multi_server url: "https://pub.dartlang.org" source: hosted - version: "2.2.0" + version: "3.2.0" http_parser: dependency: transitive description: name: http_parser url: "https://pub.dartlang.org" source: hosted - version: "3.1.4" - intl: + version: "4.0.0" + image_picker: dependency: "direct main" description: - name: intl + name: image_picker url: "https://pub.dartlang.org" source: hosted - version: "0.16.1" - intl_translation: - dependency: "direct dev" + version: "0.8.5" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + url: "https://pub.dartlang.org" + source: hosted + version: "0.8.4+11" + image_picker_for_web: + dependency: "direct main" + description: + name: image_picker_for_web + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.6" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + url: "https://pub.dartlang.org" + source: hosted + version: "0.8.5" + image_picker_platform_interface: + dependency: transitive description: - name: intl_translation + name: image_picker_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "0.17.9" + version: "2.4.4" + intl: + dependency: "direct main" + description: + name: intl + url: "https://pub.dartlang.org" + source: hosted + version: "0.17.0" io: dependency: transitive description: name: io url: "https://pub.dartlang.org" source: hosted - version: "0.3.4" + version: "1.0.3" js: dependency: transitive description: name: js url: "https://pub.dartlang.org" source: hosted - version: "0.6.1+1" + version: "0.6.3" json_annotation: dependency: "direct main" description: name: json_annotation url: "https://pub.dartlang.org" source: hosted - version: "3.0.1" + version: "4.5.0" json_serializable: dependency: "direct dev" description: name: json_serializable url: "https://pub.dartlang.org" source: hosted - version: "3.3.0" + version: "6.2.0" kt_dart: dependency: "direct main" description: name: kt_dart url: "https://pub.dartlang.org" source: hosted - version: "0.7.0+1" + version: "0.10.0" logging: dependency: "direct main" description: name: logging url: "https://pub.dartlang.org" source: hosted - version: "0.11.4" + version: "1.0.2" + markdown: + dependency: transitive + description: + name: markdown + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.0" matcher: dependency: transitive description: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.6" + version: "0.12.11" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.3" material_design_icons_flutter: dependency: "direct main" description: name: material_design_icons_flutter url: "https://pub.dartlang.org" source: hosted - version: "3.4.5045+1" + version: "5.0.6595" meta: dependency: transitive description: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.1.8" + version: "1.7.0" mime: dependency: transitive description: name: mime url: "https://pub.dartlang.org" source: hosted - version: "0.9.6+3" - mockito: - dependency: transitive - description: - name: mockito - url: "https://pub.dartlang.org" - source: hosted - version: "4.1.1" - multi_server_socket: + version: "1.0.1" + mocktail: dependency: transitive description: - name: multi_server_socket + name: mocktail url: "https://pub.dartlang.org" source: hosted - version: "1.0.2" + version: "0.3.0" nested: dependency: transitive description: name: nested url: "https://pub.dartlang.org" source: hosted - version: "0.0.4" - node_interop: - dependency: transitive - description: - name: node_interop - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.3" - node_io: - dependency: transitive - description: - name: node_io - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.1+2" + version: "1.0.0" node_preamble: dependency: transitive description: name: node_preamble url: "https://pub.dartlang.org" source: hosted - version: "1.4.8" + version: "2.0.1" package_config: dependency: transitive description: name: package_config url: "https://pub.dartlang.org" source: hosted - version: "1.9.3" + version: "2.0.2" package_info: dependency: "direct main" description: name: package_info url: "https://pub.dartlang.org" source: hosted - version: "0.4.0+16" + version: "2.0.2" path: dependency: "direct main" description: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.6.4" + version: "1.8.0" path_provider: dependency: "direct main" description: name: path_provider url: "https://pub.dartlang.org" source: hosted - version: "1.6.5" + version: "2.0.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" path_provider_macos: dependency: transitive description: name: path_provider_macos url: "https://pub.dartlang.org" source: hosted - version: "0.0.4" + version: "2.0.0" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" - pedantic: + version: "2.0.1" + path_provider_windows: dependency: transitive description: - name: pedantic + name: path_provider_windows url: "https://pub.dartlang.org" source: hosted - version: "1.9.0" - petitparser: + version: "2.0.0" + pedantic: dependency: transitive description: - name: petitparser + name: pedantic url: "https://pub.dartlang.org" source: hosted - version: "2.4.0" + version: "1.11.0" platform: dependency: transitive description: name: platform url: "https://pub.dartlang.org" source: hosted - version: "2.2.1" + version: "3.0.0" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "1.0.2" + version: "2.1.2" pool: dependency: transitive description: name: pool url: "https://pub.dartlang.org" source: hosted - version: "1.4.0" + version: "1.5.0" + process: + dependency: transitive + description: + name: process + url: "https://pub.dartlang.org" + source: hosted + version: "4.1.0" provider: dependency: transitive description: name: provider url: "https://pub.dartlang.org" source: hosted - version: "4.0.5" + version: "6.0.2" pub_semver: dependency: transitive description: name: pub_semver url: "https://pub.dartlang.org" source: hosted - version: "1.4.4" + version: "2.0.0" pubspec_parse: dependency: transitive description: name: pubspec_parse url: "https://pub.dartlang.org" source: hosted - version: "0.1.5" + version: "1.2.0" quiver: dependency: transitive description: name: quiver url: "https://pub.dartlang.org" source: hosted - version: "2.1.3" + version: "3.0.0" rxdart: dependency: "direct main" description: name: rxdart url: "https://pub.dartlang.org" source: hosted - version: "0.23.1" + version: "0.27.3" + share: + dependency: "direct main" + description: + name: share + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.4" shared_preferences: dependency: "direct main" description: name: shared_preferences url: "https://pub.dartlang.org" source: hosted - version: "0.5.6+3" + version: "2.0.5" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" shared_preferences_macos: dependency: transitive description: name: shared_preferences_macos url: "https://pub.dartlang.org" source: hosted - version: "0.0.1+6" + version: "2.0.0" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "1.0.3" + version: "2.0.0" shared_preferences_web: dependency: transitive description: name: shared_preferences_web url: "https://pub.dartlang.org" source: hosted - version: "0.1.2+4" + version: "2.0.0" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" shelf: dependency: transitive description: name: shelf url: "https://pub.dartlang.org" source: hosted - version: "0.7.5" + version: "1.0.0" shelf_packages_handler: dependency: transitive description: name: shelf_packages_handler url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "3.0.0" shelf_static: dependency: transitive description: name: shelf_static url: "https://pub.dartlang.org" source: hosted - version: "0.2.8" + version: "1.0.0" shelf_web_socket: dependency: transitive description: name: shelf_web_socket url: "https://pub.dartlang.org" source: hosted - version: "0.2.3" + version: "1.0.0" + sign_in_with_apple: + dependency: "direct main" + description: + name: sign_in_with_apple + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0" sky_engine: dependency: transitive description: flutter @@ -718,161 +949,238 @@ packages: name: source_gen url: "https://pub.dartlang.org" source: hosted - version: "0.9.5" + version: "1.2.2" + source_helper: + dependency: transitive + description: + name: source_helper + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.2" source_map_stack_trace: dependency: transitive description: name: source_map_stack_trace url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.1.0" source_maps: dependency: transitive description: name: source_maps url: "https://pub.dartlang.org" source: hosted - version: "0.10.9" + version: "0.10.10" source_span: dependency: transitive description: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.7.0" + version: "1.8.1" stack_trace: dependency: transitive description: name: stack_trace url: "https://pub.dartlang.org" source: hosted - version: "1.9.3" + version: "1.10.0" stream_channel: dependency: transitive description: name: stream_channel url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.1.0" stream_transform: dependency: transitive description: name: stream_transform url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "2.0.0" string_scanner: dependency: transitive description: name: string_scanner url: "https://pub.dartlang.org" source: hosted - version: "1.0.5" + version: "1.1.0" term_glyph: dependency: transitive description: name: term_glyph url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.2.0" test: dependency: transitive description: name: test url: "https://pub.dartlang.org" source: hosted - version: "1.14.2" + version: "1.19.5" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.15" + version: "0.4.8" test_core: dependency: transitive description: name: test_core url: "https://pub.dartlang.org" source: hosted - version: "0.3.3" + version: "0.4.9" time: dependency: transitive description: name: time url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "2.0.0" timeago: dependency: "direct main" description: name: timeago url: "https://pub.dartlang.org" source: hosted - version: "2.0.26" + version: "3.2.2" timing: dependency: transitive description: name: timing url: "https://pub.dartlang.org" source: hosted - version: "0.1.1+2" + version: "1.0.0" typed_data: dependency: transitive description: name: typed_data url: "https://pub.dartlang.org" source: hosted - version: "1.1.6" + version: "1.3.0" + url_launcher: + dependency: transitive + description: + name: url_launcher + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.2" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + uuid: + dependency: "direct main" + description: + name: uuid + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" vector_math: dependency: transitive description: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.0.8" + version: "2.1.1" vm_service: dependency: transitive description: name: vm_service url: "https://pub.dartlang.org" source: hosted - version: "4.0.0" + version: "6.1.0+1" watcher: dependency: transitive description: name: watcher url: "https://pub.dartlang.org" source: hosted - version: "0.9.7+14" + version: "1.0.0" web_socket_channel: dependency: transitive description: name: web_socket_channel url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "2.2.0" webkit_inspection_protocol: dependency: transitive description: name: webkit_inspection_protocol url: "https://pub.dartlang.org" source: hosted - version: "0.5.0+1" + version: "1.0.0" webview_flutter: dependency: "direct main" description: name: webview_flutter url: "https://pub.dartlang.org" source: hosted - version: "0.3.20+2" + version: "2.0.2" + win32: + dependency: transitive + description: + name: win32 + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.4" + wiredash: + dependency: "direct main" + description: + name: wiredash + url: "https://pub.dartlang.org" + source: hosted + version: "0.7.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0" yaml: dependency: transitive description: name: yaml url: "https://pub.dartlang.org" source: hosted - version: "2.2.0" + version: "3.1.0" sdks: - dart: ">=2.7.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" + dart: ">=2.16.0 <3.0.0" + flutter: ">=2.8.0" diff --git a/pubspec.yaml b/pubspec.yaml index 688dacb..68b5858 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -5,20 +5,10 @@ description: A CaH application for both iOS and Android # pub.dev using `pub publish`. This is preferred for private packages. publish_to: 'none' # Remove this line if you wish to publish to pub.dev -# The following defines the version and build number for your application. -# A version number is three numbers separated by dots, like 1.2.43 -# followed by an optional build number separated by a +. -# Both the version and the builder number may be overridden in flutter -# build by specifying --build-name and --build-number, respectively. -# In Android, build-name is used as versionName while build-number used as versionCode. -# Read more about Android versioning at https://developer.android.com/studio/publish/versioning -# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. -# Read more about iOS versioning at -# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.0.0+2 +version: 1.1.1+11 environment: - sdk: ">=2.7.0 <3.0.0" + sdk: ">=2.12.0 <3.0.0" dependencies: flutter: @@ -27,50 +17,56 @@ dependencies: sdk: flutter # System Packages - intl: ^0.16.0 - logging: ^0.11.3+2 - shared_preferences: ^0.5.6 - package_info: ^0.4.0+14 - device_info: ^0.4.1+5 - path_provider: ^1.6.5 + intl: ^0.17.0 + logging: ^1.0.2 + shared_preferences: ^2.0.4 + package_info: ^2.0.2 + device_info: ^2.0.3 + path_provider: ^2.0.1 path: ^1.6.4 + uuid: ^3.0.1 + share: ^2.0.4 + json_annotation: ^4.4.0 + wiredash: ^0.7.0+1 + email_validator: ^2.0.1 + dartx: ^1.0.0 # Architecture and Tools - bloc: ^3.0.0 - flutter_bloc: ^3.2.0 - rxdart: ^0.23.1 - dartx: ^0.3.0 - kt_dart: ^0.7.0+1 - equatable: ^1.1.1 + bloc: ^8.0.2 + flutter_bloc: ^8.0.1 + rxdart: ^0.27.3 + kt_dart: ^0.10.0 + equatable: ^2.0.3 # FlutterFire - firebase_core: 0.4.4 - firebase_auth: 0.15.3 -# firebase_analytics: ^5.0.11 - firebase_storage: ^3.1.3 - cloud_firestore: ^0.13.4+1 - cloud_functions: ^0.4.2+3 - google_sign_in: ^4.4.1 - apple_sign_in: ^0.1.0 + firebase_core: ^1.0.1 + firebase_auth: ^3.3.5 + firebase_analytics: ^9.0.5 + firebase_storage: ^10.2.5 + firebase_messaging: ^11.2.5 + firebase_dynamic_links: ^4.0.4 + cloud_firestore: ^3.1.6 + cloud_functions: ^3.2.5 + google_sign_in: ^5.0.0 + sign_in_with_apple: ^3.0.0 # UI - material_design_icons_flutter: ^3.4.5045 - timeago: ^2.0.26 - webview_flutter: ^0.3.20+2 - flutter_signin_button: ^1.0.0 - - json_annotation: ^3.0.0 + material_design_icons_flutter: ^5.0.6595 + timeago: ^3.1.0 + webview_flutter: ^2.0.2 + easy_web_view: ^1.4.0-nullsafety + flutter_signin_button: ^2.0.0 + image_picker: ^0.8.4+4 + image_picker_for_web: ^2.0.0 + google_fonts: ^2.0.0 + clipboard: ^0.1.3 dev_dependencies: flutter_test: sdk: flutter - intl_translation: ^0.17.7 - bloc_test: ^3.0.0 - build_runner: ^1.0.0 - json_serializable: ^3.2.0 - -builders: - json_config_builder: 0.0.6 + bloc_test: ^9.0.2 + build_runner: ^2.1.7 + json_serializable: ^6.1.4 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/regenerate.sh b/regenerate.sh new file mode 100755 index 0000000..4967654 --- /dev/null +++ b/regenerate.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +flutter pub get +flutter packages pub run build_runner build --delete-conflicting-outputs + +echo "Code Regenerated!" diff --git a/test/widget_test.dart b/test/widget_test.dart index 885f18b..553e64a 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -5,7 +5,6 @@ // gestures. You can also use WidgetTester to find child widgets in the widget // tree, read text, and verify that the values of widget properties are correct. -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:appsagainsthumanity/main.dart'; +import 'package:flutter/material.dart'; // ignore: unused_import +import 'package:flutter_test/flutter_test.dart'; // ignore: unused_import +import 'package:appsagainsthumanity/main.dart'; // ignore: unused_import diff --git a/web/index.html b/web/index.html index d732dd9..c6d5ea0 100644 --- a/web/index.html +++ b/web/index.html @@ -1,6 +1,18 @@ + + + @@ -9,32 +21,39 @@ - + - + Apps Against Humanity + application. For more information, see: + https://developers.google.com/web/fundamentals/primers/service-workers --> - + + - + - - + + + + + +