├── .github └── workflows │ └── develop-actions.yml ├── .gitignore ├── .metadata ├── CHANGELOG.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── assets └── logo_grey.png ├── dartdoc_options.yaml ├── doc ├── Develop.md └── FlutterClientSdk.md ├── example ├── .gitignore ├── .metadata ├── README.md ├── android │ ├── .gitignore │ ├── app │ │ ├── build.gradle │ │ └── src │ │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── kotlin │ │ │ │ └── com │ │ │ │ │ └── example │ │ │ │ │ └── example │ │ │ │ │ └── MainActivity.kt │ │ │ └── res │ │ │ │ ├── drawable-v21 │ │ │ │ └── launch_background.xml │ │ │ │ ├── drawable │ │ │ │ └── launch_background.xml │ │ │ │ ├── mipmap-hdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── values-night │ │ │ │ └── styles.xml │ │ │ │ └── values │ │ │ │ └── styles.xml │ │ │ └── profile │ │ │ └── AndroidManifest.xml │ ├── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ └── settings.gradle ├── assets │ └── chatwoot_logo.png ├── ios │ ├── .gitignore │ ├── Flutter │ │ ├── AppFrameworkInfo.plist │ │ ├── Debug.xcconfig │ │ └── Release.xcconfig │ ├── Podfile │ ├── Podfile.lock │ ├── Runner.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ │ └── WorkspaceSettings.xcsettings │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ └── Runner │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ └── Icon-App-83.5x83.5@2x.png │ │ └── LaunchImage.imageset │ │ │ ├── Contents.json │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ └── README.md │ │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ │ ├── Info.plist │ │ └── Runner-Bridging-Header.h ├── lib │ └── main.dart ├── pubspec.lock ├── pubspec.yaml ├── test │ └── widget_test.dart └── web │ ├── favicon.png │ ├── icons │ ├── Icon-192.png │ └── Icon-512.png │ ├── index.html │ └── manifest.json ├── lib ├── chatwoot_callbacks.dart ├── chatwoot_client.dart ├── chatwoot_parameters.dart ├── chatwoot_sdk.dart ├── data │ ├── chatwoot_repository.dart │ ├── local │ │ ├── dao │ │ │ ├── chatwoot_contact_dao.dart │ │ │ ├── chatwoot_conversation_dao.dart │ │ │ ├── chatwoot_messages_dao.dart │ │ │ └── chatwoot_user_dao.dart │ │ ├── entity │ │ │ ├── chatwoot_contact.dart │ │ │ ├── chatwoot_conversation.dart │ │ │ ├── chatwoot_message.dart │ │ │ └── chatwoot_user.dart │ │ └── local_storage.dart │ └── remote │ │ ├── chatwoot_client_exception.dart │ │ ├── requests │ │ ├── chatwoot_action.dart │ │ ├── chatwoot_action_data.dart │ │ └── chatwoot_new_message_request.dart │ │ ├── responses │ │ └── chatwoot_event.dart │ │ └── service │ │ ├── chatwoot_client_api_interceptor.dart │ │ ├── chatwoot_client_auth_service.dart │ │ └── chatwoot_client_service.dart ├── di │ └── modules.dart ├── repository_parameters.dart └── ui │ ├── chatwoot_chat_dialog.dart │ ├── chatwoot_chat_page.dart │ ├── chatwoot_chat_theme.dart │ ├── chatwoot_l10n.dart │ └── webview_widget │ ├── chatwoot_widget.dart │ ├── constants.dart │ ├── utils.dart │ └── webview.dart ├── pubspec.lock ├── pubspec.yaml └── test ├── chatwoot_client_test.dart ├── chatwoot_client_test.mocks.dart ├── chatwoot_sdk_test.dart ├── data ├── chatwoot_repository_test.dart ├── chatwoot_repository_test.mocks.dart ├── local │ ├── dao │ │ ├── non_persisted_chatwoot_contact_dao_test.dart │ │ ├── non_persisted_chatwoot_conversation_dao_test.dart │ │ ├── non_persisted_chatwoot_messages_dao_test.dart │ │ ├── non_persisted_chatwoot_users_dao_test.dart │ │ ├── persisted_chatwoot_contact_dao_test.dart │ │ ├── persisted_chatwoot_conversation_dao_test.dart │ │ ├── persisted_chatwoot_messages_dao_test.dart │ │ └── persisted_chatwoot_users_dao_test.dart │ ├── local_storage_test.dart │ └── local_storage_test.mocks.dart └── remote │ ├── chatwoot_client_api_interceptor_test.dart │ ├── chatwoot_client_api_interceptor_test.mocks.dart │ ├── chatwoot_client_auth_service_test.dart │ ├── chatwoot_client_auth_service_test.mocks.dart │ ├── chatwoot_client_service_test.dart │ └── chatwoot_client_service_test.mocks.dart ├── di └── modules_test.dart ├── hive_testing_path ├── chatwootcontactboxnames.client_instance_to_contacts.hive ├── chatwootcontactboxnames.contacts.hive ├── chatwootconversationboxnames.client_instance_to_conversations.hive ├── chatwootconversationboxnames.conversations.hive ├── chatwootmessagesboxnames.messages.hive ├── chatwootmessagesboxnames.messages_to_client_instance_key.hive ├── chatwootuserboxnames.client_instance_to_user.hive └── chatwootuserboxnames.users.hive ├── resources ├── contact.json ├── conversation.json ├── conversations.json ├── message.json ├── messages.json ├── websocket_conversation_status_changed.json ├── websocket_conversation_typing_off.json ├── websocket_conversation_typing_on.json ├── websocket_message_created.json ├── websocket_message_updated.json └── websocket_presence_update.json └── utils └── test_resources_util.dart /.github/workflows/develop-actions.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | 6 | name: build 7 | 8 | on: 9 | push: 10 | branches: [ develop ] 11 | pull_request: 12 | branches: [ develop ] 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | - uses: actions/setup-java@v1 21 | with: 22 | java-version: '12.x' 23 | - uses: subosito/flutter-action@v1 24 | with: 25 | channel: 'stable' # or: 'beta', 'dev' or 'master' 26 | - run: flutter pub get 27 | - run: flutter pub run build_runner clean 28 | - run: flutter pub run build_runner build --delete-conflicting-outputs 29 | - run: flutter test 30 | - run: flutter analyze 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | *.g.dart 12 | *.mocks.dart 13 | applicationDocumentsPath 14 | hive_testing_path 15 | 16 | # IntelliJ related 17 | *.iml 18 | *.ipr 19 | *.iws 20 | .idea/ 21 | 22 | # The .vscode folder contains launch configuration and tasks you configure in 23 | # VS Code which you may wish to be included in version control, so this line 24 | # is commented out by default. 25 | #.vscode/ 26 | 27 | # Flutter/Dart/Pub related 28 | **/doc/api/ 29 | .dart_tool/ 30 | .flutter-plugins 31 | .flutter-plugins-dependencies 32 | .packages 33 | .pub-cache/ 34 | .pub/ 35 | build/ 36 | 37 | # Android related 38 | **/android/**/gradle-wrapper.jar 39 | **/android/.gradle 40 | **/android/captures/ 41 | **/android/gradlew 42 | **/android/gradlew.bat 43 | **/android/local.properties 44 | **/android/**/GeneratedPluginRegistrant.java 45 | 46 | # iOS/XCode related 47 | **/ios/**/*.mode1v3 48 | **/ios/**/*.mode2v3 49 | **/ios/**/*.moved-aside 50 | **/ios/**/*.pbxuser 51 | **/ios/**/*.perspectivev3 52 | **/ios/**/*sync/ 53 | **/ios/**/.sconsign.dblite 54 | **/ios/**/.tags* 55 | **/ios/**/.vagrant/ 56 | **/ios/**/DerivedData/ 57 | **/ios/**/Icon? 58 | **/ios/**/Pods/ 59 | **/ios/**/.symlinks/ 60 | **/ios/**/profile 61 | **/ios/**/xcuserdata 62 | **/ios/.generated/ 63 | **/ios/Flutter/App.framework 64 | **/ios/Flutter/Flutter.framework 65 | **/ios/Flutter/Flutter.podspec 66 | **/ios/Flutter/Generated.xcconfig 67 | **/ios/Flutter/app.flx 68 | **/ios/Flutter/app.zip 69 | **/ios/Flutter/flutter_assets/ 70 | **/ios/Flutter/flutter_export_environment.sh 71 | **/ios/ServiceDefinitions.json 72 | **/ios/Runner/GeneratedPluginRegistrant.* 73 | 74 | # Exceptions to above rules. 75 | !**/ios/**/default.mode1v3 76 | !**/ios/**/default.mode2v3 77 | !**/ios/**/default.pbxuser 78 | !**/ios/**/default.perspectivev3 79 | /coverage/ 80 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: b1395592de68cc8ac4522094ae59956dd21a91db 8 | channel: stable 9 | 10 | project_type: package 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [0.0.1] - Jul 15,2021 2 | 3 | - Setup initial client sdk flow 4 | 5 | ## [0.0.2] - Jul 15,2021 6 | 7 | - Updated example 8 | 9 | ## [0.0.3] - Jul 15,2021 10 | 11 | - Fixed multiple Hive adapter registration issue 12 | - Fixed theme background issue 13 | - Resolved pub analysis issues 14 | 15 | ## [0.0.4] - Jul 15,2021 16 | 17 | - Updated build_runner dependency to null safety version 18 | 19 | ## [0.0.5] - Jul 20,2021 20 | 21 | - Added ChatwootChatModal 22 | - Updated README.md 23 | 24 | ## [0.0.6] - Jul 20,2021 25 | 26 | - Fixed received message widget overflow on mobile screens 27 | 28 | ## [0.0.7] - Jul 22,2021 29 | 30 | - Fixed ChatwootChatDialog avatar color 31 | - Fixed ChatwootChatDialog chat bubble overflow 32 | 33 | ## [0.0.8] - Jul 22,2021 34 | 35 | - Update dependencies 36 | 37 | ## [0.0.9] - Jul 22,2021 38 | 39 | - Fixed message sending issues 40 | - Adds development docs 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Nartey Ephraim 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | analyzer: 2 | exclude: 3 | - "**.g.dart" 4 | - "**.mocks.dart" 5 | -------------------------------------------------------------------------------- /assets/logo_grey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatwoot/chatwoot-flutter-sdk/0c59ef3d272dfa32c70dd64a183aa3cf55c84eb0/assets/logo_grey.png -------------------------------------------------------------------------------- /dartdoc_options.yaml: -------------------------------------------------------------------------------- 1 | dartdoc: 2 | categories: 3 | "Installation": 4 | markdown: README.md 5 | name: Installation 6 | "Flutter Client Sdk": 7 | name: FlutterClientSdk 8 | categoryOrder: ["Installation", "Flutter Client Sdk"] 9 | examplePathPrefix: '/example' 10 | showUndocumentedCategories: false 11 | ignore: 12 | - ambiguous-doc-reference 13 | errors: 14 | - unresolved-doc-reference 15 | warnings: 16 | - tool-error -------------------------------------------------------------------------------- /doc/Develop.md: -------------------------------------------------------------------------------- 1 | # How to set up the SDK for development 2 | 3 | ## Setup your local environment 4 | 5 | Set up your local environment for developing a Flutter app by using their [guide](https://docs.flutter.dev/get-started/install). 6 | 7 | ## Clone the repository 8 | 9 | Clone the [chatwoot-flutter-sdk](https://github.com/chatwoot/chatwoot-flutter-sdk) repository. 10 | 11 | ```bash 12 | git clone git@github.com:chatwoot/chatwoot-flutter-sdk.git 13 | ``` 14 | 15 | ## Run the example project for developing 16 | 17 | Go to the example directory in the repository. 18 | 19 | ``` 20 | cd /chatwoot-flutter-sdk/example 21 | ``` 22 | 23 | ### Add the latest dependencies 24 | 25 | ``` 26 | flutter pub get 27 | ``` 28 | 29 | This command helps to add all the latest dependencies listed in the pubspec. yaml file. 30 | 31 | ### Integration guide 32 | 33 | After the above steps, you can follow [how to use steps](https://github.com/chatwoot/chatwoot-flutter-sdk#3-how-to-use). 34 | 35 | ### Run the project 36 | 37 | Use any of the methods mentioned below for running a sample project. 38 | 39 | #### Run using terminal 40 | 41 | ``` 42 | flutter run 43 | ``` 44 | 45 | **NB:** If there is no device connected or the simulator is not open, this will open in your default browser. 46 | 47 | #### Run using XCode or Android Studio (Tested with IOS only) 48 | 49 | **IOS** 50 | 51 | If you are using IOS, Open the XCode app in macOS and go to the ios directory in the example app. 52 | 53 | ``` 54 | cd user/chatwoot-flutter-sdk/example/ios 55 | ``` 56 | 57 | And try installing [cocoapods](https://cocoapods.org/) and then try to run build. 58 | 59 | **Android** 60 | 61 | If you are using Android, Open Android Studio and try build. 62 | 63 | ### Help 64 | 65 | **NB:** 66 | If you are facing ` Error: Member not found: 'packageRoot'.` while running flutter then try running the commands mentioned below. 67 | 68 | ``` 69 | flutter pub upgrade 70 | ``` 71 | 72 | or 73 | 74 | ``` 75 | flutter channel stable 76 | flutter upgrade 77 | flutter pub upgrade 78 | ``` 79 | 80 | ### Ensure correctness 81 | 82 | If you want to check the correctness of the code try running the commands below. 83 | 84 | ``` 85 | flutter pub run build_runner clean 86 | flutter pub run build_runner--delete-conflicting-outputs 87 | flutter test 88 | flutter analyze 89 | ``` 90 | -------------------------------------------------------------------------------- /doc/FlutterClientSdk.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatwoot/chatwoot-flutter-sdk/0c59ef3d272dfa32c70dd64a183aa3cf55c84eb0/doc/FlutterClientSdk.md -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | **/ios/Flutter/.last_build_id 26 | .dart_tool/ 27 | .flutter-plugins 28 | .flutter-plugins-dependencies 29 | .packages 30 | .pub-cache/ 31 | .pub/ 32 | /build/ 33 | 34 | # Web related 35 | lib/generated_plugin_registrant.dart 36 | 37 | # Symbolication related 38 | app.*.symbols 39 | 40 | # Obfuscation related 41 | app.*.map.json 42 | 43 | # Android Studio will place build artifacts here 44 | /android/app/debug 45 | /android/app/profile 46 | /android/app/release 47 | -------------------------------------------------------------------------------- /example/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: b1395592de68cc8ac4522094ae59956dd21a91db 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # example 2 | 3 | An example project for chatwoot client sdk 4 | 5 | ## Getting Started 6 | 7 | This project is a starting point for a Flutter application. 8 | 9 | A few resources to get you started if this is your first Flutter project: 10 | 11 | - [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) 12 | - [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) 13 | 14 | For help getting started with Flutter, view our 15 | [online documentation](https://flutter.dev/docs), which offers tutorials, 16 | samples, guidance on mobile development, and a full API reference. 17 | -------------------------------------------------------------------------------- /example/android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | -------------------------------------------------------------------------------- /example/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | android { 29 | compileSdkVersion 33 30 | 31 | sourceSets { 32 | main.java.srcDirs += 'src/main/kotlin' 33 | } 34 | 35 | defaultConfig { 36 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 37 | applicationId "com.chatwoot.example" 38 | minSdkVersion 19 39 | targetSdkVersion 33 40 | versionCode flutterVersionCode.toInteger() 41 | versionName flutterVersionName 42 | } 43 | 44 | buildTypes { 45 | release { 46 | // TODO: Add your own signing config for the release build. 47 | // Signing with the debug keys for now, so `flutter run --release` works. 48 | signingConfig signingConfigs.debug 49 | } 50 | } 51 | } 52 | 53 | flutter { 54 | source '../..' 55 | } 56 | 57 | dependencies { 58 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 59 | } 60 | -------------------------------------------------------------------------------- /example/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 14 | 18 | 22 | 27 | 31 | 32 | 33 | 34 | 35 | 36 | 38 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /example/android/app/src/main/kotlin/com/example/example/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.example 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatwoot/chatwoot-flutter-sdk/0c59ef3d272dfa32c70dd64a183aa3cf55c84eb0/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatwoot/chatwoot-flutter-sdk/0c59ef3d272dfa32c70dd64a183aa3cf55c84eb0/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatwoot/chatwoot-flutter-sdk/0c59ef3d272dfa32c70dd64a183aa3cf55c84eb0/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatwoot/chatwoot-flutter-sdk/0c59ef3d272dfa32c70dd64a183aa3cf55c84eb0/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatwoot/chatwoot-flutter-sdk/0c59ef3d272dfa32c70dd64a183aa3cf55c84eb0/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /example/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.7.20' 3 | repositories { 4 | google() 5 | jcenter() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:7.4.1' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | jcenter() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | } 25 | subprojects { 26 | project.evaluationDependsOn(':app') 27 | } 28 | 29 | task clean(type: Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-all.zip 7 | -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | -------------------------------------------------------------------------------- /example/assets/chatwoot_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatwoot/chatwoot-flutter-sdk/0c59ef3d272dfa32c70dd64a183aa3cf55c84eb0/example/assets/chatwoot_logo.png -------------------------------------------------------------------------------- /example/ios/.gitignore: -------------------------------------------------------------------------------- 1 | *.mode1v3 2 | *.mode2v3 3 | *.moved-aside 4 | *.pbxuser 5 | *.perspectivev3 6 | **/*sync/ 7 | .sconsign.dblite 8 | .tags* 9 | **/.vagrant/ 10 | **/DerivedData/ 11 | Icon? 12 | **/Pods/ 13 | **/.symlinks/ 14 | profile 15 | xcuserdata 16 | **/.generated/ 17 | Flutter/App.framework 18 | Flutter/Flutter.framework 19 | Flutter/Flutter.podspec 20 | Flutter/Generated.xcconfig 21 | Flutter/app.flx 22 | Flutter/app.zip 23 | Flutter/flutter_assets/ 24 | Flutter/flutter_export_environment.sh 25 | ServiceDefinitions.json 26 | Runner/GeneratedPluginRegistrant.* 27 | 28 | # Exceptions to above rules. 29 | !default.mode1v3 30 | !default.mode2v3 31 | !default.pbxuser 32 | !default.perspectivev3 33 | -------------------------------------------------------------------------------- /example/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 11.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '11.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | end 36 | 37 | post_install do |installer| 38 | installer.pods_project.targets.each do |target| 39 | flutter_additional_ios_build_settings(target) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /example/ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Flutter (1.0.0) 3 | - path_provider_ios (0.0.1): 4 | - Flutter 5 | - url_launcher_ios (0.0.1): 6 | - Flutter 7 | 8 | DEPENDENCIES: 9 | - Flutter (from `Flutter`) 10 | - path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`) 11 | - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) 12 | 13 | EXTERNAL SOURCES: 14 | Flutter: 15 | :path: Flutter 16 | path_provider_ios: 17 | :path: ".symlinks/plugins/path_provider_ios/ios" 18 | url_launcher_ios: 19 | :path: ".symlinks/plugins/url_launcher_ios/ios" 20 | 21 | SPEC CHECKSUMS: 22 | Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 23 | path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02 24 | url_launcher_ios: ae1517e5e344f5544fb090b079e11f399dfbe4d2 25 | 26 | PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3 27 | 28 | COCOAPODS: 1.11.3 29 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatwoot/chatwoot-flutter-sdk/0c59ef3d272dfa32c70dd64a183aa3cf55c84eb0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatwoot/chatwoot-flutter-sdk/0c59ef3d272dfa32c70dd64a183aa3cf55c84eb0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatwoot/chatwoot-flutter-sdk/0c59ef3d272dfa32c70dd64a183aa3cf55c84eb0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatwoot/chatwoot-flutter-sdk/0c59ef3d272dfa32c70dd64a183aa3cf55c84eb0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatwoot/chatwoot-flutter-sdk/0c59ef3d272dfa32c70dd64a183aa3cf55c84eb0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatwoot/chatwoot-flutter-sdk/0c59ef3d272dfa32c70dd64a183aa3cf55c84eb0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatwoot/chatwoot-flutter-sdk/0c59ef3d272dfa32c70dd64a183aa3cf55c84eb0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatwoot/chatwoot-flutter-sdk/0c59ef3d272dfa32c70dd64a183aa3cf55c84eb0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatwoot/chatwoot-flutter-sdk/0c59ef3d272dfa32c70dd64a183aa3cf55c84eb0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatwoot/chatwoot-flutter-sdk/0c59ef3d272dfa32c70dd64a183aa3cf55c84eb0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatwoot/chatwoot-flutter-sdk/0c59ef3d272dfa32c70dd64a183aa3cf55c84eb0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatwoot/chatwoot-flutter-sdk/0c59ef3d272dfa32c70dd64a183aa3cf55c84eb0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatwoot/chatwoot-flutter-sdk/0c59ef3d272dfa32c70dd64a183aa3cf55c84eb0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatwoot/chatwoot-flutter-sdk/0c59ef3d272dfa32c70dd64a183aa3cf55c84eb0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatwoot/chatwoot-flutter-sdk/0c59ef3d272dfa32c70dd64a183aa3cf55c84eb0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatwoot/chatwoot-flutter-sdk/0c59ef3d272dfa32c70dd64a183aa3cf55c84eb0/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatwoot/chatwoot-flutter-sdk/0c59ef3d272dfa32c70dd64a183aa3cf55c84eb0/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatwoot/chatwoot-flutter-sdk/0c59ef3d272dfa32c70dd64a183aa3cf55c84eb0/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | example 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(FLUTTER_BUILD_NAME) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UIViewControllerBasedStatusBarAppearance 43 | 44 | CADisableMinimumFrameDurationOnPhone 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /example/ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:chatwoot_sdk/chatwoot_sdk.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter/services.dart'; 6 | import 'package:image/image.dart' as image; 7 | import 'package:image_picker/image_picker.dart' as image_picker; 8 | import 'package:path_provider/path_provider.dart'; 9 | 10 | void main() { 11 | runApp(MyApp()); 12 | } 13 | 14 | class MyApp extends StatelessWidget { 15 | // This widget is the root of your application. 16 | @override 17 | Widget build(BuildContext context) { 18 | return MaterialApp( 19 | title: 'Flutter Demo', 20 | theme: ThemeData( 21 | primarySwatch: Colors.blue, 22 | ), 23 | home: MyHomePage(title: 'Flutter Demo Home Page'), 24 | ); 25 | } 26 | } 27 | 28 | class MyHomePage extends StatefulWidget { 29 | MyHomePage({Key key, this.title}) : super(key: key); 30 | 31 | final String title; 32 | 33 | @override 34 | _MyHomePageState createState() => _MyHomePageState(); 35 | } 36 | 37 | class _MyHomePageState extends State { 38 | @override 39 | void initState() { 40 | super.initState(); 41 | } 42 | 43 | @override 44 | Widget build(BuildContext context) { 45 | return Scaffold( 46 | appBar: AppBar( 47 | title: Text("Chatwoot Example"), 48 | ), 49 | body: ChatwootWidget( 50 | websiteToken: "websiteToken", 51 | baseUrl: "https://app.chatwoot.com", 52 | user: ChatwootUser( 53 | identifier: "test@test.com", 54 | name: "Tester test", 55 | email: "test@test.com", 56 | ), 57 | locale: "en", 58 | closeWidget: () { 59 | if (Platform.isAndroid) { 60 | SystemNavigator.pop(); 61 | } else if (Platform.isIOS) { 62 | exit(0); 63 | } 64 | }, 65 | //attachment only works on android for now 66 | onAttachFile: _androidFilePicker, 67 | onLoadStarted: () { 68 | print("loading widget"); 69 | }, 70 | onLoadProgress: (int progress) { 71 | print("loading... ${progress}"); 72 | }, 73 | onLoadCompleted: () { 74 | print("widget loaded"); 75 | }, 76 | ), 77 | ); 78 | } 79 | 80 | Future> _androidFilePicker() async { 81 | final picker = image_picker.ImagePicker(); 82 | final photo = 83 | await picker.pickImage(source: image_picker.ImageSource.gallery); 84 | 85 | if (photo == null) { 86 | return []; 87 | } 88 | 89 | final imageData = await photo.readAsBytes(); 90 | final decodedImage = image.decodeImage(imageData); 91 | final scaledImage = image.copyResize(decodedImage, width: 500); 92 | final jpg = image.encodeJpg(scaledImage, quality: 90); 93 | 94 | final filePath = (await getTemporaryDirectory()).uri.resolve( 95 | './image_${DateTime.now().microsecondsSinceEpoch}.jpg', 96 | ); 97 | final file = await File.fromUri(filePath).create(recursive: true); 98 | await file.writeAsBytes(jpg, flush: true); 99 | 100 | return [file.uri.toString()]; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: example 2 | description: An example project for chatwoot client sdk 3 | 4 | # The following line prevents the package from being accidentally published to 5 | # pub.dev using `pub publish`. This is preferred for private packages. 6 | publish_to: "none" # Remove this line if you wish to publish to pub.dev 7 | 8 | # The following defines the version and build number for your application. 9 | # A version number is three numbers separated by dots, like 1.2.43 10 | # followed by an optional build number separated by a +. 11 | # Both the version and the builder number may be overridden in flutter 12 | # build by specifying --build-name and --build-number, respectively. 13 | # In Android, build-name is used as versionName while build-number used as versionCode. 14 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 15 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. 16 | # Read more about iOS versioning at 17 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 18 | version: 1.0.0+1 19 | 20 | environment: 21 | sdk: ">=2.7.0 <3.0.0" 22 | 23 | dependencies: 24 | flutter: 25 | sdk: flutter 26 | chatwoot_sdk: 27 | path: ../ 28 | 29 | # The following adds the Cupertino Icons font to your application. 30 | # Use with the CupertinoIcons class for iOS style icons. 31 | cupertino_icons: ^1.0.2 32 | image_picker: ^0.8.7 33 | image: ^4.0.15 34 | path_provider: ^2.0.13 35 | 36 | dev_dependencies: 37 | flutter_test: 38 | sdk: flutter 39 | 40 | # For information on the generic Dart part of this file, see the 41 | # following page: https://dart.dev/tools/pub/pubspec 42 | 43 | # The following section is specific to Flutter. 44 | flutter: 45 | # The following line ensures that the Material Icons font is 46 | # included with your application, so that you can use the icons in 47 | # the material Icons class. 48 | uses-material-design: true 49 | 50 | # To add assets to your application, add an assets section, like this: 51 | assets: 52 | - assets/chatwoot_logo.png 53 | 54 | # An image asset can refer to one or more resolution-specific "variants", see 55 | # https://flutter.dev/assets-and-images/#resolution-aware. 56 | 57 | # For details regarding adding assets from package dependencies, see 58 | # https://flutter.dev/assets-and-images/#from-packages 59 | 60 | # To add custom fonts to your application, add a fonts section here, 61 | # in this "flutter" section. Each entry in this list should have a 62 | # "family" key with the font family name, and a "fonts" key with a 63 | # list giving the asset and other descriptors for the font. For 64 | # example: 65 | # fonts: 66 | # - family: Schyler 67 | # fonts: 68 | # - asset: fonts/Schyler-Regular.ttf 69 | # - asset: fonts/Schyler-Italic.ttf 70 | # style: italic 71 | # - family: Trajan Pro 72 | # fonts: 73 | # - asset: fonts/TrajanPro.ttf 74 | # - asset: fonts/TrajanPro_Bold.ttf 75 | # weight: 700 76 | # 77 | # For details regarding fonts from package dependencies, 78 | # see https://flutter.dev/custom-fonts/#from-packages 79 | -------------------------------------------------------------------------------- /example/test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility that Flutter provides. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | import '../lib/main.dart'; 12 | 13 | void main() { 14 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 15 | // Build our app and trigger a frame. 16 | await tester.pumpWidget(MyApp()); 17 | 18 | // Verify that our counter starts at 0. 19 | expect(find.text('0'), findsOneWidget); 20 | expect(find.text('1'), findsNothing); 21 | 22 | // Tap the '+' icon and trigger a frame. 23 | await tester.tap(find.byIcon(Icons.add)); 24 | await tester.pump(); 25 | 26 | // Verify that our counter has incremented. 27 | expect(find.text('0'), findsNothing); 28 | expect(find.text('1'), findsOneWidget); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /example/web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatwoot/chatwoot-flutter-sdk/0c59ef3d272dfa32c70dd64a183aa3cf55c84eb0/example/web/favicon.png -------------------------------------------------------------------------------- /example/web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatwoot/chatwoot-flutter-sdk/0c59ef3d272dfa32c70dd64a183aa3cf55c84eb0/example/web/icons/Icon-192.png -------------------------------------------------------------------------------- /example/web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatwoot/chatwoot-flutter-sdk/0c59ef3d272dfa32c70dd64a183aa3cf55c84eb0/example/web/icons/Icon-512.png -------------------------------------------------------------------------------- /example/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | example 30 | 31 | 32 | 33 | 36 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /example/web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "short_name": "example", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "An example project for chatwoot client sdk", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [ 12 | { 13 | "src": "icons/Icon-192.png", 14 | "sizes": "192x192", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "icons/Icon-512.png", 19 | "sizes": "512x512", 20 | "type": "image/png" 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /lib/chatwoot_callbacks.dart: -------------------------------------------------------------------------------- 1 | import 'package:chatwoot_sdk/data/chatwoot_repository.dart'; 2 | import 'package:chatwoot_sdk/data/local/entity/chatwoot_message.dart'; 3 | import 'package:chatwoot_sdk/data/remote/chatwoot_client_exception.dart'; 4 | import 'package:chatwoot_sdk/data/remote/responses/chatwoot_event.dart'; 5 | 6 | ///Chatwoot callback are specified for each created client instance. Methods are triggered 7 | ///when a method satisfying their respective conditions occur. 8 | /// 9 | /// 10 | /// {@category FlutterClientSdk} 11 | class ChatwootCallbacks { 12 | ///Triggered when a welcome event/message is received after connecting to 13 | ///the chatwoot websocket. See [ChatwootRepository.listenForEvents] 14 | void Function()? onWelcome; 15 | 16 | ///Triggered when a ping event/message is received after connecting to 17 | ///the chatwoot websocket. See [ChatwootRepository.listenForEvents] 18 | void Function()? onPing; 19 | 20 | ///Triggered when a subscription confirmation event/message is received after connecting to 21 | ///the chatwoot websocket. See [ChatwootRepository.listenForEvents] 22 | void Function()? onConfirmedSubscription; 23 | 24 | ///Triggered when a conversation typing on event/message [ChatwootEventMessageType.conversation_typing_on] 25 | ///is received after connecting to the chatwoot websocket. See [ChatwootRepository.listenForEvents] 26 | void Function()? onConversationStartedTyping; 27 | 28 | ///Triggered when a presence update event/message [ChatwootEventMessageType.presence_update] 29 | ///is received after connecting to the chatwoot websocket and conversation is online. See [ChatwootRepository.listenForEvents] 30 | void Function()? onConversationIsOnline; 31 | 32 | ///Triggered when a presence update event/message [ChatwootEventMessageType.presence_update] 33 | ///is received after connecting to the chatwoot websocket and conversation is offline. 34 | ///See [ChatwootRepository.listenForEvents] 35 | void Function()? onConversationIsOffline; 36 | 37 | ///Triggered when a conversation typing off event/message [ChatwootEventMessageType.conversation_typing_off] 38 | ///is received after connecting to the chatwoot websocket. See [ChatwootRepository.listenForEvents] 39 | void Function()? onConversationStoppedTyping; 40 | 41 | ///Triggered when a message created event/message [ChatwootEventMessageType.message_created] 42 | ///is received and message doesn't belong to current user after connecting to the chatwoot websocket. 43 | ///See [ChatwootRepository.listenForEvents] 44 | void Function(ChatwootMessage)? onMessageReceived; 45 | 46 | ///Triggered when a message created event/message [ChatwootEventMessageType.message_updated] 47 | ///is received after connecting to the chatwoot websocket. 48 | ///See [ChatwootRepository.listenForEvents] 49 | void Function(ChatwootMessage)? onMessageUpdated; 50 | 51 | void Function(ChatwootMessage, String)? onMessageSent; 52 | 53 | ///Triggered when a message created event/message [ChatwootEventMessageType.message_created] 54 | ///is received and message belongs to current user after connecting to the chatwoot websocket. 55 | ///See [ChatwootRepository.listenForEvents] 56 | void Function(ChatwootMessage, String)? onMessageDelivered; 57 | 58 | ///Triggered when a conversation's messages persisted on device are successfully retrieved 59 | void Function(List)? onPersistedMessagesRetrieved; 60 | 61 | ///Triggered when a conversation's messages is successfully retrieved from remote server 62 | void Function(List)? onMessagesRetrieved; 63 | 64 | ///Triggered when an agent resolves the current conversation 65 | void Function()? onConversationResolved; 66 | 67 | /// Triggered when any error occurs in chatwoot client's operations with the error 68 | /// 69 | /// See [ChatwootClientExceptionType] for the various types of exceptions that can be triggered 70 | void Function(ChatwootClientException)? onError; 71 | 72 | ChatwootCallbacks({ 73 | this.onWelcome, 74 | this.onPing, 75 | this.onConfirmedSubscription, 76 | this.onMessageReceived, 77 | this.onMessageSent, 78 | this.onMessageDelivered, 79 | this.onMessageUpdated, 80 | this.onPersistedMessagesRetrieved, 81 | this.onMessagesRetrieved, 82 | this.onConversationStartedTyping, 83 | this.onConversationStoppedTyping, 84 | this.onConversationIsOnline, 85 | this.onConversationIsOffline, 86 | this.onConversationResolved, 87 | this.onError, 88 | }); 89 | } 90 | -------------------------------------------------------------------------------- /lib/chatwoot_client.dart: -------------------------------------------------------------------------------- 1 | import 'package:chatwoot_sdk/chatwoot_sdk.dart'; 2 | import 'package:chatwoot_sdk/data/chatwoot_repository.dart'; 3 | import 'package:chatwoot_sdk/data/local/entity/chatwoot_contact.dart'; 4 | import 'package:chatwoot_sdk/data/local/entity/chatwoot_conversation.dart'; 5 | import 'package:chatwoot_sdk/data/remote/requests/chatwoot_action_data.dart'; 6 | import 'package:chatwoot_sdk/data/remote/requests/chatwoot_new_message_request.dart'; 7 | import 'package:chatwoot_sdk/di/modules.dart'; 8 | import 'package:chatwoot_sdk/chatwoot_parameters.dart'; 9 | import 'package:chatwoot_sdk/repository_parameters.dart'; 10 | import 'package:riverpod/riverpod.dart'; 11 | 12 | import 'data/local/local_storage.dart'; 13 | 14 | /// Represents a chatwoot client instance. All chatwoot operations (Example: sendMessages) are 15 | /// passed through chatwoot client. For more info visit 16 | /// https://www.chatwoot.com/docs/product/channels/api/client-apis 17 | /// 18 | /// {@category FlutterClientSdk} 19 | class ChatwootClient { 20 | late final ChatwootRepository _repository; 21 | final ChatwootParameters _parameters; 22 | final ChatwootCallbacks? callbacks; 23 | final ChatwootUser? user; 24 | 25 | String get baseUrl => _parameters.baseUrl; 26 | 27 | String get inboxIdentifier => _parameters.inboxIdentifier; 28 | 29 | ChatwootClient._(this._parameters, {this.user, this.callbacks}) { 30 | providerContainerMap.putIfAbsent( 31 | _parameters.clientInstanceKey, () => ProviderContainer()); 32 | final container = providerContainerMap[_parameters.clientInstanceKey]!; 33 | _repository = container.read(chatwootRepositoryProvider( 34 | RepositoryParameters( 35 | params: _parameters, callbacks: callbacks ?? ChatwootCallbacks()))); 36 | } 37 | 38 | void _init() { 39 | try { 40 | _repository.initialize(user); 41 | } on ChatwootClientException catch (e) { 42 | callbacks?.onError?.call(e); 43 | } 44 | } 45 | 46 | ///Retrieves chatwoot client's messages. If persistence is enabled [ChatwootCallbacks.onPersistedMessagesRetrieved] 47 | ///will be triggered with persisted messages. On successfully fetch from remote server 48 | ///[ChatwootCallbacks.onMessagesRetrieved] will be triggered 49 | void loadMessages() async { 50 | _repository.getPersistedMessages(); 51 | await _repository.getMessages(); 52 | } 53 | 54 | /// Sends chatwoot message. The echoId is your temporary message id. When message sends successfully 55 | /// [ChatwootMessage] will be returned with the [echoId] on [ChatwootCallbacks.onMessageSent]. If 56 | /// message fails to send [ChatwootCallbacks.onError] will be triggered [echoId] as data. 57 | Future sendMessage( 58 | {required String content, required String echoId}) async { 59 | final request = ChatwootNewMessageRequest(content: content, echoId: echoId); 60 | await _repository.sendMessage(request); 61 | } 62 | 63 | ///Send chatwoot action performed by user. 64 | /// 65 | /// Example: User started typing 66 | Future sendAction(ChatwootActionType action) async { 67 | _repository.sendAction(action); 68 | } 69 | 70 | ///Disposes chatwoot client and cancels all stream subscriptions 71 | dispose() { 72 | final container = providerContainerMap[_parameters.clientInstanceKey]!; 73 | _repository.dispose(); 74 | container.dispose(); 75 | providerContainerMap.remove(_parameters.clientInstanceKey); 76 | } 77 | 78 | /// Clears all chatwoot client data 79 | clearClientData() { 80 | final container = providerContainerMap[_parameters.clientInstanceKey]!; 81 | final localStorage = container.read(localStorageProvider(_parameters)); 82 | localStorage.clear(clearChatwootUserStorage: false); 83 | } 84 | 85 | /// Creates an instance of [ChatwootClient] with the [baseUrl] of your chatwoot installation, 86 | /// [inboxIdentifier] for the targeted inbox. Specify custom user details using [user] and [callbacks] for 87 | /// handling chatwoot events. By default persistence is enabled, to disable persistence set [enablePersistence] as false 88 | static Future create( 89 | {required String baseUrl, 90 | required String inboxIdentifier, 91 | ChatwootUser? user, 92 | bool enablePersistence = true, 93 | ChatwootCallbacks? callbacks}) async { 94 | if (enablePersistence) { 95 | await LocalStorage.openDB(); 96 | } 97 | 98 | final chatwootParams = ChatwootParameters( 99 | clientInstanceKey: getClientInstanceKey( 100 | baseUrl: baseUrl, 101 | inboxIdentifier: inboxIdentifier, 102 | userIdentifier: user?.identifier), 103 | isPersistenceEnabled: enablePersistence, 104 | baseUrl: baseUrl, 105 | inboxIdentifier: inboxIdentifier, 106 | userIdentifier: user?.identifier); 107 | 108 | final client = 109 | ChatwootClient._(chatwootParams, callbacks: callbacks, user: user); 110 | 111 | client._init(); 112 | 113 | return client; 114 | } 115 | 116 | static final _keySeparator = "|||"; 117 | 118 | ///Create a chatwoot client instance key using the chatwoot client instance baseurl, inboxIdentifier 119 | ///and userIdentifier. Client instance keys are used to differentiate between client instances and their data 120 | ///(contact ([ChatwootContact]),conversation ([ChatwootConversation]) and messages ([ChatwootMessage])) 121 | /// 122 | /// Create separate [ChatwootClient] instances with same baseUrl, inboxIdentifier, userIdentifier and persistence 123 | /// enabled will be regarded as same therefore use same contact and conversation. 124 | static String getClientInstanceKey( 125 | {required String baseUrl, 126 | required String inboxIdentifier, 127 | String? userIdentifier}) { 128 | return "$baseUrl$_keySeparator$userIdentifier$_keySeparator$inboxIdentifier"; 129 | } 130 | 131 | static Map providerContainerMap = Map(); 132 | 133 | ///Clears all persisted chatwoot data on device for a particular chatwoot client instance. 134 | ///See [getClientInstanceKey] on how chatwoot client instance are differentiated 135 | static Future clearData( 136 | {required String baseUrl, 137 | required String inboxIdentifier, 138 | String? userIdentifier}) async { 139 | final clientInstanceKey = getClientInstanceKey( 140 | baseUrl: baseUrl, 141 | inboxIdentifier: inboxIdentifier, 142 | userIdentifier: userIdentifier); 143 | providerContainerMap.putIfAbsent( 144 | clientInstanceKey, () => ProviderContainer()); 145 | final container = providerContainerMap[clientInstanceKey]!; 146 | final params = ChatwootParameters( 147 | isPersistenceEnabled: true, 148 | baseUrl: "", 149 | inboxIdentifier: "", 150 | clientInstanceKey: ""); 151 | 152 | final localStorage = container.read(localStorageProvider(params)); 153 | await localStorage.clear(); 154 | 155 | localStorage.dispose(); 156 | container.dispose(); 157 | providerContainerMap.remove(clientInstanceKey); 158 | } 159 | 160 | /// Clears all persisted chatwoot data on device. 161 | static Future clearAllData() async { 162 | providerContainerMap.putIfAbsent("all", () => ProviderContainer()); 163 | final container = providerContainerMap["all"]!; 164 | final params = ChatwootParameters( 165 | isPersistenceEnabled: true, 166 | baseUrl: "", 167 | inboxIdentifier: "", 168 | clientInstanceKey: ""); 169 | 170 | final localStorage = container.read(localStorageProvider(params)); 171 | await localStorage.clearAll(); 172 | 173 | localStorage.dispose(); 174 | container.dispose(); 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /lib/chatwoot_parameters.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | class ChatwootParameters extends Equatable { 4 | final bool isPersistenceEnabled; 5 | final String baseUrl; 6 | final String clientInstanceKey; 7 | final String inboxIdentifier; 8 | final String? userIdentifier; 9 | 10 | ChatwootParameters( 11 | {required this.isPersistenceEnabled, 12 | required this.baseUrl, 13 | required this.inboxIdentifier, 14 | required this.clientInstanceKey, 15 | this.userIdentifier}); 16 | 17 | @override 18 | List get props => [ 19 | isPersistenceEnabled, 20 | baseUrl, 21 | clientInstanceKey, 22 | inboxIdentifier, 23 | userIdentifier 24 | ]; 25 | } 26 | -------------------------------------------------------------------------------- /lib/chatwoot_sdk.dart: -------------------------------------------------------------------------------- 1 | library chatwoot_sdk; 2 | 3 | export 'package:chatwoot_sdk/data/local/entity/chatwoot_message.dart'; 4 | export 'package:chatwoot_sdk/data/local/entity/chatwoot_user.dart'; 5 | export 'package:chatwoot_sdk/data/remote/chatwoot_client_exception.dart'; 6 | export 'package:chatwoot_sdk/ui/chatwoot_chat_dialog.dart'; 7 | export 'package:chatwoot_sdk/ui/chatwoot_chat_page.dart'; 8 | export 'package:chatwoot_sdk/ui/webview_widget/chatwoot_widget.dart'; 9 | 10 | export 'chatwoot_callbacks.dart'; 11 | export 'chatwoot_client.dart'; 12 | -------------------------------------------------------------------------------- /lib/data/local/dao/chatwoot_contact_dao.dart: -------------------------------------------------------------------------------- 1 | import 'package:hive_flutter/hive_flutter.dart'; 2 | 3 | import '../entity/chatwoot_contact.dart'; 4 | 5 | ///Data access object for retriving chatwoot contact from local storage 6 | abstract class ChatwootContactDao { 7 | Future saveContact(ChatwootContact contact); 8 | ChatwootContact? getContact(); 9 | Future deleteContact(); 10 | Future onDispose(); 11 | Future clearAll(); 12 | } 13 | 14 | //Only used when persistence is enabled 15 | enum ChatwootContactBoxNames { CONTACTS, CLIENT_INSTANCE_TO_CONTACTS } 16 | 17 | class PersistedChatwootContactDao extends ChatwootContactDao { 18 | //box containing all persisted contacts 19 | Box _box; 20 | 21 | //_box with one to one relation between generated client instance id and conversation id 22 | final Box _clientInstanceIdToContactIdentifierBox; 23 | 24 | final String _clientInstanceKey; 25 | 26 | PersistedChatwootContactDao(this._box, 27 | this._clientInstanceIdToContactIdentifierBox, this._clientInstanceKey); 28 | 29 | @override 30 | Future deleteContact() async { 31 | final contactIdentifier = 32 | _clientInstanceIdToContactIdentifierBox.get(_clientInstanceKey); 33 | await _clientInstanceIdToContactIdentifierBox.delete(_clientInstanceKey); 34 | await _box.delete(contactIdentifier); 35 | } 36 | 37 | @override 38 | Future saveContact(ChatwootContact contact) async { 39 | await _clientInstanceIdToContactIdentifierBox.put( 40 | _clientInstanceKey, contact.contactIdentifier!); 41 | await _box.put(contact.contactIdentifier, contact); 42 | } 43 | 44 | @override 45 | ChatwootContact? getContact() { 46 | if (_box.values.length == 0) { 47 | return null; 48 | } 49 | 50 | final contactIdentifier = 51 | _clientInstanceIdToContactIdentifierBox.get(_clientInstanceKey); 52 | 53 | if (contactIdentifier == null) { 54 | return null; 55 | } 56 | 57 | return _box.get(contactIdentifier, defaultValue: null); 58 | } 59 | 60 | @override 61 | Future onDispose() async {} 62 | 63 | Future clearAll() async { 64 | await _box.clear(); 65 | await _clientInstanceIdToContactIdentifierBox.clear(); 66 | } 67 | 68 | static Future openDB() async { 69 | await Hive.openBox( 70 | ChatwootContactBoxNames.CONTACTS.toString()); 71 | await Hive.openBox( 72 | ChatwootContactBoxNames.CLIENT_INSTANCE_TO_CONTACTS.toString()); 73 | } 74 | } 75 | 76 | class NonPersistedChatwootContactDao extends ChatwootContactDao { 77 | ChatwootContact? _contact; 78 | 79 | @override 80 | Future deleteContact() async { 81 | _contact = null; 82 | } 83 | 84 | @override 85 | ChatwootContact? getContact() { 86 | return _contact; 87 | } 88 | 89 | @override 90 | Future onDispose() async { 91 | _contact = null; 92 | } 93 | 94 | @override 95 | Future saveContact(ChatwootContact contact) async { 96 | _contact = contact; 97 | } 98 | 99 | Future clearAll() async { 100 | _contact = null; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /lib/data/local/dao/chatwoot_conversation_dao.dart: -------------------------------------------------------------------------------- 1 | import 'package:chatwoot_sdk/data/local/entity/chatwoot_conversation.dart'; 2 | import 'package:hive_flutter/hive_flutter.dart'; 3 | 4 | abstract class ChatwootConversationDao { 5 | Future saveConversation(ChatwootConversation conversation); 6 | ChatwootConversation? getConversation(); 7 | Future deleteConversation(); 8 | Future onDispose(); 9 | Future clearAll(); 10 | } 11 | 12 | //Only used when persistence is enabled 13 | enum ChatwootConversationBoxNames { 14 | CONVERSATIONS, 15 | CLIENT_INSTANCE_TO_CONVERSATIONS 16 | } 17 | 18 | class PersistedChatwootConversationDao extends ChatwootConversationDao { 19 | //box containing all persisted conversations 20 | Box _box; 21 | 22 | //box with one to one relation between generated client instance id and conversation id 23 | final Box _clientInstanceIdToConversationIdentifierBox; 24 | 25 | final String _clientInstanceKey; 26 | 27 | PersistedChatwootConversationDao( 28 | this._box, 29 | this._clientInstanceIdToConversationIdentifierBox, 30 | this._clientInstanceKey); 31 | 32 | @override 33 | Future deleteConversation() async { 34 | final conversationIdentifier = 35 | _clientInstanceIdToConversationIdentifierBox.get(_clientInstanceKey); 36 | await _clientInstanceIdToConversationIdentifierBox 37 | .delete(_clientInstanceKey); 38 | await _box.delete(conversationIdentifier); 39 | } 40 | 41 | @override 42 | Future saveConversation(ChatwootConversation conversation) async { 43 | await _clientInstanceIdToConversationIdentifierBox.put( 44 | _clientInstanceKey, conversation.id.toString()); 45 | await _box.put(conversation.id, conversation); 46 | } 47 | 48 | @override 49 | ChatwootConversation? getConversation() { 50 | if (_box.values.length == 0) { 51 | return null; 52 | } 53 | 54 | final conversationidentifierString = 55 | _clientInstanceIdToConversationIdentifierBox.get(_clientInstanceKey); 56 | final conversationIdentifier = 57 | int.tryParse(conversationidentifierString ?? ""); 58 | 59 | if (conversationIdentifier == null) { 60 | return null; 61 | } 62 | 63 | return _box.get(conversationIdentifier); 64 | } 65 | 66 | @override 67 | Future onDispose() async {} 68 | 69 | static Future openDB() async { 70 | await Hive.openBox( 71 | ChatwootConversationBoxNames.CONVERSATIONS.toString()); 72 | await Hive.openBox(ChatwootConversationBoxNames 73 | .CLIENT_INSTANCE_TO_CONVERSATIONS 74 | .toString()); 75 | } 76 | 77 | @override 78 | Future clearAll() async { 79 | await _box.clear(); 80 | await _clientInstanceIdToConversationIdentifierBox.clear(); 81 | } 82 | } 83 | 84 | class NonPersistedChatwootConversationDao extends ChatwootConversationDao { 85 | ChatwootConversation? _conversation; 86 | 87 | @override 88 | Future deleteConversation() async { 89 | _conversation = null; 90 | } 91 | 92 | @override 93 | ChatwootConversation? getConversation() { 94 | return _conversation; 95 | } 96 | 97 | @override 98 | Future onDispose() async { 99 | _conversation = null; 100 | } 101 | 102 | @override 103 | Future saveConversation(ChatwootConversation conversation) async { 104 | _conversation = conversation; 105 | } 106 | 107 | @override 108 | Future clearAll() async { 109 | _conversation = null; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /lib/data/local/dao/chatwoot_messages_dao.dart: -------------------------------------------------------------------------------- 1 | import 'dart:collection'; 2 | 3 | import 'package:chatwoot_sdk/data/local/entity/chatwoot_message.dart'; 4 | import 'package:hive_flutter/hive_flutter.dart'; 5 | 6 | abstract class ChatwootMessagesDao { 7 | Future saveMessage(ChatwootMessage message); 8 | Future saveAllMessages(List messages); 9 | ChatwootMessage? getMessage(int messageId); 10 | List getMessages(); 11 | Future clear(); 12 | Future deleteMessage(int messageId); 13 | Future onDispose(); 14 | 15 | Future clearAll(); 16 | } 17 | 18 | //Only used when persistence is enabled 19 | enum ChatwootMessagesBoxNames { MESSAGES, MESSAGES_TO_CLIENT_INSTANCE_KEY } 20 | 21 | class PersistedChatwootMessagesDao extends ChatwootMessagesDao { 22 | // box containing all persisted messages 23 | final Box _box; 24 | 25 | final String _clientInstanceKey; 26 | 27 | //box with one to many relation 28 | final Box _messageIdToClientInstanceKeyBox; 29 | 30 | PersistedChatwootMessagesDao(this._box, this._messageIdToClientInstanceKeyBox, 31 | this._clientInstanceKey); 32 | 33 | @override 34 | Future clear() async { 35 | //filter current client instance message ids 36 | Iterable clientMessageIds = _messageIdToClientInstanceKeyBox.keys.where( 37 | (key) => 38 | _messageIdToClientInstanceKeyBox.get(key) == _clientInstanceKey); 39 | 40 | await _box.deleteAll(clientMessageIds); 41 | await _messageIdToClientInstanceKeyBox.deleteAll(clientMessageIds); 42 | } 43 | 44 | @override 45 | Future saveMessage(ChatwootMessage message) async { 46 | await _box.put(message.id, message); 47 | await _messageIdToClientInstanceKeyBox.put(message.id, _clientInstanceKey); 48 | print("saved"); 49 | } 50 | 51 | @override 52 | Future saveAllMessages(List messages) async { 53 | for (ChatwootMessage message in messages) await saveMessage(message); 54 | } 55 | 56 | @override 57 | List getMessages() { 58 | final messageClientInstancekey = _clientInstanceKey; 59 | 60 | //filter current client instance message ids 61 | Set clientMessageIds = _messageIdToClientInstanceKeyBox.keys 62 | .map((e) => e as int) 63 | .where((key) => 64 | _messageIdToClientInstanceKeyBox.get(key) == 65 | messageClientInstancekey) 66 | .toSet(); 67 | 68 | //retrieve messages with ids 69 | List sortedMessages = _box.values 70 | .where((message) => clientMessageIds.contains(message.id)) 71 | .toList(growable: false); 72 | 73 | //sort message using creation dates 74 | sortedMessages.sort((a, b) { 75 | return b.createdAt.compareTo(a.createdAt); 76 | }); 77 | 78 | return sortedMessages; 79 | } 80 | 81 | @override 82 | Future onDispose() async {} 83 | 84 | @override 85 | Future deleteMessage(int messageId) async { 86 | await _box.delete(messageId); 87 | await _messageIdToClientInstanceKeyBox.delete(messageId); 88 | } 89 | 90 | @override 91 | ChatwootMessage? getMessage(int messageId) { 92 | return _box.get(messageId, defaultValue: null); 93 | } 94 | 95 | @override 96 | Future clearAll() async { 97 | await _box.clear(); 98 | await _messageIdToClientInstanceKeyBox.clear(); 99 | } 100 | 101 | static Future openDB() async { 102 | await Hive.openBox( 103 | ChatwootMessagesBoxNames.MESSAGES.toString()); 104 | await Hive.openBox( 105 | ChatwootMessagesBoxNames.MESSAGES_TO_CLIENT_INSTANCE_KEY.toString()); 106 | } 107 | } 108 | 109 | class NonPersistedChatwootMessagesDao extends ChatwootMessagesDao { 110 | HashMap _messages = new HashMap(); 111 | 112 | @override 113 | Future clear() async { 114 | _messages.clear(); 115 | } 116 | 117 | @override 118 | Future deleteMessage(int messageId) async { 119 | _messages.remove(messageId); 120 | } 121 | 122 | @override 123 | ChatwootMessage? getMessage(int messageId) { 124 | return _messages[messageId]; 125 | } 126 | 127 | @override 128 | List getMessages() { 129 | List sortedMessages = 130 | _messages.values.toList(growable: false); 131 | sortedMessages.sort((a, b) { 132 | return a.createdAt.compareTo(b.createdAt); 133 | }); 134 | return sortedMessages; 135 | } 136 | 137 | @override 138 | Future onDispose() async { 139 | _messages.clear(); 140 | } 141 | 142 | @override 143 | Future saveAllMessages(List messages) async { 144 | messages.forEach((element) async { 145 | await saveMessage(element); 146 | }); 147 | } 148 | 149 | @override 150 | Future saveMessage(ChatwootMessage message) async { 151 | _messages.update(message.id, (value) => message, ifAbsent: () => message); 152 | } 153 | 154 | @override 155 | Future clearAll() async { 156 | _messages.clear(); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /lib/data/local/dao/chatwoot_user_dao.dart: -------------------------------------------------------------------------------- 1 | import 'package:chatwoot_sdk/data/local/entity/chatwoot_user.dart'; 2 | import 'package:hive_flutter/hive_flutter.dart'; 3 | 4 | abstract class ChatwootUserDao { 5 | Future saveUser(ChatwootUser user); 6 | ChatwootUser? getUser(); 7 | Future deleteUser(); 8 | Future onDispose(); 9 | Future clearAll(); 10 | } 11 | 12 | //Only used when persistence is enabled 13 | enum ChatwootUserBoxNames { USERS, CLIENT_INSTANCE_TO_USER } 14 | 15 | class PersistedChatwootUserDao extends ChatwootUserDao { 16 | //box containing chat users 17 | Box _box; 18 | //box with one to one relation between generated client instance id and user identifier 19 | final Box _clientInstanceIdToUserIdentifierBox; 20 | 21 | final String _clientInstanceKey; 22 | 23 | PersistedChatwootUserDao(this._box, this._clientInstanceIdToUserIdentifierBox, 24 | this._clientInstanceKey); 25 | 26 | @override 27 | Future deleteUser() async { 28 | final userIdentifier = 29 | _clientInstanceIdToUserIdentifierBox.get(_clientInstanceKey); 30 | await _clientInstanceIdToUserIdentifierBox.delete(_clientInstanceKey); 31 | await _box.delete(userIdentifier); 32 | } 33 | 34 | @override 35 | Future saveUser(ChatwootUser user) async { 36 | await _clientInstanceIdToUserIdentifierBox.put( 37 | _clientInstanceKey, user.identifier.toString()); 38 | await _box.put(user.identifier, user); 39 | } 40 | 41 | @override 42 | ChatwootUser? getUser() { 43 | if (_box.values.length == 0) { 44 | return null; 45 | } 46 | final userIdentifier = 47 | _clientInstanceIdToUserIdentifierBox.get(_clientInstanceKey); 48 | 49 | return _box.get(userIdentifier); 50 | } 51 | 52 | @override 53 | Future onDispose() async {} 54 | 55 | @override 56 | Future clearAll() async { 57 | await _box.clear(); 58 | await _clientInstanceIdToUserIdentifierBox.clear(); 59 | } 60 | 61 | static Future openDB() async { 62 | await Hive.openBox(ChatwootUserBoxNames.USERS.toString()); 63 | await Hive.openBox( 64 | ChatwootUserBoxNames.CLIENT_INSTANCE_TO_USER.toString()); 65 | } 66 | } 67 | 68 | class NonPersistedChatwootUserDao extends ChatwootUserDao { 69 | ChatwootUser? _user; 70 | 71 | @override 72 | Future deleteUser() async { 73 | _user = null; 74 | } 75 | 76 | @override 77 | ChatwootUser? getUser() { 78 | return _user; 79 | } 80 | 81 | @override 82 | Future onDispose() async { 83 | _user = null; 84 | } 85 | 86 | @override 87 | Future saveUser(ChatwootUser user) async { 88 | _user = user; 89 | } 90 | 91 | @override 92 | Future clearAll() async { 93 | _user = null; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /lib/data/local/entity/chatwoot_contact.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:hive_flutter/adapters.dart'; 3 | import 'package:json_annotation/json_annotation.dart'; 4 | 5 | import '../local_storage.dart'; 6 | 7 | part 'chatwoot_contact.g.dart'; 8 | 9 | @JsonSerializable(explicitToJson: true) 10 | @HiveType(typeId: CHATWOOT_CONTACT_HIVE_TYPE_ID) 11 | class ChatwootContact extends Equatable { 12 | ///unique identifier of contact 13 | @JsonKey(name: "id") 14 | @HiveField(0) 15 | final int id; 16 | 17 | ///Source id of contact obtained on contact create 18 | @JsonKey(name: "source_id") 19 | @HiveField(1) 20 | final String? contactIdentifier; 21 | 22 | ///Token for subscribing to websocket stream events 23 | @JsonKey(name: "pubsub_token") 24 | @HiveField(2) 25 | final String? pubsubToken; 26 | 27 | ///Full name of contact 28 | @JsonKey() 29 | @HiveField(3) 30 | final String name; 31 | 32 | ///Email of contact 33 | @JsonKey() 34 | @HiveField(4) 35 | final String email; 36 | 37 | ChatwootContact({ 38 | required this.id, 39 | required this.contactIdentifier, 40 | required this.pubsubToken, 41 | required this.name, 42 | required this.email, 43 | }); 44 | 45 | factory ChatwootContact.fromJson(Map json) => 46 | _$ChatwootContactFromJson(json); 47 | 48 | Map toJson() => _$ChatwootContactToJson(this); 49 | 50 | @override 51 | List get props => [id, contactIdentifier, pubsubToken, name, email]; 52 | } 53 | -------------------------------------------------------------------------------- /lib/data/local/entity/chatwoot_conversation.dart: -------------------------------------------------------------------------------- 1 | import 'package:chatwoot_sdk/chatwoot_sdk.dart'; 2 | import 'package:chatwoot_sdk/data/local/entity/chatwoot_contact.dart'; 3 | import 'package:chatwoot_sdk/data/local/local_storage.dart'; 4 | import 'package:equatable/equatable.dart'; 5 | import 'package:hive/hive.dart'; 6 | import 'package:json_annotation/json_annotation.dart'; 7 | part 'chatwoot_conversation.g.dart'; 8 | 9 | @JsonSerializable(explicitToJson: true) 10 | @HiveType(typeId: CHATWOOT_CONVERSATION_HIVE_TYPE_ID) 11 | class ChatwootConversation extends Equatable { 12 | ///The numeric ID of the conversation 13 | @JsonKey() 14 | @HiveField(0) 15 | final int id; 16 | 17 | ///The numeric ID of the inbox 18 | @JsonKey(name: "inbox_id") 19 | @HiveField(1) 20 | final int inboxId; 21 | 22 | ///List of all messages from the conversation 23 | @JsonKey() 24 | @HiveField(2) 25 | final List messages; 26 | 27 | ///Contact of the conversation 28 | @JsonKey() 29 | @HiveField(3) 30 | final ChatwootContact contact; 31 | 32 | ChatwootConversation( 33 | {required this.id, 34 | required this.inboxId, 35 | required this.messages, 36 | required this.contact}); 37 | 38 | factory ChatwootConversation.fromJson(Map json) => 39 | _$ChatwootConversationFromJson(json); 40 | 41 | Map toJson() => _$ChatwootConversationToJson(this); 42 | 43 | @override 44 | List get props => [id, inboxId, messages, contact]; 45 | } 46 | -------------------------------------------------------------------------------- /lib/data/local/entity/chatwoot_message.dart: -------------------------------------------------------------------------------- 1 | import 'package:chatwoot_sdk/data/remote/responses/chatwoot_event.dart'; 2 | import 'package:equatable/equatable.dart'; 3 | import 'package:hive/hive.dart'; 4 | import 'package:json_annotation/json_annotation.dart'; 5 | 6 | import '../local_storage.dart'; 7 | 8 | part 'chatwoot_message.g.dart'; 9 | 10 | /// {@category FlutterClientSdk} 11 | @JsonSerializable(explicitToJson: true) 12 | @HiveType(typeId: CHATWOOT_MESSAGE_HIVE_TYPE_ID) 13 | class ChatwootMessage extends Equatable { 14 | ///unique identifier of the message 15 | @JsonKey(fromJson: idFromJson) 16 | @HiveField(0) 17 | final int id; 18 | 19 | ///text content of the message 20 | @JsonKey() 21 | @HiveField(1) 22 | final String? content; 23 | 24 | ///type of message 25 | /// 26 | ///returns 1 if message belongs to contact making the request 27 | @JsonKey(name: "message_type", fromJson: messageTypeFromJson) 28 | @HiveField(2) 29 | final int? messageType; 30 | 31 | ///content type of message 32 | @JsonKey(name: "content_type") 33 | @HiveField(3) 34 | final String? contentType; 35 | 36 | @JsonKey(name: "content_attributes") 37 | @HiveField(4) 38 | final dynamic contentAttributes; 39 | 40 | ///date and time message was created 41 | @JsonKey(name: "created_at", fromJson: createdAtFromJson) 42 | @HiveField(5) 43 | final String createdAt; 44 | 45 | ///id of the conversation the message belongs to 46 | @JsonKey(name: "conversation_id", fromJson: idFromJson) 47 | @HiveField(6) 48 | final int? conversationId; 49 | 50 | ///list of media/doc/file attachment for message 51 | @JsonKey() 52 | @HiveField(7) 53 | final List? attachments; 54 | 55 | ///The user this message belongs to 56 | @JsonKey(name: "sender") 57 | @HiveField(8) 58 | final ChatwootEventMessageUser? sender; 59 | 60 | ///checks if message belongs to contact making the request 61 | bool get isMine => messageType != 1; 62 | 63 | ChatwootMessage( 64 | {required this.id, 65 | required this.content, 66 | required this.messageType, 67 | required this.contentType, 68 | required this.contentAttributes, 69 | required this.createdAt, 70 | required this.conversationId, 71 | required this.attachments, 72 | required this.sender}); 73 | 74 | factory ChatwootMessage.fromJson(Map json) => 75 | _$ChatwootMessageFromJson(json); 76 | 77 | Map toJson() => _$ChatwootMessageToJson(this); 78 | 79 | @override 80 | List get props => [ 81 | id, 82 | content, 83 | messageType, 84 | contentType, 85 | contentAttributes, 86 | createdAt, 87 | conversationId, 88 | attachments, 89 | sender 90 | ]; 91 | } 92 | 93 | int idFromJson(value) { 94 | if (value is String) { 95 | return int.tryParse(value) ?? 0; 96 | } 97 | return value; 98 | } 99 | 100 | int messageTypeFromJson(value) { 101 | if (value is String) { 102 | return int.tryParse(value) ?? 0; 103 | } 104 | return value; 105 | } 106 | 107 | String createdAtFromJson(value) { 108 | if (value is int) { 109 | return DateTime.fromMillisecondsSinceEpoch(value * 1000).toString(); 110 | } 111 | return value.toString(); 112 | } 113 | -------------------------------------------------------------------------------- /lib/data/local/entity/chatwoot_user.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:hive_flutter/hive_flutter.dart'; 3 | import 'package:json_annotation/json_annotation.dart'; 4 | 5 | import '../local_storage.dart'; 6 | 7 | part 'chatwoot_user.g.dart'; 8 | 9 | /// 10 | @JsonSerializable(explicitToJson: true) 11 | @HiveType(typeId: CHATWOOT_USER_HIVE_TYPE_ID) 12 | class ChatwootUser extends Equatable { 13 | ///custom chatwoot user identifier 14 | @JsonKey() 15 | @HiveField(0) 16 | final String? identifier; 17 | 18 | ///custom user identifier hash 19 | @JsonKey(name: "identifier_hash") 20 | @HiveField(1) 21 | final String? identifierHash; 22 | 23 | ///name of chatwoot user 24 | @JsonKey() 25 | @HiveField(2) 26 | final String? name; 27 | 28 | ///email of chatwoot user 29 | @JsonKey() 30 | @HiveField(3) 31 | final String? email; 32 | 33 | ///profile picture url of user 34 | @JsonKey(name: "avatar_url") 35 | @HiveField(4) 36 | final String? avatarUrl; 37 | 38 | ///any other custom attributes to be linked to the user 39 | @JsonKey(name: "custom_attributes") 40 | @HiveField(5) 41 | final dynamic customAttributes; 42 | 43 | ChatwootUser( 44 | {this.identifier, 45 | this.identifierHash, 46 | this.name, 47 | this.email, 48 | this.avatarUrl, 49 | this.customAttributes}); 50 | 51 | @override 52 | List get props => 53 | [identifier, identifierHash, name, email, avatarUrl, customAttributes]; 54 | 55 | factory ChatwootUser.fromJson(Map json) => 56 | _$ChatwootUserFromJson(json); 57 | 58 | Map toJson() => _$ChatwootUserToJson(this); 59 | } 60 | -------------------------------------------------------------------------------- /lib/data/local/local_storage.dart: -------------------------------------------------------------------------------- 1 | import 'package:chatwoot_sdk/data/local/dao/chatwoot_contact_dao.dart'; 2 | import 'package:chatwoot_sdk/data/local/dao/chatwoot_conversation_dao.dart'; 3 | import 'package:chatwoot_sdk/data/local/dao/chatwoot_messages_dao.dart'; 4 | import 'package:chatwoot_sdk/data/local/dao/chatwoot_user_dao.dart'; 5 | import 'package:chatwoot_sdk/data/local/entity/chatwoot_conversation.dart'; 6 | import 'package:chatwoot_sdk/data/remote/responses/chatwoot_event.dart'; 7 | import 'package:hive_flutter/hive_flutter.dart'; 8 | 9 | import 'entity/chatwoot_contact.dart'; 10 | import 'entity/chatwoot_message.dart'; 11 | import 'entity/chatwoot_user.dart'; 12 | 13 | const CHATWOOT_CONTACT_HIVE_TYPE_ID = 0; 14 | const CHATWOOT_CONVERSATION_HIVE_TYPE_ID = 1; 15 | const CHATWOOT_MESSAGE_HIVE_TYPE_ID = 2; 16 | const CHATWOOT_USER_HIVE_TYPE_ID = 3; 17 | const CHATWOOT_EVENT_USER_HIVE_TYPE_ID = 4; 18 | 19 | class LocalStorage { 20 | ChatwootUserDao userDao; 21 | ChatwootConversationDao conversationDao; 22 | ChatwootContactDao contactDao; 23 | ChatwootMessagesDao messagesDao; 24 | 25 | LocalStorage({ 26 | required this.userDao, 27 | required this.conversationDao, 28 | required this.contactDao, 29 | required this.messagesDao, 30 | }); 31 | 32 | static Future openDB({void Function()? onInitializeHive}) async { 33 | if (onInitializeHive == null) { 34 | await Hive.initFlutter(); 35 | if (!Hive.isAdapterRegistered(CHATWOOT_CONTACT_HIVE_TYPE_ID)) { 36 | Hive..registerAdapter(ChatwootContactAdapter()); 37 | } 38 | if (!Hive.isAdapterRegistered(CHATWOOT_CONVERSATION_HIVE_TYPE_ID)) { 39 | Hive..registerAdapter(ChatwootConversationAdapter()); 40 | } 41 | if (!Hive.isAdapterRegistered(CHATWOOT_MESSAGE_HIVE_TYPE_ID)) { 42 | Hive..registerAdapter(ChatwootMessageAdapter()); 43 | } 44 | if (!Hive.isAdapterRegistered(CHATWOOT_EVENT_USER_HIVE_TYPE_ID)) { 45 | Hive..registerAdapter(ChatwootEventMessageUserAdapter()); 46 | } 47 | if (!Hive.isAdapterRegistered(CHATWOOT_USER_HIVE_TYPE_ID)) { 48 | Hive..registerAdapter(ChatwootUserAdapter()); 49 | } 50 | } else { 51 | onInitializeHive(); 52 | } 53 | 54 | await PersistedChatwootContactDao.openDB(); 55 | await PersistedChatwootConversationDao.openDB(); 56 | await PersistedChatwootMessagesDao.openDB(); 57 | await PersistedChatwootUserDao.openDB(); 58 | } 59 | 60 | Future clear({bool clearChatwootUserStorage = true}) async { 61 | await conversationDao.deleteConversation(); 62 | await messagesDao.clear(); 63 | if (clearChatwootUserStorage) { 64 | await userDao.deleteUser(); 65 | await contactDao.deleteContact(); 66 | } 67 | } 68 | 69 | Future clearAll() async { 70 | await conversationDao.clearAll(); 71 | await contactDao.clearAll(); 72 | await messagesDao.clearAll(); 73 | await userDao.clearAll(); 74 | } 75 | 76 | dispose() { 77 | userDao.onDispose(); 78 | conversationDao.onDispose(); 79 | contactDao.onDispose(); 80 | messagesDao.onDispose(); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /lib/data/remote/chatwoot_client_exception.dart: -------------------------------------------------------------------------------- 1 | /// {@category FlutterClientSdk} 2 | class ChatwootClientException implements Exception { 3 | String cause; 4 | dynamic data; 5 | ChatwootClientExceptionType type; 6 | 7 | ChatwootClientException(this.cause, this.type, {this.data}); 8 | } 9 | 10 | /// {@category FlutterClientSdk} 11 | enum ChatwootClientExceptionType { 12 | CREATE_CLIENT_FAILED, 13 | SEND_MESSAGE_FAILED, 14 | CREATE_CONTACT_FAILED, 15 | CREATE_CONVERSATION_FAILED, 16 | GET_MESSAGES_FAILED, 17 | GET_CONTACT_FAILED, 18 | GET_CONVERSATION_FAILED, 19 | UPDATE_CONTACT_FAILED, 20 | UPDATE_MESSAGE_FAILED 21 | } 22 | -------------------------------------------------------------------------------- /lib/data/remote/requests/chatwoot_action.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | 3 | import 'chatwoot_action_data.dart'; 4 | 5 | part 'chatwoot_action.g.dart'; 6 | 7 | @JsonSerializable(explicitToJson: true) 8 | class ChatwootAction { 9 | @JsonKey() 10 | final String identifier; 11 | 12 | @JsonKey() 13 | final String command; 14 | 15 | @JsonKey() 16 | final ChatwootActionData? data; 17 | 18 | ChatwootAction({required this.identifier, this.data, required this.command}); 19 | 20 | factory ChatwootAction.fromJson(Map json) => 21 | _$ChatwootActionFromJson(json); 22 | 23 | Map toJson() => _$ChatwootActionToJson(this); 24 | } 25 | -------------------------------------------------------------------------------- /lib/data/remote/requests/chatwoot_action_data.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | 3 | part 'chatwoot_action_data.g.dart'; 4 | 5 | @JsonSerializable(explicitToJson: true) 6 | class ChatwootActionData { 7 | @JsonKey(toJson: actionTypeToJson, fromJson: actionTypeFromJson) 8 | final ChatwootActionType action; 9 | 10 | ChatwootActionData({required this.action}); 11 | 12 | factory ChatwootActionData.fromJson(Map json) => 13 | _$ChatwootActionDataFromJson(json); 14 | 15 | Map toJson() => _$ChatwootActionDataToJson(this); 16 | } 17 | 18 | enum ChatwootActionType { subscribe, update_presence } 19 | 20 | String actionTypeToJson(ChatwootActionType actionType) { 21 | switch (actionType) { 22 | case ChatwootActionType.update_presence: 23 | return "update_presence"; 24 | case ChatwootActionType.subscribe: 25 | return "subscribe"; 26 | } 27 | } 28 | 29 | ChatwootActionType actionTypeFromJson(String? value) { 30 | switch (value) { 31 | case "update_presence": 32 | return ChatwootActionType.update_presence; 33 | case "subscribe": 34 | return ChatwootActionType.subscribe; 35 | default: 36 | return ChatwootActionType.update_presence; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/data/remote/requests/chatwoot_new_message_request.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:json_annotation/json_annotation.dart'; 3 | 4 | part 'chatwoot_new_message_request.g.dart'; 5 | 6 | @JsonSerializable(explicitToJson: true) 7 | class ChatwootNewMessageRequest extends Equatable { 8 | @JsonKey() 9 | final String content; 10 | @JsonKey(name: "echo_id") 11 | final String echoId; 12 | 13 | ChatwootNewMessageRequest({required this.content, required this.echoId}); 14 | 15 | @override 16 | List get props => [content, echoId]; 17 | 18 | factory ChatwootNewMessageRequest.fromJson(Map json) => 19 | _$ChatwootNewMessageRequestFromJson(json); 20 | 21 | Map toJson() => _$ChatwootNewMessageRequestToJson(this); 22 | } 23 | -------------------------------------------------------------------------------- /lib/data/remote/responses/chatwoot_event.dart: -------------------------------------------------------------------------------- 1 | import 'package:chatwoot_sdk/chatwoot_sdk.dart'; 2 | import 'package:chatwoot_sdk/data/local/local_storage.dart'; 3 | import 'package:equatable/equatable.dart'; 4 | import 'package:hive/hive.dart'; 5 | import 'package:json_annotation/json_annotation.dart'; 6 | 7 | part 'chatwoot_event.g.dart'; 8 | 9 | @JsonSerializable(explicitToJson: true) 10 | class ChatwootEvent { 11 | @JsonKey(toJson: eventTypeToJson, fromJson: eventTypeFromJson) 12 | final ChatwootEventType? type; 13 | 14 | @JsonKey() 15 | final String? identifier; 16 | 17 | @JsonKey(fromJson: eventMessageFromJson) 18 | final ChatwootEventMessage? message; 19 | 20 | ChatwootEvent({this.type, this.message, this.identifier}); 21 | 22 | factory ChatwootEvent.fromJson(Map json) => 23 | _$ChatwootEventFromJson(json); 24 | 25 | Map toJson() => _$ChatwootEventToJson(this); 26 | } 27 | 28 | ChatwootEventMessage? eventMessageFromJson(value) { 29 | if (value == null) { 30 | return null; 31 | } else if (value is num) { 32 | return ChatwootEventMessage(); 33 | } else if (value is String) { 34 | return ChatwootEventMessage(); 35 | } else { 36 | return ChatwootEventMessage.fromJson(value as Map); 37 | } 38 | } 39 | 40 | @JsonSerializable(explicitToJson: true) 41 | class ChatwootEventMessage { 42 | @JsonKey() 43 | final ChatwootEventMessageData? data; 44 | 45 | @JsonKey(toJson: eventMessageTypeToJson, fromJson: eventMessageTypeFromJson) 46 | final ChatwootEventMessageType? event; 47 | 48 | ChatwootEventMessage({this.data, this.event}); 49 | 50 | factory ChatwootEventMessage.fromJson(Map json) => 51 | _$ChatwootEventMessageFromJson(json); 52 | 53 | Map toJson() => _$ChatwootEventMessageToJson(this); 54 | } 55 | 56 | @JsonSerializable(explicitToJson: true) 57 | class ChatwootEventMessageData { 58 | @JsonKey(name: "account_id") 59 | final int? accountId; 60 | 61 | @JsonKey() 62 | final String? content; 63 | 64 | @JsonKey(name: "content_attributes") 65 | final dynamic contentAttributes; 66 | 67 | @JsonKey(name: "content_type") 68 | final String? contentType; 69 | 70 | @JsonKey(name: "conversation_id") 71 | final int? conversationId; 72 | 73 | @JsonKey(name: "created_at") 74 | final dynamic createdAt; 75 | 76 | @JsonKey(name: "echo_id") 77 | final String? echoId; 78 | 79 | @JsonKey(name: "external_source_ids") 80 | final dynamic externalSourceIds; 81 | 82 | @JsonKey() 83 | final int? id; 84 | 85 | @JsonKey(name: "inbox_id") 86 | final int? inboxId; 87 | 88 | @JsonKey(name: "message_type") 89 | final int? messageType; 90 | 91 | @JsonKey(name: "private") 92 | final bool? private; 93 | 94 | @JsonKey() 95 | final ChatwootEventMessageUser? sender; 96 | 97 | @JsonKey(name: "sender_id") 98 | final int? senderId; 99 | 100 | @JsonKey(name: "source_id") 101 | final String? sourceId; 102 | 103 | @JsonKey() 104 | final String? status; 105 | 106 | @JsonKey(name: "updated_at") 107 | final dynamic updatedAt; 108 | 109 | @JsonKey() 110 | final dynamic conversation; 111 | 112 | @JsonKey() 113 | final ChatwootEventMessageUser? user; 114 | 115 | @JsonKey() 116 | final dynamic users; 117 | 118 | ChatwootEventMessageData( 119 | {this.id, 120 | this.user, 121 | this.conversation, 122 | this.echoId, 123 | this.sender, 124 | this.conversationId, 125 | this.createdAt, 126 | this.contentAttributes, 127 | this.contentType, 128 | this.messageType, 129 | this.content, 130 | this.inboxId, 131 | this.sourceId, 132 | this.updatedAt, 133 | this.status, 134 | this.accountId, 135 | this.externalSourceIds, 136 | this.private, 137 | this.senderId, 138 | this.users}); 139 | 140 | factory ChatwootEventMessageData.fromJson(Map json) => 141 | _$ChatwootEventMessageDataFromJson(json); 142 | 143 | Map toJson() => _$ChatwootEventMessageDataToJson(this); 144 | 145 | getMessage() { 146 | return ChatwootMessage.fromJson(toJson()); 147 | } 148 | } 149 | 150 | /// {@category FlutterClientSdk} 151 | @HiveType(typeId: CHATWOOT_EVENT_USER_HIVE_TYPE_ID) 152 | @JsonSerializable(explicitToJson: true) 153 | class ChatwootEventMessageUser extends Equatable { 154 | @JsonKey(name: "avatar_url") 155 | @HiveField(0) 156 | final String? avatarUrl; 157 | 158 | @JsonKey() 159 | @HiveField(1) 160 | final int? id; 161 | 162 | @JsonKey() 163 | @HiveField(2) 164 | final String? name; 165 | 166 | @JsonKey() 167 | @HiveField(3) 168 | final String? thumbnail; 169 | 170 | ChatwootEventMessageUser( 171 | {this.id, this.avatarUrl, this.name, this.thumbnail}); 172 | 173 | factory ChatwootEventMessageUser.fromJson(Map json) => 174 | _$ChatwootEventMessageUserFromJson(json); 175 | 176 | Map toJson() => _$ChatwootEventMessageUserToJson(this); 177 | 178 | @override 179 | List get props => [id, avatarUrl, name, thumbnail]; 180 | } 181 | 182 | enum ChatwootEventType { welcome, ping, confirm_subscription } 183 | 184 | String? eventTypeToJson(ChatwootEventType? actionType) { 185 | return actionType.toString(); 186 | } 187 | 188 | ChatwootEventType? eventTypeFromJson(String? value) { 189 | switch (value) { 190 | case "welcome": 191 | return ChatwootEventType.welcome; 192 | case "ping": 193 | return ChatwootEventType.ping; 194 | case "confirm_subscription": 195 | return ChatwootEventType.confirm_subscription; 196 | default: 197 | return null; 198 | } 199 | } 200 | 201 | enum ChatwootEventMessageType { 202 | presence_update, 203 | message_created, 204 | message_updated, 205 | conversation_typing_off, 206 | conversation_typing_on, 207 | conversation_status_changed 208 | } 209 | 210 | String? eventMessageTypeToJson(ChatwootEventMessageType? actionType) { 211 | switch (actionType) { 212 | case null: 213 | return null; 214 | case ChatwootEventMessageType.conversation_typing_on: 215 | return "conversation.typing_on"; 216 | case ChatwootEventMessageType.conversation_typing_off: 217 | return "conversation.typing_off"; 218 | case ChatwootEventMessageType.presence_update: 219 | return "presence.update"; 220 | case ChatwootEventMessageType.message_created: 221 | return "message.created"; 222 | case ChatwootEventMessageType.message_updated: 223 | return "message.updated"; 224 | case ChatwootEventMessageType.conversation_status_changed: 225 | return "conversation.status_changed"; 226 | default: 227 | return actionType.toString(); 228 | } 229 | } 230 | 231 | ChatwootEventMessageType? eventMessageTypeFromJson(String? value) { 232 | switch (value) { 233 | case "presence.update": 234 | return ChatwootEventMessageType.presence_update; 235 | case "message.created": 236 | return ChatwootEventMessageType.message_created; 237 | case "message.updated": 238 | return ChatwootEventMessageType.message_updated; 239 | case "conversation.typing_on": 240 | return ChatwootEventMessageType.conversation_typing_on; 241 | case "conversation.typing_off": 242 | return ChatwootEventMessageType.conversation_typing_off; 243 | case "conversation.status_changed": 244 | return ChatwootEventMessageType.conversation_status_changed; 245 | default: 246 | return null; 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /lib/data/remote/service/chatwoot_client_api_interceptor.dart: -------------------------------------------------------------------------------- 1 | import 'package:chatwoot_sdk/data/local/entity/chatwoot_contact.dart'; 2 | import 'package:chatwoot_sdk/data/local/entity/chatwoot_conversation.dart'; 3 | import 'package:chatwoot_sdk/data/local/local_storage.dart'; 4 | import 'package:chatwoot_sdk/data/remote/service/chatwoot_client_auth_service.dart'; 5 | import 'package:dio/dio.dart'; 6 | import 'package:synchronized/synchronized.dart' as synchronized; 7 | 8 | ///Intercepts network requests and attaches inbox identifier, contact identifiers, conversation identifiers 9 | class ChatwootClientApiInterceptor extends Interceptor { 10 | static const INTERCEPTOR_INBOX_IDENTIFIER_PLACEHOLDER = "{INBOX_IDENTIFIER}"; 11 | static const INTERCEPTOR_CONTACT_IDENTIFIER_PLACEHOLDER = 12 | "{CONTACT_IDENTIFIER}"; 13 | static const INTERCEPTOR_CONVERSATION_IDENTIFIER_PLACEHOLDER = 14 | "{CONVERSATION_IDENTIFIER}"; 15 | 16 | final String _inboxIdentifier; 17 | final LocalStorage _localStorage; 18 | final ChatwootClientAuthService _authService; 19 | final requestLock = synchronized.Lock(); 20 | final responseLock = synchronized.Lock(); 21 | 22 | ChatwootClientApiInterceptor( 23 | this._inboxIdentifier, this._localStorage, this._authService); 24 | 25 | /// Creates a new contact and conversation when no persisted contact is found when an api call is made 26 | @override 27 | Future onRequest( 28 | RequestOptions options, RequestInterceptorHandler handler) async { 29 | await requestLock.synchronized(() async { 30 | RequestOptions newOptions = options; 31 | ChatwootContact? contact = _localStorage.contactDao.getContact(); 32 | ChatwootConversation? conversation = 33 | _localStorage.conversationDao.getConversation(); 34 | 35 | if (contact == null) { 36 | // create new contact from user if no token found 37 | contact = await _authService.createNewContact( 38 | _inboxIdentifier, _localStorage.userDao.getUser()); 39 | conversation = await _authService.createNewConversation( 40 | _inboxIdentifier, contact.contactIdentifier!); 41 | await _localStorage.conversationDao.saveConversation(conversation); 42 | await _localStorage.contactDao.saveContact(contact); 43 | } 44 | 45 | if (conversation == null) { 46 | conversation = await _authService.createNewConversation( 47 | _inboxIdentifier, contact.contactIdentifier!); 48 | await _localStorage.conversationDao.saveConversation(conversation); 49 | } 50 | 51 | newOptions.path = newOptions.path.replaceAll( 52 | INTERCEPTOR_INBOX_IDENTIFIER_PLACEHOLDER, _inboxIdentifier); 53 | newOptions.path = newOptions.path.replaceAll( 54 | INTERCEPTOR_CONTACT_IDENTIFIER_PLACEHOLDER, 55 | contact.contactIdentifier!); 56 | newOptions.path = newOptions.path.replaceAll( 57 | INTERCEPTOR_CONVERSATION_IDENTIFIER_PLACEHOLDER, 58 | "${conversation.id}"); 59 | 60 | handler.next(newOptions); 61 | }); 62 | } 63 | 64 | /// Clears and recreates contact when a 401 (Unauthorized), 403 (Forbidden) or 404 (Not found) 65 | /// response is returned from chatwoot public client api 66 | @override 67 | Future onResponse( 68 | Response response, ResponseInterceptorHandler handler) async { 69 | await responseLock.synchronized(() async { 70 | if (response.statusCode == 401 || 71 | response.statusCode == 403 || 72 | response.statusCode == 404) { 73 | await _localStorage.clear(clearChatwootUserStorage: false); 74 | 75 | // create new contact from user if unauthorized,forbidden or not found 76 | final contact = _localStorage.contactDao.getContact()!; 77 | final conversation = await _authService.createNewConversation( 78 | _inboxIdentifier, contact.contactIdentifier!); 79 | await _localStorage.contactDao.saveContact(contact); 80 | await _localStorage.conversationDao.saveConversation(conversation); 81 | 82 | RequestOptions newOptions = response.requestOptions; 83 | 84 | newOptions.path = newOptions.path.replaceAll( 85 | INTERCEPTOR_INBOX_IDENTIFIER_PLACEHOLDER, _inboxIdentifier); 86 | newOptions.path = newOptions.path.replaceAll( 87 | INTERCEPTOR_CONTACT_IDENTIFIER_PLACEHOLDER, 88 | contact.contactIdentifier!); 89 | newOptions.path = newOptions.path.replaceAll( 90 | INTERCEPTOR_CONVERSATION_IDENTIFIER_PLACEHOLDER, 91 | "${conversation.id}"); 92 | 93 | //use authservice's dio without the interceptor for subsequent call 94 | handler.next(await _authService.dio.fetch(newOptions)); 95 | } else { 96 | // if response is not unauthorized, forbidden or not found forward response 97 | handler.next(response); 98 | } 99 | }); 100 | } 101 | } 102 | 103 | extension Range on num { 104 | bool isBetween(num from, num to) { 105 | return from < this && this < to; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /lib/data/remote/service/chatwoot_client_auth_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:chatwoot_sdk/data/local/entity/chatwoot_contact.dart'; 4 | import 'package:chatwoot_sdk/data/local/entity/chatwoot_conversation.dart'; 5 | import 'package:chatwoot_sdk/data/local/entity/chatwoot_user.dart'; 6 | import 'package:chatwoot_sdk/data/remote/chatwoot_client_exception.dart'; 7 | import 'package:chatwoot_sdk/data/remote/service/chatwoot_client_api_interceptor.dart'; 8 | import 'package:dio/dio.dart'; 9 | import 'package:web_socket_channel/web_socket_channel.dart'; 10 | 11 | /// Service for handling chatwoot user authentication api calls 12 | /// See [ChatwootClientAuthServiceImpl] 13 | abstract class ChatwootClientAuthService { 14 | WebSocketChannel? connection; 15 | final Dio dio; 16 | 17 | ChatwootClientAuthService(this.dio); 18 | 19 | Future createNewContact( 20 | String inboxIdentifier, ChatwootUser? user); 21 | 22 | Future createNewConversation( 23 | String inboxIdentifier, String contactIdentifier); 24 | } 25 | 26 | /// Default Implementation for [ChatwootClientAuthService] 27 | class ChatwootClientAuthServiceImpl extends ChatwootClientAuthService { 28 | ChatwootClientAuthServiceImpl({required Dio dio}) : super(dio); 29 | 30 | ///Creates new contact for inbox with [inboxIdentifier] and passes [user] body to be linked to created contact 31 | @override 32 | Future createNewContact( 33 | String inboxIdentifier, ChatwootUser? user) async { 34 | try { 35 | final createResponse = await dio.post( 36 | "/public/api/v1/inboxes/$inboxIdentifier/contacts", 37 | data: user?.toJson()); 38 | if ((createResponse.statusCode ?? 0).isBetween(199, 300)) { 39 | //creating contact successful continue with request 40 | final contact = ChatwootContact.fromJson(createResponse.data); 41 | return contact; 42 | } else { 43 | throw ChatwootClientException( 44 | createResponse.statusMessage ?? "unknown error", 45 | ChatwootClientExceptionType.CREATE_CONTACT_FAILED); 46 | } 47 | } on DioError catch (e) { 48 | throw ChatwootClientException( 49 | e.message, ChatwootClientExceptionType.CREATE_CONTACT_FAILED); 50 | } 51 | } 52 | 53 | ///Creates a new conversation for inbox with [inboxIdentifier] and contact with source id [contactIdentifier] 54 | @override 55 | Future createNewConversation( 56 | String inboxIdentifier, String contactIdentifier) async { 57 | try { 58 | final createResponse = await dio.post( 59 | "/public/api/v1/inboxes/$inboxIdentifier/contacts/$contactIdentifier/conversations"); 60 | if ((createResponse.statusCode ?? 0).isBetween(199, 300)) { 61 | //creating contact successful continue with request 62 | final newConversation = 63 | ChatwootConversation.fromJson(createResponse.data); 64 | return newConversation; 65 | } else { 66 | throw ChatwootClientException( 67 | createResponse.statusMessage ?? "unknown error", 68 | ChatwootClientExceptionType.CREATE_CONVERSATION_FAILED); 69 | } 70 | } on DioError catch (e) { 71 | throw ChatwootClientException( 72 | e.message, ChatwootClientExceptionType.CREATE_CONVERSATION_FAILED); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /lib/repository_parameters.dart: -------------------------------------------------------------------------------- 1 | import 'package:chatwoot_sdk/chatwoot_callbacks.dart'; 2 | import 'package:chatwoot_sdk/chatwoot_parameters.dart'; 3 | import 'package:chatwoot_sdk/di/modules.dart'; 4 | 5 | /// Represent all needed parameters necessary for [chatwootRepositoryProvider] to successfully provide an instance 6 | /// of [ChatwootRepository]. 7 | class RepositoryParameters { 8 | /// See [ChatwootParameters] 9 | ChatwootParameters params; 10 | 11 | /// See [ChatwootCallbacks] 12 | ChatwootCallbacks callbacks; 13 | 14 | RepositoryParameters({required this.params, required this.callbacks}); 15 | } 16 | -------------------------------------------------------------------------------- /lib/ui/chatwoot_chat_theme.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_chat_ui/flutter_chat_ui.dart'; 3 | 4 | const CHATWOOT_COLOR_PRIMARY = Color(0xff1f93ff); 5 | const CHATWOOT_BG_COLOR = Color(0xfff4f6fb); 6 | const CHATWOOT_AVATAR_COLORS = [CHATWOOT_COLOR_PRIMARY]; 7 | 8 | /// Default chatwoot chat theme which extends [ChatTheme] 9 | @immutable 10 | class ChatwootChatTheme extends ChatTheme { 11 | /// Creates a chatwoot chat theme. Use this constructor if you want to 12 | /// override only a couple of variables. 13 | const ChatwootChatTheme({ 14 | Widget? attachmentButtonIcon, 15 | Color backgroundColor = CHATWOOT_BG_COLOR, 16 | TextStyle dateDividerTextStyle = const TextStyle( 17 | color: Colors.black26, 18 | fontSize: 12, 19 | fontWeight: FontWeight.w800, 20 | height: 1.333, 21 | ), 22 | Widget? deliveredIcon, 23 | Widget? documentIcon, 24 | TextStyle emptyChatPlaceholderTextStyle = const TextStyle( 25 | color: NEUTRAL_2, 26 | fontSize: 16, 27 | fontWeight: FontWeight.w500, 28 | height: 1.5, 29 | ), 30 | Color errorColor = ERROR, 31 | Widget? errorIcon, 32 | Color inputBackgroundColor = Colors.white, 33 | BorderRadius inputBorderRadius = const BorderRadius.all( 34 | Radius.circular(10), 35 | ), 36 | Color inputTextColor = Colors.black87, 37 | TextStyle inputTextStyle = const TextStyle( 38 | fontSize: 16, 39 | fontWeight: FontWeight.w500, 40 | height: 1.5, 41 | ), 42 | double messageBorderRadius = 20.0, 43 | Color primaryColor = CHATWOOT_COLOR_PRIMARY, 44 | TextStyle receivedMessageBodyTextStyle = const TextStyle( 45 | color: Colors.black87, 46 | fontSize: 16, 47 | fontWeight: FontWeight.w500, 48 | height: 1.5, 49 | ), 50 | TextStyle receivedMessageCaptionTextStyle = const TextStyle( 51 | color: NEUTRAL_2, 52 | fontSize: 12, 53 | fontWeight: FontWeight.w500, 54 | height: 1.333, 55 | ), 56 | Color receivedMessageDocumentIconColor = PRIMARY, 57 | TextStyle receivedMessageLinkDescriptionTextStyle = const TextStyle( 58 | color: NEUTRAL_0, 59 | fontSize: 14, 60 | fontWeight: FontWeight.w400, 61 | height: 1.428, 62 | ), 63 | TextStyle receivedMessageLinkTitleTextStyle = const TextStyle( 64 | color: NEUTRAL_0, 65 | fontSize: 16, 66 | fontWeight: FontWeight.w800, 67 | height: 1.375, 68 | ), 69 | Color secondaryColor = Colors.white, 70 | Widget? seenIcon, 71 | Widget? sendButtonIcon, 72 | Widget? sendingIcon, 73 | TextStyle sentMessageBodyTextStyle = const TextStyle( 74 | color: Colors.white, 75 | fontSize: 16, 76 | fontWeight: FontWeight.w500, 77 | height: 1.5, 78 | ), 79 | TextStyle sentMessageCaptionTextStyle = const TextStyle( 80 | color: NEUTRAL_7_WITH_OPACITY, 81 | fontSize: 12, 82 | fontWeight: FontWeight.w500, 83 | height: 1.333, 84 | ), 85 | Color sentMessageDocumentIconColor = NEUTRAL_7, 86 | TextStyle sentMessageLinkDescriptionTextStyle = const TextStyle( 87 | color: NEUTRAL_7, 88 | fontSize: 14, 89 | fontWeight: FontWeight.w400, 90 | height: 1.428, 91 | ), 92 | TextStyle sentMessageLinkTitleTextStyle = const TextStyle( 93 | color: NEUTRAL_7, 94 | fontSize: 16, 95 | fontWeight: FontWeight.w800, 96 | height: 1.375, 97 | ), 98 | List userAvatarNameColors = CHATWOOT_AVATAR_COLORS, 99 | TextStyle userAvatarTextStyle = const TextStyle( 100 | color: NEUTRAL_7, 101 | fontSize: 12, 102 | fontWeight: FontWeight.w800, 103 | height: 1.333, 104 | ), 105 | TextStyle userNameTextStyle = const TextStyle( 106 | color: Colors.black87, 107 | fontSize: 12, 108 | fontWeight: FontWeight.w800, 109 | height: 1.333, 110 | ), 111 | }) : super( 112 | attachmentButtonIcon: attachmentButtonIcon, 113 | backgroundColor: backgroundColor, 114 | dateDividerTextStyle: dateDividerTextStyle, 115 | deliveredIcon: deliveredIcon, 116 | documentIcon: documentIcon, 117 | emptyChatPlaceholderTextStyle: emptyChatPlaceholderTextStyle, 118 | errorColor: errorColor, 119 | errorIcon: errorIcon, 120 | inputBackgroundColor: inputBackgroundColor, 121 | inputBorderRadius: inputBorderRadius, 122 | inputTextColor: inputTextColor, 123 | inputTextStyle: inputTextStyle, 124 | messageBorderRadius: messageBorderRadius, 125 | primaryColor: primaryColor, 126 | receivedMessageBodyTextStyle: receivedMessageBodyTextStyle, 127 | receivedMessageCaptionTextStyle: receivedMessageCaptionTextStyle, 128 | receivedMessageDocumentIconColor: receivedMessageDocumentIconColor, 129 | receivedMessageLinkDescriptionTextStyle: 130 | receivedMessageLinkDescriptionTextStyle, 131 | receivedMessageLinkTitleTextStyle: receivedMessageLinkTitleTextStyle, 132 | secondaryColor: secondaryColor, 133 | seenIcon: seenIcon, 134 | sendButtonIcon: sendButtonIcon, 135 | sendingIcon: sendingIcon, 136 | sentMessageBodyTextStyle: sentMessageBodyTextStyle, 137 | sentMessageCaptionTextStyle: sentMessageCaptionTextStyle, 138 | sentMessageDocumentIconColor: sentMessageDocumentIconColor, 139 | sentMessageLinkDescriptionTextStyle: 140 | sentMessageLinkDescriptionTextStyle, 141 | sentMessageLinkTitleTextStyle: sentMessageLinkTitleTextStyle, 142 | userAvatarNameColors: userAvatarNameColors, 143 | userAvatarTextStyle: userAvatarTextStyle, 144 | userNameTextStyle: userNameTextStyle, 145 | ); 146 | } 147 | -------------------------------------------------------------------------------- /lib/ui/chatwoot_l10n.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_chat_ui/flutter_chat_ui.dart'; 2 | 3 | /// Base chat l10n containing all required variables to provide localized chatwoot chat 4 | class ChatwootL10n extends ChatL10n { 5 | /// Accessibility label (hint) for the attachment button 6 | final String attachmentButtonAccessibilityLabel; 7 | 8 | /// Placeholder when there are no messages 9 | final String emptyChatPlaceholder; 10 | 11 | /// Accessibility label (hint) for the tap action on file message 12 | final String fileButtonAccessibilityLabel; 13 | 14 | /// Placeholder for the text field 15 | final String inputPlaceholder; 16 | 17 | /// Placeholder for the text field 18 | final String onlineText; 19 | 20 | /// Placeholder for the text field 21 | final String offlineText; 22 | 23 | /// Placeholder for the text field 24 | final String typingText; 25 | 26 | /// Accessibility label (hint) for the send button 27 | final String sendButtonAccessibilityLabel; 28 | 29 | /// Message when agent resolves conversation 30 | final String conversationResolvedMessage; 31 | 32 | /// Creates a new chatwoot l10n 33 | const ChatwootL10n( 34 | {this.attachmentButtonAccessibilityLabel = "", 35 | this.emptyChatPlaceholder = "", 36 | this.fileButtonAccessibilityLabel = "", 37 | this.onlineText = "Typically replies in a few hours", 38 | this.offlineText = "We're away at the moment", 39 | this.typingText = "typing...", 40 | this.inputPlaceholder = "Type your message", 41 | this.sendButtonAccessibilityLabel = "Send Message", 42 | this.conversationResolvedMessage = 43 | "Your ticket has been marked as resolved"}) 44 | : super( 45 | attachmentButtonAccessibilityLabel: 46 | attachmentButtonAccessibilityLabel, 47 | emptyChatPlaceholder: emptyChatPlaceholder, 48 | fileButtonAccessibilityLabel: fileButtonAccessibilityLabel, 49 | inputPlaceholder: inputPlaceholder, 50 | sendButtonAccessibilityLabel: sendButtonAccessibilityLabel); 51 | } 52 | -------------------------------------------------------------------------------- /lib/ui/webview_widget/chatwoot_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:chatwoot_sdk/data/local/entity/chatwoot_user.dart'; 2 | import 'package:chatwoot_sdk/ui/webview_widget/webview.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | ///ChatwootWidget 6 | /// {@category FlutterClientSdk} 7 | class ChatwootWidget extends StatefulWidget { 8 | ///Website channel token 9 | final String websiteToken; 10 | 11 | ///Installation url for chatwoot 12 | final String baseUrl; 13 | 14 | ///User information about the user like email, username and avatar_url 15 | final ChatwootUser? user; 16 | 17 | ///User locale 18 | final String locale; 19 | 20 | ///Widget Close event 21 | final void Function()? closeWidget; 22 | 23 | ///Additional information about the customer 24 | final customAttributes; 25 | 26 | ///Widget Attachment event. Currently supported only on Android devices 27 | final Future> Function()? onAttachFile; 28 | 29 | ///Widget Load started event 30 | final void Function()? onLoadStarted; 31 | 32 | ///Widget Load progress event 33 | final void Function(int)? onLoadProgress; 34 | 35 | ///Widget Load completed event 36 | final void Function()? onLoadCompleted; 37 | ChatwootWidget( 38 | {Key? key, 39 | required this.websiteToken, 40 | required this.baseUrl, 41 | this.user, 42 | this.locale = "en", 43 | this.customAttributes, 44 | this.closeWidget, 45 | this.onAttachFile, 46 | this.onLoadStarted, 47 | this.onLoadProgress, 48 | this.onLoadCompleted}) 49 | : super(key: key); 50 | 51 | @override 52 | _ChatwootWidgetState createState() => _ChatwootWidgetState(); 53 | } 54 | 55 | class _ChatwootWidgetState extends State { 56 | @override 57 | void initState() { 58 | super.initState(); 59 | } 60 | 61 | @override 62 | Widget build(BuildContext context) { 63 | return Webview( 64 | websiteToken: widget.websiteToken, 65 | baseUrl: widget.baseUrl, 66 | user: widget.user, 67 | locale: widget.locale, 68 | customAttributes: widget.customAttributes, 69 | closeWidget: widget.closeWidget, 70 | onAttachFile: widget.onAttachFile, 71 | onLoadStarted: widget.onLoadStarted, 72 | onLoadCompleted: widget.onLoadCompleted, 73 | onLoadProgress: widget.onLoadProgress, 74 | ); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /lib/ui/webview_widget/constants.dart: -------------------------------------------------------------------------------- 1 | const WOOT_PREFIX = 'chatwoot-widget:'; 2 | 3 | class PostMessageEvents { 4 | static const SET_LOCALE = 'set-locale'; 5 | static const SET_CUSTOM_ATTRIBUTES = 'set-custom-attributes'; 6 | static const SET_USER = 'set-user'; 7 | } 8 | 9 | const BG_COLOR = '#f1f5f8'; 10 | const COLOR_WHITE = '#fff'; 11 | -------------------------------------------------------------------------------- /lib/ui/webview_widget/utils.dart: -------------------------------------------------------------------------------- 1 | import "dart:convert"; 2 | 3 | import "package:chatwoot_sdk/data/local/entity/chatwoot_user.dart"; 4 | import "package:flutter_secure_storage/flutter_secure_storage.dart"; 5 | 6 | import "constants.dart"; 7 | 8 | bool isJsonString(string) { 9 | try { 10 | jsonDecode(string); 11 | } catch (e) { 12 | return false; 13 | } 14 | return true; 15 | } 16 | 17 | String createWootPostMessage(object) { 18 | final stringfyObject = "${WOOT_PREFIX}${jsonEncode(object)}"; 19 | final script = 'window.postMessage(\'${stringfyObject}\');'; 20 | return script; 21 | } 22 | 23 | String getMessage(String data) { 24 | return data.replaceAll(WOOT_PREFIX, ''); 25 | } 26 | 27 | String generateScripts( 28 | {ChatwootUser? user, String? locale, dynamic customAttributes}) { 29 | String script = ''; 30 | if (user != null) { 31 | final userObject = { 32 | "event": PostMessageEvents.SET_USER, 33 | "identifier": user.identifier, 34 | "user": user, 35 | }; 36 | script += createWootPostMessage(userObject); 37 | } 38 | if (locale != null) { 39 | final localeObject = { 40 | "event": PostMessageEvents.SET_LOCALE, 41 | "locale": locale 42 | }; 43 | script += createWootPostMessage(localeObject); 44 | } 45 | if (customAttributes != null) { 46 | final attributeObject = { 47 | "event": PostMessageEvents.SET_CUSTOM_ATTRIBUTES, 48 | "customAttributes": customAttributes, 49 | }; 50 | script += createWootPostMessage(attributeObject); 51 | } 52 | return script; 53 | } 54 | 55 | const _androidOptions = AndroidOptions( 56 | encryptedSharedPreferences: true, 57 | ); 58 | final secureStorage = new FlutterSecureStorage(aOptions: _androidOptions); 59 | const cookieKey = 'cwCookie'; 60 | 61 | class StoreHelper { 62 | static Future getCookie() async { 63 | final cookie = await secureStorage.read(key: cookieKey); 64 | return cookie ?? ""; 65 | } 66 | 67 | static storeCookie(value) async { 68 | await secureStorage.write(key: cookieKey, value: value); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /lib/ui/webview_widget/webview.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import 'package:chatwoot_sdk/chatwoot_sdk.dart'; 5 | import 'package:chatwoot_sdk/ui/webview_widget/utils.dart'; 6 | import 'package:flutter/material.dart'; 7 | import 'package:url_launcher/url_launcher.dart'; 8 | import 'package:webview_flutter/webview_flutter.dart'; 9 | import 'package:webview_flutter_android/webview_flutter_android.dart' 10 | as webview_flutter_android; 11 | 12 | ///Chatwoot webview widget 13 | /// {@category FlutterClientSdk} 14 | class Webview extends StatefulWidget { 15 | /// Url for Chatwoot widget in webview 16 | late final String widgetUrl; 17 | 18 | /// Chatwoot user & locale initialisation script 19 | late final String injectedJavaScript; 20 | 21 | /// See [ChatwootWidget.closeWidget] 22 | final void Function()? closeWidget; 23 | 24 | /// See [ChatwootWidget.onAttachFile] 25 | final Future> Function()? onAttachFile; 26 | 27 | /// See [ChatwootWidget.onLoadStarted] 28 | final void Function()? onLoadStarted; 29 | 30 | /// See [ChatwootWidget.onLoadProgress] 31 | final void Function(int)? onLoadProgress; 32 | 33 | /// See [ChatwootWidget.onLoadCompleted] 34 | final void Function()? onLoadCompleted; 35 | 36 | Webview( 37 | {Key? key, 38 | required String websiteToken, 39 | required String baseUrl, 40 | ChatwootUser? user, 41 | String locale = "en", 42 | customAttributes, 43 | this.closeWidget, 44 | this.onAttachFile, 45 | this.onLoadStarted, 46 | this.onLoadProgress, 47 | this.onLoadCompleted}) 48 | : super(key: key) { 49 | widgetUrl = 50 | "${baseUrl}/widget?website_token=${websiteToken}&locale=${locale}"; 51 | 52 | injectedJavaScript = generateScripts( 53 | user: user, locale: locale, customAttributes: customAttributes); 54 | } 55 | 56 | @override 57 | _WebviewState createState() => _WebviewState(); 58 | } 59 | 60 | class _WebviewState extends State { 61 | WebViewController? _controller; 62 | @override 63 | void initState() { 64 | super.initState(); 65 | WidgetsBinding.instance.addPostFrameCallback((_) async { 66 | String webviewUrl = widget.widgetUrl; 67 | final cwCookie = await StoreHelper.getCookie(); 68 | if (cwCookie.isNotEmpty) { 69 | webviewUrl = "${webviewUrl}&cw_conversation=${cwCookie}"; 70 | } 71 | setState(() { 72 | _controller = WebViewController() 73 | ..setJavaScriptMode(JavaScriptMode.unrestricted) 74 | ..setBackgroundColor(Colors.white) 75 | ..setNavigationDelegate( 76 | NavigationDelegate( 77 | onProgress: (int progress) { 78 | // Update loading bar. 79 | widget.onLoadProgress?.call(progress); 80 | }, 81 | onPageStarted: (String url) { 82 | widget.onLoadStarted?.call(); 83 | }, 84 | onPageFinished: (String url) async { 85 | widget.onLoadCompleted?.call(); 86 | }, 87 | onWebResourceError: (WebResourceError error) {}, 88 | onNavigationRequest: (NavigationRequest request) { 89 | _goToUrl(request.url); 90 | return NavigationDecision.prevent; 91 | }, 92 | ), 93 | ) 94 | ..addJavaScriptChannel("ReactNativeWebView", 95 | onMessageReceived: (JavaScriptMessage jsMessage) { 96 | print("Chatwoot message received: ${jsMessage.message}"); 97 | final message = getMessage(jsMessage.message); 98 | if (isJsonString(message)) { 99 | final parsedMessage = jsonDecode(message); 100 | final eventType = parsedMessage["event"]; 101 | final type = parsedMessage["type"]; 102 | if (eventType == 'loaded') { 103 | final authToken = parsedMessage["config"]["authToken"]; 104 | StoreHelper.storeCookie(authToken); 105 | _controller?.runJavaScript(widget.injectedJavaScript); 106 | } 107 | if (type == 'close-widget') { 108 | widget.closeWidget?.call(); 109 | } 110 | } 111 | }) 112 | ..loadRequest(Uri.parse(webviewUrl)); 113 | 114 | if (Platform.isAndroid && widget.onAttachFile != null) { 115 | final androidController = _controller!.platform 116 | as webview_flutter_android.AndroidWebViewController; 117 | androidController 118 | .setOnShowFileSelector((_) => widget.onAttachFile!.call()); 119 | } 120 | }); 121 | }); 122 | } 123 | 124 | @override 125 | Widget build(BuildContext context) { 126 | return _controller != null 127 | ? WebViewWidget(controller: _controller!) 128 | : SizedBox(); 129 | } 130 | 131 | _goToUrl(String url) { 132 | launchUrl(Uri.parse(url)); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: chatwoot_sdk 2 | description: A flutter client sdk for chatwoot. Integrate Chatwoot flutter client into your flutter app and talk to your visitors/users in real time. 3 | version: 0.0.9 4 | homepage: https://github.com/chatwoot/chatwoot-flutter-sdk 5 | 6 | environment: 7 | sdk: ">=2.12.0 <3.0.0" 8 | flutter: ">=1.17.0" 9 | 10 | dependencies: 11 | flutter: 12 | sdk: flutter 13 | flutter_chat_ui: ^1.1.9 14 | hive: ^2.2.3 15 | hive_flutter: ^1.1.0 16 | uuid: ^3.0.4 17 | web_socket_channel: ^2.1.0 18 | json_annotation: ^4.0.1 19 | dio: ^4.0.0 20 | equatable: ^2.0.3 21 | riverpod: ^0.14.0+3 22 | synchronized: ^3.0.0 23 | intl: ^0.17.0 24 | flutter_chat_types: ^2.4.0 25 | stream_channel: ^2.1.0 26 | async: ^2.5.0 27 | webview_flutter: ^4.0.6 28 | flutter_secure_storage: ^8.0.0 29 | url_launcher: ^6.1.10 30 | path_provider: ^2.0.13 31 | 32 | dev_dependencies: 33 | flutter_test: 34 | sdk: flutter 35 | mockito: ^5.3.2 36 | json_serializable: ^6.6.1 37 | hive_generator: ^2.0.0 38 | build_runner: ^2.3.3 39 | flutter_lints: ^2.0.1 40 | 41 | # For information on the generic Dart part of this file, see the 42 | # following page: https://dart.dev/tools/pub/pubspec 43 | 44 | # The following section is specific to Flutter. 45 | flutter: 46 | # To add assets to your package, add an assets section, like this: 47 | assets: 48 | - assets/ 49 | # 50 | # For details regarding assets in packages, see 51 | # https://flutter.dev/assets-and-images/#from-packages 52 | # 53 | # An image asset can refer to one or more resolution-specific "variants", see 54 | # https://flutter.dev/assets-and-images/#resolution-aware. 55 | 56 | # To add custom fonts to your package, add a fonts section here, 57 | # in this "flutter" section. Each entry in this list should have a 58 | # "family" key with the font family name, and a "fonts" key with a 59 | # list giving the asset and other descriptors for the font. For 60 | # example: 61 | # fonts: 62 | # - family: Schyler 63 | # fonts: 64 | # - asset: fonts/Schyler-Regular.ttf 65 | # - asset: fonts/Schyler-Italic.ttf 66 | # style: italic 67 | # - family: Trajan Pro 68 | # fonts: 69 | # - asset: fonts/TrajanPro.ttf 70 | # - asset: fonts/TrajanPro_Bold.ttf 71 | # weight: 700 72 | # 73 | # For details regarding fonts in packages, see 74 | # https://flutter.dev/custom-fonts/#from-packages 75 | -------------------------------------------------------------------------------- /test/chatwoot_client_test.mocks.dart: -------------------------------------------------------------------------------- 1 | // Mocks generated by Mockito 5.0.15 from annotations 2 | // in chatwoot_sdk/test/chatwoot_client_test.dart. 3 | // Do not manually edit this file. 4 | 5 | import 'dart:async' as _i6; 6 | 7 | import 'package:chatwoot_sdk/chatwoot_callbacks.dart' as _i4; 8 | import 'package:chatwoot_sdk/data/chatwoot_repository.dart' as _i5; 9 | import 'package:chatwoot_sdk/data/local/entity/chatwoot_user.dart' as _i7; 10 | import 'package:chatwoot_sdk/data/local/local_storage.dart' as _i3; 11 | import 'package:chatwoot_sdk/data/remote/requests/chatwoot_action_data.dart' 12 | as _i9; 13 | import 'package:chatwoot_sdk/data/remote/requests/chatwoot_new_message_request.dart' 14 | as _i8; 15 | import 'package:chatwoot_sdk/data/remote/service/chatwoot_client_service.dart' 16 | as _i2; 17 | import 'package:mockito/mockito.dart' as _i1; 18 | 19 | // ignore_for_file: avoid_redundant_argument_values 20 | // ignore_for_file: avoid_setters_without_getters 21 | // ignore_for_file: comment_references 22 | // ignore_for_file: implementation_imports 23 | // ignore_for_file: invalid_use_of_visible_for_testing_member 24 | // ignore_for_file: prefer_const_constructors 25 | // ignore_for_file: unnecessary_parenthesis 26 | 27 | class _FakeChatwootClientService_0 extends _i1.Fake 28 | implements _i2.ChatwootClientService {} 29 | 30 | class _FakeLocalStorage_1 extends _i1.Fake implements _i3.LocalStorage {} 31 | 32 | class _FakeChatwootCallbacks_2 extends _i1.Fake 33 | implements _i4.ChatwootCallbacks {} 34 | 35 | /// A class which mocks [ChatwootRepository]. 36 | /// 37 | /// See the documentation for Mockito's code generation for more information. 38 | class MockChatwootRepository extends _i1.Mock 39 | implements _i5.ChatwootRepository { 40 | MockChatwootRepository() { 41 | _i1.throwOnMissingStub(this); 42 | } 43 | 44 | @override 45 | _i2.ChatwootClientService get clientService => 46 | (super.noSuchMethod(Invocation.getter(#clientService), 47 | returnValue: _FakeChatwootClientService_0()) 48 | as _i2.ChatwootClientService); 49 | @override 50 | _i3.LocalStorage get localStorage => 51 | (super.noSuchMethod(Invocation.getter(#localStorage), 52 | returnValue: _FakeLocalStorage_1()) as _i3.LocalStorage); 53 | @override 54 | _i4.ChatwootCallbacks get callbacks => 55 | (super.noSuchMethod(Invocation.getter(#callbacks), 56 | returnValue: _FakeChatwootCallbacks_2()) as _i4.ChatwootCallbacks); 57 | @override 58 | set callbacks(_i4.ChatwootCallbacks? _callbacks) => 59 | super.noSuchMethod(Invocation.setter(#callbacks, _callbacks), 60 | returnValueForMissingStub: null); 61 | @override 62 | _i6.Future initialize(_i7.ChatwootUser? user) => 63 | (super.noSuchMethod(Invocation.method(#initialize, [user]), 64 | returnValue: Future.value(), 65 | returnValueForMissingStub: Future.value()) as _i6.Future); 66 | @override 67 | void getPersistedMessages() => 68 | super.noSuchMethod(Invocation.method(#getPersistedMessages, []), 69 | returnValueForMissingStub: null); 70 | @override 71 | _i6.Future getMessages() => 72 | (super.noSuchMethod(Invocation.method(#getMessages, []), 73 | returnValue: Future.value(), 74 | returnValueForMissingStub: Future.value()) as _i6.Future); 75 | @override 76 | void listenForEvents() => 77 | super.noSuchMethod(Invocation.method(#listenForEvents, []), 78 | returnValueForMissingStub: null); 79 | @override 80 | _i6.Future sendMessage(_i8.ChatwootNewMessageRequest? request) => 81 | (super.noSuchMethod(Invocation.method(#sendMessage, [request]), 82 | returnValue: Future.value(), 83 | returnValueForMissingStub: Future.value()) as _i6.Future); 84 | @override 85 | void sendAction(_i9.ChatwootActionType? action) => 86 | super.noSuchMethod(Invocation.method(#sendAction, [action]), 87 | returnValueForMissingStub: null); 88 | @override 89 | _i6.Future clear() => (super.noSuchMethod(Invocation.method(#clear, []), 90 | returnValue: Future.value(), 91 | returnValueForMissingStub: Future.value()) as _i6.Future); 92 | @override 93 | void dispose() => super.noSuchMethod(Invocation.method(#dispose, []), 94 | returnValueForMissingStub: null); 95 | @override 96 | String toString() => super.toString(); 97 | } 98 | -------------------------------------------------------------------------------- /test/chatwoot_sdk_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | 3 | 4 | void main() { 5 | test('adds one to input values', () { 6 | expect(1+1, 2); 7 | }); 8 | } 9 | -------------------------------------------------------------------------------- /test/data/local/dao/non_persisted_chatwoot_contact_dao_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:chatwoot_sdk/data/local/dao/chatwoot_contact_dao.dart'; 2 | import 'package:chatwoot_sdk/data/local/entity/chatwoot_contact.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | 5 | import '../../../utils/test_resources_util.dart'; 6 | 7 | void main() { 8 | group("Non Persisted Chatwoot Contact Dao Test", () { 9 | late NonPersistedChatwootContactDao dao; 10 | late final ChatwootContact testContact; 11 | 12 | setUpAll(() async { 13 | testContact = ChatwootContact.fromJson( 14 | await TestResourceUtil.readJsonResource(fileName: "contact")); 15 | dao = NonPersistedChatwootContactDao(); 16 | }); 17 | 18 | test( 19 | 'Given contact is successfully deleted when deleteContact is called, then getContact should return null', 20 | () { 21 | //GIVEN 22 | dao.saveContact(testContact); 23 | 24 | //WHEN 25 | dao.deleteContact(); 26 | 27 | //THEN 28 | expect(dao.getContact(), null); 29 | }); 30 | 31 | test( 32 | 'Given contact is successfully save when saveContact is called, then getContact should return saved contact', 33 | () { 34 | //WHEN 35 | dao.saveContact(testContact); 36 | 37 | //THEN 38 | expect(dao.getContact(), testContact); 39 | }); 40 | 41 | test( 42 | 'Given contact is successfully retrieved when getContact is called, then retrieved contact should not be null', 43 | () { 44 | //GIVEN 45 | dao.saveContact(testContact); 46 | 47 | //WHEN 48 | final retrievedContact = dao.getContact(); 49 | 50 | //THEN 51 | expect(retrievedContact, testContact); 52 | }); 53 | 54 | test( 55 | 'Given contacts are successfully cleared when clearAll is called, then retrieving a contact should be null', 56 | () { 57 | //GIVEN 58 | dao.saveContact(testContact); 59 | 60 | //WHEN 61 | dao.clearAll(); 62 | 63 | //THEN 64 | expect(dao.getContact(), null); 65 | }); 66 | 67 | test( 68 | 'Given dao is successfully disposed when onDispose is called, then saved contact should be null', 69 | () { 70 | //GIVEN 71 | dao.saveContact(testContact); 72 | 73 | //WHEN 74 | dao.onDispose(); 75 | 76 | //THEN 77 | final retrievedContact = dao.getContact(); 78 | expect(retrievedContact, null); 79 | }); 80 | 81 | tearDown(() { 82 | dao.clearAll(); 83 | }); 84 | }); 85 | } 86 | -------------------------------------------------------------------------------- /test/data/local/dao/non_persisted_chatwoot_conversation_dao_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:chatwoot_sdk/data/local/dao/chatwoot_conversation_dao.dart'; 2 | import 'package:chatwoot_sdk/data/local/entity/chatwoot_conversation.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | 5 | import '../../../utils/test_resources_util.dart'; 6 | 7 | void main() { 8 | group("Non Persisted Chatwoot Conversation Dao Test", () { 9 | late NonPersistedChatwootConversationDao dao; 10 | late final ChatwootConversation testConversation; 11 | 12 | setUpAll(() async { 13 | testConversation = ChatwootConversation.fromJson( 14 | await TestResourceUtil.readJsonResource(fileName: "conversation")); 15 | dao = NonPersistedChatwootConversationDao(); 16 | }); 17 | 18 | test( 19 | 'Given conversation is successfully deleted when deleteConversation is called, then getConversation should return null', 20 | () { 21 | //GIVEN 22 | dao.saveConversation(testConversation); 23 | 24 | //WHEN 25 | dao.deleteConversation(); 26 | 27 | //THEN 28 | expect(dao.getConversation(), null); 29 | }); 30 | 31 | test( 32 | 'Given conversation is successfully save when saveConversation is called, then getConversation should return saved conversation', 33 | () { 34 | //WHEN 35 | dao.saveConversation(testConversation); 36 | 37 | //THEN 38 | expect(dao.getConversation(), testConversation); 39 | }); 40 | 41 | test( 42 | 'Given conversation is successfully retrieved when getConversation is called, then retrieved conversation should not be null', 43 | () { 44 | //GIVEN 45 | dao.saveConversation(testConversation); 46 | 47 | //WHEN 48 | final retrievedConversation = dao.getConversation(); 49 | 50 | //THEN 51 | expect(retrievedConversation, testConversation); 52 | }); 53 | 54 | test( 55 | 'Given conversations are successfully cleared when clearAll is called, then retrieving a conversation should be null', 56 | () { 57 | //GIVEN 58 | dao.saveConversation(testConversation); 59 | 60 | //WHEN 61 | dao.clearAll(); 62 | 63 | //THEN 64 | expect(dao.getConversation(), null); 65 | }); 66 | 67 | test( 68 | 'Given dao is successfully disposed when onDispose is called, then saved conversation should be null', 69 | () { 70 | //GIVEN 71 | dao.saveConversation(testConversation); 72 | 73 | //WHEN 74 | dao.onDispose(); 75 | 76 | //THEN 77 | final retrievedConversation = dao.getConversation(); 78 | expect(retrievedConversation, null); 79 | }); 80 | 81 | tearDown(() { 82 | dao.clearAll(); 83 | }); 84 | }); 85 | } 86 | -------------------------------------------------------------------------------- /test/data/local/dao/non_persisted_chatwoot_messages_dao_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:chatwoot_sdk/data/local/dao/chatwoot_messages_dao.dart'; 2 | import 'package:chatwoot_sdk/data/local/entity/chatwoot_message.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | 5 | import '../../../utils/test_resources_util.dart'; 6 | 7 | void main() { 8 | group("Non Persisted Chatwoot Messages Dao Test", () { 9 | late NonPersistedChatwootMessagesDao dao; 10 | late final ChatwootMessage testMessage; 11 | 12 | setUpAll(() async { 13 | testMessage = ChatwootMessage.fromJson( 14 | await TestResourceUtil.readJsonResource(fileName: "message")); 15 | dao = NonPersistedChatwootMessagesDao(); 16 | }); 17 | 18 | test( 19 | 'Given message is successfully deleted when deleteMessage is called, then getMessage should return null', 20 | () { 21 | //GIVEN 22 | dao.saveMessage(testMessage); 23 | 24 | //WHEN 25 | dao.deleteMessage(testMessage.id); 26 | 27 | //THEN 28 | expect(dao.getMessage(testMessage.id), null); 29 | }); 30 | 31 | test( 32 | 'Given message is successfully saved when saveMessage is called, then getMessage should return saved message', 33 | () { 34 | //WHEN 35 | dao.saveMessage(testMessage); 36 | 37 | //THEN 38 | expect(dao.getMessage(testMessage.id), testMessage); 39 | }); 40 | 41 | test( 42 | 'Given messages are successfully saved when saveMessages is called, then getMessages should return saved messages', 43 | () { 44 | final messages = [testMessage]; 45 | 46 | //WHEN 47 | dao.saveAllMessages(messages); 48 | 49 | //THEN 50 | expect(dao.getMessages(), messages); 51 | }); 52 | 53 | test( 54 | 'Given message is successfully retrieved when getMessage is called, then retrieved message should not be null', 55 | () { 56 | //GIVEN 57 | dao.saveMessage(testMessage); 58 | 59 | //WHEN 60 | final retrievedMessage = dao.getMessage(testMessage.id); 61 | 62 | //THEN 63 | expect(retrievedMessage, testMessage); 64 | }); 65 | 66 | test( 67 | 'Given messages exist in database when getMessages is called, then retrieved messages should not be empty', 68 | () { 69 | //GIVEN 70 | dao.saveMessage(testMessage); 71 | 72 | //WHEN 73 | final retrievedMessages = dao.getMessages(); 74 | 75 | //THEN 76 | expect(retrievedMessages.length, 1); 77 | expect(retrievedMessages[0], testMessage); 78 | }); 79 | 80 | test( 81 | 'Given messages do not exist in database when getMessages is called, then retrieved messages should be empty', 82 | () { 83 | //GIVEN 84 | dao.clear(); 85 | 86 | //WHEN 87 | final retrievedMessages = dao.getMessages(); 88 | 89 | //THEN 90 | expect(retrievedMessages.length, 0); 91 | }); 92 | 93 | test( 94 | 'Given messages are successfully cleared when clear is called, then no message should exist in database', 95 | () { 96 | //GIVEN 97 | dao.saveMessage(testMessage); 98 | 99 | //WHEN 100 | dao.clear(); 101 | 102 | //THEN 103 | final retrievedMessages = dao.getMessages(); 104 | expect(retrievedMessages.length, 0); 105 | }); 106 | 107 | test( 108 | 'Given messages are successfully cleared when clearAll is called, then retrieving messages should be empty', 109 | () { 110 | //GIVEN 111 | dao.saveMessage(testMessage); 112 | 113 | //WHEN 114 | dao.clearAll(); 115 | 116 | //THEN 117 | expect(dao.getMessages().isEmpty, true); 118 | }); 119 | 120 | test( 121 | 'Given dao is successfully disposed when onDispose is called, then saved message should be null', 122 | () { 123 | //GIVEN 124 | dao.saveMessage(testMessage); 125 | 126 | //WHEN 127 | dao.onDispose(); 128 | 129 | //THEN 130 | final retrievedMessage = dao.getMessage(testMessage.id); 131 | expect(retrievedMessage, null); 132 | }); 133 | 134 | tearDown(() { 135 | dao.clearAll(); 136 | }); 137 | }); 138 | } 139 | -------------------------------------------------------------------------------- /test/data/local/dao/non_persisted_chatwoot_users_dao_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:chatwoot_sdk/data/local/dao/chatwoot_user_dao.dart'; 2 | import 'package:chatwoot_sdk/data/local/entity/chatwoot_user.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | 5 | void main() { 6 | group("Non Persisted Chatwoot Users Dao Test", () { 7 | late NonPersistedChatwootUserDao dao; 8 | final testUser = ChatwootUser( 9 | identifier: "identifier", 10 | identifierHash: "identifierHash", 11 | name: "name", 12 | email: "email", 13 | avatarUrl: "avatarUrl", 14 | customAttributes: {}); 15 | 16 | setUp(() { 17 | dao = NonPersistedChatwootUserDao(); 18 | }); 19 | 20 | test( 21 | 'Given user is successfully deleted when deleteUser is called, then getUser should return null', 22 | () { 23 | //GIVEN 24 | dao.saveUser(testUser); 25 | 26 | //WHEN 27 | dao.deleteUser(); 28 | 29 | //THEN 30 | expect(dao.getUser(), null); 31 | }); 32 | 33 | test( 34 | 'Given user is successfully saved when saveUser is called, then getUser should return saved user', 35 | () { 36 | //WHEN 37 | dao.saveUser(testUser); 38 | 39 | //THEN 40 | expect(dao.getUser(), testUser); 41 | }); 42 | 43 | test( 44 | 'Given user is successfully retrieved when getUser is called, then retrieved user should not be null', 45 | () { 46 | //GIVEN 47 | dao.saveUser(testUser); 48 | 49 | //WHEN 50 | final retrievedUser = dao.getUser(); 51 | 52 | //THEN 53 | expect(retrievedUser, testUser); 54 | }); 55 | 56 | test( 57 | 'Given users are successfully cleared when clearAll is called, then retrieving a user should be null', 58 | () { 59 | //GIVEN 60 | dao.saveUser(testUser); 61 | 62 | //WHEN 63 | dao.clearAll(); 64 | 65 | //THEN 66 | expect(dao.getUser(), null); 67 | }); 68 | 69 | test( 70 | 'Given dao is successfully disposed when onDispose is called, then saved user should be null', 71 | () { 72 | //GIVEN 73 | dao.saveUser(testUser); 74 | 75 | //WHEN 76 | dao.onDispose(); 77 | 78 | //THEN 79 | final retrievedUser = dao.getUser(); 80 | expect(retrievedUser, null); 81 | }); 82 | }); 83 | } 84 | -------------------------------------------------------------------------------- /test/data/local/dao/persisted_chatwoot_contact_dao_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:chatwoot_sdk/data/local/dao/chatwoot_contact_dao.dart'; 4 | import 'package:chatwoot_sdk/data/local/entity/chatwoot_contact.dart'; 5 | import 'package:flutter_test/flutter_test.dart'; 6 | import 'package:hive_flutter/hive_flutter.dart'; 7 | 8 | import '../../../utils/test_resources_util.dart'; 9 | 10 | void main() { 11 | group("Persisted Chatwoot Contact Dao Tests", () { 12 | late PersistedChatwootContactDao dao; 13 | late Box mockClientInstanceKeyToContactBox; 14 | late Box mockContactBox; 15 | final testClientInstanceKey = "testKey"; 16 | 17 | late final ChatwootContact testContact; 18 | 19 | setUpAll(() { 20 | return Future(() async { 21 | testContact = ChatwootContact.fromJson( 22 | await TestResourceUtil.readJsonResource(fileName: "contact")); 23 | final hiveTestPath = Directory.current.path + '/test/hive_testing_path'; 24 | Hive 25 | ..init(hiveTestPath) 26 | ..registerAdapter(ChatwootContactAdapter()); 27 | }); 28 | }); 29 | 30 | setUp(() { 31 | return Future(() async { 32 | mockContactBox = 33 | await Hive.openBox(ChatwootContactBoxNames.CONTACTS.toString()); 34 | mockClientInstanceKeyToContactBox = await Hive.openBox( 35 | ChatwootContactBoxNames.CLIENT_INSTANCE_TO_CONTACTS.toString()); 36 | 37 | dao = PersistedChatwootContactDao(mockContactBox, 38 | mockClientInstanceKeyToContactBox, testClientInstanceKey); 39 | }); 40 | }); 41 | 42 | test( 43 | 'Given contact is successfully deleted when deleteContact is called, then getContact should return null', 44 | () async { 45 | //GIVEN 46 | await dao.saveContact(testContact); 47 | 48 | //WHEN 49 | await dao.deleteContact(); 50 | 51 | //THEN 52 | expect(dao.getContact(), null); 53 | }); 54 | 55 | test( 56 | 'Given contact is successfully save when saveContact is called, then getContact should return saved contact', 57 | () async { 58 | //WHEN 59 | await dao.saveContact(testContact); 60 | 61 | //THEN 62 | expect(dao.getContact(), testContact); 63 | }); 64 | 65 | test( 66 | 'Given contact is successfully retrieved when getContact is called, then retrieved contact should not be null', 67 | () async { 68 | //GIVEN 69 | await dao.saveContact(testContact); 70 | 71 | //WHEN 72 | final retrievedContact = dao.getContact(); 73 | 74 | //THEN 75 | expect(retrievedContact, testContact); 76 | }); 77 | 78 | test( 79 | 'Given contacts are successfully cleared when clearAll is called, then retrieved contact should be null', 80 | () async { 81 | //GIVEN 82 | await dao.saveContact(testContact); 83 | 84 | //WHEN 85 | await dao.clearAll(); 86 | 87 | //THEN 88 | expect(dao.getContact(), null); 89 | }); 90 | 91 | tearDown(() { 92 | return Future(() async { 93 | try { 94 | await mockContactBox.clear(); 95 | await mockClientInstanceKeyToContactBox.clear(); 96 | } on HiveError catch (e) { 97 | print(e); 98 | } 99 | }); 100 | }); 101 | 102 | tearDownAll(() { 103 | return Future(() async { 104 | await mockContactBox.close(); 105 | await mockClientInstanceKeyToContactBox.close(); 106 | }); 107 | }); 108 | }); 109 | } 110 | -------------------------------------------------------------------------------- /test/data/local/dao/persisted_chatwoot_conversation_dao_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:chatwoot_sdk/data/local/dao/chatwoot_conversation_dao.dart'; 4 | import 'package:chatwoot_sdk/data/local/entity/chatwoot_contact.dart'; 5 | import 'package:chatwoot_sdk/data/local/entity/chatwoot_conversation.dart'; 6 | import 'package:chatwoot_sdk/data/local/entity/chatwoot_message.dart'; 7 | import 'package:flutter_test/flutter_test.dart'; 8 | import 'package:hive_flutter/hive_flutter.dart'; 9 | 10 | import '../../../utils/test_resources_util.dart'; 11 | 12 | void main() { 13 | group("Persisted Chatwoot Conversation Dao Tests", () { 14 | late PersistedChatwootConversationDao dao; 15 | late Box mockClientInstanceKeyToConversationBox; 16 | late Box mockConversationBox; 17 | final testClientInstanceKey = "testKey"; 18 | 19 | late final ChatwootConversation testConversation; 20 | 21 | setUpAll(() { 22 | return Future(() async { 23 | testConversation = ChatwootConversation.fromJson( 24 | await TestResourceUtil.readJsonResource(fileName: "conversation")); 25 | 26 | final hiveTestPath = Directory.current.path + '/test/hive_testing_path'; 27 | Hive 28 | ..init(hiveTestPath) 29 | ..registerAdapter(ChatwootConversationAdapter()) 30 | ..registerAdapter(ChatwootContactAdapter()) 31 | ..registerAdapter(ChatwootMessageAdapter()); 32 | }); 33 | }); 34 | 35 | setUp(() { 36 | return Future(() async { 37 | mockConversationBox = await Hive.openBox( 38 | ChatwootConversationBoxNames.CONVERSATIONS.toString()); 39 | mockClientInstanceKeyToConversationBox = await Hive.openBox( 40 | ChatwootConversationBoxNames.CLIENT_INSTANCE_TO_CONVERSATIONS 41 | .toString()); 42 | 43 | dao = PersistedChatwootConversationDao(mockConversationBox, 44 | mockClientInstanceKeyToConversationBox, testClientInstanceKey); 45 | }); 46 | }); 47 | 48 | test( 49 | 'Given conversation is successfully deleted when deleteConversation is called, then getConversation should return null', 50 | () async { 51 | //GIVEN 52 | await dao.saveConversation(testConversation); 53 | 54 | //WHEN 55 | await dao.deleteConversation(); 56 | 57 | //THEN 58 | expect(dao.getConversation(), null); 59 | }); 60 | 61 | test( 62 | 'Given conversation is successfully save when saveConversation is called, then getConversation should return saved conversation', 63 | () async { 64 | //WHEN 65 | await dao.saveConversation(testConversation); 66 | 67 | //THEN 68 | expect(dao.getConversation(), testConversation); 69 | }); 70 | 71 | test( 72 | 'Given conversation is successfully retrieved when getConversation is called, then retrieved conversation should not be null', 73 | () async { 74 | //GIVEN 75 | await dao.saveConversation(testConversation); 76 | 77 | //WHEN 78 | final retrievedConversation = dao.getConversation(); 79 | 80 | //THEN 81 | expect(retrievedConversation, testConversation); 82 | }); 83 | 84 | test( 85 | 'Given conversations are successfully cleared when clearAll is called, then retrieving a conversation should be null', 86 | () async { 87 | //GIVEN 88 | await dao.saveConversation(testConversation); 89 | 90 | //WHEN 91 | await dao.clearAll(); 92 | 93 | //THEN 94 | expect(dao.getConversation(), null); 95 | }); 96 | 97 | tearDown(() { 98 | return Future(() async { 99 | try { 100 | await mockConversationBox.clear(); 101 | await mockClientInstanceKeyToConversationBox.clear(); 102 | } on HiveError catch (e) { 103 | print(e); 104 | } 105 | }); 106 | }); 107 | 108 | tearDownAll(() { 109 | return Future(() async { 110 | await mockConversationBox.close(); 111 | await mockClientInstanceKeyToConversationBox.close(); 112 | }); 113 | }); 114 | }); 115 | } 116 | -------------------------------------------------------------------------------- /test/data/local/dao/persisted_chatwoot_messages_dao_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:chatwoot_sdk/data/local/dao/chatwoot_messages_dao.dart'; 4 | import 'package:chatwoot_sdk/data/local/entity/chatwoot_message.dart'; 5 | import 'package:chatwoot_sdk/data/remote/responses/chatwoot_event.dart'; 6 | import 'package:flutter_test/flutter_test.dart'; 7 | import 'package:hive_flutter/hive_flutter.dart'; 8 | 9 | import '../../../utils/test_resources_util.dart'; 10 | 11 | void main() { 12 | group("Persisted Chatwoot Message Dao Tests", () { 13 | late PersistedChatwootMessagesDao dao; 14 | late Box mockClientInstanceKeyToMessageBox; 15 | late Box mockMessageBox; 16 | final testClientInstanceKey = "testKey"; 17 | 18 | late final ChatwootMessage testMessage; 19 | 20 | setUpAll(() { 21 | return Future(() async { 22 | final hiveTestPath = Directory.current.path + '/test/hive_testing_path'; 23 | Hive 24 | ..init(hiveTestPath) 25 | ..registerAdapter(ChatwootMessageAdapter()) 26 | ..registerAdapter(ChatwootEventMessageUserAdapter()); 27 | 28 | testMessage = ChatwootMessage.fromJson( 29 | await TestResourceUtil.readJsonResource(fileName: "message")); 30 | }); 31 | }); 32 | 33 | setUp(() { 34 | return Future(() async { 35 | mockMessageBox = 36 | await Hive.openBox(ChatwootMessagesBoxNames.MESSAGES.toString()); 37 | mockClientInstanceKeyToMessageBox = await Hive.openBox( 38 | ChatwootMessagesBoxNames.MESSAGES_TO_CLIENT_INSTANCE_KEY 39 | .toString()); 40 | 41 | dao = PersistedChatwootMessagesDao(mockMessageBox, 42 | mockClientInstanceKeyToMessageBox, testClientInstanceKey); 43 | }); 44 | }); 45 | 46 | test( 47 | 'Given message is successfully deleted when deleteMessage is called, then getMessage should return null', 48 | () async { 49 | //GIVEN 50 | await dao.saveMessage(testMessage); 51 | 52 | //WHEN 53 | await dao.deleteMessage(testMessage.id); 54 | 55 | //THEN 56 | expect(dao.getMessage(testMessage.id), null); 57 | }); 58 | 59 | test( 60 | 'Given message is successfully save when saveMessage is called, then getMessage should return saved message', 61 | () async { 62 | //WHEN 63 | await dao.saveMessage(testMessage); 64 | 65 | //THEN 66 | expect(dao.getMessage(testMessage.id), testMessage); 67 | }); 68 | 69 | test( 70 | 'Given messages are successfully saved when saveMessages is called, then getMessages should return saved messages', 71 | () async { 72 | final messages = [testMessage]; 73 | 74 | //WHEN 75 | await dao.saveAllMessages(messages); 76 | 77 | //THEN 78 | expect(dao.getMessages(), messages); 79 | }); 80 | 81 | test( 82 | 'Given message is successfully retrieved when getMessage is called, then retrieved message should not be null', 83 | () async { 84 | //GIVEN 85 | await dao.saveMessage(testMessage); 86 | 87 | //WHEN 88 | final retrievedMessage = dao.getMessage(testMessage.id); 89 | 90 | //THEN 91 | expect(retrievedMessage, testMessage); 92 | }); 93 | 94 | test( 95 | 'Given messages exist in database when getMessages is called, then retrieved messages should not be empty', 96 | () async { 97 | //GIVEN 98 | await dao.saveMessage(testMessage); 99 | 100 | //WHEN 101 | final retrievedMessages = dao.getMessages(); 102 | 103 | //THEN 104 | expect(retrievedMessages.length, 1); 105 | expect(retrievedMessages[0], testMessage); 106 | }); 107 | 108 | test( 109 | 'Given messages do not exist in database when getMessages is called, then retrieved messages should be empty', 110 | () async { 111 | //GIVEN 112 | await dao.clear(); 113 | 114 | //WHEN 115 | final retrievedMessages = dao.getMessages(); 116 | 117 | //THEN 118 | expect(retrievedMessages.length, 0); 119 | }); 120 | 121 | test( 122 | 'Given messages are successfully cleared when clear is called, then no message should exist in database', 123 | () async { 124 | //GIVEN 125 | await dao.saveMessage(testMessage); 126 | 127 | //WHEN 128 | await dao.clear(); 129 | 130 | //THEN 131 | final retrievedMessages = dao.getMessages(); 132 | expect(retrievedMessages.length, 0); 133 | }); 134 | 135 | test( 136 | 'Given messages are successfully cleared when clearAll is called, then retrieving messages should be empty', 137 | () async { 138 | //GIVEN 139 | await dao.saveMessage(testMessage); 140 | 141 | //WHEN 142 | await dao.clearAll(); 143 | 144 | //THEN 145 | expect(dao.getMessages().isEmpty, true); 146 | }); 147 | 148 | tearDown(() { 149 | return Future(() async { 150 | try { 151 | await mockMessageBox.clear(); 152 | await mockClientInstanceKeyToMessageBox.clear(); 153 | } on HiveError catch (e) { 154 | print(e); 155 | } 156 | }); 157 | }); 158 | 159 | tearDownAll(() { 160 | return Future(() async { 161 | await mockMessageBox.close(); 162 | await mockClientInstanceKeyToMessageBox.close(); 163 | }); 164 | }); 165 | }); 166 | } 167 | -------------------------------------------------------------------------------- /test/data/local/dao/persisted_chatwoot_users_dao_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:chatwoot_sdk/data/local/dao/chatwoot_user_dao.dart'; 4 | import 'package:chatwoot_sdk/data/local/entity/chatwoot_user.dart'; 5 | import 'package:flutter_test/flutter_test.dart'; 6 | import 'package:hive_flutter/hive_flutter.dart'; 7 | 8 | void main() { 9 | group("Persisted Chatwoot User Dao Tests", () { 10 | late PersistedChatwootUserDao dao; 11 | late Box mockClientInstanceKeyToUserBox; 12 | late Box mockUserBox; 13 | final testClientInstanceKey = "testKey"; 14 | 15 | final testUser = ChatwootUser( 16 | identifier: "identifier", 17 | identifierHash: "identifierHash", 18 | name: "name", 19 | email: "email", 20 | avatarUrl: "avatarUrl", 21 | customAttributes: {}); 22 | 23 | setUpAll(() { 24 | return Future(() async { 25 | final hiveTestPath = Directory.current.path + '/test/hive_testing_path'; 26 | Hive 27 | ..init(hiveTestPath) 28 | ..registerAdapter(ChatwootUserAdapter()); 29 | }); 30 | }); 31 | 32 | setUp(() { 33 | return Future(() async { 34 | mockUserBox = await Hive.openBox(ChatwootUserBoxNames.USERS.toString()); 35 | mockClientInstanceKeyToUserBox = await Hive.openBox( 36 | ChatwootUserBoxNames.CLIENT_INSTANCE_TO_USER.toString()); 37 | 38 | dao = PersistedChatwootUserDao( 39 | mockUserBox, mockClientInstanceKeyToUserBox, testClientInstanceKey); 40 | }); 41 | }); 42 | 43 | test( 44 | 'Given user is successfully deleted when deleteUser is called, then getUser should return null', 45 | () async { 46 | //GIVEN 47 | await dao.saveUser(testUser); 48 | 49 | //WHEN 50 | await dao.deleteUser(); 51 | 52 | //THEN 53 | expect(dao.getUser(), null); 54 | }); 55 | 56 | test( 57 | 'Given user is successfully save when saveUser is called, then getUser should return saved user', 58 | () async { 59 | //WHEN 60 | await dao.saveUser(testUser); 61 | 62 | //THEN 63 | expect(dao.getUser(), testUser); 64 | }); 65 | 66 | test( 67 | 'Given user is successfully retrieved when getUser is called, then retrieved user should not be null', 68 | () async { 69 | //GIVEN 70 | await dao.saveUser(testUser); 71 | 72 | //WHEN 73 | final retrievedUser = dao.getUser(); 74 | 75 | //THEN 76 | expect(retrievedUser, testUser); 77 | }); 78 | 79 | test( 80 | 'Given users are successfully cleared when clearAll is called, then retrieving a user should be null', 81 | () async { 82 | //GIVEN 83 | await dao.saveUser(testUser); 84 | 85 | //WHEN 86 | await dao.clearAll(); 87 | 88 | //THEN 89 | expect(dao.getUser(), null); 90 | }); 91 | 92 | tearDown(() { 93 | return Future(() async { 94 | try { 95 | await mockUserBox.clear(); 96 | await mockClientInstanceKeyToUserBox.clear(); 97 | } on HiveError catch (e) { 98 | print(e); 99 | } 100 | }); 101 | }); 102 | 103 | tearDownAll(() { 104 | return Future(() async { 105 | await mockUserBox.close(); 106 | await mockClientInstanceKeyToUserBox.close(); 107 | }); 108 | }); 109 | }); 110 | } 111 | -------------------------------------------------------------------------------- /test/data/local/local_storage_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:chatwoot_sdk/data/local/dao/chatwoot_contact_dao.dart'; 4 | import 'package:chatwoot_sdk/data/local/dao/chatwoot_conversation_dao.dart'; 5 | import 'package:chatwoot_sdk/data/local/dao/chatwoot_messages_dao.dart'; 6 | import 'package:chatwoot_sdk/data/local/dao/chatwoot_user_dao.dart'; 7 | import 'package:chatwoot_sdk/data/local/entity/chatwoot_contact.dart'; 8 | import 'package:chatwoot_sdk/data/local/entity/chatwoot_conversation.dart'; 9 | import 'package:chatwoot_sdk/data/local/entity/chatwoot_message.dart'; 10 | import 'package:chatwoot_sdk/data/local/entity/chatwoot_user.dart'; 11 | import 'package:chatwoot_sdk/data/local/local_storage.dart'; 12 | import 'package:flutter_test/flutter_test.dart'; 13 | import 'package:hive_flutter/hive_flutter.dart'; 14 | import 'package:mockito/annotations.dart'; 15 | import 'package:mockito/mockito.dart'; 16 | 17 | import 'local_storage_test.mocks.dart'; 18 | 19 | @GenerateMocks([ 20 | ChatwootConversationDao, 21 | ChatwootContactDao, 22 | ChatwootMessagesDao, 23 | PersistedChatwootConversationDao, 24 | PersistedChatwootContactDao, 25 | PersistedChatwootMessagesDao, 26 | ChatwootUserDao, 27 | PersistedChatwootUserDao 28 | ]) 29 | void main() { 30 | group("Local Storage Tests", () { 31 | final mockContactDao = MockChatwootContactDao(); 32 | final mockConversationDao = MockChatwootConversationDao(); 33 | final mockUserDao = MockChatwootUserDao(); 34 | final mockMessagesDao = MockChatwootMessagesDao(); 35 | 36 | late final LocalStorage localStorage; 37 | 38 | setUpAll(() { 39 | final hiveTestPath = Directory.current.path + '/test/hive_testing_path'; 40 | 41 | Hive 42 | ..init(hiveTestPath) 43 | ..registerAdapter(ChatwootContactAdapter()) 44 | ..registerAdapter(ChatwootConversationAdapter()) 45 | ..registerAdapter(ChatwootMessageAdapter()) 46 | ..registerAdapter(ChatwootUserAdapter()); 47 | 48 | localStorage = LocalStorage( 49 | userDao: mockUserDao, 50 | conversationDao: mockConversationDao, 51 | contactDao: mockContactDao, 52 | messagesDao: mockMessagesDao); 53 | }); 54 | 55 | test( 56 | 'Given persisted db is successfully opened when openDB is called, then all hive boxes should be open', 57 | () async { 58 | //WHEN 59 | await LocalStorage.openDB(onInitializeHive: () {}); 60 | 61 | //THEN 62 | expect(true, Hive.isBoxOpen(ChatwootContactBoxNames.CONTACTS.toString())); 63 | expect( 64 | true, 65 | Hive.isBoxOpen( 66 | ChatwootContactBoxNames.CLIENT_INSTANCE_TO_CONTACTS.toString())); 67 | expect( 68 | true, 69 | Hive.isBoxOpen( 70 | ChatwootConversationBoxNames.CONVERSATIONS.toString())); 71 | expect( 72 | true, 73 | Hive.isBoxOpen(ChatwootConversationBoxNames 74 | .CLIENT_INSTANCE_TO_CONVERSATIONS 75 | .toString())); 76 | expect( 77 | true, Hive.isBoxOpen(ChatwootMessagesBoxNames.MESSAGES.toString())); 78 | expect( 79 | true, 80 | Hive.isBoxOpen(ChatwootMessagesBoxNames 81 | .MESSAGES_TO_CLIENT_INSTANCE_KEY 82 | .toString())); 83 | expect(true, Hive.isBoxOpen(ChatwootUserBoxNames.USERS.toString())); 84 | expect(true, Hive.isBoxOpen(ChatwootUserBoxNames.USERS.toString())); 85 | }); 86 | 87 | test( 88 | 'Given localStorage is successfully cleared when clear is called, then daos should be cleared', 89 | () async { 90 | //WHEN 91 | await localStorage.clear(clearChatwootUserStorage: true); 92 | 93 | //THEN 94 | verify(mockContactDao.deleteContact()); 95 | verify(mockConversationDao.deleteConversation()); 96 | verify(mockMessagesDao.clear()); 97 | verify(mockUserDao.deleteUser()); 98 | }); 99 | 100 | test( 101 | 'Given localStorage is successfully cleared except user db when clear is called, then daos should be cleared except user db', 102 | () async { 103 | //WHEN 104 | await localStorage.clear(clearChatwootUserStorage: false); 105 | 106 | //THEN 107 | verifyNever(mockContactDao.deleteContact()); 108 | verify(mockConversationDao.deleteConversation()); 109 | verify(mockMessagesDao.clear()); 110 | verifyNever(mockUserDao.deleteUser()); 111 | }); 112 | 113 | test( 114 | 'Given all data is successfully cleared when clearAll is called, then all data daos should be cleared', 115 | () async { 116 | //WHEN 117 | await localStorage.clearAll(); 118 | 119 | //THEN 120 | verify(mockContactDao.clearAll()); 121 | verify(mockConversationDao.clearAll()); 122 | verify(mockMessagesDao.clearAll()); 123 | verify(mockUserDao.clearAll()); 124 | }); 125 | 126 | test( 127 | 'Given localStorage is successfully disposed when dispose is called, then all daos should be disposed', 128 | () { 129 | //WHEN 130 | localStorage.dispose(); 131 | 132 | //THEN 133 | verify(mockContactDao.onDispose()); 134 | verify(mockConversationDao.onDispose()); 135 | verify(mockMessagesDao.onDispose()); 136 | verify(mockUserDao.onDispose()); 137 | }); 138 | 139 | tearDownAll(() async { 140 | await Hive.close(); 141 | }); 142 | }); 143 | } 144 | -------------------------------------------------------------------------------- /test/data/remote/chatwoot_client_api_interceptor_test.mocks.dart: -------------------------------------------------------------------------------- 1 | // Mocks generated by Mockito 5.0.15 from annotations 2 | // in chatwoot_sdk/test/data/remote/chatwoot_client_api_interceptor_test.dart. 3 | // Do not manually edit this file. 4 | 5 | import 'dart:async' as _i7; 6 | 7 | import 'package:chatwoot_sdk/data/local/entity/chatwoot_contact.dart' as _i4; 8 | import 'package:chatwoot_sdk/data/local/entity/chatwoot_conversation.dart' 9 | as _i5; 10 | import 'package:chatwoot_sdk/data/local/entity/chatwoot_user.dart' as _i13; 11 | import 'package:chatwoot_sdk/data/remote/service/chatwoot_client_auth_service.dart' 12 | as _i11; 13 | import 'package:dio/dio.dart' as _i6; 14 | import 'package:dio/src/dio.dart' as _i3; 15 | import 'package:dio/src/dio_error.dart' as _i9; 16 | import 'package:dio/src/dio_mixin.dart' as _i2; 17 | import 'package:dio/src/options.dart' as _i10; 18 | import 'package:dio/src/response.dart' as _i8; 19 | import 'package:mockito/mockito.dart' as _i1; 20 | import 'package:web_socket_channel/web_socket_channel.dart' as _i12; 21 | 22 | // ignore_for_file: avoid_redundant_argument_values 23 | // ignore_for_file: avoid_setters_without_getters 24 | // ignore_for_file: comment_references 25 | // ignore_for_file: implementation_imports 26 | // ignore_for_file: invalid_use_of_visible_for_testing_member 27 | // ignore_for_file: prefer_const_constructors 28 | // ignore_for_file: unnecessary_parenthesis 29 | 30 | class _FakeInterceptorState_0 extends _i1.Fake 31 | implements _i2.InterceptorState {} 32 | 33 | class _FakeDio_1 extends _i1.Fake implements _i3.Dio {} 34 | 35 | class _FakeChatwootContact_2 extends _i1.Fake implements _i4.ChatwootContact {} 36 | 37 | class _FakeChatwootConversation_3 extends _i1.Fake 38 | implements _i5.ChatwootConversation {} 39 | 40 | /// A class which mocks [ResponseInterceptorHandler]. 41 | /// 42 | /// See the documentation for Mockito's code generation for more information. 43 | class MockResponseInterceptorHandler extends _i1.Mock 44 | implements _i6.ResponseInterceptorHandler { 45 | MockResponseInterceptorHandler() { 46 | _i1.throwOnMissingStub(this); 47 | } 48 | 49 | @override 50 | _i7.Future<_i2.InterceptorState> get future => 51 | (super.noSuchMethod(Invocation.getter(#future), 52 | returnValue: Future<_i2.InterceptorState>.value( 53 | _FakeInterceptorState_0())) 54 | as _i7.Future<_i2.InterceptorState>); 55 | @override 56 | bool get isCompleted => 57 | (super.noSuchMethod(Invocation.getter(#isCompleted), returnValue: false) 58 | as bool); 59 | @override 60 | void next(_i8.Response? response) => 61 | super.noSuchMethod(Invocation.method(#next, [response]), 62 | returnValueForMissingStub: null); 63 | @override 64 | void resolve(_i8.Response? response) => 65 | super.noSuchMethod(Invocation.method(#resolve, [response]), 66 | returnValueForMissingStub: null); 67 | @override 68 | void reject(_i9.DioError? error, 69 | [bool? callFollowingErrorInterceptor = false]) => 70 | super.noSuchMethod( 71 | Invocation.method(#reject, [error, callFollowingErrorInterceptor]), 72 | returnValueForMissingStub: null); 73 | @override 74 | String toString() => super.toString(); 75 | } 76 | 77 | /// A class which mocks [RequestInterceptorHandler]. 78 | /// 79 | /// See the documentation for Mockito's code generation for more information. 80 | class MockRequestInterceptorHandler extends _i1.Mock 81 | implements _i6.RequestInterceptorHandler { 82 | MockRequestInterceptorHandler() { 83 | _i1.throwOnMissingStub(this); 84 | } 85 | 86 | @override 87 | _i7.Future<_i2.InterceptorState> get future => 88 | (super.noSuchMethod(Invocation.getter(#future), 89 | returnValue: Future<_i2.InterceptorState>.value( 90 | _FakeInterceptorState_0())) 91 | as _i7.Future<_i2.InterceptorState>); 92 | @override 93 | bool get isCompleted => 94 | (super.noSuchMethod(Invocation.getter(#isCompleted), returnValue: false) 95 | as bool); 96 | @override 97 | void next(_i10.RequestOptions? requestOptions) => 98 | super.noSuchMethod(Invocation.method(#next, [requestOptions]), 99 | returnValueForMissingStub: null); 100 | @override 101 | void resolve(_i8.Response? response, 102 | [bool? callFollowingResponseInterceptor = false]) => 103 | super.noSuchMethod( 104 | Invocation.method( 105 | #resolve, [response, callFollowingResponseInterceptor]), 106 | returnValueForMissingStub: null); 107 | @override 108 | void reject(_i9.DioError? error, 109 | [bool? callFollowingErrorInterceptor = false]) => 110 | super.noSuchMethod( 111 | Invocation.method(#reject, [error, callFollowingErrorInterceptor]), 112 | returnValueForMissingStub: null); 113 | @override 114 | String toString() => super.toString(); 115 | } 116 | 117 | /// A class which mocks [ChatwootClientAuthService]. 118 | /// 119 | /// See the documentation for Mockito's code generation for more information. 120 | class MockChatwootClientAuthService extends _i1.Mock 121 | implements _i11.ChatwootClientAuthService { 122 | MockChatwootClientAuthService() { 123 | _i1.throwOnMissingStub(this); 124 | } 125 | 126 | @override 127 | set connection(_i12.WebSocketChannel? _connection) => 128 | super.noSuchMethod(Invocation.setter(#connection, _connection), 129 | returnValueForMissingStub: null); 130 | @override 131 | _i3.Dio get dio => 132 | (super.noSuchMethod(Invocation.getter(#dio), returnValue: _FakeDio_1()) 133 | as _i3.Dio); 134 | @override 135 | _i7.Future<_i4.ChatwootContact> createNewContact( 136 | String? inboxIdentifier, _i13.ChatwootUser? user) => 137 | (super.noSuchMethod( 138 | Invocation.method(#createNewContact, [inboxIdentifier, user]), 139 | returnValue: 140 | Future<_i4.ChatwootContact>.value(_FakeChatwootContact_2())) 141 | as _i7.Future<_i4.ChatwootContact>); 142 | @override 143 | _i7.Future<_i5.ChatwootConversation> createNewConversation( 144 | String? inboxIdentifier, String? contactIdentifier) => 145 | (super.noSuchMethod( 146 | Invocation.method( 147 | #createNewConversation, [inboxIdentifier, contactIdentifier]), 148 | returnValue: Future<_i5.ChatwootConversation>.value( 149 | _FakeChatwootConversation_3())) 150 | as _i7.Future<_i5.ChatwootConversation>); 151 | @override 152 | String toString() => super.toString(); 153 | } 154 | -------------------------------------------------------------------------------- /test/data/remote/chatwoot_client_auth_service_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:chatwoot_sdk/data/local/entity/chatwoot_contact.dart'; 2 | import 'package:chatwoot_sdk/data/local/entity/chatwoot_conversation.dart'; 3 | import 'package:chatwoot_sdk/data/local/entity/chatwoot_user.dart'; 4 | import 'package:chatwoot_sdk/data/remote/chatwoot_client_exception.dart'; 5 | import 'package:chatwoot_sdk/data/remote/service/chatwoot_client_auth_service.dart'; 6 | import 'package:dio/dio.dart'; 7 | import 'package:flutter_test/flutter_test.dart'; 8 | import 'package:mockito/annotations.dart'; 9 | import 'package:mockito/mockito.dart'; 10 | 11 | import '../../utils/test_resources_util.dart'; 12 | import 'chatwoot_client_service_test.mocks.dart'; 13 | 14 | @GenerateMocks([Dio]) 15 | void main() { 16 | group("Client Auth Service Tests", () { 17 | late final ChatwootClientAuthService clientService; 18 | final mockDio = MockDio(); 19 | final testInboxIdentifier = "inboxIdentifier"; 20 | final testContactIdentifier = "contactIdentifier"; 21 | final testUser = ChatwootUser( 22 | identifier: "identifier", 23 | identifierHash: "identifierHash", 24 | name: "name", 25 | email: "email", 26 | avatarUrl: "avatarUrl", 27 | customAttributes: {}); 28 | 29 | setUpAll(() { 30 | clientService = ChatwootClientAuthServiceImpl(dio: mockDio); 31 | }); 32 | 33 | _createSuccessResponse(body) { 34 | return Response( 35 | data: body, 36 | statusCode: 200, 37 | requestOptions: RequestOptions(path: "")); 38 | } 39 | 40 | _createErrorResponse({required int statusCode, body}) { 41 | return Response( 42 | data: body, 43 | statusCode: statusCode, 44 | requestOptions: RequestOptions(path: "")); 45 | } 46 | 47 | test( 48 | 'Given contact is successfully created when createNewContact is called, then return created contact', 49 | () async { 50 | //GIVEN 51 | final responseBody = 52 | await TestResourceUtil.readJsonResource(fileName: "contact"); 53 | when(mockDio.post(any, data: testUser.toJson())).thenAnswer( 54 | (_) => Future.value(_createSuccessResponse(responseBody))); 55 | 56 | //WHEN 57 | final result = 58 | await clientService.createNewContact(testInboxIdentifier, testUser); 59 | 60 | //THEN 61 | expect(result, ChatwootContact.fromJson(responseBody)); 62 | }); 63 | 64 | test( 65 | 'Given contact creation returns with error response when createNewContact is called, then throw error', 66 | () async { 67 | //GIVEN 68 | when(mockDio.post(any, data: testUser.toJson())).thenAnswer( 69 | (_) => Future.value(_createErrorResponse(statusCode: 401, body: {}))); 70 | 71 | //WHEN 72 | ChatwootClientException? chatwootClientException; 73 | try { 74 | await clientService.createNewContact(testInboxIdentifier, testUser); 75 | } on ChatwootClientException catch (e) { 76 | chatwootClientException = e; 77 | } 78 | 79 | //THEN 80 | expect(chatwootClientException, isNotNull); 81 | expect(chatwootClientException!.type, 82 | equals(ChatwootClientExceptionType.CREATE_CONTACT_FAILED)); 83 | }); 84 | 85 | test( 86 | 'Given contact creation fails when createNewContact is called, then throw error', 87 | () async { 88 | //GIVEN 89 | final testError = DioError(requestOptions: RequestOptions(path: "")); 90 | when(mockDio.post(any, data: testUser.toJson())).thenThrow(testError); 91 | 92 | //WHEN 93 | ChatwootClientException? chatwootClientException; 94 | try { 95 | await clientService.createNewContact(testInboxIdentifier, testUser); 96 | } on ChatwootClientException catch (e) { 97 | chatwootClientException = e; 98 | } 99 | 100 | //THEN 101 | expect(chatwootClientException, isNotNull); 102 | expect(chatwootClientException!.type, 103 | equals(ChatwootClientExceptionType.CREATE_CONTACT_FAILED)); 104 | }); 105 | 106 | test( 107 | 'Given conversation is successfully created when createNewConversation is called, then return created conversation', 108 | () async { 109 | //GIVEN 110 | final responseBody = 111 | await TestResourceUtil.readJsonResource(fileName: "conversation"); 112 | when(mockDio.post(any)).thenAnswer( 113 | (_) => Future.value(_createSuccessResponse(responseBody))); 114 | 115 | //WHEN 116 | final result = await clientService.createNewConversation( 117 | testInboxIdentifier, testContactIdentifier); 118 | 119 | //THEN 120 | expect(result, ChatwootConversation.fromJson(responseBody)); 121 | }); 122 | 123 | test( 124 | 'Given conversation creation returns with error response when createNewConversation is called, then throw error', 125 | () async { 126 | //GIVEN 127 | when(mockDio.post(any)).thenAnswer( 128 | (_) => Future.value(_createErrorResponse(statusCode: 401, body: {}))); 129 | 130 | //WHEN 131 | ChatwootClientException? chatwootClientException; 132 | try { 133 | await clientService.createNewConversation( 134 | testInboxIdentifier, testContactIdentifier); 135 | } on ChatwootClientException catch (e) { 136 | chatwootClientException = e; 137 | } 138 | 139 | //THEN 140 | expect(chatwootClientException, isNotNull); 141 | expect(chatwootClientException!.type, 142 | equals(ChatwootClientExceptionType.CREATE_CONVERSATION_FAILED)); 143 | }); 144 | 145 | test( 146 | 'Given conversation creation fails when createNewConversation is called, then throw error', 147 | () async { 148 | //GIVEN 149 | final testError = DioError(requestOptions: RequestOptions(path: "")); 150 | when(mockDio.post(any)).thenThrow(testError); 151 | 152 | //WHEN 153 | ChatwootClientException? chatwootClientException; 154 | try { 155 | await clientService.createNewConversation( 156 | testInboxIdentifier, testContactIdentifier); 157 | } on ChatwootClientException catch (e) { 158 | chatwootClientException = e; 159 | } 160 | 161 | //THEN 162 | expect(chatwootClientException, isNotNull); 163 | expect(chatwootClientException!.type, 164 | equals(ChatwootClientExceptionType.CREATE_CONVERSATION_FAILED)); 165 | }); 166 | }); 167 | } 168 | -------------------------------------------------------------------------------- /test/hive_testing_path/chatwootcontactboxnames.client_instance_to_contacts.hive: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatwoot/chatwoot-flutter-sdk/0c59ef3d272dfa32c70dd64a183aa3cf55c84eb0/test/hive_testing_path/chatwootcontactboxnames.client_instance_to_contacts.hive -------------------------------------------------------------------------------- /test/hive_testing_path/chatwootcontactboxnames.contacts.hive: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatwoot/chatwoot-flutter-sdk/0c59ef3d272dfa32c70dd64a183aa3cf55c84eb0/test/hive_testing_path/chatwootcontactboxnames.contacts.hive -------------------------------------------------------------------------------- /test/hive_testing_path/chatwootconversationboxnames.client_instance_to_conversations.hive: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatwoot/chatwoot-flutter-sdk/0c59ef3d272dfa32c70dd64a183aa3cf55c84eb0/test/hive_testing_path/chatwootconversationboxnames.client_instance_to_conversations.hive -------------------------------------------------------------------------------- /test/hive_testing_path/chatwootconversationboxnames.conversations.hive: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatwoot/chatwoot-flutter-sdk/0c59ef3d272dfa32c70dd64a183aa3cf55c84eb0/test/hive_testing_path/chatwootconversationboxnames.conversations.hive -------------------------------------------------------------------------------- /test/hive_testing_path/chatwootmessagesboxnames.messages.hive: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatwoot/chatwoot-flutter-sdk/0c59ef3d272dfa32c70dd64a183aa3cf55c84eb0/test/hive_testing_path/chatwootmessagesboxnames.messages.hive -------------------------------------------------------------------------------- /test/hive_testing_path/chatwootmessagesboxnames.messages_to_client_instance_key.hive: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatwoot/chatwoot-flutter-sdk/0c59ef3d272dfa32c70dd64a183aa3cf55c84eb0/test/hive_testing_path/chatwootmessagesboxnames.messages_to_client_instance_key.hive -------------------------------------------------------------------------------- /test/hive_testing_path/chatwootuserboxnames.client_instance_to_user.hive: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatwoot/chatwoot-flutter-sdk/0c59ef3d272dfa32c70dd64a183aa3cf55c84eb0/test/hive_testing_path/chatwootuserboxnames.client_instance_to_user.hive -------------------------------------------------------------------------------- /test/hive_testing_path/chatwootuserboxnames.users.hive: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatwoot/chatwoot-flutter-sdk/0c59ef3d272dfa32c70dd64a183aa3cf55c84eb0/test/hive_testing_path/chatwootuserboxnames.users.hive -------------------------------------------------------------------------------- /test/resources/contact.json: -------------------------------------------------------------------------------- 1 | { 2 | "source_id": "bab6c264-901b-4821-bdc4-e6493ca84289", 3 | "id": 1, 4 | "name": "test", 5 | "email": "test@test.com", 6 | "pubsub_token": "vxrAesh9TjFkEeQbSTyyBNYY" 7 | } -------------------------------------------------------------------------------- /test/resources/conversation.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 7, 3 | "inbox_id": 1, 4 | "contact_last_seen_at": 0, 5 | "status": "open", 6 | "messages": [], 7 | "contact": { 8 | "id": 1, 9 | "name": "test", 10 | "email": "test@test.com", 11 | "phone_number": null, 12 | "account_id": 5, 13 | "created_at": "2021-07-12T16:09:34.854Z", 14 | "updated_at": "2021-07-12T16:34:14.048Z", 15 | "pubsub_token": "vxrAesh9TjFkEeQbSTyyBNYY", 16 | "additional_attributes": {}, 17 | "identifier": null, 18 | "custom_attributes": {}, 19 | "last_activity_at": "2021-07-12T16:34:13.924Z", 20 | "label_list": [] 21 | } 22 | } -------------------------------------------------------------------------------- /test/resources/conversations.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 2, 4 | "inbox_id": 1, 5 | "contact_last_seen_at": 0, 6 | "status": "open", 7 | "messages": [ 8 | { 9 | "id": 8, 10 | "content": "postman message", 11 | "message_type": 0, 12 | "content_type": "text", 13 | "content_attributes": {}, 14 | "created_at": 1626155918, 15 | "conversation_id": 2, 16 | "sender": { 17 | "additional_attributes": {}, 18 | "email": "test@test.com", 19 | "id": 1, 20 | "identifier": null, 21 | "name": "test", 22 | "phone_number": null, 23 | "pubsub_token": "vxrAesh9TjFkEeQbSTyyBNYY", 24 | "thumbnail": "https://www.gravatar.com/avatar/b642b4217b34b1e8d3bd915fc65c4452?d=404", 25 | "type": "contact" 26 | } 27 | }, 28 | { 29 | "id": 9, 30 | "content": "Reply to postman", 31 | "message_type": 1, 32 | "content_type": null, 33 | "content_attributes": {}, 34 | "created_at": 1626156303, 35 | "conversation_id": 2, 36 | "sender": { 37 | "id": 5, 38 | "name": "Ephraim Nartey", 39 | "available_name": "Ephraim Nartey", 40 | "avatar_url": "https://www.gravatar.com/avatar/4dfa265292bd0da849b08559e9c905b1?d=404", 41 | "type": "user", 42 | "availability_status": "online" 43 | } 44 | } 45 | ], 46 | "contact": { 47 | "id": 1, 48 | "name": "test", 49 | "email": "test@test.com", 50 | "phone_number": null, 51 | "account_id": 5, 52 | "created_at": "2021-07-12T16:09:34.854Z", 53 | "updated_at": "2021-07-13T05:58:38.772Z", 54 | "pubsub_token": "vxrAesh9TjFkEeQbSTyyBNYY", 55 | "additional_attributes": {}, 56 | "identifier": null, 57 | "custom_attributes": {}, 58 | "last_activity_at": "2021-07-13T05:58:38.752Z", 59 | "label_list": [] 60 | } 61 | } 62 | ] -------------------------------------------------------------------------------- /test/resources/message.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 8, 3 | "content": "postman message", 4 | "message_type": 0, 5 | "content_type": "text", 6 | "content_attributes": {}, 7 | "created_at": 1626155918, 8 | "conversation_id": 2, 9 | "sender": { 10 | "additional_attributes": {}, 11 | "email": "test@test.com", 12 | "id": 1, 13 | "identifier": null, 14 | "name": "test", 15 | "phone_number": null, 16 | "pubsub_token": "vxrAesh9TjFkEeQbSTyyBNYY", 17 | "thumbnail": "https://www.gravatar.com/avatar/b642b4217b34b1e8d3bd915fc65c4452?d=404", 18 | "type": "contact" 19 | } 20 | } -------------------------------------------------------------------------------- /test/resources/messages.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 8, 4 | "content": "postman message", 5 | "message_type": 0, 6 | "content_type": "text", 7 | "content_attributes": {}, 8 | "created_at": 1626155918, 9 | "conversation_id": 2, 10 | "sender": { 11 | "additional_attributes": {}, 12 | "email": "test@test.com", 13 | "id": 1, 14 | "identifier": null, 15 | "name": "test", 16 | "phone_number": null, 17 | "pubsub_token": "vxrAesh9TjFkEeQbSTyyBNYY", 18 | "thumbnail": "https://www.gravatar.com/avatar/b642b4217b34b1e8d3bd915fc65c4452?d=404", 19 | "type": "contact" 20 | } 21 | }, 22 | { 23 | "id": 9, 24 | "content": "Reply to postman", 25 | "message_type": 1, 26 | "content_type": null, 27 | "content_attributes": {}, 28 | "created_at": 1626156303, 29 | "conversation_id": 2, 30 | "sender": { 31 | "id": 5, 32 | "name": "Ephraim Nartey", 33 | "available_name": "Ephraim Nartey", 34 | "avatar_url": "https://www.gravatar.com/avatar/4dfa265292bd0da849b08559e9c905b1?d=404", 35 | "type": "user", 36 | "availability_status": "online" 37 | } 38 | } 39 | ] -------------------------------------------------------------------------------- /test/resources/websocket_conversation_status_changed.json: -------------------------------------------------------------------------------- 1 | { 2 | "identifier": "{\"channel\":\"RoomChannel\",\"pubsub_token\":\"token\",\"account_id\":1,\"user_id\":1}", 3 | "message": { 4 | "event": "conversation.status_changed", 5 | "data": { 6 | "additional_attributes": { 7 | "browser": { 8 | "device_name": "Unknown", 9 | "browser_name": "Chrome", 10 | "platform_name": "macOS", 11 | "browser_version": "92.0.4515.107", 12 | "platform_version": "10.15.7" 13 | }, 14 | "referer": "http://localhost:3000/widget_tests", 15 | "initiated_at": { 16 | "timestamp": "Fri Jul 23 2021 22:43:14 GMT+0530 (India Standard Time)" 17 | } 18 | }, 19 | "can_reply": true, 20 | "channel": "Channel::WebWidget", 21 | "id": 7, 22 | "inbox_id": 1, 23 | "contact_inbox": { 24 | "id": 8, 25 | "contact_id": 10, 26 | "inbox_id": 1, 27 | "source_id": "4f5c785e-abba-4527-82a9-bbdb2117c167", 28 | "created_at": "2021-07-23T17:08:53.771Z", 29 | "updated_at": "2021-07-23T17:08:53.771Z", 30 | "hmac_verified": false 31 | }, 32 | "messages": [{ 33 | "id": 107, 34 | "content": "sfdfd", 35 | "account_id": 1, 36 | "inbox_id": 1, 37 | "conversation_id": 7, 38 | "message_type": 0, 39 | "created_at": 1627060756, 40 | "updated_at": "2021-07-23T17:19:16.000Z", 41 | "private": false, 42 | "status": "sent", 43 | "source_id": null, 44 | "content_type": "text", 45 | "content_attributes": {}, 46 | "sender_type": "Contact", 47 | "sender_id": 10, 48 | "external_source_ids": {}, 49 | "sender": { 50 | "additional_attributes": {}, 51 | "custom_attributes": {}, 52 | "email": null, 53 | "id": 10, 54 | "identifier": null, 55 | "name": "white-shadow-394", 56 | "phone_number": null, 57 | "pubsub_token": "token", 58 | "thumbnail": "", 59 | "type": "contact" 60 | } 61 | }], 62 | "meta": { 63 | "sender": { 64 | "additional_attributes": {}, 65 | "custom_attributes": {}, 66 | "email": null, 67 | "id": 10, 68 | "identifier": null, 69 | "name": "white-shadow-394", 70 | "phone_number": null, 71 | "pubsub_token": "token", 72 | "thumbnail": "", 73 | "type": "contact" 74 | }, 75 | "assignee": { 76 | "id": 1, 77 | "name": "John", 78 | "available_name": "John", 79 | "avatar_url": "https://www.gravatar.com/avatar/0d722ac7bc3b3c92c030d0da9690d981?d=404", 80 | "type": "user", 81 | "availability_status": "offline" 82 | } 83 | }, 84 | "status": "resolved", 85 | "unread_count": 0, 86 | "agent_last_seen_at": 1627060756, 87 | "contact_last_seen_at": 0, 88 | "timestamp": 1627060756, 89 | "account_id": 1 90 | } 91 | } 92 | } -------------------------------------------------------------------------------- /test/resources/websocket_conversation_typing_off.json: -------------------------------------------------------------------------------- 1 | { 2 | "identifier": "{\"channel\":\"RoomChannel\",\"pubsub_token\":\"token\",\"account_id\":1,\"user_id\":1}", 3 | "message": { 4 | "event": "conversation.typing_off", 5 | "data": { 6 | "conversation": { 7 | "additional_attributes": { 8 | "browser": { 9 | "device_name": "Unknown", 10 | "browser_name": "Chrome", 11 | "platform_name": "macOS", 12 | "browser_version": "92.0.4515.107", 13 | "platform_version": "10.15.7" 14 | }, 15 | "referer": "http://localhost:3000/widget_tests", 16 | "initiated_at": { 17 | "timestamp": "Fri Jul 23 2021 22:43:14 GMT+0530 (India Standard Time)" 18 | } 19 | }, 20 | "can_reply": true, 21 | "channel": "Channel::WebWidget", 22 | "id": 7, 23 | "inbox_id": 1, 24 | "contact_inbox": { 25 | "id": 8, 26 | "contact_id": 10, 27 | "inbox_id": 1, 28 | "source_id": "4f5c785e-abba-4527-82a9-bbdb2117c167", 29 | "created_at": "2021-07-23T17:08:53.771Z", 30 | "updated_at": "2021-07-23T17:08:53.771Z", 31 | "hmac_verified": false 32 | }, 33 | "messages": [{ 34 | "id": 106, 35 | "content": "hi", 36 | "account_id": 1, 37 | "inbox_id": 1, 38 | "conversation_id": 7, 39 | "message_type": 0, 40 | "created_at": 1627060394, 41 | "updated_at": "2021-07-23T17:13:14.000Z", 42 | "private": false, 43 | "status": "sent", 44 | "source_id": null, 45 | "content_type": "text", 46 | "content_attributes": {}, 47 | "sender_type": "Contact", 48 | "sender_id": 10, 49 | "external_source_ids": {}, 50 | "sender": { 51 | "additional_attributes": {}, 52 | "custom_attributes": {}, 53 | "email": null, 54 | "id": 10, 55 | "identifier": null, 56 | "name": "white-shadow-394", 57 | "phone_number": null, 58 | "pubsub_token": "token", 59 | "thumbnail": "", 60 | "type": "contact" 61 | } 62 | }], 63 | "meta": { 64 | "sender": { 65 | "additional_attributes": {}, 66 | "custom_attributes": {}, 67 | "email": null, 68 | "id": 10, 69 | "identifier": null, 70 | "name": "white-shadow-394", 71 | "phone_number": null, 72 | "pubsub_token": "token", 73 | "thumbnail": "", 74 | "type": "contact" 75 | }, 76 | "assignee": { 77 | "id": 1, 78 | "name": "John", 79 | "available_name": "John", 80 | "avatar_url": "https://www.gravatar.com/avatar/0d722ac7bc3b3c92c030d0da9690d981?d=404", 81 | "type": "user", 82 | "availability_status": "online" 83 | } 84 | }, 85 | "status": "open", 86 | "unread_count": 0, 87 | "agent_last_seen_at": 1627060549, 88 | "contact_last_seen_at": 0, 89 | "timestamp": 1627060394 90 | }, 91 | "user": { 92 | "additional_attributes": {}, 93 | "custom_attributes": {}, 94 | "email": null, 95 | "id": 10, 96 | "identifier": null, 97 | "name": "white-shadow-394", 98 | "phone_number": null, 99 | "pubsub_token": "token", 100 | "thumbnail": "", 101 | "type": "contact" 102 | }, 103 | "account_id": 1 104 | } 105 | } 106 | } -------------------------------------------------------------------------------- /test/resources/websocket_conversation_typing_on.json: -------------------------------------------------------------------------------- 1 | { 2 | "identifier": "{\"channel\":\"RoomChannel\",\"pubsub_token\":\"token\",\"account_id\":1,\"user_id\":1}", 3 | "message": { 4 | "event": "conversation.typing_on", 5 | "data": { 6 | "conversation": { 7 | "additional_attributes": { 8 | "browser": { 9 | "device_name": "Unknown", 10 | "browser_name": "Chrome", 11 | "platform_name": "macOS", 12 | "browser_version": "92.0.4515.107", 13 | "platform_version": "10.15.7" 14 | }, 15 | "referer": "http://localhost:3000/widget_tests", 16 | "initiated_at": { 17 | "timestamp": "Fri Jul 23 2021 22:43:14 GMT+0530 (India Standard Time)" 18 | } 19 | }, 20 | "can_reply": true, 21 | "channel": "Channel::WebWidget", 22 | "id": 7, 23 | "inbox_id": 1, 24 | "contact_inbox": { 25 | "id": 8, 26 | "contact_id": 10, 27 | "inbox_id": 1, 28 | "source_id": "4f5c785e-abba-4527-82a9-bbdb2117c167", 29 | "created_at": "2021-07-23T17:08:53.771Z", 30 | "updated_at": "2021-07-23T17:08:53.771Z", 31 | "hmac_verified": false 32 | }, 33 | "messages": [{ 34 | "id": 106, 35 | "content": "hi", 36 | "account_id": 1, 37 | "inbox_id": 1, 38 | "conversation_id": 7, 39 | "message_type": 0, 40 | "created_at": 1627060394, 41 | "updated_at": "2021-07-23T17:13:14.000Z", 42 | "private": false, 43 | "status": "sent", 44 | "source_id": null, 45 | "content_type": "text", 46 | "content_attributes": {}, 47 | "sender_type": "Contact", 48 | "sender_id": 10, 49 | "external_source_ids": {}, 50 | "sender": { 51 | "additional_attributes": {}, 52 | "custom_attributes": {}, 53 | "email": null, 54 | "id": 10, 55 | "identifier": null, 56 | "name": "white-shadow-394", 57 | "phone_number": null, 58 | "pubsub_token": "token", 59 | "thumbnail": "", 60 | "type": "contact" 61 | } 62 | }], 63 | "meta": { 64 | "sender": { 65 | "additional_attributes": {}, 66 | "custom_attributes": {}, 67 | "email": null, 68 | "id": 10, 69 | "identifier": null, 70 | "name": "white-shadow-394", 71 | "phone_number": null, 72 | "pubsub_token": "token", 73 | "thumbnail": "", 74 | "type": "contact" 75 | }, 76 | "assignee": { 77 | "id": 1, 78 | "name": "John", 79 | "available_name": "John", 80 | "avatar_url": "https://www.gravatar.com/avatar/0d722ac7bc3b3c92c030d0da9690d981?d=404", 81 | "type": "user", 82 | "availability_status": "online" 83 | } 84 | }, 85 | "status": "open", 86 | "unread_count": 0, 87 | "agent_last_seen_at": 1627060549, 88 | "contact_last_seen_at": 0, 89 | "timestamp": 1627060394 90 | }, 91 | "user": { 92 | "additional_attributes": {}, 93 | "custom_attributes": {}, 94 | "email": null, 95 | "id": 10, 96 | "identifier": null, 97 | "name": "white-shadow-394", 98 | "phone_number": null, 99 | "pubsub_token": "token", 100 | "thumbnail": "", 101 | "type": "contact" 102 | }, 103 | "account_id": 1 104 | } 105 | } 106 | } -------------------------------------------------------------------------------- /test/resources/websocket_message_created.json: -------------------------------------------------------------------------------- 1 | { 2 | "identifier": "{\"channel\":\"RoomChannel\",\"pubsub_token\":\"token\",\"account_id\":1,\"user_id\":1}", 3 | "message": { 4 | "event": "message.created", 5 | "data": { 6 | "id": 106, 7 | "content": "hi", 8 | "account_id": 1, 9 | "inbox_id": 1, 10 | "conversation_id": 7, 11 | "message_type": 0, 12 | "created_at": 1627060394, 13 | "updated_at": "2021-07-23T17:13:14.000Z", 14 | "private": false, 15 | "status": "sent", 16 | "source_id": null, 17 | "content_type": "text", 18 | "content_attributes": {}, 19 | "sender_type": "Contact", 20 | "sender_id": 10, 21 | "external_source_ids": {}, 22 | "sender": { 23 | "additional_attributes": {}, 24 | "custom_attributes": {}, 25 | "email": null, 26 | "id": 10, 27 | "identifier": null, 28 | "name": "white-shadow-394", 29 | "phone_number": null, 30 | "pubsub_token": "token", 31 | "thumbnail": "", 32 | "type": "contact" 33 | } 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /test/resources/websocket_message_updated.json: -------------------------------------------------------------------------------- 1 | { 2 | "identifier": "{\"channel\":\"RoomChannel\",\"pubsub_token\":\"token\",\"account_id\":1,\"user_id\":1}", 3 | "message": { 4 | "event": "message.updated", 5 | "data": { 6 | "id": 111, 7 | "content_type": "input_email", 8 | "content_attributes": { 9 | "submitted_email": "jane@acme.inc" 10 | }, 11 | "account_id": 1, 12 | "inbox_id": 1, 13 | "conversation_id": 8, 14 | "content": "Get notified by email", 15 | "message_type": 3, 16 | "created_at": 1627060984, 17 | "updated_at": "2021-07-23T17:23:14.000Z", 18 | "private": false, 19 | "status": "sent", 20 | "source_id": null, 21 | "sender_type": null, 22 | "sender_id": null, 23 | "external_source_ids": {} 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /test/resources/websocket_presence_update.json: -------------------------------------------------------------------------------- 1 | { 2 | "identifier": "{\"channel\":\"RoomChannel\",\"pubsub_token\":\"token\",\"account_id\":1,\"user_id\":1}", 3 | "message": { 4 | "event": "presence.update", 5 | "data": { 6 | "account_id": 1, 7 | "users": { 8 | "1": "online" 9 | }, 10 | "contacts": { 11 | "1": "online" 12 | } 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /test/utils/test_resources_util.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import 'package:flutter_test/flutter_test.dart'; 5 | import 'package:mockito/mockito.dart'; 6 | import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; 7 | import 'package:plugin_platform_interface/plugin_platform_interface.dart'; 8 | 9 | class TestResourceUtil { 10 | static Future readJsonResource({required String fileName}) async { 11 | final file = new File('test/resources/$fileName.json'); 12 | final json = jsonDecode(await file.readAsString()); 13 | return json; 14 | } 15 | } 16 | 17 | const String kTemporaryPath = 'temporaryPath'; 18 | const String kApplicationSupportPath = 'applicationSupportPath'; 19 | const String kDownloadsPath = 'downloadsPath'; 20 | const String kLibraryPath = 'libraryPath'; 21 | const String kApplicationDocumentsPath = 'applicationDocumentsPath'; 22 | const String kExternalCachePath = 'externalCachePath'; 23 | const String kExternalStoragePath = 'externalStoragePath'; 24 | 25 | class MockPathProviderPlatform extends Mock 26 | with MockPlatformInterfaceMixin 27 | implements PathProviderPlatform { 28 | Future getTemporaryPath() async { 29 | return kTemporaryPath; 30 | } 31 | 32 | Future getApplicationSupportPath() async { 33 | return kApplicationSupportPath; 34 | } 35 | 36 | Future getLibraryPath() async { 37 | return kLibraryPath; 38 | } 39 | 40 | Future getApplicationDocumentsPath() async { 41 | return kApplicationDocumentsPath; 42 | } 43 | 44 | Future getExternalStoragePath() async { 45 | return kExternalStoragePath; 46 | } 47 | 48 | Future> getExternalCachePaths() async { 49 | return [kExternalCachePath]; 50 | } 51 | 52 | Future> getExternalStoragePaths({ 53 | StorageDirectory? type, 54 | }) async { 55 | return [kExternalStoragePath]; 56 | } 57 | 58 | Future getDownloadsPath() async { 59 | return kDownloadsPath; 60 | } 61 | } 62 | --------------------------------------------------------------------------------