From 3ab6e985b648e7ec39185df671cfb4cdbbd09629 Mon Sep 17 00:00:00 2001 From: Kacper Kogut Date: Sun, 12 Nov 2023 23:32:35 +0100 Subject: [PATCH] Feature/resolve conflicts (#225) * Update secret extractor * Fix app build * Split release workflows * Remove google services generation * Fixes - launch learning resource when logged out, new filter for all tab, remove campaign description new line characters * Fix all analyze warnings (#224) * Fix all analyze warnings * Regenerated pod lockfile * Don't fail analyze on infos * Fix analysis_options sections * Regenerated pod lockfile * Fix nullability issues * Regenerated pod lockfile * Remove unrecognized `--set-exit-if-change` from Flutter formatter --------- Co-authored-by: kackogut * tech/update-inappwebview (#223) * Fix iOs builds by upgrading webview version * Regenerated pod lockfile * Regenerated pod lockfile --------- Co-authored-by: kackogut * Regenerated pod lockfile --------- Co-authored-by: James Elgar Co-authored-by: kackogut --- .github/actions/setup-app/action.yml | 2 +- .github/workflows/android-release.yml | 56 +++++ .github/workflows/flutter-ci-cd.yaml | 189 +++------------ .github/workflows/flutter-test.yml | 6 +- .github/workflows/ios-release.yml | 89 +++++++ .github/workflows/secret-extractor.yml | 5 +- CHANGELOG.md | 223 ++++++++---------- analysis_options.yaml | 1 - ios/.gitignore | 2 +- ios/Podfile.lock | 22 +- ios/Runner.xcodeproj/project.pbxproj | 6 +- ios/Runner/ExportOptions.plist | 2 +- ios/Runner/GoogleService-Info.plist | 36 +++ lib/models/Action.dart | 6 +- lib/models/Campaign.dart | 3 +- lib/models/article.dart | 4 +- lib/services/api_service.dart | 1 + lib/services/causes_service.dart | 5 +- lib/services/dynamicLinks.dart | 8 +- .../internal_notification_service.dart | 5 +- lib/services/pushNotifications.dart | 2 +- lib/services/search_service.dart | 9 +- lib/ui/dialogs/basic/basic_dialog.dart | 5 +- .../campaign_info/campaign_info_view.dart | 2 +- .../views/explore/explore_page_viewmodel.dart | 23 +- lib/ui/views/home/home_viewmodel.dart | 15 +- lib/ui/views/more/more_view.dart | 183 +++++++------- lib/ui/views/more/more_viewmodel.dart | 10 +- lib/utils/new_since.dart | 1 + pubspec.lock | 12 +- pubspec.yaml | 5 +- test/setup/helpers/image_helpers.dart | 2 +- 32 files changed, 499 insertions(+), 441 deletions(-) create mode 100644 .github/workflows/android-release.yml create mode 100644 .github/workflows/ios-release.yml create mode 100644 ios/Runner/GoogleService-Info.plist create mode 100644 lib/utils/new_since.dart diff --git a/.github/actions/setup-app/action.yml b/.github/actions/setup-app/action.yml index e7e1f5363..80ed16d80 100644 --- a/.github/actions/setup-app/action.yml +++ b/.github/actions/setup-app/action.yml @@ -20,7 +20,7 @@ runs: channel: 'beta' # 'dev', 'alpha', default to: 'stable' flutter-version: '3.14.x' cache: true - + - name: Flutter version shell: bash run: | diff --git a/.github/workflows/android-release.yml b/.github/workflows/android-release.yml new file mode 100644 index 000000000..27dda69ff --- /dev/null +++ b/.github/workflows/android-release.yml @@ -0,0 +1,56 @@ +name: Android release + +on: + workflow_dispatch: + workflow_call: + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref }} + cancel-in-progress: true + +jobs: + release_android: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v2 + + - name: ๐Ÿงฐ Setup app + uses: ./.github/actions/setup-app + + - name: ๐Ÿ”‘ Add secrets + env: + KEY_PROPERTIES_BASE64: ${{ secrets.KEY_PROPERTIES_BASE64 }} + KEY_JKS_BASE64: ${{ secrets.KEY_JKS_BASE64 }} + run: | + echo $KEY_PROPERTIES_BASE64 | base64 -d > ./android/key.properties + echo $KEY_JKS_BASE64 | base64 -d > ./android/app/key.jks + + - name: ๐Ÿดโ€โ˜ ๏ธ Java setup + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + cache: 'gradle' + + - name: ๐Ÿ› ๏ธ Build app + run: flutter build appbundle --release + + - name: โซ Upload bundle as gh artifact + uses: actions/upload-artifact@v2 + with: + name: Signed app bundle + path: ${{steps.sign_app.outputs.signedReleaseFile}} + + - name: โซ Upload bundle to Play Store + uses: r0adkll/upload-google-play@v1 + with: + serviceAccountJsonPlainText: ${{ secrets.ANDROID_SERVICE_ACCOUNT_JSON }} + packageName: com.nowu.app + releaseFiles: /home/runner/work/now-u-app/now-u-app/build/app/outputs/bundle/release/app-release.aab + track: production + status: completed + # inAppUpdatePriority: 2 + # userFraction: 0.33 + # whatsNewDirectory: distribution/whatsnew + # mappingFile: app/build/outputs/mapping/release/mapping.txt diff --git a/.github/workflows/flutter-ci-cd.yaml b/.github/workflows/flutter-ci-cd.yaml index d3f5444c5..df5681f3e 100644 --- a/.github/workflows/flutter-ci-cd.yaml +++ b/.github/workflows/flutter-ci-cd.yaml @@ -1,169 +1,58 @@ name: Flutter CI CD -# This workflow is triggered on pushes to the repository. - on: - push: - # branches: [staging, master] - branches: [staging] - # pull_request: - # branches: [ dev ] + push: + branches: [main] + tags: + - '[0-9]+.[0-9]+.[0-9]+' + workflow_dispatch: # to manually run this workflow - -# on: push # Default will running for every branch. - concurrency: group: ${{ github.workflow }}-${{ github.head_ref }} cancel-in-progress: true jobs: - android: - # This job will run on ubuntu virtual machine - runs-on: ubuntu-latest - timeout-minutes: 30 - env: - KEY_JKS: ${{ secrets.ANDROID_KEY_JKS }} - KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} - ALIAS_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} - steps: - - uses: actions/checkout@v2 - - - name: Key base64 to file - id: write_file_key - uses: timheuer/base64-to-file@v1 - with: - fileName: 'key.jks' - encodedString: ${{ secrets.KEY_JKS_BASE64 }} - - - name: Key properties base64 to file - id: write_file_key_properties - uses: timheuer/base64-to-file@v1 - with: - fileName: 'key.properties' - encodedString: ${{ secrets.KEY_PROPERTIES_BASE64 }} - - - name: Move key.jks and key.properties to proper dir - run: | - mv ${{ steps.write_file_key.outputs.filePath }} ./android/app/ - mv ${{ steps.write_file_key_properties.outputs.filePath }} ./android/ - - - name: Create firebase google-service.json - env: - FIREBASE_CONFIG: ${{ secrets.FIREBASE_CONFIG_PROD }} - run: echo $FIREBASE_CONFIG > ./android/app/google-services.json - - # Setup Java environment in order to build the Android app. - - uses: actions/setup-java@v3 - with: - java-version: '17' - distribution: 'temurin' - cache: 'gradle' - - - name: Setup app - uses: ./.github/actions/setup-app - - # env: - # ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true' - # env: - # KEY_JKS: ${{ secrets.ANDROID_KEY_JKS }} - # KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} - # ALIAS_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} - # run: echo $KEY_JKS > android/key.jks && flutter pub get && flutter build apk --release --no-shrink - - # Export key into key.jks file - # - run: pwd - # - run: echo $TEST_SECRET - # - run: echo $KEY_JKS > android/key.jks - - # Build apk - - run: flutter build appbundle --release - - # Upload generated apk to the artifacts. - - uses: actions/upload-artifact@v1 - with: - name: release-android - path: build/app/outputs/bundle/release/app-release.aab - - - name: Upload to Play Store # Finish setting this up - uses: r0adkll/upload-google-play@v1 - with: - serviceAccountJsonPlainText: ${{ secrets.ANDROID_SERVICE_ACCOUNT_JSON }} - packageName: com.nowu.app - releaseFiles: /home/runner/work/now-u-app/now-u-app/build/app/outputs/bundle/release/app-release.aab - track: beta - status: completed - # inAppUpdatePriority: 2 - # userFraction: 0.33 - # whatsNewDirectory: distribution/whatsnew - # mappingFile: app/build/outputs/mapping/release/mapping.txt - - - # TODO https://damienaicheh.github.io/flutter/github/actions/2021/04/22/build-sign-flutter-ios-github-actions-en.html - build_ios: + prepare: runs-on: macos-latest timeout-minutes: 30 steps: - # Checks-out our repository under $GITHUB_WORKSPACE, so our job can access it - - name: Checkout repository - uses: actions/checkout@v2 + - uses: actions/checkout@v2 + + - name: Extract version from tag + uses: damienaicheh/extract-version-from-tag-action@v1.1.0 - # Install the Apple certificate and provisioning profile - - name: Install the Apple certificate and provisioning profile - env: - BUILD_CERTIFICATE_BASE64: ${{ secrets.APPSTORE_CERT_BASE64 }} - P12_PASSWORD: ${{ secrets.APPSTORE_CERT_PASSWORD }} - BUILD_PROVISION_PROFILE_BASE64: ${{ secrets.MOBILEPROVISION_BASE64 }} - KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} + - name: Setup cider run: | - # create variables - CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12 - PP_PATH=$RUNNER_TEMP/build_pp.mobileprovision - KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db - # import certificate and provisioning profile from secrets - echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode --output $CERTIFICATE_PATH - echo -n "$BUILD_PROVISION_PROFILE_BASE64" | base64 --decode --output $PP_PATH - # create temporary keychain - security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH - security set-keychain-settings -lut 21600 $KEYCHAIN_PATH - security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH - # import certificate to keychain - security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH - security list-keychain -d user -s $KEYCHAIN_PATH - # apply provisioning profile - mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles - cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles + export PATH="$PATH":"$HOME/.pub-cache/bin" + pub global activate cider - - name: Create firebase GoogleService-Info.plist - env: - FIREBASE_CONFIG: ${{ secrets.FIREBASE_IOS_CONFIG_PROD }} - run: echo $FIREBASE_CONFIG > ./ios/Runner/GoogleService-Info.plist + - name: Set version + run: | + echo "Setting version to ${{ github.ref_name }}" + cider version ${{ github.ref_name }} - - name: Setup app - uses: ./.github/actions/setup-app + - name: Update change log + run: cider release - # Build and sign the ipa using a single flutter command - - name: Building IPA - run: flutter build ipa --release --export-options-plist=ios/Runner/ExportOptions.plist - - # Collect the file and upload as artifact - - name: collect ipa artifacts - uses: actions/upload-artifact@v2 + - name: Commit changes + uses: EndBug/add-and-commit@v7 with: - name: release-ipa - # Path to the release files - path: build/ios/ipa/*.ipa - - # Important! Cleanup: remove the certificate and provisioning profile from the runner! - - name: Clean up keychain and provisioning profile - if: ${{ always() }} - run: | - security delete-keychain $RUNNER_TEMP/app-signing.keychain-db - rm ~/Library/MobileDevice/Provisioning\ Profiles/build_pp.mobileprovision - - - name: Publishing app to TestFlight - env: - APPLEID_USERNAME: ${{ secrets.APPLEID_USERNAME }} - APPLEID_PASSWORD: ${{ secrets.APPLEID_PASSWORD }} - IPA_PATH: build/ios/ipa/now-u.ipa - run: ./.github/scripts/publish_testflight.sh + author_name: GitHub Actions + author_email: actions@github.com + branch: main + message: "Update version on pubspec.yaml & update change log" + + - name: Release ios + uses: now-u/now-u-backend/.github/workflows/android-release.yml@main + secrets: inherit + + release-ios: + needs: prepare + uses: now-u/now-u-app/.github/workflows/ios-release.yml@main + secrets: inherit + + release-android: + needs: prepare + uses: now-u/now-u-app/.github/workflows/android-release.yml@main + secrets: inherit diff --git a/.github/workflows/flutter-test.yml b/.github/workflows/flutter-test.yml index d8ae16b25..55b614b1f 100644 --- a/.github/workflows/flutter-test.yml +++ b/.github/workflows/flutter-test.yml @@ -23,10 +23,10 @@ jobs: - run: pod repo update - name: Flutter analyze - run: flutter analyze + run: flutter analyze --no-fatal-infos - - name: Flutter analyze - run: dart format -o none --set-exit-if-change + - name: Flutter format + run: dart format -o none - name: Run tests run: flutter test diff --git a/.github/workflows/ios-release.yml b/.github/workflows/ios-release.yml new file mode 100644 index 000000000..a243eb46a --- /dev/null +++ b/.github/workflows/ios-release.yml @@ -0,0 +1,89 @@ +name: IOS release + +on: + workflow_dispatch: + workflow_call: + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref }} + cancel-in-progress: true + +# TODO https://damienaicheh.github.io/flutter/github/actions/2021/04/22/build-sign-flutter-ios-github-actions-en.html +jobs: + release_ios: + runs-on: macos-latest + timeout-minutes: 30 + steps: + # Checks-out our repository under $GITHUB_WORKSPACE, so our job can access it + - name: Checkout repository + uses: actions/checkout@v2 + + # Install the Apple certificate and provisioning profile + - name: ๐Ÿ”‘ Install the Apple certificate and provisioning profile + env: + # NOTE: Once a year this certificate will expire. + # This secret stores the base64 encoded p12. This can be generated with the following: + # + # Follow this guide to convert the downloaded certifacte to p12 + # https://gist.github.com/jcward/d08b33fc3e6c5f90c18437956e5ccc35 + # Rough overview + # Generate certifacte (.cer) or use the same file as before + # Upload cert here and download output: https://developer.apple.com/account/resources/profiles/list + # Do some stuff to generate p12 cert (ios_distribution.p12) + # NOTE: When running `openssl pkcs12 -export` use the -legacy flag + # NOTE: Download apple cert from https://developer.apple.com/certificationauthority/AppleWWDRCA.cer + # + # After generating use `cat ios_distribution.p12 | base64 | xclip + # -selection c` to copy the output and update the secret. If passowrd + # is chnage update P12_PASSWORD as well + BUILD_CERTIFICATE_BASE64: ${{ secrets.APPSTORE_CERT_BASE64 }} + P12_PASSWORD: ${{ secrets.APPSTORE_CERT_PASSWORD }} + BUILD_PROVISION_PROFILE_BASE64: ${{ secrets.MOBILEPROVISION_BASE64 }} + KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} + run: | + # create variables + CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12 + PP_PATH=$RUNNER_TEMP/build_pp.mobileprovision + KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db + # import certificate and provisioning profile from secrets + echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode --output $CERTIFICATE_PATH + echo -n "$BUILD_PROVISION_PROFILE_BASE64" | base64 --decode --output $PP_PATH + # create temporary keychain + security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + security set-keychain-settings -lut 21600 $KEYCHAIN_PATH + security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + # import certificate to keychain + security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH + security list-keychain -d user -s $KEYCHAIN_PATH + # apply provisioning profile + mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles + cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles + + - name: ๐Ÿงฐ Setup app + uses: ./.github/actions/setup-app + + # Build and sign the ipa using a single flutter command + - name: ๐Ÿ› ๏ธ Building IPA + run: flutter build ipa --release --export-options-plist=ios/Runner/ExportOptions.plist + + # Collect the file and upload as artifact + - name: โซ Upload bundle as gh artifact + uses: actions/upload-artifact@v2 + with: + name: release-ipa + # Path to the release files + path: build/ios/ipa/*.ipa + + # Important! Cleanup: remove the certificate and provisioning profile from the runner! + - name: ๐Ÿงน Clean up keychain and provisioning profile + if: ${{ always() }} + run: | + security delete-keychain $RUNNER_TEMP/app-signing.keychain-db + rm ~/Library/MobileDevice/Provisioning\ Profiles/build_pp.mobileprovision + + - name: โซ Publishing app to TestFlight + env: + APPLEID_USERNAME: ${{ secrets.APPLEID_USERNAME }} + APPLEID_PASSWORD: ${{ secrets.APPLEID_PASSWORD }} + IPA_PATH: build/ios/ipa/now-u.ipa + run: ./.github/scripts/publish_testflight.sh diff --git a/.github/workflows/secret-extractor.yml b/.github/workflows/secret-extractor.yml index 0b1187cce..2509d560e 100644 --- a/.github/workflows/secret-extractor.yml +++ b/.github/workflows/secret-extractor.yml @@ -4,8 +4,7 @@ on: workflow_dispatch: jobs: - android: - # This job will run on ubuntu virtual machine + extract: runs-on: ubuntu-latest timeout-minutes: 30 env: @@ -17,8 +16,10 @@ jobs: - name: Extract secrets env: + KEY_PROPERTIES: ${{ secrets.KEY_PROPERTIES_BASE64}} KEY_JKS: ${{ secrets.KEY_JKS_BASE64}} OPENSSL_PASSWORD: ${{ secrets.SECRET_EXTRACTION_OPENSSL_PASSWORD }} OPENSSL_ITER: ${{ secrets.SECRET_EXTRACTION_OPENSSL_ITER }} run: | + echo "KEY_PROPERTIES = $(echo "${KEY_PROPERTIES}" | openssl enc -e -aes-256-cbc -a -pbkdf2 -iter ${OPENSSL_ITER} -k "${OPENSSL_PASSWORD}")" echo "KEY_JKS = $(echo "${KEY_JKS}" | openssl enc -e -aes-256-cbc -a -pbkdf2 -iter ${OPENSSL_ITER} -k "${OPENSSL_PASSWORD}")" diff --git a/CHANGELOG.md b/CHANGELOG.md index 757639ccf..78ffdf8f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,132 +1,93 @@ # now-u app changelog - -## What is a changelog? - -A changelog is a file which contains a curated, chronologically ordered list of -notable changes for each version of a project. - -## Why keep a changelog? - -To make it easier for users and contributors to see precisely what notable -changes have been made between each release (or version) of the project. - -## Who needs a changelog? - -People do. Whether consumers or developers, the end users of software are human -beings who care about what's in the software. When the software changes, people -want to know why and how. - -## How do I make a good changelog? - -### Guiding Principles - -- Changelogs are for humans, not machines. -- There should be an entry for every single version. -- The same types of changes should be grouped. -- Versions and sections should be linkable. -- The latest version comes first. -- The release date of each version is displayed. -- Mention whether you follow Semantic Versioning. - -### Types of changes - -- `Added` for new features. -- `Changed` for changes in existing functionality. -- `Deprecated` for soon-to-be removed features. -- `Removed` for now removed features. -- `Fixed` for any bug fixes. -- `Security` in case of vulnerabilities. - -The above is taken from: https://keepachangelog.com/en/1.0.0/ - -## Whats the structure? - -- # - Description of what has changed (following guiding - principles) - -## The log - -### Unreleased - -- #155 [feature] Drop down button is added to be opened on external browser - -### Version 1.2.0 -- #127 [feature] Login popups added which notify users of error -- #129 [feature] The number of actions completed by now-u users now shows on - each campaign page -- #131 [fix] Http links can now be viewed in the internal webview -- #135 [feature] Campaign researchers (user_role_id=3) can now see future and - disabled campaigns -- #136 [feature] Links can now be user to link to different pages of the app -- #82 [feature] About us link added to more menu -- #148 [fix] App no longer crashes on NetworkImage error -- #144 [fix] Fix for mailto links - -### Version 1.1.9 - Hot Fix - -- #125 [fix] Fixed action page - -### Version 1.1.8 - -- #123 [fix] Text links in internal notifcations are now clickable and the - text now longer overflows - -### Version 1.1.7 - -- #110 [feature] Campaign page design update -- #111 [fix] App no longer crashes if no action time provided -- #112 [feature] The app now uses stacked instead of redux -- #113 [feature] An early version of internal notifications have been added -- #114 [feature] Campaign page campaign tiles have been updated. Campaigns can - now be joined directly from this page -- #120 [feature] A new popup service has been added to allow for more - information about error -- #121 [feature] Updated login flow allowing for token entry if email link is - not working - -### Version 1.1.6 - -- #NA [fix] Fix ios incorrect supprorted version and ios push - notifcation - -### Version 1.1.5 - -- #94 [fix] Fixed overflow issue (grey tiles) on actions page -- #95 [fix] Actions completed on the home page now show the correct - value -- #98 [feature] Actions for past campaigns can now be viewed -- #101 [feature] Past campaign feedback form added to more menu - -### Version 1.1.4 - -- #91 [fix] Action's 'take action' now open in webview and learning - resouces are checked off when clicked - -### Version 1.1.3 - -- #85 [change] The style of the organisation page has been updated -- #56 [change] Web links now open within the app -- #64 [change] Users can now signup for the newsletter during the signup - process - -### Version 1.1.2 - -- #66 [change] Campaigns can now be shared from the campaigns page -- #78 [change] The more menu is now split into sections - -### Version 1.1.1 - -- #60 [change] Learning resources now display their source -- #75 [change] Past campaigns can now be viewd on the app -- #71 [change] Learning resources can now be completed by viewing the resource - link -- #58 [change] New actions and now campaigns now have the 'new' tag -- #63 [change] Users have to agree to terms and conditions before logging in -- #57 [change] Deveopers can now use the staging branch api -- #NA [upgrade] Updated to flutter v1.2.0 (master) -- #39 [change] The menu icons have been updated -- #40 [change] The page route and campaign/action completion has been added to - firebase analytics -- #72 [bug] Improved dynamic links service -- #46 [bug] The app no longer crashed when clicking 'Rate the app' in the - menu +## Unreleased +### Added +- Added login button to more menu for unauthenticated users +- Fix new filter for all explore tab + +### Fixed +- Fix launching learning resource for unlogged in user +- Add padding to buttons in basic dialog +- Fix rendering of campaign descriptions to remove newline characters + +## 2.0.1 - 2023-09-18 +### Changed +- Updated app to use api v2 +- Updated explore to new UX +- \#155 Drop down button is added to be opened on external browser + +## 1.2.0 - 2022-01-01 +### Added +- \#127 Login popups added which notify users of error +- \#129 The number of actions completed by now-u users now shows on each campaign page +- \#135 Campaign researchers (user\_role\_id=3) can now see future and disabled campaigns +- \#136 Links can now be user to link to different pages of the app +- \#82 About us link added to more menu + +### Fixed +- \#148 App no longer crashes on NetworkImage error +- \#144 Fix for mailto links +- \#131 Http links can now be viewed in the internal webview + +## 1.1.9 - 2022-01-01 +### Fixed +- \#125 Fixed action page + +## 1.1.8 - 2022-01-01 +### Fixed +- \#123 Text links in internal notifcations are now clickable and the text now longer overflows + +## 1.1.7 - 2022-01-01 +### Added +- \#110 Campaign page design update +- \#112 The app now uses stacked instead of redux +- \#113 An early version of internal notifications have been added +- \#114 Campaign page campaign tiles have been updated. Campaigns can now be jy from this page +- \#120 A new popup service has been added to allow for more information +- \#121 Updated login flow allowing for token entry if email link is not working + +### Fixed +- \#111 App no longer crashes if no action time provided + +## 1.1.6 - 2022-01-01 +### Fixed +- \#NA Fix ios incorrect supprorted version and ios push notifcation + +## 1.1.5 - 2022-01-01 +### Added +- \#98 Actions for past campaigns can now be viewed +- \#101 Past campaign feedback form added to more menu + +### Fixed +- \#94 Fixed overflow issue (grey tiles) on actions page +- \#95 Actions completed on the home page now show the correct value + +## 1.1.4 - 2022-01-01 +### Fixed +- \#91 Action's 'take action' now open in webview and learning resouces are checked off when clicked + +## 1.1.3 - 2022-01-01 +### Changed +- \#85 The style of the organisation page has been updated +- \#56 Web links now open within the app +- \#64 Users can now signup for the newsletter during the signup process + +## 1.1.2 - 2022-01-01 +### Changed +- \#66 Campaigns can now be shared from the campaigns page +- \#78 The more menu is now split into sections + +## 1.1.1 - 2022-01-01 +### Changed +- \#60 Learning resources now display their source +- \#75 Past campaigns can now be viewd on the app +- \#71 Learning resources can now be completed by viewing the resource link +- \#58 New actions and now campaigns now have the 'new' tag +- \#63 Users have to agree to terms and conditions before logging in +- \#57 Deveopers can now use the staging branch api +- \#NA Updated to flutter v1.2.0 (master) +- \#39 The menu icons have been updated +- \#40 The page route and campaign/action completion has been added to firebase analytics + +### Fixed +- \#72 Improved dynamic links service +- \#46 The app no longer crashed when clicking 'Rate the app' in the menu diff --git a/analysis_options.yaml b/analysis_options.yaml index 5ef14eb4a..795830f20 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -7,7 +7,6 @@ # The following line activates a set of recommended lints for Flutter apps, # packages, and plugins designed to encourage good coding practices. -include: package:flutter_lints/flutter.yaml analyzer: exclude: diff --git a/ios/.gitignore b/ios/.gitignore index c5f1ed45f..61a66774d 100644 --- a/ios/.gitignore +++ b/ios/.gitignore @@ -26,7 +26,7 @@ Flutter/flutter_assets/ Flutter/flutter_export_environment.sh ServiceDefinitions.json Runner/GeneratedPluginRegistrant.* -Runner/GoogleService-Info.plist +# Runner/GoogleService-Info.plist # Exceptions to above rules. !default.mode1v3 diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 3474322bc..16fb64e68 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -45,7 +45,7 @@ PODS: - Firebase/RemoteConfig (= 10.12.0) - firebase_core - Flutter - - FirebaseABTesting (10.15.0): + - FirebaseABTesting (10.16.0): - FirebaseCore (~> 10.0) - FirebaseAnalytics (10.12.0): - FirebaseAnalytics/AdIdSupport (= 10.12.0) @@ -69,9 +69,9 @@ PODS: - FirebaseCoreInternal (~> 10.0) - GoogleUtilities/Environment (~> 7.8) - GoogleUtilities/Logger (~> 7.8) - - FirebaseCoreExtension (10.15.0): + - FirebaseCoreExtension (10.16.0): - FirebaseCore (~> 10.0) - - FirebaseCoreInternal (10.15.0): + - FirebaseCoreInternal (10.16.0): - "GoogleUtilities/NSData+zlib (~> 7.8)" - FirebaseCrashlytics (10.12.0): - FirebaseCore (~> 10.5) @@ -83,7 +83,7 @@ PODS: - PromisesObjC (~> 2.1) - FirebaseDynamicLinks (10.12.0): - FirebaseCore (~> 10.0) - - FirebaseInstallations (10.15.0): + - FirebaseInstallations (10.16.0): - FirebaseCore (~> 10.0) - GoogleUtilities/Environment (~> 7.8) - GoogleUtilities/UserDefaults (~> 7.8) @@ -103,7 +103,7 @@ PODS: - FirebaseInstallations (~> 10.0) - GoogleUtilities/Environment (~> 7.8) - "GoogleUtilities/NSData+zlib (~> 7.8)" - - FirebaseSessions (10.15.0): + - FirebaseSessions (10.16.0): - FirebaseCore (~> 10.5) - FirebaseCoreExtension (~> 10.0) - FirebaseInstallations (~> 10.0) @@ -317,19 +317,19 @@ SPEC CHECKSUMS: firebase_dynamic_links: d85cf455646322fd101c8a5a5942c3d47132fe80 firebase_messaging: 334d68c3a36b6d4d5cd91e4f42509e0d4ae49828 firebase_remote_config: 2202a8929d451170be6505f90b2953f42ae238ec - FirebaseABTesting: 7fa3bca17f79ac433301d20d5cd33401f7738dca + FirebaseABTesting: 03f0a8b88cf618350527f2c6a2234e29b9c65064 FirebaseAnalytics: 0270389efbe3022b54ec4588862dabec3477ee98 FirebaseCore: f86a1394906b97ac445ae49c92552a9425831bed - FirebaseCoreExtension: d3f1ea3725fb41f56e8fbfb29eeaff54e7ffb8f6 - FirebaseCoreInternal: 2f4bee5ed00301b5e56da0849268797a2dd31fb4 + FirebaseCoreExtension: 2dbc745b337eb99d2026a7a309ae037bd873f45e + FirebaseCoreInternal: 26233f705cc4531236818a07ac84d20c333e505a FirebaseCrashlytics: c4d111b7430c49744c74bcc6346ea00868661ac8 FirebaseDynamicLinks: 1a387da899779e5ef34f4d6f8bdba882f90d0e67 - FirebaseInstallations: cae95cab0f965ce05b805189de1d4c70b11c76fb + FirebaseInstallations: b822f91a61f7d1ba763e5ccc9d4f2e6f2ed3b3ee FirebaseMessaging: bb2c4f6422a753038fe137d90ae7c1af57251316 FirebaseRemoteConfig: bc7f260e6596956fafbb532443c19bd3c30f5258 - FirebaseSessions: ee59a7811bef4c15f65ef6472f3210faa293f9c8 + FirebaseSessions: 96e7781e545929cde06dd91088ddbb0841391b43 Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 - flutter_inappwebview: bfd58618f49dc62f2676de690fc6dcda1d6c3721 + flutter_inappwebview: 3d32228f1304635e7c028b0d4252937730bbc6cf flutter_secure_storage: 7953c38a04c3fdbb00571bcd87d8e3b5ceb9daec FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a GoogleAppMeasurement: 2d800fab85e7848b1e66a6f8ce5bca06c5aad892 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index cc4e4d7c3..109877c1a 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -392,7 +392,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.nowu.app; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = nowu; + PROVISIONING_PROFILE_SPECIFIER = now-u; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; @@ -526,7 +526,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.nowu.app; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = nowu; + PROVISIONING_PROFILE_SPECIFIER = now-u; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -552,7 +552,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.nowu.app; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = nowu; + PROVISIONING_PROFILE_SPECIFIER = now-u; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; diff --git a/ios/Runner/ExportOptions.plist b/ios/Runner/ExportOptions.plist index 760799354..1dc777176 100644 --- a/ios/Runner/ExportOptions.plist +++ b/ios/Runner/ExportOptions.plist @@ -9,7 +9,7 @@ provisioningProfiles com.nowu.app - nowu + now-u signingCertificate Apple Distribution diff --git a/ios/Runner/GoogleService-Info.plist b/ios/Runner/GoogleService-Info.plist new file mode 100644 index 000000000..e874e4eb2 --- /dev/null +++ b/ios/Runner/GoogleService-Info.plist @@ -0,0 +1,36 @@ + + + + + CLIENT_ID + 938145287148-3b96qect9a9drnhsio206ld9go22fk1h.apps.googleusercontent.com + REVERSED_CLIENT_ID + com.googleusercontent.apps.938145287148-3b96qect9a9drnhsio206ld9go22fk1h + API_KEY + AIzaSyCra_DdnjjKhFrbMadUtnKPKbRYd0qEotY + GCM_SENDER_ID + 938145287148 + PLIST_VERSION + 1 + BUNDLE_ID + com.nowu.app + PROJECT_ID + now-u-d140a + STORAGE_BUCKET + now-u-d140a.appspot.com + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:938145287148:ios:1527edd0f9d24278c68ec9 + DATABASE_URL + https://now-u-d140a.firebaseio.com + + \ No newline at end of file diff --git a/lib/models/Action.dart b/lib/models/Action.dart index 73a446d26..8f9f29b21 100644 --- a/lib/models/Action.dart +++ b/lib/models/Action.dart @@ -108,7 +108,8 @@ extension ListActionExtension on ListAction { // TODO This is not quite right, it could be enabled - update API to provide releasedTime // which is set based on when the action was enabled/released - DateTime get releaseTime => releaseAt ?? createdAt; + // DateTime get releaseTime => releaseAt ?? createdAt; + DateTime get releaseTime => releaseAt; bool get isNew => isNewDate(releaseTime); String get timeText => getTimeText(time); @@ -124,7 +125,8 @@ extension ActionExtension on Action { // TODO This is not quite right, it could be enabled - update API to provide releasedTime // which is set based on when the action was enabled/released - DateTime get releaseTime => releaseAt ?? createdAt; + // DateTime get releaseTime => releaseAt ?? createdAt; + DateTime get releaseTime => releaseAt; bool get isNew => isNewDate(releaseTime); String get timeText => getTimeText(time); diff --git a/lib/models/Campaign.dart b/lib/models/Campaign.dart index 33d3000d9..82ba0921f 100644 --- a/lib/models/Campaign.dart +++ b/lib/models/Campaign.dart @@ -16,7 +16,8 @@ extension CampaignExtension on Campaign { return this.causes[0]; } - String get description => this.description.replaceAll('\\n', '\n\n'); + // TODO This is really sad, it would be easy to use description by accident + String getDescription() => this.description.replaceAll('\\n', '\n\n'); Future getShareText() async { Uri uri = await _dynamicLinkService.createDynamicLink( diff --git a/lib/models/article.dart b/lib/models/article.dart index 7304b4080..679e5999e 100644 --- a/lib/models/article.dart +++ b/lib/models/article.dart @@ -24,7 +24,9 @@ export 'package:causeApiClient/causeApiClient.dart' show NewsArticle; extension NewsArticleExtension on NewsArticle { String? get dateString { - return releaseAt != null ? DateFormat('d MMM y').format(releaseAt) : null; + // TODO: Fix nullability + // return releaseAt != null ? DateFormat('d MMM y').format(releaseAt) : null; + return DateFormat('d MMM y').format(releaseAt); } // TODO Try harder diff --git a/lib/services/api_service.dart b/lib/services/api_service.dart index 21528b201..929e01995 100644 --- a/lib/services/api_service.dart +++ b/lib/services/api_service.dart @@ -23,6 +23,7 @@ class ApiService { late AuthInterceptor _authInterceptor; CauseApiClient? _apiClient; + CauseApiClient get apiClient { if (_apiClient == null) { throw Exception( diff --git a/lib/services/causes_service.dart b/lib/services/causes_service.dart index 6919907f1..6ce8c30be 100644 --- a/lib/services/causes_service.dart +++ b/lib/services/causes_service.dart @@ -154,7 +154,10 @@ class CausesService { } Future openLearningResource(LearningResource learningResource) async { - await completeLearningResource(learningResource.id); + if (_authService.isUserLoggedIn()) { + // TODO Should this if be inside here? + await completeLearningResource(learningResource.id); + } _navigationService.launchLink( learningResource.link, ); diff --git a/lib/services/dynamicLinks.dart b/lib/services/dynamicLinks.dart index c206ebf4f..d265a2ebf 100644 --- a/lib/services/dynamicLinks.dart +++ b/lib/services/dynamicLinks.dart @@ -64,8 +64,9 @@ class DynamicLinkService { if (RegExp('campaigns/[0-9]+').hasMatch(deepLink.path)) { String campaignNumberString = deepLink.path.substring(11); print('_handleDeepLink | campaign number: $campaignNumberString'); - int campaignNumber = int.parse(campaignNumberString); + // TODO Handle this -> should this use link?? Look up how deep links work these days + // int campaignNumber = int.parse(campaignNumberString); // _routerService.navigateToCampaignInfoView(arguments: campaignNumber); } } else { @@ -121,7 +122,8 @@ class DynamicLinkService { } // Handling deeplinks - StreamSubscription? _sub; + // TODO: Don't forget to call _sub.cancel() in dispose() + // StreamSubscription? _sub; Future initUniLinks() async { // Example deeplink @@ -129,7 +131,7 @@ class DynamicLinkService { bool gotLink = false; // Attach a listener to the stream - _sub = uriLinkStream.listen( + uriLinkStream.listen( (Uri? deepLink) { if (deepLink != null) { gotLink = true; diff --git a/lib/services/internal_notification_service.dart b/lib/services/internal_notification_service.dart index abbaabb75..3f58c408b 100644 --- a/lib/services/internal_notification_service.dart +++ b/lib/services/internal_notification_service.dart @@ -1,11 +1,8 @@ import 'package:nowu/models/Notification.dart'; -import 'package:nowu/services/api_service.dart'; - -import 'package:nowu/app/app.locator.dart'; // TODO Move to apiv2 class InternalNotificationService { - final ApiService _apiService = locator(); + // final ApiService _apiService = locator(); List? _notifications = []; List? get notifications { diff --git a/lib/services/pushNotifications.dart b/lib/services/pushNotifications.dart index b03b5c4e5..246e0ddde 100644 --- a/lib/services/pushNotifications.dart +++ b/lib/services/pushNotifications.dart @@ -10,7 +10,7 @@ class PushNotificationService { Future init() async { if (Platform.isIOS) { - NotificationSettings settings = await _fcm.requestPermission( + await _fcm.requestPermission( alert: true, announcement: false, badge: true, diff --git a/lib/services/search_service.dart b/lib/services/search_service.dart index d5a338a16..685073745 100644 --- a/lib/services/search_service.dart +++ b/lib/services/search_service.dart @@ -1,11 +1,10 @@ +import 'package:built_collection/built_collection.dart'; +import 'package:built_value/serializer.dart'; import 'package:causeApiClient/causeApiClient.dart'; import 'package:collection/collection.dart'; -import 'package:logging/logging.dart'; import 'package:meilisearch/meilisearch.dart'; -import 'package:built_value/serializer.dart'; -import 'package:built_collection/built_collection.dart'; -import 'package:nowu/assets/constants.dart'; import 'package:nowu/app/app.locator.dart'; +import 'package:nowu/assets/constants.dart'; import 'package:nowu/services/causes_service.dart'; import 'package:tuple/tuple.dart'; @@ -40,6 +39,7 @@ String newToMeiliSearchFilter( class BaseResourceSearchFilter { final String? query; final Iterable? causeIds; + // TODO This is really weird because we merge base resource filter with other filters, how do we use this field for those? // do we need 2 different filters? One for searching resources or should we just return empty when the resource type is not include // for the merge stuff?? @@ -386,7 +386,6 @@ class SearchService { final _causeServiceClient = CauseApiClient(); final _causesService = locator(); - final _logger = Logger('SearchService'); List _searchHitsToActions(List> hits) { final results = _causeServiceClient.serializers.deserialize( diff --git a/lib/ui/dialogs/basic/basic_dialog.dart b/lib/ui/dialogs/basic/basic_dialog.dart index 3ff32d19c..49c2c5226 100644 --- a/lib/ui/dialogs/basic/basic_dialog.dart +++ b/lib/ui/dialogs/basic/basic_dialog.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:nowu/themes.dart'; +import 'package:nowu/utils/intersperse.dart'; import 'package:stacked/stacked.dart'; import 'package:stacked_services/stacked_services.dart'; @@ -55,7 +56,7 @@ class BasicDialog extends StackedView { const SizedBox(height: 20), Column( crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ + children: [ if (request.mainButtonTitle != null) TextButton( child: Text(request.mainButtonTitle!), @@ -69,7 +70,7 @@ class BasicDialog extends StackedView { onPressed: () => completer(DialogResponse(confirmed: false)), ), - ], + ].intersperse(const SizedBox(height: 10,)).toList(), ), const SizedBox(height: 20), ], diff --git a/lib/ui/views/campaign_info/campaign_info_view.dart b/lib/ui/views/campaign_info/campaign_info_view.dart index fd6503efb..7b4af38a4 100644 --- a/lib/ui/views/campaign_info/campaign_info_view.dart +++ b/lib/ui/views/campaign_info/campaign_info_view.dart @@ -166,7 +166,7 @@ class CampaignInfoView extends StackedView { .copyWith(fontSize: 18), ), Text( - viewModel.campaign?.description ?? '', + viewModel.campaign?.getDescription() ?? '', style: Theme.of(context).textTheme.bodyLarge, textAlign: TextAlign.left, ), diff --git a/lib/ui/views/explore/explore_page_viewmodel.dart b/lib/ui/views/explore/explore_page_viewmodel.dart index e57c40847..f9931be33 100644 --- a/lib/ui/views/explore/explore_page_viewmodel.dart +++ b/lib/ui/views/explore/explore_page_viewmodel.dart @@ -7,6 +7,7 @@ import 'package:nowu/services/bottom_sheet_service.dart'; import 'package:nowu/services/causes_service.dart'; import 'package:nowu/services/search_service.dart'; import 'package:nowu/ui/bottom_sheets/explore_filter/explore_filter_sheet.dart'; +import 'package:nowu/utils/new_since.dart'; import 'package:stacked/stacked.dart'; import 'package:tuple/tuple.dart'; @@ -93,7 +94,7 @@ class ExplorePageViewModel extends FormViewModel { completed: filterData.filterCompleted == true ? true : null, recommended: filterData.filterRecommended == true ? true : null, releasedSince: filterData.filterNew == true - ? (DateTime.now().subtract(const Duration(days: 2))) + ? newSinceDate() : null, query: searchBarValue, ), @@ -116,7 +117,7 @@ class ExplorePageViewModel extends FormViewModel { completed: filterData.filterCompleted == true ? true : null, recommended: filterData.filterRecommended == true ? true : null, releasedSince: filterData.filterNew == true - ? (DateTime.now().subtract(const Duration(days: 2))) + ? newSinceDate() : null, query: searchBarValue, ), @@ -133,7 +134,7 @@ class ExplorePageViewModel extends FormViewModel { completed: filterData.filterCompleted == true ? true : null, recommended: filterData.filterRecommended == true ? true : null, releasedSince: filterData.filterNew == true - ? (DateTime.now().subtract(const Duration(days: 2))) + ? newSinceDate() : null, query: searchBarValue, ), @@ -148,7 +149,7 @@ class ExplorePageViewModel extends FormViewModel { ? null : filterData.filterCauseIds.toList(), releasedSince: filterData.filterNew == true - ? (DateTime.now().subtract(const Duration(days: 2))) + ? newSinceDate() : null, query: searchBarValue, ), @@ -164,6 +165,7 @@ class ExplorePageViewModel extends FormViewModel { ? null : filterData.filterCauseIds.toList(), query: searchBarValue, + releasedSince: filterData.filterNew == true ? newSinceDate() : null, ), ); notifyListeners(); @@ -273,17 +275,16 @@ class ExplorePageViewModel extends FormViewModel { return _causesService.openNewArticle(article); } - bool isActionComplete(ListAction action) { - return _causesService.actionIsComplete(action.id) == true; + bool? isActionComplete(ListAction action) { + return _causesService.actionIsComplete(action.id); } - bool isLearningResourceComplete(LearningResource learningResource) { - return _causesService.learningResourceIsComplete(learningResource.id) == - true; + bool? isLearningResourceComplete(LearningResource learningResource) { + return _causesService.learningResourceIsComplete(learningResource.id); } - bool isCampaignComplete(ListCampaign campaign) { - return _causesService.campaignIsComplete(campaign.id) == true; + bool? isCampaignComplete(ListCampaign campaign) { + return _causesService.campaignIsComplete(campaign.id); } } diff --git a/lib/ui/views/home/home_viewmodel.dart b/lib/ui/views/home/home_viewmodel.dart index 67d9f4d89..4c07b0150 100644 --- a/lib/ui/views/home/home_viewmodel.dart +++ b/lib/ui/views/home/home_viewmodel.dart @@ -40,9 +40,16 @@ class HomeViewModel extends BaseViewModel { Future init() async { fetchNotifications(); _causes = _causesService.causes; + + // TODO Filter by selected causes + List? selectedCausesId = _causesService.userInfo?.selectedCausesIds.toList(); + List? causesFilter = selectedCausesId?.isEmpty == false ? selectedCausesId : null; + // TODO Multiple futures await Future.wait([ - _searchService.searchCampaigns().then( + _searchService.searchCampaigns(filter: CampaignSearchFilter( + causeIds: causesFilter, + )).then( (value) => _myCampaigns = value .map( (campaign) => CampaignExploreTileData( @@ -53,7 +60,7 @@ class HomeViewModel extends BaseViewModel { .toList(), ), _searchService - .searchCampaigns(filter: CampaignSearchFilter(recommended: true)) + .searchCampaigns(filter: CampaignSearchFilter(recommended: true, causeIds: causesFilter)) .then( (value) => _suggestedCampaigns = value .map( @@ -64,7 +71,7 @@ class HomeViewModel extends BaseViewModel { ) .toList(), ), - _searchService.searchActions().then( + _searchService.searchActions(filter: ActionSearchFilter(causeIds: causesFilter)).then( (value) => _myActions = value .map( (action) => ActionExploreTileData( @@ -74,7 +81,7 @@ class HomeViewModel extends BaseViewModel { ) .toList(), ), - _searchService.searchNewsArticles().then( + _searchService.searchNewsArticles(filter: NewsArticleSearchFilter(causeIds: causesFilter)).then( (value) => _inTheNews = value .map((action) => NewsArticleExploreTileData(action)) .toList(), diff --git a/lib/ui/views/more/more_view.dart b/lib/ui/views/more/more_view.dart index ce9704c68..d15d99406 100644 --- a/lib/ui/views/more/more_view.dart +++ b/lib/ui/views/more/more_view.dart @@ -1,17 +1,11 @@ import 'package:flutter/foundation.dart'; -import 'package:nowu/app/app.router.dart'; -import 'package:nowu/assets/constants.dart'; import 'package:flutter/material.dart'; - import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -import 'package:nowu/assets/icons/customIcons.dart'; - +import 'package:nowu/app/app.router.dart'; import 'package:nowu/assets/components/customTile.dart'; - +import 'package:nowu/assets/constants.dart'; +import 'package:nowu/assets/icons/customIcons.dart'; import 'package:nowu/pages/more/ProfileTile.dart'; - -import 'package:url_launcher/url_launcher.dart'; - import 'package:stacked/stacked.dart'; import 'more_viewmodel.dart'; @@ -40,75 +34,83 @@ class ActionMenuItem extends MenuItemData { }); } -final menuItems = [ - const SectionHeadingMenuItem(title: 'The app'), - const ActionMenuItem( - title: 'About Us', - icon: FontAwesomeIcons.infoCircle, - action: LinkMenuItemAction('https://now-u.com/aboutus'), - ), - const ActionMenuItem( - title: 'Our partners', - icon: CustomIcons.ic_partners, - action: RouteMenuItemAction(PartnersViewRoute()), - ), - const ActionMenuItem( - title: 'FAQ', - icon: CustomIcons.ic_faq, - action: RouteMenuItemAction(FaqViewRoute()), - ), - const SectionHeadingMenuItem(title: 'Feedback'), - const ActionMenuItem( - title: 'Give feedback on the app', - icon: CustomIcons.ic_feedback, - action: LinkMenuItemAction( - 'https://docs.google.com/forms/d/e/1FAIpQLSflMOarmyXRv7DRbDQPWRayCpE5X4d8afOpQ1hjXfdvzbnzQQ/viewform', - ), - ), - if (defaultTargetPlatform == TargetPlatform.iOS || - defaultTargetPlatform == TargetPlatform.android) - ActionMenuItem( - title: 'Rate us on the app store', - icon: CustomIcons.ic_rateus, - action: LinkMenuItemAction( - defaultTargetPlatform == TargetPlatform.iOS - ? 'https://apps.apple.com/us/app/now-u/id1516126639' - : 'https://play.google.com/store/apps/details?id=com.nowu.app', - isExternal: true, +List getMenuItems(MoreViewModel viewModel) => [ + const SectionHeadingMenuItem(title: 'The app'), + const ActionMenuItem( + title: 'About Us', + icon: FontAwesomeIcons.infoCircle, + action: LinkMenuItemAction('https://now-u.com/aboutus'), + ), + const ActionMenuItem( + title: 'Our partners', + icon: CustomIcons.ic_partners, + action: RouteMenuItemAction(PartnersViewRoute()), + ), + const ActionMenuItem( + title: 'FAQ', + icon: CustomIcons.ic_faq, + action: RouteMenuItemAction(FaqViewRoute()), ), - ), - const ActionMenuItem( - title: 'Send us a message', - icon: CustomIcons.ic_social_fb, - action: LinkMenuItemAction('http://m.me/nowufb', isExternal: true), - ), - const ActionMenuItem( - title: 'Send us an email', - icon: CustomIcons.ic_email, - action: LinkMenuItemAction( - 'mailto:hello@now-.com?subject=Hi', - isExternal: true, - ), - ), - const SectionHeadingMenuItem(title: 'Legal'), - const ActionMenuItem( - title: 'Terms & conditions', - icon: CustomIcons.ic_tc, - action: LinkMenuItemAction(TERMS_AND_CONDITIONS_URL, isExternal: true), - ), - const ActionMenuItem( - title: 'Privacy Notice', - icon: CustomIcons.ic_privacy, - action: LinkMenuItemAction(PRIVACY_POLICY_URL, isExternal: true), - ), - const SectionHeadingMenuItem(title: 'User'), - ActionMenuItem( - title: 'Log out', - // TODO Get icon - icon: FontAwesomeIcons.solidUser, - action: FunctionMenuItemAction((model) => model.logout()), - ), -]; + const SectionHeadingMenuItem(title: 'Feedback'), + const ActionMenuItem( + title: 'Give feedback on the app', + icon: CustomIcons.ic_feedback, + action: LinkMenuItemAction( + 'https://docs.google.com/forms/d/e/1FAIpQLSflMOarmyXRv7DRbDQPWRayCpE5X4d8afOpQ1hjXfdvzbnzQQ/viewform', + ), + ), + if (defaultTargetPlatform == TargetPlatform.iOS || + defaultTargetPlatform == TargetPlatform.android) + ActionMenuItem( + title: 'Rate us on the app store', + icon: CustomIcons.ic_rateus, + action: LinkMenuItemAction( + defaultTargetPlatform == TargetPlatform.iOS + ? 'https://apps.apple.com/us/app/now-u/id1516126639' + : 'https://play.google.com/store/apps/details?id=com.nowu.app', + isExternal: true, + ), + ), + const ActionMenuItem( + title: 'Send us a message', + icon: CustomIcons.ic_social_fb, + action: LinkMenuItemAction('http://m.me/nowufb', isExternal: true), + ), + const ActionMenuItem( + title: 'Send us an email', + icon: CustomIcons.ic_email, + action: LinkMenuItemAction( + 'mailto:hello@now-.com?subject=Hi', + isExternal: true, + ), + ), + const SectionHeadingMenuItem(title: 'Legal'), + const ActionMenuItem( + title: 'Terms & conditions', + icon: CustomIcons.ic_tc, + action: LinkMenuItemAction(TERMS_AND_CONDITIONS_URL, isExternal: true), + ), + const ActionMenuItem( + title: 'Privacy Notice', + icon: CustomIcons.ic_privacy, + action: LinkMenuItemAction(PRIVACY_POLICY_URL, isExternal: true), + ), + const SectionHeadingMenuItem(title: 'User'), + if (viewModel.isLoggedIn) + ActionMenuItem( + title: 'Log out', + // TODO Get icon + icon: FontAwesomeIcons.solidUser, + action: FunctionMenuItemAction(viewModel.logout), + ) + else + const ActionMenuItem( + title: 'Log in', + // TODO Get icon + icon: FontAwesomeIcons.solidUser, + action: RouteMenuItemAction(LoginViewRoute()), + ), + ]; ///The More page ![More Page](https://i.ibb.co/xDHyMPj/slack.png) /// @@ -141,8 +143,8 @@ class MoreView extends StackedView { ListView( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), - children: menuItems - .map((tile) => MenuItem(tile, viewModel: viewModel, index: 2)) + children: getMenuItems(viewModel) + .map((tile) => MenuItem(tile, viewModel: viewModel)) .toList(), ), const SizedBox(height: 15), @@ -153,14 +155,17 @@ class MoreView extends StackedView { SocialButton( icon: FontAwesomeIcons.instagram, link: 'https://www.instagram.com/now_u_app/', + viewModel: viewModel, ), SocialButton( icon: FontAwesomeIcons.facebookF, link: 'https://www.facebook.com/nowufb', + viewModel: viewModel, ), SocialButton( icon: FontAwesomeIcons.twitter, link: 'https://twitter.com/now_u_app', + viewModel: viewModel, ), ], ), @@ -174,18 +179,16 @@ class MoreView extends StackedView { class MenuItem extends StatelessWidget { final MenuItemData data; - final int index; final MoreViewModel viewModel; - const MenuItem(this.data, {required this.index, required this.viewModel}); + const MenuItem(this.data, {required this.viewModel}); Widget build(BuildContext context) { final data = this.data; switch (data) { case SectionHeadingMenuItem(): return Padding( - padding: - EdgeInsets.only(left: 20, top: index == 0 ? 0 : 25, bottom: 5), + padding: const EdgeInsets.only(left: 20, top: 25, bottom: 5), child: Text( data.title, style: Theme.of(context).textTheme.displayMedium, @@ -230,20 +233,20 @@ class MenuItem extends StatelessWidget { class SocialButton extends StatelessWidget { final double size = 50; - final IconData? icon; - final String? link; + final IconData icon; + final String link; + final MoreViewModel viewModel; SocialButton({ - this.icon, - this.link, + required this.icon, + required this.link, + required this.viewModel, }); @override Widget build(BuildContext context) { return GestureDetector( - onTap: () { - launch(link!); - }, + onTap: () => viewModel.launchLinkExternal(link), child: CustomTile( borderRadius: size / 2, child: Container( diff --git a/lib/ui/views/more/more_viewmodel.dart b/lib/ui/views/more/more_viewmodel.dart index 7eb6f61dd..3d662afa9 100644 --- a/lib/ui/views/more/more_viewmodel.dart +++ b/lib/ui/views/more/more_viewmodel.dart @@ -20,7 +20,7 @@ class LinkMenuItemAction extends MenuItemAction { } class FunctionMenuItemAction extends MenuItemAction { - final Future Function(MoreViewModel model) function; + final Future Function() function; const FunctionMenuItemAction(this.function); } @@ -29,6 +29,8 @@ class MoreViewModel extends BaseViewModel { final _navigationService = locator(); final _routerService = locator(); + bool get isLoggedIn => _authenticationService.isUserLoggedIn(); + Future logout() async { await _authenticationService.logout(); _routerService.clearStackAndShow(const LoginViewRoute()); @@ -47,7 +49,11 @@ class MoreViewModel extends BaseViewModel { case RouteMenuItemAction(): return _routerService.navigateTo(action.route); case FunctionMenuItemAction(): - return action.function(this); + return action.function(); } } + + Future launchLinkExternal(String link) { + return _navigationService.launchLink(link, isExternal: true); + } } diff --git a/lib/utils/new_since.dart b/lib/utils/new_since.dart new file mode 100644 index 000000000..5d1d53f12 --- /dev/null +++ b/lib/utils/new_since.dart @@ -0,0 +1 @@ +DateTime newSinceDate() => DateTime.now().subtract(const Duration(days: 2)); diff --git a/pubspec.lock b/pubspec.lock index 6dffbed95..b1362ccb2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -497,10 +497,10 @@ packages: dependency: "direct main" description: name: flutter_inappwebview - sha256: f31169b4559939acb74272a41a6f54fe0d395834ecca2b1c6920dc7e30d61fd1 + sha256: d198297060d116b94048301ee6749cd2e7d03c1f2689783f52d210a6b7aba350 url: "https://pub.dev" source: hosted - version: "5.4.4+3" + version: "5.8.0" flutter_launcher_icons: dependency: "direct dev" description: @@ -1448,10 +1448,10 @@ packages: dependency: transitive description: name: test - sha256: "9b0dd8e36af4a5b1569029949d50a52cb2a2a2fdaa20cebb96e6603b9ae241f9" + sha256: a1f7595805820fcc05e5c52e3a231aedd0b72972cb333e8c738a8b1239448b6f url: "https://pub.dev" source: hosted - version: "1.24.6" + version: "1.24.9" test_api: dependency: transitive description: @@ -1464,10 +1464,10 @@ packages: dependency: transitive description: name: test_core - sha256: "4bef837e56375537055fdbbbf6dd458b1859881f4c7e6da936158f77d61ab265" + sha256: a757b14fc47507060a162cc2530d9a4a2f92f5100a952c7443b5cad5ef5b106a url: "https://pub.dev" source: hosted - version: "0.5.6" + version: "0.5.9" time: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 2a7d79996..b23f2cdc6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,8 @@ description: now-u App # 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: 2.0.1-dev+51 +version: 2.0.2 +publish_to: none environment: sdk: '>=3.0.0 <4.0.0' @@ -52,7 +53,7 @@ dependencies: cupertino_icons: ^1.0.3 flutter_secure_storage: ^4.2.1 - flutter_inappwebview: 5.4.4+3 + flutter_inappwebview: 5.8.0 uni_links: ^0.5.1 device_info: ^2.0.2 diff --git a/test/setup/helpers/image_helpers.dart b/test/setup/helpers/image_helpers.dart index 936fc4f98..f61d13e3c 100644 --- a/test/setup/helpers/image_helpers.dart +++ b/test/setup/helpers/image_helpers.dart @@ -91,7 +91,7 @@ MockHttpClient _createMockImageHttpClient( final MockHttpClientRequest request = MockHttpClientRequest(); final MockHttpClientResponse response = MockHttpClientResponse(); final MockHttpHeaders headers = MockHttpHeaders(); - when(client.getUrl(any!)) + when(client.getUrl(any ?? Uri())) .thenAnswer((_) => Future.value(request)); when(request.headers).thenReturn(headers); when(request.close())