diff --git a/.fvmrc b/.fvmrc index 07114cc..1a01499 100644 --- a/.fvmrc +++ b/.fvmrc @@ -2,6 +2,6 @@ "flutter": "stable", "flavors": { "development": "stable", - "production": "3.29.3" + "production": "3.41.2" } } \ No newline at end of file diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..79d8aee --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,13 @@ +# These are supported funding model platforms + +#github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +#patreon: ziqq +#open_collective: # Replace with a single Open Collective username +#ko_fi: # Replace with a single Ko-fi username +#tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +#community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +#liberapay: # Replace with a single Liberapay username +#issuehunt: # Replace with a single IssueHunt username +#otechie: # Replace with a single Otechie username +#lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry +custom: ['https://www.buymeacoffee.com/ziqq', 'https://boosty.to/ziqq'] \ No newline at end of file diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..0f32f3d --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,120 @@ +# Contactos + +This is a **Flutter package** (not an app). The package is used to access and manage the device's contacts. The codebase includes platform-specific implementations for Android and iOS, as well as a common Dart interface. The package also includes example usage and documentation. + +**Important**: Before doing anything, always try to clarify everything possible, highlight weak points, corner cases that were not taken into account, and ask a lot of clarifying questions. + + +## Lint Rules +Include the package in the `analysis_options.yaml` file. Use the following +analysis_options.yaml file as a starting point: + +```yaml +include: package:flutter_lints/flutter.yaml + +linter: + rules: + # Add additional lint rules here: + # avoid_print: false + # prefer_single_quotes: true +``` + + +## Code Generation +* **Build Runner:** If the project uses code generation, ensure that + `build_runner` is listed as a dev dependency in `pubspec.yaml`. +* **Running Build Runner:** After modifying files that require code generation, + run the build command: + ```shell + dart run build_runner build --delete-conflicting-outputs --release + ``` + + +## Testing +* **Running Tests:** To run tests, use the `run_tests` tool if it is available, + otherwise use `flutter test`. +* **Unit Tests:** Use `package:test` for unit tests. +* **Widget Tests:** Use `package:flutter_test` for widget tests. +* **Integration Tests:** Use `package:integration_test` for integration tests. +* **Assertions:** Prefer using `package:checks` for more expressive and readable + assertions over the default `matchers`. + + +### Testing Best practices +* **Convention:** Follow the Arrange-Act-Assert (or Given-When-Then) pattern. +* **Unit Tests:** Write unit tests for domain logic, data layer, and state + management. +* **Widget Tests:** Write widget tests for UI components. +* **Integration Tests:** For broader application validation, use integration + tests to verify end-to-end user flows. +* **integration_test package:** Use the `integration_test` package from the + Flutter SDK for integration tests. Add it as a `dev_dependency` in + `pubspec.yaml` by specifying `sdk: flutter`. +* **Mocks:** Prefer fakes or stubs over mocks. If mocks are absolutely + necessary, use `mockito` or `mocktail` to create mocks for dependencies. While + code generation is common for state management (e.g., with `freezed`), try to + avoid it for mocks. +* **Coverage:** Aim for high test coverage. + + +## Documentation +* **`dartdoc`:** Write `dartdoc`-style comments for all public APIs. + +### Documentation Philosophy +* **Comment wisely:** Use comments to explain why the code is written a certain + way, not what the code does. The code itself should be self-explanatory. +* **Document for the user:** Write documentation with the reader in mind. If you + had a question and found the answer, add it to the documentation where you + first looked. This ensures the documentation answers real-world questions. +* **No useless documentation:** If the documentation only restates the obvious + from the code's name, it's not helpful. Good documentation provides context + and explains what isn't immediately apparent. +* **Consistency is key:** Use consistent terminology throughout your + documentation. + +### Commenting Style +* **Use `///` for doc comments:** This allows documentation generation tools to + pick them up. +* **Start with a single-sentence summary:** The first sentence should be a + concise, user-centric summary ending with a period. +* **Separate the summary:** Add a blank line after the first sentence to create + a separate paragraph. This helps tools create better summaries. +* **Avoid redundancy:** Don't repeat information that's obvious from the code's + context, like the class name or signature. +* **Don't document both getter and setter:** For properties with both, only + document one. The documentation tool will treat them as a single field. +* **Important**: Don't delete comments, but feel free to add more if you think it would help the reader understand the code better. The goal is to make the code as clear and self-explanatory as possible, so that even someone new to Flutter could understand it. + +### Writing Style +* **Be brief:** Write concisely. +* **Avoid jargon and acronyms:** Don't use abbreviations unless they are widely + understood. +* **Use Markdown sparingly:** Avoid excessive markdown and never use HTML for + formatting. +* **Use backticks for code:** Enclose code blocks in backtick fences, and + specify the language. + +### What to Document +* **Public APIs are a priority:** Always document public APIs. +* **Consider private APIs:** It's a good idea to document private APIs as well. +* **Library-level comments are helpful:** Consider adding a doc comment at the + library level to provide a general overview. +* **Include code samples:** Where appropriate, add code samples to illustrate usage. +* **Explain parameters, return values, and exceptions:** Use prose to describe + what a function expects, what it returns, and what errors it might throw. +* **Place doc comments before annotations:** Documentation should come before + any metadata annotations. + +## Accessibility (A11Y) +Implement accessibility features to empower all users, assuming a wide variety +of users with different physical abilities, mental abilities, age groups, +education levels, and learning styles. + +* **Color Contrast:** Ensure text has a contrast ratio of at least **4.5:1** + against its background. +* **Dynamic Text Scaling:** Test your UI to ensure it remains usable when users + increase the system font size. +* **Semantic Labels:** Use the `Semantics` widget to provide clear, descriptive + labels for UI elements. +* **Screen Reader Testing:** Regularly test your app with TalkBack (Android) and + VoiceOver (iOS). \ No newline at end of file diff --git a/.docs/example.gif b/.github/images/example.gif similarity index 100% rename from .docs/example.gif rename to .github/images/example.gif diff --git a/.github/workflows/checkout.yml b/.github/workflows/checkout.yml index 51bb841..b9c8816 100644 --- a/.github/workflows/checkout.yml +++ b/.github/workflows/checkout.yml @@ -1,177 +1,145 @@ -name: "Checkout" - +name: CI on: - workflow_call: - + push: + branches: [ main ] + paths: + - ".github/workflows/**" + - "contactos/**" + - "contactos_android/**" + - "contactos_foundation/**" + - "contactos_platform_interface/**" + pull_request: + branches: [ main ] + paths: + - ".github/workflows/**" + - "contactos/**" + - "contactos_android/**" + - "contactos_foundation/**" + - "contactos_platform_interface/**" + workflow_dispatch: + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true jobs: - checkout: + detect-changes: + name: Detect changed packages runs-on: ubuntu-latest - defaults: - run: - working-directory: ./contactos/ - env: - pub-cache-name: pub - threshold: 50 - timeout-minutes: 15 + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} steps: - - name: πŸš‚ Get latest code - id: checkout - uses: actions/checkout@v4 - with: - sparse-checkout: | - .github - pubspec.yaml - lib - test - analysis_options.yaml - CHANGELOG.md - - - name: πŸ‘· Install flutter - uses: subosito/flutter-action@v2 - id: install-flutter + - uses: actions/checkout@v4 with: - channel: 'stable' - - - name: πŸ”Ž Check flutter version - id: check-flutter-version - run: flutter --version - - - name: πŸ“€ Restore Pub modules - id: cache-pub-restore - uses: actions/cache/restore@v4 - with: - path: | - $PWD/.pub_cache/ - key: ${{ runner.os }}-pub-${{ env.pub-cache-name }}-${{ hashFiles('**/pubspec.yaml') }} - - - name: πŸ—„οΈ Export Pub cache directory - id: export-pub-cache - timeout-minutes: 1 - run: | - export PUB_CACHE=$PWD/.pub_cache/ - export PATH="$PATH":"$HOME/.pub-cache/bin" - echo "${HOME}/.pub-cache/bin" >> $GITHUB_PATH - - - name: πŸ‘· Install Dependencies - id: install-dependencies - timeout-minutes: 2 + fetch-depth: 0 + - id: set-matrix run: | - apt-get update && apt-get install -y lcov - flutter pub get --no-example - - - name: πŸ“₯ Save Pub modules - id: cache-pub-save - uses: actions/cache/save@v4 - with: - path: | - $PWD/.pub_cache/ - key: ${{ runner.os }}-pub-${{ env.pub-cache-name }}-${{ hashFiles('**/pubspec.yaml') }} - - - name: πŸ”Ž Check content - id: check_files - uses: andstor/file-existence-action@v1 - with: - files: "README.md, CHANGELOG.md, LICENSE, example" - - - name: ❔ File exists - if: steps.check_files.outputs.files_exists == 'true' - run: echo Content is ok! - - - name: πŸ”Ž Check description - run: echo | grep -q Description README.md ; echo $? - - - name: πŸ”Ž Check example - run: echo | grep -q Example README.md ; echo $? + MATRIX_JSON='{"include":[{"name":"plugin","path":"contactos"},{"name":"android","path":"contactos_android"},{"name":"foundation","path":"contactos_foundation"},{"name":"interface","path":"contactos_platform_interface"}]}' + echo "matrix=$MATRIX_JSON" >> $GITHUB_OUTPUT - - name: πŸ”Ž Check installation - run: echo | grep -q Installation README.md ; echo $? - - - name: πŸ‘· Install dependencies - run: flutter pub get - - - name: πŸ§ͺ Run dependency validator - run: | - dart pub global activate dependency_validator - dart pub global run dependency_validator:dependency_validator - - - name: πŸ”Ž Check format - id: check_format - run: dart format --set-exit-if-changed -l 80 -o none lib/ - - - name: πŸ“ˆ Check analyzer - id: check_analyzer - run: dart analyze --fatal-infos --fatal-warnings lib/ - - testing: + package-ci: + name: Package (${{ matrix.name }}) + needs: detect-changes runs-on: ubuntu-latest - defaults: - run: - working-directory: ./contactos/ - env: - pub-cache-name: pub - threshold: 50 - timeout-minutes: 15 + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.detect-changes.outputs.matrix) }} steps: - - name: πŸš‚ Get latest code - id: checkout + - name: Checkout uses: actions/checkout@v4 - with: - sparse-checkout: | - .github - pubspec.yaml - lib - test - analysis_options.yaml - CHANGELOG.md - - name: πŸ‘· Install flutter + - name: Install Flutter uses: subosito/flutter-action@v2 - id: install-flutter with: - channel: 'stable' + channel: stable - - name: πŸ”Ž Check flutter version - id: check-flutter-version + - name: Flutter version run: flutter --version - - name: πŸ“€ Restore Pub modules - id: cache-pub-restore - uses: actions/cache/restore@v4 + - name: Cache pub + uses: actions/cache@v4 with: path: | - $PWD/.pub_cache/ - key: ${{ runner.os }}-pub-${{ env.pub-cache-name }}-${{ hashFiles('**/pubspec.yaml') }} + ~/.pub-cache + key: ${{ runner.os }}-pub-${{ matrix.name }}-${{ hashFiles(format('{0}/pubspec.lock', matrix.path)) }} + restore-keys: | + ${{ runner.os }}-pub-${{ matrix.name }}- + ${{ runner.os }}-pub- + + - name: Pub get + working-directory: ${{ matrix.path }} + run: flutter pub get - - name: πŸ—„οΈ Export Pub cache directory - id: export-pub-cache - timeout-minutes: 1 - run: | - export PUB_CACHE=$PWD/.pub_cache/ - export PATH="$PATH":"$HOME/.pub-cache/bin" - echo "${HOME}/.pub-cache/bin" >> $GITHUB_PATH + - name: Format check + working-directory: ${{ matrix.path }} + run: dart format -l 80 lib test - - name: πŸ‘· Install Dependencies - id: install-dependencies - timeout-minutes: 2 + - name: Analyze + working-directory: ${{ matrix.path }} + run: dart analyze --fatal-infos --fatal-warnings lib/ + + - name: Test (with coverage if Dart/Flutter tests exist) + working-directory: ${{ matrix.path }} run: | - apt-get update && apt-get install -y lcov - flutter pub get --no-example + if ls test/*.dart >/dev/null 2>&1; then + flutter test --coverage + else + echo "No tests" + fi - - name: πŸ“₯ Save Pub modules - id: cache-pub-save - uses: actions/cache/save@v4 + - name: Upload partial coverage artifact + if: success() + uses: actions/upload-artifact@v4 with: - path: | - $PWD/.pub_cache/ - key: ${{ runner.os }}-pub-${{ env.pub-cache-name }}-${{ hashFiles('**/pubspec.yaml') }} + name: coverage-${{ matrix.name }} + path: ${{ matrix.path }}/coverage/lcov.info + if-no-files-found: ignore - - name: πŸ§ͺ Run tests - timeout-minutes: 2 + coverage-merge: + name: Merge coverage & Codecov + needs: package-ci + runs-on: ubuntu-latest + if: always() + steps: + - uses: actions/checkout@v4 + - name: Download coverage artifacts + uses: actions/download-artifact@v4 + with: + path: coverage_parts + pattern: coverage-* + merge-multiple: true + - name: Merge lcov + run: | + sudo apt-get update && sudo apt-get install -y lcov + mkdir merged || true + FILES=$(find coverage_parts -name "lcov.info") + if [ -z "$FILES" ]; then + echo "No coverage files"; exit 0 + fi + # ΠΠ°Ρ‡Π°Π»ΡŒΠ½Ρ‹ΠΉ Ρ„Π°ΠΉΠ» + FIRST=true + for f in $FILES; do + if $FIRST; then + cp "$f" merged/lcov.info + FIRST=false + else + lcov -a merged/lcov.info -a "$f" -o merged/lcov.tmp + mv merged/lcov.tmp merged/lcov.info + fi + done + lcov --remove merged/lcov.info '*_test.dart' -o merged/lcov.info || true + echo "Merged coverage:" + lcov --summary merged/lcov.info || true + - name: Upload merged artifact + uses: actions/upload-artifact@v4 + with: + name: coverage-merged + path: merged/lcov.info + if-no-files-found: ignore + - name: Upload to Codecov + if: env.CODECOV_TOKEN != '' env: - CODECOV_TOKEN: ${{secrets.CODECOV_TOKEN}} - if: success() + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} run: | - flutter test --coverage - bash <(curl -s https://codecov.io/bash) -f coverage/lcov.info \ No newline at end of file + bash <(curl -s https://codecov.io/bash) -f merged/lcov.info || echo "Codecov upload skipped" diff --git a/.github/workflows/checkout_platform_interface.yml b/.github/workflows/checkout_platform_interface.yml deleted file mode 100644 index 348e94d..0000000 --- a/.github/workflows/checkout_platform_interface.yml +++ /dev/null @@ -1,171 +0,0 @@ -name: "Checkout platform interface" - - -on: - workflow_call: - - -jobs: - checkout: - runs-on: ubuntu-latest - defaults: - run: - working-directory: ./contactos_platform_interface/ - env: - pub-cache-name: pub - threshold: 50 - timeout-minutes: 15 - steps: - - name: πŸš‚ Get latest code - id: checkout - uses: actions/checkout@v4 - with: - sparse-checkout: | - .github - pubspec.yaml - lib - test - analysis_options.yaml - CHANGELOG.md - - - name: πŸ‘· Install flutter - uses: subosito/flutter-action@v2 - id: install-flutter - with: - channel: 'stable' - - - name: πŸ”Ž Check flutter version - id: check-flutter-version - run: flutter --version - - - name: πŸ“€ Restore Pub modules - id: cache-pub-restore - uses: actions/cache/restore@v4 - with: - path: | - $PWD/.pub_cache/ - key: ${{ runner.os }}-pub-${{ env.pub-cache-name }}-${{ hashFiles('**/pubspec.yaml') }} - - - name: πŸ—„οΈ Export Pub cache directory - id: export-pub-cache - timeout-minutes: 1 - run: | - export PUB_CACHE=$PWD/.pub_cache/ - export PATH="$PATH":"$HOME/.pub-cache/bin" - echo "${HOME}/.pub-cache/bin" >> $GITHUB_PATH - - - name: πŸ‘· Install Dependencies - id: install-dependencies - timeout-minutes: 2 - run: | - apt-get update && apt-get install -y lcov - flutter pub get --no-example - - - name: πŸ“₯ Save Pub modules - id: cache-pub-save - uses: actions/cache/save@v4 - with: - path: | - $PWD/.pub_cache/ - key: ${{ runner.os }}-pub-${{ env.pub-cache-name }}-${{ hashFiles('**/pubspec.yaml') }} - - - name: πŸ”Ž Check content - id: check_files - uses: andstor/file-existence-action@v1 - with: - files: "README.md, CHANGELOG.md, LICENSE" - - - name: ❔ File exists - if: steps.check_files.outputs.files_exists == 'true' - run: echo Content is ok! - - - name: πŸ”Ž Check description - run: echo | grep -q Description README.md ; echo $? - - - name: πŸ‘· Install dependencies - run: flutter pub get - - - name: πŸ§ͺ Run dependency validator - run: | - dart pub global activate dependency_validator - dart pub global run dependency_validator:dependency_validator - - - name: πŸ”Ž Check format - id: check_format - run: dart format --set-exit-if-changed -l 80 -o none lib/ - - - name: πŸ“ˆ Check analyzer - id: check_analyzer - run: dart analyze --fatal-infos --fatal-warnings lib/ - - testing: - runs-on: ubuntu-latest - defaults: - run: - working-directory: ./contactos_platform_interface/ - env: - pub-cache-name: pub - threshold: 50 - timeout-minutes: 15 - steps: - - name: πŸš‚ Get latest code - id: checkout - uses: actions/checkout@v4 - with: - sparse-checkout: | - .github - pubspec.yaml - lib - test - analysis_options.yaml - CHANGELOG.md - - - name: πŸ‘· Install flutter - uses: subosito/flutter-action@v2 - id: install-flutter - with: - channel: 'stable' - - - name: πŸ”Ž Check flutter version - id: check-flutter-version - run: flutter --version - - - name: πŸ“€ Restore Pub modules - id: cache-pub-restore - uses: actions/cache/restore@v4 - with: - path: | - $PWD/.pub_cache/ - key: ${{ runner.os }}-pub-${{ env.pub-cache-name }}-${{ hashFiles('**/pubspec.yaml') }} - - - name: πŸ—„οΈ Export Pub cache directory - id: export-pub-cache - timeout-minutes: 1 - run: | - export PUB_CACHE=$PWD/.pub_cache/ - export PATH="$PATH":"$HOME/.pub-cache/bin" - echo "${HOME}/.pub-cache/bin" >> $GITHUB_PATH - - - name: πŸ‘· Install Dependencies - id: install-dependencies - timeout-minutes: 2 - run: | - apt-get update && apt-get install -y lcov - flutter pub get --no-example - - - name: πŸ“₯ Save Pub modules - id: cache-pub-save - uses: actions/cache/save@v4 - with: - path: | - $PWD/.pub_cache/ - key: ${{ runner.os }}-pub-${{ env.pub-cache-name }}-${{ hashFiles('**/pubspec.yaml') }} - - - name: πŸ§ͺ Run tests - timeout-minutes: 2 - env: - CODECOV_TOKEN: ${{secrets.CODECOV_TOKEN}} - if: success() - run: | - flutter test --coverage - bash <(curl -s https://codecov.io/bash) -f coverage/lcov.info \ No newline at end of file diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 92c10cf..609ffb9 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -2,16 +2,160 @@ name: Publish to pub.dev on: workflow_dispatch: + inputs: + packages: + description: "Packages to publish (comma-separated: android,ios,interface,main)" + required: false + default: "android,ios,interface,main" push: tags: - "v[0-9]+.[0-9]+.[0-9]+*" + - "contactos-v[0-9]+.[0-9]+.[0-9]+*" + - "contactos-android-v[0-9]+.[0-9]+.[0-9]+*" + - "contactos-foundation-v[0-9]+.[0-9]+.[0-9]+*" + - "contactos-platform-interface-v[0-9]+.[0-9]+.[0-9]+*" + +permissions: + contents: read + id-token: write # Required for authentication with Google Cloud/Pub jobs: - publish: - uses: ziqq/flutter_ci_workflows/.github/workflows/publish_to_pub.yml@main - with: - PANA_TOTAL: '110' - secrets: - PUB_CREDENTIAL_JSON: ${{ secrets.PUB_CREDENTIAL_JSON }} - PUB_OAUTH_ACCESS_TOKEN: ${{ secrets.PUB_OAUTH_ACCESS_TOKEN }} - PUB_OAUTH_REFRESH_TOKEN: ${{ secrets.PUB_OAUTH_REFRESH_TOKEN }} \ No newline at end of file + prepare: + name: "Prepare publish configuration" + runs-on: ubuntu-latest + outputs: + publish_android: ${{ steps.determine_packages.outputs.publish_android }} + publish_foundation: ${{ steps.determine_packages.outputs.publish_foundation }} + publish_interface: ${{ steps.determine_packages.outputs.publish_interface }} + publish_main: ${{ steps.determine_packages.outputs.publish_main }} + steps: + - name: Determine packages to publish + id: determine_packages + shell: bash + run: | + PACKAGES="" + if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + if [[ "${{ github.event.inputs.packages }}" != "" ]]; then + PACKAGES="${{ github.event.inputs.packages }}" + else + PACKAGES="android,ios,interface,main" + fi + elif [[ "${{ github.ref }}" =~ ^refs/tags/(.*)-v.*$ ]]; then + PACKAGE_NAME="${BASH_REMATCH[1]}" + if [[ "$PACKAGE_NAME" == "contactos" ]]; then + PACKAGES="main" + elif [[ "$PACKAGE_NAME" == "contactos_android" ]]; then + PACKAGES="android" + elif [[ "$PACKAGE_NAME" == "contactos_foundation" ]]; then + PACKAGES="ios" + elif [[ "$PACKAGE_NAME" == "contactos_platform_interface" ]]; then + PACKAGES="interface" + fi + fi + + echo "Selected packages: $PACKAGES" + + if [[ "$PACKAGES" == *"android"* ]]; then echo "publish_android=true" >> $GITHUB_OUTPUT; else echo "publish_android=false" >> $GITHUB_OUTPUT; fi + if [[ "$PACKAGES" == *"ios"* ]]; then echo "publish_foundation=true" >> $GITHUB_OUTPUT; else echo "publish_foundation=false" >> $GITHUB_OUTPUT; fi + if [[ "$PACKAGES" == *"interface"* ]]; then echo "publish_interface=true" >> $GITHUB_OUTPUT; else echo "publish_interface=false" >> $GITHUB_OUTPUT; fi + if [[ "$PACKAGES" == *"main"* ]]; then echo "publish_main=true" >> $GITHUB_OUTPUT; else echo "publish_main=false" >> $GITHUB_OUTPUT; fi + + publish_interface: + needs: prepare + if: needs.prepare.outputs.publish_interface == 'true' + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + steps: + - uses: actions/checkout@v4 + - uses: subosito/flutter-action@v2 + with: + channel: 'stable' + - name: Setup Pub Credentials + shell: bash + env: + PUB_CREDENTIAL_JSON: ${{ secrets.PUB_CREDENTIAL_JSON }} + run: | + mkdir -p ~/.config/dart + echo "$PUB_CREDENTIAL_JSON" > ~/.config/dart/pub-credentials.json + - name: Publish + working-directory: contactos_platform_interface + run: | + flutter pub get + dart pub publish --force + + publish_foundation: + needs: prepare + if: needs.prepare.outputs.publish_foundation == 'true' + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + steps: + - uses: actions/checkout@v4 + - uses: subosito/flutter-action@v2 + with: + channel: 'stable' + - name: Setup Pub Credentials + shell: bash + env: + PUB_CREDENTIAL_JSON: ${{ secrets.PUB_CREDENTIAL_JSON }} + run: | + mkdir -p ~/.config/dart + echo "$PUB_CREDENTIAL_JSON" > ~/.config/dart/pub-credentials.json + - name: Publish + working-directory: contactos_foundation + run: | + flutter pub get + dart pub publish --force + + publish_android: + needs: prepare + if: needs.prepare.outputs.publish_android == 'true' + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + steps: + - uses: actions/checkout@v4 + - uses: subosito/flutter-action@v2 + with: + channel: 'stable' + - name: Setup Pub Credentials + shell: bash + env: + PUB_CREDENTIAL_JSON: ${{ secrets.PUB_CREDENTIAL_JSON }} + run: | + mkdir -p ~/.config/dart + echo "$PUB_CREDENTIAL_JSON" > ~/.config/dart/pub-credentials.json + - name: Publish + working-directory: contactos_android + run: | + flutter pub get + dart pub publish --force + + publish_main: + needs: prepare + if: needs.prepare.outputs.publish_main == 'true' + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + steps: + - uses: actions/checkout@v4 + - uses: subosito/flutter-action@v2 + with: + channel: 'stable' + - name: Setup Pub Credentials + shell: bash + env: + PUB_CREDENTIAL_JSON: ${{ secrets.PUB_CREDENTIAL_JSON }} + run: | + mkdir -p ~/.config/dart + echo "$PUB_CREDENTIAL_JSON" > ~/.config/dart/pub-credentials.json + - name: Publish + working-directory: contactos + run: | + flutter pub get + dart pub publish --force diff --git a/.vscode/launch.json b/.vscode/launch.json index 8471a77..ae2d8aa 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -2,7 +2,7 @@ "version": "0.2.0", "configurations": [ { - "name": "Run Android or iOS Example", + "name": "Run Example", "type": "dart", "request": "launch", "cwd": "${workspaceFolder}/contactos/example", @@ -10,10 +10,25 @@ "env": {} }, { - "name": "Flutter Test", + "name": "Run Android", + "type": "dart", + "request": "launch", + "cwd": "${workspaceFolder}/contactos_android/example", + "args": [], + "env": {} + }, + { + "name": "Run IOS", "type": "dart", "request": "launch", - "cwd": "${workspaceFolder}/contactos", + "cwd": "${workspaceFolder}/contactos_foundation/example", + "args": [], + "env": {} + }, + { + "name": "Flutter Test", + "request": "launch", + "type": "dart", "program": "test/contacts_test.dart", "env": { "ENVIRONMENT": "test" diff --git a/.vscode/settings.json b/.vscode/settings.json index 914d444..589159f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -63,7 +63,7 @@ "explorer.fileNesting.expand": false, "explorer.fileNesting.patterns": { "pubspec.yaml": ".flutter-plugins, .packages, .dart_tool, .flutter-plugins-dependencies, .metadata, .packages, pubspec.lock, build.yaml, analysis_options.yaml, all_lint_rules.yaml, dart*.yaml, flutter*.yaml, icons_launcher.yaml, devtools_options.yaml, l10n.yaml", - ".gitignore": ".gitattributes, .gitmodules, .gitmessage, .mailmap, .git-blame*", + ".gitignore": ".gitattributes, .gitmodules, .gitmessage, .mailmap, .git-blame*, .pubignore", "readme*": "authors, backers.md, changelog*, citation*, code_of_conduct.md, codeowners, contributing.md, contributors, copying, credits, governance.md, history.md, license*, maintainers, readme*, security.md, sponsors.md", "*.dart": "$(capture).g.dart, $(capture).gr.dart, $(capture).freezed.dart, $(capture).config.dart, $(capture).mocks.dart, $(capture).router.dart, $(capture).locator.dart, $(capture)_skeleton.dart" }, diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..611c0d7 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,129 @@ +# Contactos + +This is a **Flutter plugin** (not an app) for accessing and managing the device's contacts. It follows the [federated plugin architecture](https://flutter.dev/go/federated-plugins) with platform-specific implementations for Android and iOS, as well as a common Dart platform interface. + + +## Environment setup + +- **Flutter version**: managed via [FVM](https://fvm.app/) (see `.fvmrc`). Always prefix Flutter/Dart commands with `fvm`. + +```sh +fvm flutter pub get +``` + +To run the example app: + +```sh +cd contactos/example +fvm flutter run +``` + + +## Project Structure + +Full project tree: see `README.md` β†’ **Project Structure**. + + +## Build, test, and validate + +All commands use `make` targets. Always run `make get` first if dependencies might be stale. + +### Key commands (root level) + +```sh +make get # Get dependencies for all packages +make format # Format all packages (line length 80) +make analyze # Analyze all packages +make check # Analyze + pana for all packages +make test-unit # Run unit tests for all packages +make all # Full pipeline: format + check + test-unit +make precommit # Same as `make all` +``` + +### Per-package commands + +Each package (`contactos/`, `contactos_platform_interface/`, `contactos_android/`, `contactos_foundation/`) has its own `Makefile` with the same targets. Run from within the package directory: + +```sh +cd contactos && make all +cd contactos_platform_interface && make all +``` + +### Test structure + +Each package has a `test/` directory with unit tests. The main test file matches the package name (e.g., `contactos_test.dart`). Use `make test-unit` to run tests with coverage. + + +## Key conventions + +### Do not edit + +- `**/generated/**`, `*.g.dart`, `*.gen.dart` β€” generated files. + +### Coding rules + +- **No `print()`** β€” use `dart:developer` (`dev.log`). +- **No `dynamic`** in JSON parsing β€” use pattern matching + `switch`. Errors β†’ `FormatException`. +- **Immutability**: models are immutable, use `copyWith`, `const` constructors where possible. +- **Platform interface**: extend `ContactosPlatform`, do **not** implement it. +- **Method channels**: use `MethodChannelContactos` for platform communication. +- **Dart format**: line length **80**, enforced via `make format`. +- **Linting**: `flutter_lints` with strict casts, strict raw types, strict inference. + +### Branching and commits + +- Branch: `author/github-/` or `author//`. +- Commits: Conventional Commits β€” `(github-): `. +- PR title: same pattern. Always run `make precommit` before submitting. + + +## Configuration files + +| File | Purpose | +|---|---| +| `/pubspec.yaml` | Package dependencies and metadata | +| `/analysis_options.yaml` | Lint rules (`flutter_lints` + custom), analyzer excludes | +| `/Makefile` | Per-package build/test/format/analyze targets | +| `.fvmrc` | FVM Flutter version configuration | +| `Makefile` | Root-level orchestration targets | + + +## Agent behavior expectations + +Before doing anything: +1. **Clarify** everything unclear β€” ask clarifying questions. +2. **Highlight** weak points, corner cases that were not accounted for. +3. Read `README.md` for project overview and usage. + + +## Critical rules + +- Before substantial work, read `CLAUDE.md` and `AGENTS.md` for full conventions. +- Never edit: `**/generated/**`, `*.g.dart`, `*.gen.dart`. +- When you've made a major change and completed a task, update the patch version in the relevant `pubspec.yaml` and add a note to the corresponding `CHANGELOG.md`. + + +## Before writing code + +For trivial fixes (typos, one-line changes, simple renames), skip discussion and just do it. + +For anything non-trivial, do NOT start implementation until all open questions are resolved. First: + +1. **Challenge the approach** β€” point out flaws, missed edge cases, and risks. Be direct. +2. **Ask about unknowns** β€” if anything is ambiguous, ask. Do not guess or assume. +3. **Propose alternatives** β€” if there is a simpler or more robust way, say so and explain why. +4. **List edge cases** β€” enumerate what can break. +5. **Wait for confirmation** β€” do not write code until the user explicitly approves the plan. + +Do only what was asked. Do not refactor surrounding code, add comments to code you did not change, or introduce abstractions for hypothetical future needs. + + +## After writing code + +Do not consider a task done until verified: + +- `make format` β€” no formatting issues +- `make analyze` β€” no analyzer warnings +- `make test-unit` β€” all tests pass + +If tests or analysis fail, fix the issue before reporting completion. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..cdef374 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,82 @@ +# Contactos + +A Flutter plugin for accessing and managing the device's contacts. Federated plugin monorepo. + +## Structure + +``` +contactos/ # App-facing package (what users depend on) +contactos_platform_interface/ # Platform interface (abstract contract + types) +contactos_android/ # Android implementation +contactos_foundation/ # iOS (Darwin) implementation +``` + +- `contactos/lib/src/contactos.dart` β€” `Contactos` class, delegates to `ContactosPlatform` +- `contactos_platform_interface/lib/src/` β€” `ContactosPlatform`, `MethodChannelContactos`, `Contact`, `Contact$Field`, `Contact$PostalAddress` +- `contactos_android/lib/src/` β€” `ContactosPluginAndroid` extends `ContactosPlatform` +- `contactos_foundation/lib/src/` β€” `ContactosPluginFoundation` extends `ContactosPlatform` + +## Key Commands + +```bash +# Setup +make get # Get dependencies for all packages + +# Build & validate +make all # Full pipeline: format + check + test-unit +make precommit # Same as `make all` +make format # Format all packages (line length 80) +make analyze # Analyze all packages +make check # Analyze + pana for all packages +make test-unit # Unit tests for all packages +``` + +### Per-package + +```bash +cd contactos && make all +cd contactos_platform_interface && make all +cd contactos_android && make all +cd contactos_foundation && make all +``` + +## Conventions + +- **Commits**: Conventional Commits β€” `(github-): ` +- **Flutter version**: managed via FVM (see `.fvmrc`). Always prefix commands with `fvm` +- **Dart format**: line length **80**, enforced by `make format` +- **No `print()`** β€” use `dart:developer` (`dev.log`) +- **No `dynamic`** in JSON parsing β€” pattern matching + `switch`, errors β†’ `FormatException` +- **Immutability**: models are immutable, use `copyWith`, `const` constructors +- **Platform interface**: extend `ContactosPlatform`, do **not** implement it + +## Critical Rules + +- Before substantial work, read `CLAUDE.md` and `AGENTS.md` for full conventions. +- Never edit: `**/generated/**`, `*.g.dart`, `*.gen.dart`. +- When you've made a major change and completed a task, update the patch version in the relevant `pubspec.yaml` (e.g. `0.1.0` β†’ `0.1.1`) and add a note to the corresponding `CHANGELOG.md`. + +## Before Writing Code + +For trivial fixes (typos, one-line changes, simple renames), skip discussion and just do it. + +For anything non-trivial, do NOT start implementation until all open questions are resolved. First: + +1. **Challenge the approach** β€” point out flaws, missed edge cases, and risks. Be direct. +2. **Ask about unknowns** β€” if anything is ambiguous, ask. Do not guess or assume. +3. **Propose alternatives** β€” if there is a simpler or more robust way, say so and explain why. +4. **List edge cases** β€” enumerate what can break. +5. **Wait for confirmation** β€” do not write code until the user explicitly approves the plan. + +Do only what was asked. Do not refactor surrounding code, add comments to code you did not change, or introduce abstractions for hypothetical future needs. + +## After Writing Code + +Do not consider a task done until verified: + +- `make format` β€” no formatting issues +- `make analyze` β€” no analyzer warnings +- `make test-unit` β€” all tests pass + +If tests or analysis fail, fix the issue before reporting completion. + diff --git a/LICENSE b/LICENSE index 789dcc0..4de1fa1 100644 --- a/LICENSE +++ b/LICENSE @@ -1,18 +1,16 @@ BSD 3-Clause License -Copyright (c) 2025, Anton Ustinoff +Copyright (c) 2025, Anton Ustinoff All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: -1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. diff --git a/Makefile b/Makefile index 4819d91..00f7ea8 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,12 @@ SHELL :=/bin/bash -e -o pipefail PWD :=$(shell pwd) +# All packages in dependency order +PACKAGES := contactos_platform_interface contactos_android contactos_foundation contactos + .DEFAULT_GOAL := all .PHONY: all -all: ## build pipeline +all: ## Full pipeline: format + check + test-unit all: format check test-unit .PHONY: ci @@ -11,12 +14,12 @@ ci: ## CI build pipeline ci: all .PHONY: precommit -precommit: ## validate the branch before commit +precommit: ## Validate the branch before commit precommit: all .PHONY: help help: ## Help dialog - @echo 'Usage: make ' + @echo 'Usage: make ' @echo '' @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' @@ -29,55 +32,66 @@ version: ## Check flutter version @fvm flutter --version .PHONY: format -format: ## Format code - @fvm dart format -l 80 lib test || (echo "Β―\_(ツ)_/Β― Format code error"; exit 1) +format: ## Format all packages + @for pkg in $(PACKAGES); do \ + echo "Formatting $$pkg..."; \ + cd $(PWD)/$$pkg && fvm dart format -l 80 lib test || (echo "Β―\_(ツ)_/Β― Format $$pkg error"; exit 1); \ + done .PHONY: fix -fix: format ## Fix code - @fvm dart fix --apply lib +fix: ## Fix all packages + @for pkg in $(PACKAGES); do \ + echo "Fixing $$pkg..."; \ + cd $(PWD)/$$pkg && fvm dart fix --apply lib || (echo "Β―\_(ツ)_/Β― Fix $$pkg error"; exit 1); \ + done .PHONY: clean-cache clean-cache: ## Clean the pub cache @fvm flutter pub cache repair .PHONY: clean -clean: ## Clean flutter - @fvm flutter clean +clean: ## Clean all packages + @for pkg in $(PACKAGES); do \ + echo "Cleaning $$pkg..."; \ + cd $(PWD)/$$pkg && fvm flutter clean || true; \ + done .PHONY: get -get: ## Get dependencies - @cd contactos && fvm flutter pub get || (echo "Β―\_(ツ)_/Β― Get contactos dependencies error"; exit 1) - @cd contactos_platform_interface && fvm flutter pub get || (echo "Β―\_(ツ)_/Β― Get contactos_platform_interface dependencies error"; exit 2) +get: ## Get dependencies for all packages + @for pkg in $(PACKAGES); do \ + echo "Getting dependencies for $$pkg..."; \ + cd $(PWD)/$$pkg && fvm flutter pub get || (echo "Β―\_(ツ)_/Β― Get $$pkg dependencies error"; exit 1); \ + done .PHONY: analyze -analyze: get format ## Analyze code - @fvm dart analyze --fatal-infos --fatal-warnings +analyze: get ## Analyze all packages + @for pkg in $(PACKAGES); do \ + echo "Analyzing $$pkg..."; \ + cd $(PWD)/$$pkg && fvm dart analyze --fatal-infos --fatal-warnings || (echo "Β―\_(ツ)_/Β― Analyze $$pkg error"; exit 1); \ + done .PHONY: check -check: analyze ## Check the code - @dart pub global activate pana - @pana --json --no-warning --line-length 80 > log.pana.json +check: analyze ## Analyze + pana for all packages + @fvm dart pub global deactivate pana > /dev/null 2>&1 || true + @fvm dart pub global activate pana + @for pkg in $(PACKAGES); do \ + echo "Running pana for $$pkg..."; \ + cd $(PWD)/$$pkg && fvm dart pub global run pana --json > log.pana.json || (echo "Β―\_(ツ)_/Β― Pana $$pkg error"; exit 1); \ + done .PHONY: publish-check -publish-check: check ## Check the code before publish - @fvm dart pub publish --dry-run - -.PHONY: publish -publish: ## Publish package - @fvm dart pub publish --server=https://pub.dartlang.org || (echo "Β―\_(ツ)_/Β― Publish error"; exit 1) - -.PHONY: coverage -coverage: ## Runs get coverage - @lcov --summary coverage/lcov.info - -.PHONY: run-genhtml -run-genhtml: ## Runs generage coverage html - @genhtml coverage/lcov.info -o coverage/html +publish-check: ## Dry-run publish for all packages + @for pkg in $(PACKAGES); do \ + echo "Publish check $$pkg..."; \ + cd $(PWD)/$$pkg && fvm dart pub publish --dry-run || (echo "Β―\_(ツ)_/Β― Publish check $$pkg error"; exit 1); \ + done .PHONY: test-unit -test-unit: ## Runs unit tests - @fvm flutter test --coverage || (echo "Β―\_(ツ)_/Β― Error while running test-unit"; exit 1) - @genhtml coverage/lcov.info --output=coverage -o coverage/html || (echo "Β―\_(ツ)_/Β― Error while running genhtml with coverage"; exit 2) +test-unit: ## Run unit tests for all packages + @for pkg in $(PACKAGES); do \ + echo "Testing $$pkg..."; \ + cd $(PWD)/$$pkg && fvm flutter test --coverage || (echo "Β―\_(ツ)_/Β― Test $$pkg error"; exit 1); \ + done .PHONY: tag tag: ## Add a tag to the current commit @@ -96,7 +110,7 @@ tag-add: ## Add TAG. E.g: make tag-add TAG=v1.0.0 @echo "" .PHONY: tag-remove -tag-remove: ## Delete TAG. E.g: make tag-delete TAG=v1.0.0 +tag-remove: ## Delete TAG. E.g: make tag-remove TAG=v1.0.0 @if [ -z "$(TAG)" ]; then echo "Β―\_(ツ)_/Β― TAG is not set"; exit 1; fi @echo "" @echo "START REMOVING TAG: $(TAG)" diff --git a/README.md b/README.md index 22a5ee3..614aa38 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,71 @@ # Contactos Plugin for Flutter -[![pub package](https://img.shields.io/pub/v/contactos.svg)](https://pub.dartlang.org/packages/contactos) +[![pub package](https://img.shields.io/pub/v/contactos.svg)](https://pub.dev/packages/contactos) [![codecov](https://codecov.io/gh/ziqq/contactos/graph/badge.svg?token=S5CVNZKDAE)](https://codecov.io/gh/ziqq/contactos) [![style: flutter lints](https://img.shields.io/badge/style-flutter__lints-blue)](https://pub.dev/packages/flutter_lints) -## Description +## Description A Flutter plugin to access and manage the device's contacts. +This plugin uses the [federated plugin architecture](https://flutter.dev/go/federated-plugins). The main packages are: - +| Package | Description | Pub | +|---|---|---| +| [`contactos`](contactos/) | App-facing package | [![pub](https://img.shields.io/pub/v/contactos.svg)](https://pub.dev/packages/contactos) | +| [`contactos_platform_interface`](contactos_platform_interface/) | Platform interface | [![pub](https://img.shields.io/pub/v/contactos_platform_interface.svg)](https://pub.dev/packages/contactos_platform_interface) | +| [`contactos_android`](contactos_android/) | Android implementation | [![pub](https://img.shields.io/pub/v/contactos_android.svg)](https://pub.dev/packages/contactos_android) | +| [`contactos_foundation`](contactos_foundation/) | iOS implementation | [![pub](https://img.shields.io/pub/v/contactos_foundation.svg)](https://pub.dev/packages/contactos_foundation) | + + +## Package Structure + +The repository is a multi-package monorepo: + +``` +contactos/ +β”œβ”€β”€ contactos/ # App-facing package (what users depend on) +β”‚ β”œβ”€β”€ lib/ +β”‚ β”‚ β”œβ”€β”€ contactos.dart # Public API barrel file +β”‚ β”‚ └── src/ +β”‚ β”‚ β”œβ”€β”€ contactos.dart # Contactos class (delegates to platform) +β”‚ β”‚ └── contactos_legacy.dart. # Legacy API (deprecated) +β”‚ β”œβ”€β”€ example/ # Example app +β”‚ └── test/ +β”œβ”€β”€ contactos_platform_interface/ # Platform interface (abstract contract) +β”‚ β”œβ”€β”€ lib/ +β”‚ β”‚ β”œβ”€β”€ contactos_platform_interface.dart +β”‚ β”‚ └── src/ +β”‚ β”‚ β”œβ”€β”€ contactos_platform_interface.dart # ContactosPlatform abstract class +β”‚ β”‚ β”œβ”€β”€ method_channel_contactos.dart # MethodChannel implementation +β”‚ β”‚ └── types.dart # Contact, Contact$Field, Contact$PostalAddress, etc. +β”‚ └── test/ +β”œβ”€β”€ contactos_android/ # Android implementation +β”‚ β”œβ”€β”€ android/ # Native Android code (Java/Kotlin) +β”‚ β”œβ”€β”€ lib/ +β”‚ β”‚ └── src/ +β”‚ β”‚ └── contactos_android.dart # ContactosPluginAndroid +β”‚ └── test/ +β”œβ”€β”€ contactos_foundation/ # iOS (Darwin) implementation +β”‚ β”œβ”€β”€ darwin/ # Native iOS code (Swift/ObjC) +β”‚ β”œβ”€β”€ lib/ +β”‚ β”‚ └── src/ +β”‚ β”‚ └── contactos_foundation.dart # ContactosPluginFoundation +β”‚ └── test/ +β”œβ”€β”€ AGENTS.md # This file β€” agent conventions +β”œβ”€β”€ CLAUDE.md # Claude-specific instructions +β”œβ”€β”€ Makefile # Root-level make targets +└── .fvmrc # FVM Flutter version config +``` ## Installation -To use this plugin, add `contactos` as a [dependency in your `pubspec.yaml` file](https://flutter.io/platform-plugins/). +To use this plugin, add `contactos` as a [dependency in your `pubspec.yaml` file](https://flutter.dev/to/using-packages). For example: ```yaml dependencies: - contactos: ^1.0.6 + contactos: ^latest_version ``` @@ -39,7 +86,7 @@ Set the `NSContactsUsageDescription` in your `Info.plist` file. This app requires contacts access to function properly. ``` -And add PermissionGroup.contacts in your Podfile +And add PermissionGroup.contacts in your Podfile: ```Ruby target.build_configurations.each do |config| config.build_settings @@ -54,7 +101,7 @@ end **Note** -`contactos` does not handle the process of asking and checking for permissions. To check and request user permission to access contacts, try using the following plugins: [permission_handler](https://pub.dartlang.org/packages/permission_handler). +`contactos` does not handle the process of asking and checking for permissions. To check and request user permission to access contacts, try using the following plugins: [permission_handler](https://pub.dev/packages/permission_handler). If you do not request user permission or have it granted, the application will fail. For testing purposes, you can manually set the permissions for your test app in Settings for your app on the device that you are using. For Android, go to "Settings" - "Apps" - select your test app - "Permissions" - then turn "on" the slider for contacts. @@ -75,7 +122,7 @@ List contacts = await Contactos.instance.getContacts(withThumbnails: fa Uint8List avatar = await Contactos.instance.getAvatar(contact); // Get contacts matching a string. -List johns = await Contactos.instance.getContacts(query : "john"); +List johns = await Contactos.instance.getContacts(query: "john"); // Add a contact. // The contact must have a firstName / lastName to be successfully added. @@ -90,16 +137,15 @@ await Contactos.instance.deleteContact(contact); await Contactos.instance.updateContact(contact); // Usage of the native device form for creating a Contact. -// Throws a error if the Form could not be open or the Operation is canceled by the User. +// Throws an error if the form could not be opened or the operation is canceled by the user. await Contactos.instance.openContactForm(); // Usage of the native device form for editing a Contact. // The contact must have a valid identifier. -// Throws a error if the Form could not be open or the Operation is canceled by the User. +// Throws an error if the form could not be opened or the operation is canceled by the user. await Contactos.instance.openExistingContact(contact); - - ``` + **Contact Model** ```dart // Name @@ -121,12 +167,35 @@ List postalAddresses = []; Uint8List avatar; ``` -![Example](https://raw.githubusercontent.com/ziqq/contactos/refs/heads/main/.docs/example.gif "Example screenshot") +![Example](https://raw.githubusercontent.com/ziqq/contactos/refs/heads/main/.github/images/example.gif "Example screenshot") + + +## Development + +This project uses [FVM](https://fvm.app/) for Flutter version management. See `.fvmrc` for the configured version. + +```bash +# Get dependencies for all packages +make get + +# Full pipeline: format + check + test-unit +make all + +# Run before committing +make precommit +``` + +See each package's `Makefile` for per-package targets. ## Changelog -Refer to the [Changelog](https://github.com/ziqq/contactos/blob/main/CHANGELOG.md) to get all release notes. +Each package has its own changelog: + +- [contactos/CHANGELOG.md](contactos/CHANGELOG.md) +- [contactos_platform_interface/CHANGELOG.md](contactos_platform_interface/CHANGELOG.md) +- [contactos_android/CHANGELOG.md](contactos_android/CHANGELOG.md) +- [contactos_foundation/CHANGELOG.md](contactos_foundation/CHANGELOG.md) ## Maintainers @@ -143,7 +212,7 @@ Refer to the [Changelog](https://github.com/ziqq/contactos/blob/main/CHANGELOG.m Contributions are welcome! If you find a bug or want a feature, please fill an issue. -If you want to contribute code please create a pull request under the master branch. +If you want to contribute code please create a pull request. ## Funding @@ -151,10 +220,9 @@ If you want to contribute code please create a pull request under the master bra If you want to support the development of our library, there are several ways you can do it: - [Buy me a coffee](https://www.buymeacoffee.com/ziqq) -- [Support on Patreon](https://www.patreon.com/ziqq) - [Subscribe through Boosty](https://boosty.to/ziqq) -## Coverage +## Coverage - + diff --git a/contactos/CHANGELOG.md b/contactos/CHANGELOG.md index 6b708fd..7bdf460 100644 --- a/contactos/CHANGELOG.md +++ b/contactos/CHANGELOG.md @@ -1,3 +1,8 @@ +# Changelog + +## 2.0.0 +- **CHANGED**: Migrate to platform implementations + ## 1.0.1 - 1.0.6 - **CHANGED**: Refactoring - **FIXED**: Syntax error diff --git a/contactos/LICENSE b/contactos/LICENSE index 789dcc0..4de1fa1 100644 --- a/contactos/LICENSE +++ b/contactos/LICENSE @@ -1,18 +1,16 @@ BSD 3-Clause License -Copyright (c) 2025, Anton Ustinoff +Copyright (c) 2025, Anton Ustinoff All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: -1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. diff --git a/contactos/Makefile b/contactos/Makefile index d432654..d215fe2 100644 --- a/contactos/Makefile +++ b/contactos/Makefile @@ -50,16 +50,17 @@ get: ## Get dependencies .PHONY: analyze analyze: get format ## Analyze code - @fvm dart analyze --fatal-infos --fatal-warnings + @fvm dart analyze --fatal-infos --fatal-warnings || (echo "Β―\_(ツ)_/Β― Analyze code error"; exit 1) .PHONY: check check: analyze ## Check the code + @fvm dart pub global deactivate pana > /dev/null 2>&1 || true @fvm dart pub global activate pana - @pana --json --no-warning --line-length 80 > log.pana.json + @fvm dart pub global run pana --json > log.pana.json || (echo "Β―\_(ツ)_/Β― Pana analysis error"; exit 1) .PHONY: publish-check publish-check: check ## Check the code before publish - @fvm dart pub publish --dry-run + @fvm dart pub publish --dry-run || (echo "Β―\_(ツ)_/Β― Publish check error"; exit 1) .PHONY: publish publish: ## Publish package @@ -75,7 +76,7 @@ run-genhtml: ## Runs generage coverage html .PHONY: test-unit test-unit: ## Runs unit tests - @fvm flutter test --coverage || (echo "Β―\_(ツ)_/Β― Error while running test-unit"; exit 1) + @fvm flutter test --coverage test/contactos_test.dart || (echo "Β―\_(ツ)_/Β― Error while running test-unit"; exit 1) @genhtml coverage/lcov.info --output=coverage -o coverage/html || (echo "Β―\_(ツ)_/Β― Error while running genhtml with coverage"; exit 2) .PHONY: tag diff --git a/contactos/README.md b/contactos/README.md index 1a21d75..1c16bbc 100644 --- a/contactos/README.md +++ b/contactos/README.md @@ -1,5 +1,5 @@ # Contactos Plugin for Flutter -[![pub package](https://img.shields.io/pub/v/contactos.svg)](https://pub.dartlang.org/packages/contactos) +[![pub package](https://img.shields.io/pub/v/contactos.svg)](https://pub.dev/packages/contactos) [![codecov](https://codecov.io/gh/ziqq/contactos/graph/badge.svg?token=S5CVNZKDAE)](https://codecov.io/gh/ziqq/contactos) [![style: flutter lints](https://img.shields.io/badge/style-flutter__lints-blue)](https://pub.dev/packages/flutter_lints) @@ -14,11 +14,11 @@ A Flutter plugin to access and manage the device's contacts. ## Installation -To use this plugin, add `contactos` as a [dependency in your `pubspec.yaml` file](https://flutter.io/platform-plugins/). +To use this plugin, add `contactos` as a [dependency in your `pubspec.yaml` file](https://flutter.dev/to/using-packages). For example: ```yaml dependencies: - contactos: ^1.0.0 + contactos: ^latest_version ``` @@ -54,7 +54,7 @@ end **Note** -`contactos` does not handle the process of asking and checking for permissions. To check and request user permission to access contacts, try using the following plugins: [permission_handler](https://pub.dartlang.org/packages/permission_handler). +`contactos` does not handle the process of asking and checking for permissions. To check and request user permission to access contacts, try using the following plugins: [permission_handler](https://pub.dev/packages/permission_handler). If you do not request user permission or have it granted, the application will fail. For testing purposes, you can manually set the permissions for your test app in Settings for your app on the device that you are using. For Android, go to "Settings" - "Apps" - select your test app - "Permissions" - then turn "on" the slider for contacts. @@ -75,7 +75,7 @@ List contacts = await Contactos.instance.getContacts(withThumbnails: fa Uint8List avatar = await Contactos.instance.getAvatar(contact); // Get contacts matching a string. -List johns = await Contactos.instance.getContacts(query : "john"); +List johns = await Contactos.instance.getContacts(query: "john"); // Add a contact. // The contact must have a firstName / lastName to be successfully added. @@ -90,15 +90,14 @@ await Contactos.instance.deleteContact(contact); await Contactos.instance.updateContact(contact); // Usage of the native device form for creating a Contact. -// Throws a error if the Form could not be open or the Operation is canceled by the User. +// Throws an error if the form could not be opened or the operation is canceled by the user. await Contactos.instance.openContactForm(); // Usage of the native device form for editing a Contact. // The contact must have a valid identifier. -// Throws a error if the Form could not be open or the Operation is canceled by the User. +// Throws an error if the form could not be opened or the operation is canceled by the user. await Contactos.instance.openExistingContact(contact); - ``` **Contact Model** ```dart @@ -121,12 +120,12 @@ List postalAddresses = []; Uint8List avatar; ``` -![Example](https://raw.githubusercontent.com/ziqq/contactos/refs/heads/main/.docs/example.gif "Example screenshot") +![Example](https://raw.githubusercontent.com/ziqq/contactos/refs/heads/main/.github/images/example.gif "Example screenshot") ## Changelog -Refer to the [Changelog](https://github.com/ziqq/contactos/blob/main/CHANGELOG.md) to get all release notes. +Refer to the [Changelog](https://github.com/ziqq/contactos/blob/main/contactos/CHANGELOG.md) to get all release notes. ## Maintainers @@ -142,8 +141,7 @@ Refer to the [Changelog](https://github.com/ziqq/contactos/blob/main/CHANGELOG.m ## Contributions Contributions are welcome! If you find a bug or want a feature, please fill an issue. - -If you want to contribute code please create a pull request under the master branch. +If you want to contribute code please create a pull request. ## Funding @@ -151,10 +149,9 @@ If you want to contribute code please create a pull request under the master bra If you want to support the development of our library, there are several ways you can do it: - [Buy me a coffee](https://www.buymeacoffee.com/ziqq) -- [Support on Patreon](https://www.patreon.com/ziqq) - [Subscribe through Boosty](https://boosty.to/ziqq) -## Coverage +## Coverage - + diff --git a/contactos/example/lib/src/screens/contacts_list_screen.dart b/contactos/example/lib/src/screens/contacts_list_screen.dart index cda10b1..2f78c82 100644 --- a/contactos/example/lib/src/screens/contacts_list_screen.dart +++ b/contactos/example/lib/src/screens/contacts_list_screen.dart @@ -1,3 +1,8 @@ +/* + * Author: Anton Ustinoff | + * Date: 25 November 2025 + */ + import 'dart:developer'; import 'package:collection/collection.dart'; @@ -447,7 +452,7 @@ class _AddContactScreenState extends State { class _UpdateContactsPage extends StatefulWidget { const _UpdateContactsPage({ - @required this.contact, + required this.contact, super.key, // ignore: unused_element_parameter }); diff --git a/contactos/example/lib/src/screens/navite_contacts_picker_screen.dart b/contactos/example/lib/src/screens/navite_contacts_picker_screen.dart index 825e9da..5454dbf 100644 --- a/contactos/example/lib/src/screens/navite_contacts_picker_screen.dart +++ b/contactos/example/lib/src/screens/navite_contacts_picker_screen.dart @@ -1,3 +1,8 @@ +/* + * Author: Anton Ustinoff | + * Date: 26 November 2025 + */ + import 'dart:developer'; import 'package:contactos/contactos.dart'; diff --git a/contactos/example/pubspec.lock b/contactos/example/pubspec.lock index 676fbb8..65c6b70 100644 --- a/contactos/example/pubspec.lock +++ b/contactos/example/pubspec.lock @@ -21,10 +21,10 @@ packages: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" clock: dependency: transitive description: @@ -47,15 +47,31 @@ packages: path: ".." relative: true source: path - version: "1.0.5" + version: "2.0.0" + contactos_android: + dependency: transitive + description: + name: contactos_android + sha256: "9847cb2a2955c02e793dc9af803ec8130b458e6e95ec859b860431f38d7526a5" + url: "https://pub.dev" + source: hosted + version: "0.0.1" + contactos_foundation: + dependency: transitive + description: + name: contactos_foundation + sha256: "8dbcf1dd698e9776e12f23635b6a2b73fc9fcea936113b3079d6081f5d40f534" + url: "https://pub.dev" + source: hosted + version: "0.0.2" contactos_platform_interface: dependency: "direct main" description: name: contactos_platform_interface - sha256: "91cb1912e5b6f5126d43e1eb21a8385f837e6b637642449a526fc60038214529" + sha256: "6fc797fa685c84ef7e1ec8fc8c6aae1f9f09274bcc5ea26e435781a4fa907628" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.0.1" cupertino_icons: dependency: "direct main" description: @@ -122,50 +138,50 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" url: "https://pub.dev" source: hosted - version: "10.0.9" + version: "11.0.2" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" url: "https://pub.dev" source: hosted - version: "3.0.9" + version: "3.0.10" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" matcher: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.18" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: transitive description: name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" path: dependency: transitive description: @@ -303,18 +319,18 @@ packages: dependency: transitive description: name: test_api - sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" url: "https://pub.dev" source: hosted - version: "0.7.4" + version: "0.7.9" vector_math: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" vm_service: dependency: transitive description: @@ -340,5 +356,5 @@ packages: source: hosted version: "3.1.0" sdks: - dart: ">=3.7.0-0 <4.0.0" + dart: ">=3.9.0-0 <4.0.0" flutter: ">=3.29.3" diff --git a/contactos/ios/contactos/Sources/contactos/ContactosPlugin.h b/contactos/ios/contactos/Sources/contactos/ContactosPlugin.h deleted file mode 100644 index 1354260..0000000 --- a/contactos/ios/contactos/Sources/contactos/ContactosPlugin.h +++ /dev/null @@ -1,4 +0,0 @@ -#import - -@interface ContactosPlugin : NSObject -@end diff --git a/contactos/ios/contactos/Sources/contactos/ContactosPlugin.m b/contactos/ios/contactos/Sources/contactos/ContactosPlugin.m deleted file mode 100644 index 509f4ab..0000000 --- a/contactos/ios/contactos/Sources/contactos/ContactosPlugin.m +++ /dev/null @@ -1,13 +0,0 @@ -#import "ContactosPlugin.h" - -#if __has_include() -#import -#else -#import "contactos-Swift.h" -#endif - -@implementation ContactosPlugin -+ (void)registerWithRegistrar:(NSObject*)registrar { - [SwiftContactosPlugin registerWithRegistrar:registrar]; -} -@end diff --git a/contactos/lib/src/contactos.dart b/contactos/lib/src/contactos.dart index fcd56d6..4132218 100644 --- a/contactos/lib/src/contactos.dart +++ b/contactos/lib/src/contactos.dart @@ -2,54 +2,51 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// ignore_for_file: sort_constructors_first + import 'package:contactos_platform_interface/contactos_platform_interface.dart'; import 'package:flutter/foundation.dart'; +/// {@template contactos} /// The iOS implementation of [ContactosPlatform]. /// /// This class implements the `package:contactos` functionality for iOS. +/// {@endtemplate} class Contactos extends ContactosPlatform { - Contactos._({@visibleForTesting MethodChannelContactos? channel}) - : channel = channel ?? MethodChannelContactos.instance, - super(); + /// Use [SharePlus.instance] to access the [share] method. + /// {@macro contactos} + Contactos._(this._platform); - /// Underlying channel-based implementation. - final MethodChannelContactos channel; + /// Platform interface + final ContactosPlatform _platform; + /// Singleton instance (instance API). static Contactos? _instance; - /// Returns the [Contactos] singleton instance. - /// Also registers this as the default platform implementation. - // ignore: prefer_constructors_over_static_methods - static Contactos get instance => _instance ??= Contactos._(); - - /// Registers this class as the default instance of [Contactos]. - static void registerWith() { - ContactosPlatform.instance = Contactos.instance; - } + /// The default instance of [Contactos]. + static final Contactos instance = + _instance ??= Contactos._(ContactosPlatform.instance); - @protected - @override - ContactosPlatform delegateFor({ - required MethodChannelContactos channel, - }) => - Contactos._(channel: channel); + /// Create a custom instance of [Contactos]. + /// Use this constructor for testing purposes only. + @visibleForTesting + factory Contactos.custom(ContactosPlatform platform) => Contactos._(platform); @override - Future addContact(Contact c) => channel.addContact(c); + Future addContact(Contact c) => _platform.addContact(c); @override - Future deleteContact(Contact c) => channel.deleteContact(c); + Future deleteContact(Contact c) => _platform.deleteContact(c); @override - Future updateContact(Contact c) => channel.updateContact(c); + Future updateContact(Contact c) => _platform.updateContact(c); @override Future getAvatar( Contact contact, { bool photoHighRes = true, }) => - channel.getAvatar(contact, photoHighRes: photoHighRes); + _platform.getAvatar(contact, photoHighRes: photoHighRes); @override Future> getContacts({ @@ -60,7 +57,7 @@ class Contactos extends ContactosPlatform { bool iOSLocalizedLabels = true, bool androidLocalizedLabels = true, }) => - channel.getContacts( + _platform.getContacts( query: query, withThumbnails: withThumbnails, photoHighResolution: photoHighResolution, @@ -78,7 +75,7 @@ class Contactos extends ContactosPlatform { bool iOSLocalizedLabels = true, bool androidLocalizedLabels = true, }) => - channel.getContactsForEmail( + _platform.getContactsForEmail( email, withThumbnails: withThumbnails, photoHighResolution: photoHighResolution, @@ -96,7 +93,7 @@ class Contactos extends ContactosPlatform { bool iOSLocalizedLabels = true, bool androidLocalizedLabels = true, }) => - channel.getContactsForPhone( + _platform.getContactsForPhone( phone, withThumbnails: withThumbnails, photoHighResolution: photoHighResolution, @@ -110,7 +107,7 @@ class Contactos extends ContactosPlatform { bool iOSLocalizedLabels = true, bool androidLocalizedLabels = true, }) => - channel.openContactForm( + _platform.openContactForm( iOSLocalizedLabels: iOSLocalizedLabels, androidLocalizedLabels: androidLocalizedLabels, ); @@ -120,7 +117,7 @@ class Contactos extends ContactosPlatform { bool iOSLocalizedLabels = true, bool androidLocalizedLabels = true, }) => - channel.openDeviceContactPicker( + _platform.openDeviceContactPicker( iOSLocalizedLabels: iOSLocalizedLabels, androidLocalizedLabels: androidLocalizedLabels, ); @@ -131,7 +128,7 @@ class Contactos extends ContactosPlatform { bool iOSLocalizedLabels = true, bool androidLocalizedLabels = true, }) => - channel.openExistingContact( + _platform.openExistingContact( contact, iOSLocalizedLabels: iOSLocalizedLabels, androidLocalizedLabels: androidLocalizedLabels, diff --git a/contactos/lib/src/contactos_legacy.dart b/contactos/lib/src/contactos_legacy.dart new file mode 100644 index 0000000..fcd56d6 --- /dev/null +++ b/contactos/lib/src/contactos_legacy.dart @@ -0,0 +1,139 @@ +// Copyright 2025 Anton Ustinoff. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:contactos_platform_interface/contactos_platform_interface.dart'; +import 'package:flutter/foundation.dart'; + +/// The iOS implementation of [ContactosPlatform]. +/// +/// This class implements the `package:contactos` functionality for iOS. +class Contactos extends ContactosPlatform { + Contactos._({@visibleForTesting MethodChannelContactos? channel}) + : channel = channel ?? MethodChannelContactos.instance, + super(); + + /// Underlying channel-based implementation. + final MethodChannelContactos channel; + + static Contactos? _instance; + + /// Returns the [Contactos] singleton instance. + /// Also registers this as the default platform implementation. + // ignore: prefer_constructors_over_static_methods + static Contactos get instance => _instance ??= Contactos._(); + + /// Registers this class as the default instance of [Contactos]. + static void registerWith() { + ContactosPlatform.instance = Contactos.instance; + } + + @protected + @override + ContactosPlatform delegateFor({ + required MethodChannelContactos channel, + }) => + Contactos._(channel: channel); + + @override + Future addContact(Contact c) => channel.addContact(c); + + @override + Future deleteContact(Contact c) => channel.deleteContact(c); + + @override + Future updateContact(Contact c) => channel.updateContact(c); + + @override + Future getAvatar( + Contact contact, { + bool photoHighRes = true, + }) => + channel.getAvatar(contact, photoHighRes: photoHighRes); + + @override + Future> getContacts({ + String? query, + bool withThumbnails = true, + bool photoHighResolution = true, + bool orderByGivenName = true, + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) => + channel.getContacts( + query: query, + withThumbnails: withThumbnails, + photoHighResolution: photoHighResolution, + orderByGivenName: orderByGivenName, + iOSLocalizedLabels: iOSLocalizedLabels, + androidLocalizedLabels: androidLocalizedLabels, + ); + + @override + Future> getContactsForEmail( + String email, { + bool withThumbnails = true, + bool photoHighResolution = true, + bool orderByGivenName = true, + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) => + channel.getContactsForEmail( + email, + withThumbnails: withThumbnails, + photoHighResolution: photoHighResolution, + orderByGivenName: orderByGivenName, + iOSLocalizedLabels: iOSLocalizedLabels, + androidLocalizedLabels: androidLocalizedLabels, + ); + + @override + Future> getContactsForPhone( + String? phone, { + bool withThumbnails = true, + bool photoHighResolution = true, + bool orderByGivenName = true, + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) => + channel.getContactsForPhone( + phone, + withThumbnails: withThumbnails, + photoHighResolution: photoHighResolution, + orderByGivenName: orderByGivenName, + iOSLocalizedLabels: iOSLocalizedLabels, + androidLocalizedLabels: androidLocalizedLabels, + ); + + @override + Future openContactForm({ + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) => + channel.openContactForm( + iOSLocalizedLabels: iOSLocalizedLabels, + androidLocalizedLabels: androidLocalizedLabels, + ); + + @override + Future openDeviceContactPicker({ + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) => + channel.openDeviceContactPicker( + iOSLocalizedLabels: iOSLocalizedLabels, + androidLocalizedLabels: androidLocalizedLabels, + ); + + @override + Future openExistingContact( + Contact contact, { + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) => + channel.openExistingContact( + contact, + iOSLocalizedLabels: iOSLocalizedLabels, + androidLocalizedLabels: androidLocalizedLabels, + ); +} diff --git a/contactos/pubspec.lock b/contactos/pubspec.lock index d2a1da7..2ef8d26 100644 --- a/contactos/pubspec.lock +++ b/contactos/pubspec.lock @@ -21,10 +21,10 @@ packages: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" clock: dependency: transitive description: @@ -41,14 +41,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" + contactos_android: + dependency: "direct main" + description: + name: contactos_android + sha256: "9847cb2a2955c02e793dc9af803ec8130b458e6e95ec859b860431f38d7526a5" + url: "https://pub.dev" + source: hosted + version: "0.0.1" + contactos_foundation: + dependency: "direct main" + description: + name: contactos_foundation + sha256: "8dbcf1dd698e9776e12f23635b6a2b73fc9fcea936113b3079d6081f5d40f534" + url: "https://pub.dev" + source: hosted + version: "0.0.2" contactos_platform_interface: dependency: "direct main" description: name: contactos_platform_interface - sha256: "91cb1912e5b6f5126d43e1eb21a8385f837e6b637642449a526fc60038214529" + sha256: "6fc797fa685c84ef7e1ec8fc8c6aae1f9f09274bcc5ea26e435781a4fa907628" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.0.1" fake_async: dependency: transitive description: @@ -57,24 +73,47 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.3" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" flutter: dependency: "direct main" description: flutter source: sdk version: "0.0.0" + flutter_driver: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" flutter_lints: dependency: "direct dev" description: name: flutter_lints - sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "6.0.0" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" + fuchsia_remote_debug_protocol: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + integration_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" l: dependency: "direct dev" description: @@ -87,58 +126,58 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" url: "https://pub.dev" source: hosted - version: "10.0.9" + version: "11.0.2" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" url: "https://pub.dev" source: hosted - version: "3.0.9" + version: "3.0.10" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" lints: dependency: transitive description: name: lints - sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 + sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0 url: "https://pub.dev" source: hosted - version: "5.1.1" + version: "6.0.0" matcher: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.18" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: transitive description: name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" path: dependency: transitive description: @@ -147,14 +186,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" - plugin_platform_interface: + platform: dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: "direct dev" description: name: plugin_platform_interface sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" url: "https://pub.dev" source: hosted version: "2.1.8" + process: + dependency: transitive + description: + name: process + sha256: c6248e4526673988586e8c00bb22a49210c258dc91df5227d5da9748ecf79744 + url: "https://pub.dev" + source: hosted + version: "5.0.5" sky_engine: dependency: transitive description: flutter @@ -192,6 +247,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" + sync_http: + dependency: transitive + description: + name: sync_http + sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961" + url: "https://pub.dev" + source: hosted + version: "0.3.1" term_glyph: dependency: transitive description: @@ -204,18 +267,18 @@ packages: dependency: transitive description: name: test_api - sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" url: "https://pub.dev" source: hosted - version: "0.7.4" + version: "0.7.9" vector_math: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" vm_service: dependency: transitive description: @@ -224,6 +287,14 @@ packages: url: "https://pub.dev" source: hosted version: "15.0.0" + webdriver: + dependency: transitive + description: + name: webdriver + sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade" + url: "https://pub.dev" + source: hosted + version: "3.1.0" sdks: - dart: ">=3.7.0-0 <4.0.0" + dart: ">=3.9.0-0 <4.0.0" flutter: ">=3.29.3" diff --git a/contactos/pubspec.yaml b/contactos/pubspec.yaml index 41f98c6..45cff4e 100644 --- a/contactos/pubspec.yaml +++ b/contactos/pubspec.yaml @@ -1,37 +1,65 @@ name: contactos -description: Android and iOS implementation of the contactos plugin. +description: Plugin to access and manage contacts on Android and iOS. + +version: 2.0.0 + +homepage: https://github.com/ziqq/contactos + repository: https://github.com/ziqq/contactos/contactos + issue_tracker: https://github.com/ziqq/contactos/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+contactos%22 -version: 1.0.6 + +#screenshots: +# - description: 'Example of using the library to get contacts.' +# path: example.png + +funding: + - https://www.buymeacoffee.com/ziqq + - https://boosty.to/ziqq + +platforms: + android: + ios: + +topics: + - contacts + environment: sdk: '>=3.6.0 <4.0.0' flutter: ">=3.29.3" -flutter: - plugin: - implements: contactos - platforms: - android: - package: flutter.plugins.contactos - pluginClass: ContactosPlugin - dartPluginClass: Contactos - ios: - pluginClass: ContactosPlugin - dartPluginClass: Contactos dependencies: flutter: sdk: flutter - contactos_platform_interface: ^1.0.0 + contactos_android: ^0.0.1 + # contactos_android: + # path: ../contactos_android + + contactos_foundation: ^0.0.2 + # contactos_foundation: + # path: ../contactos_foundation + + contactos_platform_interface: ^1.0.1 + # contactos_platform_interface: + # path: ../contactos_platform_interface + dev_dependencies: + # Integration tests for Flutter + integration_test: + sdk: flutter + # Unit & Widget tests for Flutter flutter_test: sdk: flutter + # Testsing + plugin_platform_interface: ^2.1.8 + # Linting - flutter_lints: ^5.0.0 + flutter_lints: ^6.0.0 # Utilities collection: ^1.19.1 @@ -39,5 +67,11 @@ dev_dependencies: # Logging l: ^5.0.0 -topics: - - contacts + +flutter: + plugin: + platforms: + android: + default_package: contactos_android + ios: + default_package: contactos_foundation \ No newline at end of file diff --git a/contactos/test/contactos_test.dart b/contactos/test/contactos_test.dart index bae9480..413c608 100644 --- a/contactos/test/contactos_test.dart +++ b/contactos/test/contactos_test.dart @@ -1,323 +1,14 @@ -import 'package:contactos/contactos.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() => group( - 'Unit_tests -', - () => group('Contactos -', () { - const channel = MethodChannel('github.com/ziqq/contactos'); - - final log = []; - - setUp(() { - TestWidgetsFlutterBinding.ensureInitialized(); - TestWidgetsFlutterBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler( - channel, - (methodCall) async { - log.add(methodCall); - switch (methodCall.method) { - case 'getContacts': - case 'getContactsForPhone': - case 'getContactsForEmail': - return [ - {'givenName': 'givenName1'}, - { - 'givenName': 'givenName2', - 'postalAddresses': [ - {'label': 'label'} - ], - 'emails': [ - {'label': 'label'} - ], - 'birthday': '1994-02-01' - }, - ]; - case 'getAvatar': - return Uint8List.fromList([0, 1, 2, 3]); - default: - return null; - } - }, - ); - }); - - tearDown(() { - log.clear(); - TestWidgetsFlutterBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(channel, null); - }); - - test('should get contacts', () async { - final contacts = await Contactos.instance.getContacts(); - expect(contacts.length, 2); - expect(contacts, everyElement(isInstanceOf())); - expect(contacts[0].givenName, 'givenName1'); - expect(contacts[1].postalAddresses![0].label, 'label'); - expect(contacts[1].emails![0].label, 'label'); - expect(contacts[1].birthday, DateTime(1994, 2, 1)); - }); - - test('should get avatar for contact identifiers', () async { - const contact = Contact(givenName: 'givenName'); - - final avatar = await Contactos.instance.getAvatar(contact); - - expect(log, [ - isMethodCall('getAvatar', arguments: { - 'contact': contact.toJson(), - 'identifier': contact.identifier, - 'photoHighResolution': true, - }) - ]); - - expect(avatar, Uint8List.fromList([0, 1, 2, 3])); - }); - - group('getContactsForPhone', () { - test('returns empty list when no phone number specified', () async { - final contacts = await Contactos.instance.getContactsForPhone(null); - expect(contacts.length, equals(0)); - }); - - /// Tests whether phone number argument is not null and plugin call is - /// fired. - /// - /// Whether contact is returned or not depends on the plaform - /// implementation which cannot be tested in unit tests. - test('returns contacts if phone number supplied', () async { - final contacts = - await Contactos.instance.getContactsForPhone('1234567890'); - expect(contacts.length, equals(2)); - expect(contacts[0].givenName, 'givenName1'); - expect(contacts[1].givenName, 'givenName2'); - }); - }); - - group('getContactsForEmail', () { - /// Just tests whether the plugin call is fired. - test('returns contacts when email is supplied', () async { - final contacts = await Contactos.instance.getContactsForEmail( - 'abc@example.net', - ); - expect(contacts.length, equals(2)); - expect(contacts[0].givenName, 'givenName1'); - expect(contacts[1].givenName, 'givenName2'); - }); - }); +/* + * Author: Anton Ustinoff | + * Date: 26 November 2025 + */ - test('should get low-res avatar for contact identifiers', () async { - const contact = Contact(givenName: 'givenName'); - - await Contactos.instance.getAvatar(contact, photoHighRes: false); - - expect(log, [ - isMethodCall('getAvatar', arguments: { - 'contact': contact.toJson(), - 'identifier': contact.identifier, - 'photoHighResolution': false, - }) - ]); - }); - - test('should add contact', () async { - await Contactos.instance.addContact(const Contact( - givenName: 'givenName', - emails: [Contact$Field(label: 'label')], - phones: [Contact$Field(label: 'label')], - postalAddresses: [Contact$PostalAddress(label: 'label')], - )); - _expectMethodCall(log, 'addContact'); - }); - - test('should delete contact', () async { - await Contactos.instance.deleteContact(const Contact( - givenName: 'givenName', - emails: [Contact$Field(label: 'label')], - phones: [Contact$Field(label: 'label')], - postalAddresses: [Contact$PostalAddress(label: 'label')], - )); - _expectMethodCall(log, 'deleteContact'); - }); - - test('should provide initials for contact', () { - var contact1 = const Contact( - givenName: 'givenName', - familyName: 'familyName', - ); - var contact2 = const Contact(givenName: 'givenName'); - var contact3 = const Contact(familyName: 'familyName'); - var contact4 = const Contact(); - - expect(contact1.initials(), 'GF'); - expect(contact2.initials(), 'G'); - expect(contact3.initials(), 'F'); - expect(contact4.initials(), ''); - }); - - test('should update contact', () async { - await Contactos.instance.updateContact(const Contact( - givenName: 'givenName', - emails: [Contact$Field(label: 'label')], - phones: [Contact$Field(label: 'label')], - postalAddresses: [Contact$PostalAddress(label: 'label')], - )); - _expectMethodCall(log, 'updateContact'); - }); - - test('should show contacts are equal', () { - var contact1 = const Contact( - givenName: 'givenName', - familyName: 'familyName', - emails: [ - Contact$Field(label: 'Home', value: 'home@example.com'), - Contact$Field(label: 'Work', value: 'work@example.com'), - ], - ); - var contact2 = const Contact( - givenName: 'givenName', - familyName: 'familyName', - emails: [ - Contact$Field(label: 'Work', value: 'work@example.com'), - Contact$Field(label: 'Home', value: 'home@example.com'), - ], - ); - expect(contact1 == contact2, true); - expect(contact1.hashCode, contact2.hashCode); - }); - - test('should show contacts are not equal', () { - var contact1 = const Contact( - givenName: 'givenName', - familyName: 'familyName', - emails: [ - Contact$Field(label: 'Home', value: 'home@example.com'), - Contact$Field(label: 'Work', value: 'work@example.com'), - ]); - var contact2 = const Contact( - givenName: 'givenName', - familyName: 'familyName', - emails: [ - Contact$Field(label: 'Office', value: 'office@example.com'), - Contact$Field(label: 'Home', value: 'home@example.com'), - ]); - expect(contact1 == contact2, false); - expect(contact1.hashCode, contact2.hashCode); - }); - - test('should produce a valid merged contact', () { - var contact1 = const Contact( - givenName: 'givenName', - familyName: 'familyName', - emails: [ - Contact$Field(label: 'Home', value: 'home@example.com'), - Contact$Field(label: 'Work', value: 'work@example.com'), - ], - phones: [], - postalAddresses: []); - var contact2 = const Contact(familyName: 'familyName', phones: [ - Contact$Field(label: 'Mobile', value: '111-222-3344') - ], emails: [ - Contact$Field(label: 'Mobile', value: 'mobile@example.com'), - ], postalAddresses: [ - Contact$PostalAddress( - label: 'Home', - street: '1234 Middle-of Rd', - city: 'Nowhere', - postcode: '12345', - region: null, - country: null) - ]); - var mergedContact = const Contact( - givenName: 'givenName', - familyName: 'familyName', - emails: [ - Contact$Field(label: 'Home', value: 'home@example.com'), - Contact$Field(label: 'Mobile', value: 'mobile@example.com'), - Contact$Field(label: 'Work', value: 'work@example.com'), - ], - phones: [ - Contact$Field(label: 'Mobile', value: '111-222-3344') - ], - postalAddresses: [ - Contact$PostalAddress( - label: 'Home', - street: '1234 Middle-of Rd', - city: 'Nowhere', - postcode: '12345', - region: null, - country: null) - ]); - - expect(contact1 + contact2, mergedContact); - }); - - test('should provide a valid merged contact, with no extra info', () { - var contact1 = const Contact(familyName: 'familyName'); - var contact2 = const Contact(); - expect(contact1 + contact2, contact1); - }); +import 'package:flutter_test/flutter_test.dart'; - test('should provide a map of the contact', () { - var contact = - const Contact(givenName: 'givenName', familyName: 'familyName'); - expect(contact.toJson(), { - 'identifier': null, - 'displayName': null, - 'givenName': 'givenName', - 'middleName': null, - 'familyName': 'familyName', - 'prefix': null, - 'suffix': null, - 'company': null, - 'jobTitle': null, - 'androidAccountType': null, - 'androidAccountName': null, - 'emails': const [], - 'phones': const [], - 'postalAddresses': const [], - 'avatar': null, - 'birthday': null - }); - }); - }), - ); +import 'src//contactos_legacy_test.dart' as contactos_legacy_test; +import 'src/contactos_test.dart' as contactos_test; -void _expectMethodCall(List log, String methodName) { - expect(log, [ - isMethodCall( - methodName, - arguments: { - 'identifier': null, - 'displayName': null, - 'givenName': 'givenName', - 'middleName': null, - 'familyName': null, - 'prefix': null, - 'suffix': null, - 'company': null, - 'jobTitle': null, - 'androidAccountType': null, - 'androidAccountName': null, - 'emails': [ - {'label': 'label', 'value': null} - ], - 'phones': [ - {'label': 'label', 'value': null} - ], - 'postalAddresses': [ - { - 'label': 'label', - 'street': null, - 'city': null, - 'postcode': null, - 'region': null, - 'country': null - } - ], - 'avatar': null, - 'birthday': null - }, - ), - ]); -} +void main() => group('ContactosPlugin -', () { + contactos_legacy_test.main(); + contactos_test.main(); + }); diff --git a/contactos/test/src/contactos_legacy_test.dart b/contactos/test/src/contactos_legacy_test.dart new file mode 100644 index 0000000..3775024 --- /dev/null +++ b/contactos/test/src/contactos_legacy_test.dart @@ -0,0 +1,323 @@ +// Copyright 2025 Anton Ustinoff. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:contactos/contactos.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() => group('ContactosLegacy -', () { + const channel = MethodChannel('github.com/ziqq/contactos'); + final log = []; + + setUp(() { + TestWidgetsFlutterBinding.ensureInitialized(); + TestWidgetsFlutterBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler( + channel, + (methodCall) async { + log.add(methodCall); + switch (methodCall.method) { + case 'getContacts': + case 'getContactsForPhone': + case 'getContactsForEmail': + return [ + {'givenName': 'givenName1'}, + { + 'givenName': 'givenName2', + 'postalAddresses': [ + {'label': 'label'} + ], + 'emails': [ + {'label': 'label'} + ], + 'birthday': '1994-02-01' + }, + ]; + case 'getAvatar': + return Uint8List.fromList([0, 1, 2, 3]); + default: + return null; + } + }, + ); + }); + + tearDown(() { + log.clear(); + TestWidgetsFlutterBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, null); + }); + + test('should get contacts', () async { + final contacts = await Contactos.instance.getContacts(); + expect(contacts.length, 2); + expect(contacts, everyElement(isInstanceOf())); + expect(contacts[0].givenName, 'givenName1'); + expect(contacts[1].postalAddresses![0].label, 'label'); + expect(contacts[1].emails![0].label, 'label'); + expect(contacts[1].birthday, DateTime(1994, 2, 1)); + }); + + test('should get avatar for contact identifiers', () async { + const contact = Contact(givenName: 'givenName'); + + final avatar = await Contactos.instance.getAvatar(contact); + + expect(log, [ + isMethodCall('getAvatar', arguments: { + 'contact': contact.toJson(), + 'identifier': contact.identifier, + 'photoHighResolution': true, + }) + ]); + + expect(avatar, Uint8List.fromList([0, 1, 2, 3])); + }); + + group('getContactsForPhone', () { + test('returns empty list when no phone number specified', () async { + final contacts = await Contactos.instance.getContactsForPhone(null); + expect(contacts.length, equals(0)); + }); + + /// Tests whether phone number argument is not null and plugin call is + /// fired. + /// + /// Whether contact is returned or not depends on the plaform + /// implementation which cannot be tested in unit tests. + test('returns contacts if phone number supplied', () async { + final contacts = + await Contactos.instance.getContactsForPhone('1234567890'); + expect(contacts.length, equals(2)); + expect(contacts[0].givenName, 'givenName1'); + expect(contacts[1].givenName, 'givenName2'); + }); + }); + + group('getContactsForEmail', () { + /// Just tests whether the plugin call is fired. + test('returns contacts when email is supplied', () async { + final contacts = await Contactos.instance.getContactsForEmail( + 'abc@example.net', + ); + expect(contacts.length, equals(2)); + expect(contacts[0].givenName, 'givenName1'); + expect(contacts[1].givenName, 'givenName2'); + }); + }); + + test('should get low-res avatar for contact identifiers', () async { + const contact = Contact(givenName: 'givenName'); + + await Contactos.instance.getAvatar(contact, photoHighRes: false); + + expect(log, [ + isMethodCall('getAvatar', arguments: { + 'contact': contact.toJson(), + 'identifier': contact.identifier, + 'photoHighResolution': false, + }) + ]); + }); + + test('should add contact', () async { + await Contactos.instance.addContact(const Contact( + givenName: 'givenName', + emails: [Contact$Field(label: 'label')], + phones: [Contact$Field(label: 'label')], + postalAddresses: [Contact$PostalAddress(label: 'label')], + )); + _expectMethodCall(log, 'addContact'); + }); + + test('should delete contact', () async { + await Contactos.instance.deleteContact(const Contact( + givenName: 'givenName', + emails: [Contact$Field(label: 'label')], + phones: [Contact$Field(label: 'label')], + postalAddresses: [Contact$PostalAddress(label: 'label')], + )); + _expectMethodCall(log, 'deleteContact'); + }); + + test('should provide initials for contact', () { + var contact1 = const Contact( + givenName: 'givenName', + familyName: 'familyName', + ); + var contact2 = const Contact(givenName: 'givenName'); + var contact3 = const Contact(familyName: 'familyName'); + var contact4 = const Contact(); + + expect(contact1.initials(), 'GF'); + expect(contact2.initials(), 'G'); + expect(contact3.initials(), 'F'); + expect(contact4.initials(), ''); + }); + + test('should update contact', () async { + await Contactos.instance.updateContact(const Contact( + givenName: 'givenName', + emails: [Contact$Field(label: 'label')], + phones: [Contact$Field(label: 'label')], + postalAddresses: [Contact$PostalAddress(label: 'label')], + )); + _expectMethodCall(log, 'updateContact'); + }); + + test('should show contacts are equal', () { + var contact1 = const Contact( + givenName: 'givenName', + familyName: 'familyName', + emails: [ + Contact$Field(label: 'Home', value: 'home@example.com'), + Contact$Field(label: 'Work', value: 'work@example.com'), + ], + ); + var contact2 = const Contact( + givenName: 'givenName', + familyName: 'familyName', + emails: [ + Contact$Field(label: 'Work', value: 'work@example.com'), + Contact$Field(label: 'Home', value: 'home@example.com'), + ], + ); + expect(contact1 == contact2, true); + expect(contact1.hashCode, contact2.hashCode); + }); + + test('should show contacts are not equal', () { + var contact1 = const Contact( + givenName: 'givenName', + familyName: 'familyName', + emails: [ + Contact$Field(label: 'Home', value: 'home@example.com'), + Contact$Field(label: 'Work', value: 'work@example.com'), + ]); + var contact2 = const Contact( + givenName: 'givenName', + familyName: 'familyName', + emails: [ + Contact$Field(label: 'Office', value: 'office@example.com'), + Contact$Field(label: 'Home', value: 'home@example.com'), + ]); + expect(contact1 == contact2, false); + expect(contact1.hashCode, contact2.hashCode); + }); + + test('should produce a valid merged contact', () { + var contact1 = const Contact( + givenName: 'givenName', + familyName: 'familyName', + emails: [ + Contact$Field(label: 'Home', value: 'home@example.com'), + Contact$Field(label: 'Work', value: 'work@example.com'), + ], + phones: [], + postalAddresses: []); + var contact2 = const Contact(familyName: 'familyName', phones: [ + Contact$Field(label: 'Mobile', value: '111-222-3344') + ], emails: [ + Contact$Field(label: 'Mobile', value: 'mobile@example.com'), + ], postalAddresses: [ + Contact$PostalAddress( + label: 'Home', + street: '1234 Middle-of Rd', + city: 'Nowhere', + postcode: '12345', + region: null, + country: null) + ]); + var mergedContact = const Contact( + givenName: 'givenName', + familyName: 'familyName', + emails: [ + Contact$Field(label: 'Home', value: 'home@example.com'), + Contact$Field(label: 'Mobile', value: 'mobile@example.com'), + Contact$Field(label: 'Work', value: 'work@example.com'), + ], + phones: [ + Contact$Field(label: 'Mobile', value: '111-222-3344') + ], + postalAddresses: [ + Contact$PostalAddress( + label: 'Home', + street: '1234 Middle-of Rd', + city: 'Nowhere', + postcode: '12345', + region: null, + country: null) + ]); + + expect(contact1 + contact2, mergedContact); + }); + + test('should provide a valid merged contact, with no extra info', () { + var contact1 = const Contact(familyName: 'familyName'); + var contact2 = const Contact(); + expect(contact1 + contact2, contact1); + }); + + test('should provide a map of the contact', () { + var contact = + const Contact(givenName: 'givenName', familyName: 'familyName'); + expect(contact.toJson(), { + 'identifier': null, + 'displayName': null, + 'givenName': 'givenName', + 'middleName': null, + 'familyName': 'familyName', + 'prefix': null, + 'suffix': null, + 'company': null, + 'jobTitle': null, + 'androidAccountType': null, + 'androidAccountName': null, + 'emails': const [], + 'phones': const [], + 'postalAddresses': const [], + 'avatar': null, + 'birthday': null + }); + }); + }); + +void _expectMethodCall(List log, String methodName) { + expect(log, [ + isMethodCall( + methodName, + arguments: { + 'identifier': null, + 'displayName': null, + 'givenName': 'givenName', + 'middleName': null, + 'familyName': null, + 'prefix': null, + 'suffix': null, + 'company': null, + 'jobTitle': null, + 'androidAccountType': null, + 'androidAccountName': null, + 'emails': [ + {'label': 'label', 'value': null} + ], + 'phones': [ + {'label': 'label', 'value': null} + ], + 'postalAddresses': [ + { + 'label': 'label', + 'street': null, + 'city': null, + 'postcode': null, + 'region': null, + 'country': null + } + ], + 'avatar': null, + 'birthday': null + }, + ), + ]); +} diff --git a/contactos/test/src/contactos_test.dart b/contactos/test/src/contactos_test.dart new file mode 100644 index 0000000..7714603 --- /dev/null +++ b/contactos/test/src/contactos_test.dart @@ -0,0 +1,221 @@ +// Copyright 2025 Anton Ustinoff. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:typed_data'; + +import 'package:contactos/contactos.dart'; +import 'package:contactos_platform_interface/contactos_platform_interface.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +class MockContactosPlatform extends ContactosPlatform + with MockPlatformInterfaceMixin { + List contacts = []; + Contact? lastAddedContact; + Contact? lastDeletedContact; + Contact? lastUpdatedContact; + Contact? lastAvatarContact; + bool? lastAvatarHighRes; + + @override + Future addContact(Contact c) async { + lastAddedContact = c; + } + + @override + Future deleteContact(Contact c) async { + lastDeletedContact = c; + } + + @override + Future updateContact(Contact c) async { + lastUpdatedContact = c; + } + + @override + Future getAvatar( + Contact contact, { + bool photoHighRes = true, + }) async { + lastAvatarContact = contact; + lastAvatarHighRes = photoHighRes; + return Uint8List.fromList([1, 2, 3]); + } + + @override + Future> getContacts({ + String? query, + bool withThumbnails = true, + bool photoHighResolution = true, + bool orderByGivenName = true, + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) async => + contacts; + + @override + Future> getContactsForEmail( + String email, { + bool withThumbnails = true, + bool photoHighResolution = true, + bool orderByGivenName = true, + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) async => + contacts + .where((c) => c.emails?.any((e) => e.value == email) ?? false) + .toList(); + + @override + Future> getContactsForPhone( + String? phone, { + bool withThumbnails = true, + bool photoHighResolution = true, + bool orderByGivenName = true, + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) async => + contacts + .where((c) => c.phones?.any((p) => p.value == phone) ?? false) + .toList(); + + @override + Future openContactForm({ + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) async => + const Contact(identifier: 'new_contact'); + + @override + Future openDeviceContactPicker({ + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) async => + const Contact(identifier: 'picked_contact'); + + @override + Future openExistingContact( + Contact contact, { + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) async => + contact; +} + +void main() { + group('Contactos -', () { + late MockContactosPlatform mockPlatform; + late Contactos contactos; + + setUp(() { + mockPlatform = MockContactosPlatform(); + contactos = Contactos.custom(mockPlatform); + }); + + group('instance -', () { + test('returns the default instance', () { + expect(Contactos.instance, isA()); + }); + }); + + group('addContact -', () { + test('delegates to platform', () async { + const contact = Contact(identifier: '1', displayName: 'Test'); + await contactos.addContact(contact); + expect(mockPlatform.lastAddedContact, contact); + }); + }); + + group('deleteContact -', () { + test('delegates to platform', () async { + const contact = Contact(identifier: '1', displayName: 'Test'); + await contactos.deleteContact(contact); + expect(mockPlatform.lastDeletedContact, contact); + }); + }); + + group('updateContact -', () { + test('delegates to platform', () async { + const contact = Contact(identifier: '1', displayName: 'Test'); + await contactos.updateContact(contact); + expect(mockPlatform.lastUpdatedContact, contact); + }); + }); + + group('getAvatar -', () { + test('delegates to platform', () async { + const contact = Contact(identifier: '1', displayName: 'Test'); + final result = await contactos.getAvatar(contact, photoHighRes: false); + expect(mockPlatform.lastAvatarContact, contact); + expect(mockPlatform.lastAvatarHighRes, false); + expect(result, isNotNull); + }); + }); + + group('getContacts -', () { + test('delegates to platform', () async { + mockPlatform.contacts = [ + const Contact(identifier: '1', displayName: 'Test') + ]; + final result = await contactos.getContacts(); + expect(result.length, 1); + expect(result.first.identifier, '1'); + }); + }); + + group('getContactsForEmail -', () { + test('delegates to platform', () async { + mockPlatform.contacts = [ + const Contact( + identifier: '1', + displayName: 'Test', + emails: [Contact$Field(value: 'test@example.com')], + ), + const Contact(identifier: '2', displayName: 'Test 2'), + ]; + final result = await contactos.getContactsForEmail('test@example.com'); + expect(result.length, 1); + expect(result.first.identifier, '1'); + }); + }); + + group('getContactsForPhone -', () { + test('delegates to platform', () async { + mockPlatform.contacts = [ + const Contact( + identifier: '1', + displayName: 'Test', + phones: [Contact$Field(value: '123456')], + ), + const Contact(identifier: '2', displayName: 'Test 2'), + ]; + final result = await contactos.getContactsForPhone('123456'); + expect(result.length, 1); + expect(result.first.identifier, '1'); + }); + }); + + group('openContactForm -', () { + test('delegates to platform', () async { + final result = await contactos.openContactForm(); + expect(result.identifier, 'new_contact'); + }); + }); + + group('openDeviceContactPicker -', () { + test('delegates to platform', () async { + final result = await contactos.openDeviceContactPicker(); + expect(result?.identifier, 'picked_contact'); + }); + }); + + group('openExistingContact -', () { + test('delegates to platform', () async { + const contact = Contact(identifier: '1', displayName: 'Test'); + final result = await contactos.openExistingContact(contact); + expect(result, contact); + }); + }); + }); +} diff --git a/contactos_android/.gitignore b/contactos_android/.gitignore new file mode 100644 index 0000000..91f801f --- /dev/null +++ b/contactos_android/.gitignore @@ -0,0 +1,74 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +**/build +**/doc/api/ +**/Flutter/ephemeral/ +.dart_tool/ +.flutter-plugins-dependencies +.flutter-plugins +.pub-cache/ +.pub/ +.fvm/ + +# Platform-specific generated files +**/android/**/GeneratedPluginRegistrant.* +**/android/**/local.properties +**/android/**/.cxx/ +**/android/.kotlin +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +**/android/**/key.properties +**/android/**/*.keystore +**/android/**/*.jks +**/android/**/captures/ +**/android/**/.gradle +**/android/**/gradlew +**/android/**/gradle-wrapper.jar +**/android/**/gradlew.bat + +# Xcode-related +**/dgph +**/xcuserdata/ + +# Pana +log.pana.json + +# Test +.coverage/ +coverage/ +reports/ +/test/**/*.json +/test/.test_coverage.dart +!/test/**/fixtures/*.json +integration_test/screenshots + +# Temp +.temp/ +temp/ +.tmp/ +tmp/ \ No newline at end of file diff --git a/contactos_android/.pubignore b/contactos_android/.pubignore new file mode 100644 index 0000000..7b5ee92 --- /dev/null +++ b/contactos_android/.pubignore @@ -0,0 +1,5 @@ +pubspec.lock +reports/ +coverage/ +build/ +example/ \ No newline at end of file diff --git a/contactos_android/CHANGELOG.md b/contactos_android/CHANGELOG.md new file mode 100644 index 0000000..5bb8d30 --- /dev/null +++ b/contactos_android/CHANGELOG.md @@ -0,0 +1,4 @@ +# Changelog + +## 0.0.1 +- **ADDED**: Initial release \ No newline at end of file diff --git a/contactos_android/LICENSE b/contactos_android/LICENSE new file mode 100644 index 0000000..4de1fa1 --- /dev/null +++ b/contactos_android/LICENSE @@ -0,0 +1,27 @@ +BSD 3-Clause License + +Copyright (c) 2025, Anton Ustinoff +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/contactos_android/Makefile b/contactos_android/Makefile new file mode 100644 index 0000000..9be9690 --- /dev/null +++ b/contactos_android/Makefile @@ -0,0 +1,108 @@ +SHELL :=/bin/bash -e -o pipefail +PWD :=$(shell pwd) + +.DEFAULT_GOAL := all +.PHONY: all +all: ## build pipeline +all: format check test-unit publish-check + +.PHONY: ci +ci: ## CI build pipeline +ci: all + +.PHONY: precommit +precommit: ## validate the branch before commit +precommit: all + +.PHONY: help +help: ## Help dialog + @echo 'Usage: make ' + @echo '' + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' + +.PHONY: doctor +doctor: ## Check flutter doctor + @fvm flutter doctor + +.PHONY: version +version: ## Check flutter version + @fvm flutter --version + +.PHONY: format +format: ## Format code + @fvm dart format -l 80 lib test || (echo "Β―\_(ツ)_/Β― Format code error"; exit 1) + +.PHONY: fix +fix: format ## Fix code + @fvm dart fix --apply lib + +.PHONY: clean-cache +clean-cache: ## Clean the pub cache + @fvm flutter pub cache repair + +.PHONY: clean +clean: ## Clean flutter + @fvm flutter clean + +.PHONY: get +get: ## Get dependencies + @fvm flutter pub get || (echo "Β―\_(ツ)_/Β― Get contactos_android dependencies error"; exit 1) + +.PHONY: analyze +analyze: get format ## Analyze code + @fvm dart analyze --fatal-infos --fatal-warnings || (echo "Β―\_(ツ)_/Β― Analyze code error"; exit 1) + +.PHONY: check +check: analyze ## Check the code + @fvm dart pub global deactivate pana > /dev/null 2>&1 || true + @fvm dart pub global activate pana + @fvm dart pub global run pana --json > log.pana.json || (echo "Β―\_(ツ)_/Β― Pana analysis error"; exit 1) + +.PHONY: publish-check +publish-check: check ## Check the code before publish + @fvm dart pub publish --dry-run || (echo "Β―\_(ツ)_/Β― Publish check error"; exit 1) + +.PHONY: publish +publish: ## Publish package + @fvm dart pub publish --server=https://pub.dartlang.org || (echo "Β―\_(ツ)_/Β― Publish error"; exit 1) + +.PHONY: coverage +coverage: ## Runs get coverage + @lcov --summary coverage/lcov.info + +.PHONY: run-genhtml +run-genhtml: ## Runs generage coverage html + @genhtml coverage/lcov.info -o coverage/html + +.PHONY: test-unit +test-unit: ## Runs unit tests + @fvm flutter test --coverage test/contactos_android_test.dart || (echo "Β―\_(ツ)_/Β― Error while running test-unit"; exit 1) + @genhtml coverage/lcov.info --output=coverage -o coverage/html || (echo "Β―\_(ツ)_/Β― Error while running genhtml with coverage"; exit 2) + +.PHONY: tag +tag: ## Add a tag to the current commit + @fvm dart run tool/tag.dart + +.PHONY: tag-add +tag-add: ## Add TAG. E.g: make tag-add TAG=v1.0.0 + @if [ -z "$(TAG)" ]; then echo "Β―\_(ツ)_/Β― TAG is not set"; exit 1; fi + @echo "" + @echo "START ADDING TAG: $(TAG)" + @echo "" + @git tag $(TAG) + @git push origin $(TAG) + @echo "" + @echo "CREATED AND PUSHED TAG $(TAG)" + @echo "" + +.PHONY: tag-remove +tag-remove: ## Delete TAG. E.g: make tag-delete TAG=v1.0.0 + @if [ -z "$(TAG)" ]; then echo "Β―\_(ツ)_/Β― TAG is not set"; exit 1; fi + @echo "" + @echo "START REMOVING TAG: $(TAG)" + @echo "" + @git tag -d $(TAG) + @git push origin --delete $(TAG) + @echo "" + @echo "DELETED TAG $(TAG) LOCALLY AND REMOTELY" + @echo "" diff --git a/contactos_android/README.md b/contactos_android/README.md new file mode 100644 index 0000000..42cc88e --- /dev/null +++ b/contactos_android/README.md @@ -0,0 +1,27 @@ +# contactos_android + +[![pub package](https://img.shields.io/pub/v/contactos_android.svg)](https://pub.dev/packages/contactos_android) +[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT) +[![style: flutter lints](https://img.shields.io/badge/style-flutter__lints-blue)](https://pub.dev/packages/flutter_lints) + +The Android implementation of the [`contactos`](https://pub.dev/packages/contactos) plugin. + +## Usage + +This package is not meant to be used directly. It is automatically included in your app when you depend on the main [`contactos`](https://pub.dev/packages/contactos) package. + +For more information on how to use the `contactos` plugin, please see the [main package's README](https://github.com/ziqq/contactos/blob/main/contactos/README.md). + +## Contributing + +This is part of a federated plugin. Contributions to the platform-specific implementations are welcome. Please see the [main repository](https://github.com/ziqq/contactos) for more information on how to contribute. + + +## Maintainers + +[Anton Ustinoff (ziqq)](https://github.com/ziqq) + + +## License + +[MIT](https://github.com/ziqq/contactos/blob/main/LICENSE) diff --git a/contactos_android/analysis_options.yaml b/contactos_android/analysis_options.yaml new file mode 100644 index 0000000..ed15c99 --- /dev/null +++ b/contactos_android/analysis_options.yaml @@ -0,0 +1,235 @@ +include: package:flutter_lints/flutter.yaml + +analyzer: + exclude: + # Build + - "build/**" + # Tests + - "test/**.mocks.dart" + - ".test_coverage.dart" + - ".coverage/**" + - "coverage/**" + # Assets + - "assets/**" + # Generated + - "**/generated/**" + # - '**/lib/l10n/**.arb' + - "**.g.dart" + - "**.gr.dart" + - "**.gql.dart" + - "**.freezed.dart" + - "**.config.dart" + - "**.mocks.dart" + - "**.gen.dart" + - "**.pb.dart" + - "**.pbenum.dart" + - "**.pbjson.dart" + # Flutter Version Manager + - ".fvm/**" + # Tools + - ".dart_tool/**" + - "scripts/**" + - "tool/**" + # Platform + - "ios/**" + - "android/**" + - "web/**" + - "macos/**" + - "windows/**" + - "linux/**" + + # Enable the following options to enable strong mode. + language: + strict-casts: true + strict-raw-types: true + strict-inference: true + + errors: + # Allow having TODOs in the code + todo: ignore + + # Info + directives_ordering: info + always_declare_return_types: info + + # Warning + unsafe_html: warning + missing_return: warning + missing_required_param: warning + no_logic_in_create_state: warning + empty_catches: warning + + # Error + always_use_package_imports: error + avoid_relative_lib_imports: error + avoid_slow_async_io: error + avoid_types_as_parameter_names: error + valid_regexps: error + always_require_non_null_named_parameters: error + +linter: + rules: + # Public packages + public_member_api_docs: true + lines_longer_than_80_chars: true + + # Enabling rules + always_use_package_imports: true + avoid_relative_lib_imports: true + + # Disable rules + sort_pub_dependencies: false + prefer_relative_imports: false + prefer_final_locals: false + avoid_escaping_inner_quotes: false + curly_braces_in_flow_control_structures: false + + # Enabled + use_named_constants: true + unnecessary_constructor_name: true + sort_constructors_first: true + exhaustive_cases: true + sort_unnamed_constructors_first: true + type_literal_in_constant_pattern: true + always_put_required_named_parameters_first: true + avoid_annotating_with_dynamic: true + avoid_bool_literals_in_conditional_expressions: true + avoid_double_and_int_checks: true + avoid_field_initializers_in_const_classes: true + avoid_implementing_value_types: true + avoid_js_rounded_ints: true + avoid_print: true + avoid_renaming_method_parameters: true + avoid_returning_null_for_void: true + avoid_single_cascade_in_expression_statements: true + avoid_slow_async_io: true + avoid_unnecessary_containers: true + avoid_unused_constructor_parameters: true + avoid_void_async: true + await_only_futures: true + cancel_subscriptions: true + cascade_invocations: true + close_sinks: true + control_flow_in_finally: true + empty_statements: true + collection_methods_unrelated_type: true + join_return_with_assignment: true + leading_newlines_in_multiline_strings: true + literal_only_boolean_expressions: true + missing_whitespace_between_adjacent_strings: true + no_adjacent_strings_in_list: true + no_logic_in_create_state: true + no_runtimeType_toString: true + only_throw_errors: true + overridden_fields: true + package_names: true + package_prefixed_library_names: true + parameter_assignments: true + prefer_asserts_in_initializer_lists: true + prefer_asserts_with_message: true + prefer_const_constructors: true + prefer_const_constructors_in_immutables: true + prefer_const_declarations: true + prefer_const_literals_to_create_immutables: true + prefer_constructors_over_static_methods: true + prefer_expression_function_bodies: true + prefer_final_in_for_each: true + prefer_foreach: true + prefer_if_elements_to_conditional_expressions: true + prefer_inlined_adds: true + prefer_int_literals: true + prefer_is_not_operator: true + prefer_null_aware_operators: true + prefer_typing_uninitialized_variables: true + prefer_void_to_null: true + provide_deprecation_message: true + sized_box_for_whitespace: true + sort_child_properties_last: true + test_types_in_equals: true + throw_in_finally: true + unnecessary_null_aware_assignments: true + unnecessary_overrides: true + unnecessary_parenthesis: true + unnecessary_raw_strings: true + unnecessary_statements: true + unnecessary_string_escapes: true + unnecessary_string_interpolations: true + # unsafe_html: true + use_full_hex_values_for_flutter_colors: true + use_raw_strings: true + use_string_buffers: true + valid_regexps: true + void_checks: true + + # Pedantic 1.9.0 + always_declare_return_types: true + annotate_overrides: true + avoid_empty_else: true + avoid_init_to_null: true + avoid_null_checks_in_equality_operators: true + avoid_return_types_on_setters: true + avoid_shadowing_type_parameters: true + avoid_types_as_parameter_names: true + camel_case_extensions: true + empty_catches: true + empty_constructor_bodies: true + library_names: true + library_prefixes: true + no_duplicate_case_values: true + null_closures: true + omit_local_variable_types: true + prefer_adjacent_string_concatenation: true + prefer_collection_literals: true + prefer_conditional_assignment: true + prefer_contains: true + prefer_final_fields: true + prefer_for_elements_to_map_fromIterable: true + prefer_generic_function_type_aliases: true + prefer_if_null_operators: true + prefer_is_empty: true + prefer_is_not_empty: true + prefer_iterable_whereType: true + prefer_single_quotes: true + prefer_spread_collections: true + recursive_getters: true + slash_for_doc_comments: true + type_init_formals: true + unawaited_futures: true + unnecessary_const: true + unnecessary_new: true + unnecessary_null_in_if_null_operators: true + unnecessary_this: true + unrelated_type_equality_checks: true + use_function_type_syntax_for_parameters: true + use_rethrow_when_possible: true + + # Effective_dart 1.2.0 + camel_case_types: true + file_names: true + non_constant_identifier_names: true + constant_identifier_names: true + directives_ordering: true + # package_api_docs: true + implementation_imports: true + prefer_interpolation_to_compose_strings: true + unnecessary_brace_in_string_interps: true + avoid_function_literals_in_foreach_calls: true + prefer_function_declarations_over_variables: true + unnecessary_lambdas: true + unnecessary_getters_setters: true + prefer_initializing_formals: true + avoid_catches_without_on_clauses: true + avoid_catching_errors: true + use_to_and_as_if_applicable: true + one_member_abstracts: true + avoid_classes_with_only_static_members: true + prefer_mixin: true + use_setters_to_change_properties: true + avoid_setters_without_getters: true + avoid_returning_this: true + type_annotate_public_apis: true + avoid_types_on_closure_parameters: true + avoid_private_typedef_functions: true + avoid_positional_boolean_parameters: true + hash_and_equals: true + avoid_equals_and_hash_code_on_mutable_classes: true \ No newline at end of file diff --git a/contactos/android/.gitignore b/contactos_android/android/.gitignore similarity index 100% rename from contactos/android/.gitignore rename to contactos_android/android/.gitignore diff --git a/contactos/android/build.gradle b/contactos_android/android/build.gradle similarity index 100% rename from contactos/android/build.gradle rename to contactos_android/android/build.gradle diff --git a/contactos/android/gradle.properties b/contactos_android/android/gradle.properties similarity index 100% rename from contactos/android/gradle.properties rename to contactos_android/android/gradle.properties diff --git a/contactos/android/gradle/wrapper/gradle-wrapper.properties b/contactos_android/android/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from contactos/android/gradle/wrapper/gradle-wrapper.properties rename to contactos_android/android/gradle/wrapper/gradle-wrapper.properties diff --git a/contactos/android/settings.gradle b/contactos_android/android/settings.gradle similarity index 100% rename from contactos/android/settings.gradle rename to contactos_android/android/settings.gradle diff --git a/contactos/android/src/main/AndroidManifest.xml b/contactos_android/android/src/main/AndroidManifest.xml similarity index 100% rename from contactos/android/src/main/AndroidManifest.xml rename to contactos_android/android/src/main/AndroidManifest.xml diff --git a/contactos/android/src/main/java/flutter/plugins/contactos/Contact.java b/contactos_android/android/src/main/java/flutter/plugins/contactos/Contact.java similarity index 100% rename from contactos/android/src/main/java/flutter/plugins/contactos/Contact.java rename to contactos_android/android/src/main/java/flutter/plugins/contactos/Contact.java diff --git a/contactos/android/src/main/java/flutter/plugins/contactos/ContactosPlugin.java b/contactos_android/android/src/main/java/flutter/plugins/contactos/ContactosPlugin.java similarity index 100% rename from contactos/android/src/main/java/flutter/plugins/contactos/ContactosPlugin.java rename to contactos_android/android/src/main/java/flutter/plugins/contactos/ContactosPlugin.java diff --git a/contactos/android/src/main/java/flutter/plugins/contactos/Item.java b/contactos_android/android/src/main/java/flutter/plugins/contactos/Item.java similarity index 100% rename from contactos/android/src/main/java/flutter/plugins/contactos/Item.java rename to contactos_android/android/src/main/java/flutter/plugins/contactos/Item.java diff --git a/contactos/android/src/main/java/flutter/plugins/contactos/PostalAddress.java b/contactos_android/android/src/main/java/flutter/plugins/contactos/PostalAddress.java similarity index 100% rename from contactos/android/src/main/java/flutter/plugins/contactos/PostalAddress.java rename to contactos_android/android/src/main/java/flutter/plugins/contactos/PostalAddress.java diff --git a/contactos/android/src/test/java/flutter/plugins/contactsservice/contactsservice/ContactTest.java b/contactos_android/android/src/test/java/flutter/plugins/contactsservice/contactsservice/ContactTest.java similarity index 100% rename from contactos/android/src/test/java/flutter/plugins/contactsservice/contactsservice/ContactTest.java rename to contactos_android/android/src/test/java/flutter/plugins/contactsservice/contactsservice/ContactTest.java diff --git a/contactos_android/dart_dependency_validator.yaml b/contactos_android/dart_dependency_validator.yaml new file mode 100644 index 0000000..06cc68f --- /dev/null +++ b/contactos_android/dart_dependency_validator.yaml @@ -0,0 +1,8 @@ +ignore: + - dart_code_metrics + - integration_test + - flutter_lints + - collection + +exclude: + - "example/**" diff --git a/contactos_android/example/.gitignore b/contactos_android/example/.gitignore new file mode 100644 index 0000000..e96ef60 --- /dev/null +++ b/contactos_android/example/.gitignore @@ -0,0 +1,32 @@ +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/contactos_android/example/.metadata b/contactos_android/example/.metadata new file mode 100644 index 0000000..a700013 --- /dev/null +++ b/contactos_android/example/.metadata @@ -0,0 +1,8 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 8f65fec5f5f7d7afbb0965f4a44bdb330a28fb19 + channel: alpha diff --git a/contactos_android/example/README.md b/contactos_android/example/README.md new file mode 100644 index 0000000..8399213 --- /dev/null +++ b/contactos_android/example/README.md @@ -0,0 +1,8 @@ +# contactos_example + +Demonstrates how to use the contactos plugin. + +## Getting Started + +For help getting started with Flutter, view our online +[documentation](http://flutter.io/). diff --git a/contactos_android/example/android/app/build.gradle b/contactos_android/example/android/app/build.gradle new file mode 100644 index 0000000..ae7b755 --- /dev/null +++ b/contactos_android/example/android/app/build.gradle @@ -0,0 +1,60 @@ +plugins { + id "com.android.application" + id "kotlin-android" + id "dev.flutter.flutter-gradle-plugin" +} + +def localProperties = new Properties() +def localPropertiesFile = rootProject.file("local.properties") +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader("UTF-8") { reader -> + localProperties.load(reader) + } +} + +def flutterVersionCode = localProperties.getProperty("flutter.versionCode") +if (flutterVersionCode == null) { + flutterVersionCode = "1" +} + +def flutterVersionName = localProperties.getProperty("flutter.versionName") +if (flutterVersionName == null) { + flutterVersionName = "1.0" +} + +android { + namespace 'flutter.plugins.contactos.example' + compileSdkVersion flutter.compileSdkVersion + ndkVersion flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17 + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + applicationId "flutter.plugins.contactos.example" + minSdkVersion flutter.minSdkVersion + targetSdkVersion flutter.targetSdkVersion + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + buildTypes { + release { + signingConfig = signingConfigs.debug + } + } +} + +flutter { + source = "../.." +} \ No newline at end of file diff --git a/contactos_android/example/android/app/src/main/AndroidManifest.xml b/contactos_android/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..844374c --- /dev/null +++ b/contactos_android/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + diff --git a/contactos_android/example/android/app/src/main/java/flutter/plugins/contactos/example/MainActivity.java b/contactos_android/example/android/app/src/main/java/flutter/plugins/contactos/example/MainActivity.java new file mode 100644 index 0000000..271e05b --- /dev/null +++ b/contactos_android/example/android/app/src/main/java/flutter/plugins/contactos/example/MainActivity.java @@ -0,0 +1,5 @@ +package flutter.plugins.contactos.example; + +import io.flutter.embedding.android.FlutterActivity; + +public class MainActivity extends FlutterActivity {} diff --git a/contactos_android/example/android/app/src/main/res/drawable/launch_background.xml b/contactos_android/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/contactos_android/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/contactos_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/contactos_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/contactos_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/contactos_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/contactos_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/contactos_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/contactos_android/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/contactos_android/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/contactos_android/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/contactos_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/contactos_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/contactos_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/contactos_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/contactos_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/contactos_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/contactos_android/example/android/app/src/main/res/values/styles.xml b/contactos_android/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..00fa441 --- /dev/null +++ b/contactos_android/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,8 @@ + + + + diff --git a/contactos_android/example/android/build.gradle b/contactos_android/example/android/build.gradle new file mode 100644 index 0000000..8e9de9d --- /dev/null +++ b/contactos_android/example/android/build.gradle @@ -0,0 +1,18 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = "../build" +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean", Delete) { + delete rootProject.buildDir +} \ No newline at end of file diff --git a/contactos_android/example/android/gradle.properties b/contactos_android/example/android/gradle.properties new file mode 100644 index 0000000..f3dfe33 --- /dev/null +++ b/contactos_android/example/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true +android.enableJetifier=true \ No newline at end of file diff --git a/contactos_android/example/android/gradle/wrapper/gradle-wrapper.properties b/contactos_android/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..37f853b --- /dev/null +++ b/contactos_android/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/contactos_android/example/android/settings.gradle b/contactos_android/example/android/settings.gradle new file mode 100644 index 0000000..65b4c17 --- /dev/null +++ b/contactos_android/example/android/settings.gradle @@ -0,0 +1,25 @@ +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + }() + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "8.13.0" apply false + id "org.jetbrains.kotlin.android" version "2.2.0" apply false +} + +include ":app" \ No newline at end of file diff --git a/contactos_android/example/lib/main.dart b/contactos_android/example/lib/main.dart new file mode 100644 index 0000000..17359cd --- /dev/null +++ b/contactos_android/example/lib/main.dart @@ -0,0 +1,98 @@ +import 'package:contactos_example/src/screens/contacts_list_screen.dart'; +import 'package:contactos_example/src/screens/navite_contacts_picker_screen.dart'; +import 'package:flutter/material.dart'; +import 'package:permission_handler/permission_handler.dart'; + +void main() => runApp(const ContactsExampleApp()); + +/// Android only: Localized labels language setting +/// Set androidLocalizedLabels=false if you always want english labels +/// whatever is the device language setting. +const kAndroidLocalizedLabels = false; + +/// {@template main} +/// Example app for the `contactos` plugin. +/// {@endtemplate} +class ContactsExampleApp extends StatelessWidget { + /// {@macro main} + const ContactsExampleApp({super.key}); + + @override + Widget build(BuildContext context) => MaterialApp( + home: const _HomePage(), + routes: { + '/add': (_) => const AddContactScreen(), + '/contacts-list': (_) => const ContactsListScreen(), + '/native-contacts-picker': (_) => const NativeContactsPickerScreen(), + }, + ); +} + +class _HomePage extends StatefulWidget { + const _HomePage({ + super.key, // ignore: unused_element_parameter + }); + + @override + State<_HomePage> createState() => __HomePageState(); +} + +class __HomePageState extends State<_HomePage> { + @override + void initState() { + super.initState(); + _askPermissions(); + } + + Future _askPermissions([String? routeName]) async { + final navigator = Navigator.of(context); + final permissionStatus = await _getContactPermission(); + if (permissionStatus == PermissionStatus.granted) { + if (routeName != null) await navigator.pushNamed(routeName); + } else { + _handleInvalidPermissions(permissionStatus); + } + } + + Future _getContactPermission() async { + final permission = await Permission.contacts.status; + if (permission != PermissionStatus.granted && + permission != PermissionStatus.permanentlyDenied) { + final permissionStatus = await Permission.contacts.request(); + return permissionStatus; + } else { + return permission; + } + } + + void _handleInvalidPermissions(PermissionStatus permissionStatus) { + Widget? content; + if (permissionStatus == PermissionStatus.denied) { + content = const Text('Access to contact data denied'); + } else if (permissionStatus == PermissionStatus.permanentlyDenied) { + content = const Text('Contact data not available on device'); + } + if (content == null) return; + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: content)); + } + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text('Contacts Plugin Example')), + body: SafeArea( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ElevatedButton( + child: const Text('Contacts list'), + onPressed: () => _askPermissions('/contacts-list'), + ), + ElevatedButton( + child: const Text('Native Contacts picker'), + onPressed: () => _askPermissions('/native-contacts-picker'), + ), + ], + ), + ), + ); +} diff --git a/contactos_android/example/lib/src/screens/contacts_list_screen.dart b/contactos_android/example/lib/src/screens/contacts_list_screen.dart new file mode 100644 index 0000000..d7c4e5a --- /dev/null +++ b/contactos_android/example/lib/src/screens/contacts_list_screen.dart @@ -0,0 +1,603 @@ +/* + * Author: Anton Ustinoff | + * Date: 25 November 2025 + */ + +import 'dart:developer'; + +import 'package:collection/collection.dart'; +import 'package:contactos_android/contactos_android.dart'; +import 'package:contactos_example/main.dart' show kAndroidLocalizedLabels; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; + +/// {@template contacts_list_screen} +/// Contacts list screen widget. +/// {@endtemplate} +class ContactsListScreen extends StatefulWidget { + /// {@macro contacts_list_screen} + const ContactsListScreen({super.key}); + + @override + State createState() => _ContactsListPageState(); +} + +/// State for widget [ContactsListScreen]. +class _ContactsListPageState extends State { + List? _contacts; + + @override + void initState() { + super.initState(); + refreshContacts(); + } + + Future refreshContacts() async { + // final contacts = + // await ContactosPluginAndroid + // .instance + // .getContactsForPhone("8554964652"); + + // Load without thumbnails initially. + final contacts = await ContactosPluginAndroid.instance.getContacts( + androidLocalizedLabels: kAndroidLocalizedLabels, + withThumbnails: false, + ); + + _contacts = contacts; + if (!mounted) return; + setState(() {}); + + // Lazy load thumbnails after rendering initial contacts. + for (var contact in _contacts ?? contacts) { + ContactosPluginAndroid.instance.getAvatar(contact).then((avatar) { + contact = contact.copyWith(avatar: avatar); + if (mounted) setState(() {}); + }).ignore(); + } + } + + Future updateContact() async { + var contact = _contacts?.firstWhereOrNull( + (c) => c.familyName?.startsWith('Ninja') ?? false, + ); + if (contact == null) return; + await ContactosPluginAndroid.instance.updateContact(contact); + await refreshContacts(); + } + + Future _openContactForm() async { + try { + var _ = await ContactosPluginAndroid.instance.openContactForm( + androidLocalizedLabels: kAndroidLocalizedLabels, + ); + await refreshContacts(); + } on FormOperationException catch (e) { + switch (e.errorCode) { + case FormOperationErrorCode.unknown: + case FormOperationErrorCode.canceled: + case FormOperationErrorCode.couldNotBeOpen: + default: + log(e.toString()); + } + } + } + + void contactOnDeviceHasBeenUpdated(Contact contact) { + if (!mounted) return; + final id = _contacts?.indexWhere((c) => c.identifier == contact.identifier); + if (id == null || id < 0) return; + _contacts?[id] = contact; + setState(() {}); + } + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar( + title: const Text('Contacts Plugin Example'), + actions: [ + IconButton(icon: const Icon(Icons.create), onPressed: _openContactForm), + ], + ), + floatingActionButton: FloatingActionButton( + child: const Icon(Icons.add), + onPressed: () { + Navigator.of(context).pushNamed('/add').then((_) { + refreshContacts(); + }); + }, + ), + body: SafeArea( + child: _contacts != null + ? ListView.builder( + itemCount: _contacts?.length ?? 0, + itemBuilder: (context, index) { + final contact = _contacts?.elementAt(index); + if (contact == null) return const SizedBox.shrink(); + return ListTile( + onTap: () { + final route = MaterialPageRoute( + builder: (context) => _ContactDetailsPage( + contact, + onContactDeviceSave: contactOnDeviceHasBeenUpdated, + ), + ); + Navigator.of(context).push(route); + }, + leading: + (contact.avatar != null && + (contact.avatar?.length ?? 0) > 0) + ? CircleAvatar( + backgroundImage: MemoryImage(contact.avatar!), + ) + : CircleAvatar(child: Text(contact.initials())), + title: Text(contact.displayName ?? ''), + ); + }, + ) + : const Center(child: CircularProgressIndicator()), + ), + ); +} + +/// Contact details page. +/// {@macro add_contact_screen} +class _ContactDetailsPage extends StatefulWidget { + /// {@macro add_contact_screen} + const _ContactDetailsPage( + this._contact, { + this.onContactDeviceSave, + super.key, // ignore: unused_element_parameter + }); + + final Contact _contact; + final void Function(Contact)? onContactDeviceSave; + + @override + State<_ContactDetailsPage> createState() => _ContactDetailsPageState(); +} + +/// State for widget [_ContactDetailsPage]. +class _ContactDetailsPageState extends State<_ContactDetailsPage> { + Future _openExistingContactOnDevice(BuildContext context) async { + try { + final contact = await ContactosPluginAndroid.instance.openExistingContact( + widget._contact, + androidLocalizedLabels: kAndroidLocalizedLabels, + ); + if (widget.onContactDeviceSave != null) { + widget.onContactDeviceSave?.call(contact); + } + if (!context.mounted) return; + Navigator.of(context).pop(); + } on FormOperationException catch (e) { + switch (e.errorCode) { + case FormOperationErrorCode.unknown: + case FormOperationErrorCode.canceled: + case FormOperationErrorCode.couldNotBeOpen: + default: + log(e.toString()); + } + } + } + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar( + title: Text(widget._contact.displayName ?? ''), + actions: [ + /* IconButton( + icon: Icon(Icons.share), + onPressed: () => shareVCFCard(context, contact: _contact), + ), */ + IconButton( + icon: const Icon(Icons.delete), + onPressed: () { + ContactosPluginAndroid.instance.deleteContact(widget._contact); + }, + ), + IconButton( + icon: const Icon(Icons.update), + onPressed: () => Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => + _UpdateContactsPage(contact: widget._contact), + ), + ), + ), + IconButton( + icon: const Icon(Icons.edit), + onPressed: () => _openExistingContactOnDevice(context), + ), + ], + ), + body: SafeArea( + child: ListView( + children: [ + ListTile( + title: const Text('Name'), + trailing: Text(widget._contact.givenName ?? ''), + ), + ListTile( + title: const Text('Middle name'), + trailing: Text(widget._contact.middleName ?? ''), + ), + ListTile( + title: const Text('Family name'), + trailing: Text(widget._contact.familyName ?? ''), + ), + ListTile( + title: const Text('Prefix'), + trailing: Text(widget._contact.prefix ?? ''), + ), + ListTile( + title: const Text('Suffix'), + trailing: Text(widget._contact.suffix ?? ''), + ), + ListTile( + title: const Text('Birthday'), + trailing: Text( + widget._contact.birthday != null + ? DateFormat('dd-MM-yyyy').format(widget._contact.birthday!) + : '', + ), + ), + ListTile( + title: const Text('Company'), + trailing: Text(widget._contact.company ?? ''), + ), + ListTile( + title: const Text('Job'), + trailing: Text(widget._contact.jobTitle ?? ''), + ), + ListTile( + title: const Text('Account Type'), + trailing: Text( + (widget._contact.androidAccountType != null) + ? widget._contact.androidAccountType.toString() + : '', + ), + ), + _AddressesTile(widget._contact.postalAddresses!), + _ItemsTile('Phones', widget._contact.phones!), + _ItemsTile('Emails', widget._contact.emails!), + ], + ), + ), + ); +} + +/// Tile for contact addresses. +/// {@macro add_contact_screen} +class _AddressesTile extends StatelessWidget { + /// {@macro add_contact_screen} + const _AddressesTile( + this._addresses, { + super.key, // ignore: unused_element_parameter + }); + + final List _addresses; + + @override + Widget build(BuildContext context) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const ListTile(title: Text('Addresses')), + Column( + children: [ + for (final a in _addresses) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + children: [ + ListTile( + title: const Text('Street'), + trailing: Text(a.street ?? ''), + ), + ListTile( + title: const Text('Postcode'), + trailing: Text(a.postcode ?? ''), + ), + ListTile( + title: const Text('City'), + trailing: Text(a.city ?? ''), + ), + ListTile( + title: const Text('Region'), + trailing: Text(a.region ?? ''), + ), + ListTile( + title: const Text('Country'), + trailing: Text(a.country ?? ''), + ), + ], + ), + ), + ], + ), + ], + ); +} + +/// Tile for contact items like phones, emails etc. +/// {@macro add_contact_screen} +class _ItemsTile extends StatelessWidget { + /// {@macro add_contact_screen} + const _ItemsTile( + this._title, + this._items, { + super.key, // ignore: unused_element_parameter + }); + + final List _items; + final String _title; + + @override + Widget build(BuildContext context) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ListTile(title: Text(_title)), + Column( + children: [ + for (final i in _items) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: ListTile( + title: Text(i.label ?? ''), + trailing: Text(i.value ?? ''), + ), + ), + ], + ), + ], + ); +} + +/// Add contact screen. +/// {@macro add_contact_screen} +class AddContactScreen extends StatefulWidget { + /// {@macro add_contact_screen} + const AddContactScreen({ + super.key, // ignore: unused_element_parameter + }); + + @override + State createState() => _AddContactScreenState(); +} + +/// State for widget [AddContactScreen]. +class _AddContactScreenState extends State { + final GlobalKey _formKey = GlobalKey(); + + Contact$PostalAddress address = const Contact$PostalAddress(label: 'Home'); + Contact contact = const Contact(); + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar( + title: const Text('Add a contact'), + actions: [ + TextButton( + onPressed: () { + _formKey.currentState?.save(); + final newContact = contact.copyWith(postalAddresses: [address]); + ContactosPluginAndroid.instance.addContact(newContact); + Navigator.of(context).pop(); + }, + child: const Icon(Icons.save, color: Colors.white), + ), + ], + ), + body: Container( + padding: const EdgeInsets.all(12), + child: Form( + key: _formKey, + child: ListView( + children: [ + TextFormField( + decoration: const InputDecoration(labelText: 'First name'), + onSaved: (v) => contact.copyWith(givenName: v), + ), + TextFormField( + decoration: const InputDecoration(labelText: 'Middle name'), + onSaved: (v) => contact.copyWith(middleName: v), + ), + TextFormField( + decoration: const InputDecoration(labelText: 'Last name'), + onSaved: (v) => contact = contact.copyWith(familyName: v), + ), + TextFormField( + decoration: const InputDecoration(labelText: 'Prefix'), + onSaved: (v) => contact = contact.copyWith(prefix: v), + ), + TextFormField( + decoration: const InputDecoration(labelText: 'Suffix'), + onSaved: (v) => contact = contact.copyWith(suffix: v), + ), + TextFormField( + decoration: const InputDecoration(labelText: 'Phone'), + onSaved: (v) => contact = contact.copyWith( + phones: [Contact$Field(label: 'mobile', value: v)], + ), + keyboardType: TextInputType.phone, + ), + TextFormField( + decoration: const InputDecoration(labelText: 'E-mail'), + onSaved: (v) => contact = contact.copyWith( + emails: [Contact$Field(label: 'work', value: v)], + ), + keyboardType: TextInputType.emailAddress, + ), + TextFormField( + decoration: const InputDecoration(labelText: 'Company'), + onSaved: (v) => contact = contact.copyWith(company: v), + ), + TextFormField( + decoration: const InputDecoration(labelText: 'Job'), + onSaved: (v) => contact = contact.copyWith(jobTitle: v), + ), + TextFormField( + decoration: const InputDecoration(labelText: 'Street'), + onSaved: (v) => address = address.copyWith(street: v), + ), + TextFormField( + decoration: const InputDecoration(labelText: 'City'), + onSaved: (v) => address = address.copyWith(city: v), + ), + TextFormField( + decoration: const InputDecoration(labelText: 'Region'), + onSaved: (v) => address = address.copyWith(region: v), + ), + TextFormField( + decoration: const InputDecoration(labelText: 'Postal code'), + onSaved: (v) => address = address.copyWith(postcode: v), + ), + TextFormField( + decoration: const InputDecoration(labelText: 'Country'), + onSaved: (v) => address = address.copyWith(country: v), + ), + ], + ), + ), + ), + ); +} + +/// Update contact screen. +/// {@macro add_contact_screen} +class _UpdateContactsPage extends StatefulWidget { + /// {@macro add_contact_screen} + const _UpdateContactsPage({ + required this.contact, + super.key, // ignore: unused_element_parameter + }); + + final Contact? contact; + + @override + State<_UpdateContactsPage> createState() => __UpdateContactsPageState(); +} + +/// State for widget [_UpdateContactsPage]. +class __UpdateContactsPageState extends State<_UpdateContactsPage> { + final GlobalKey _formKey = GlobalKey(); + + Contact$PostalAddress address = const Contact$PostalAddress(label: 'Home'); + Contact? contact; + + @override + void initState() { + super.initState(); + contact = widget.contact; + } + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar( + title: const Text('Update Contact'), + actions: [ + IconButton( + icon: const Icon(Icons.save, color: Colors.white), + onPressed: () async { + final navigator = Navigator.of(context); + final newContact = contact?.copyWith(postalAddresses: [address]); + if (newContact == null) return; + _formKey.currentState?.save(); + await ContactosPluginAndroid.instance.updateContact(newContact); + navigator + .pushReplacement( + MaterialPageRoute( + builder: (_) => const ContactsListScreen(), + ), + ) + .ignore(); + }, + ), + ], + ), + body: Padding( + padding: const EdgeInsets.all(12), + child: Form( + key: _formKey, + child: ListView( + children: [ + TextFormField( + initialValue: contact?.givenName ?? '', + decoration: const InputDecoration(labelText: 'First name'), + onSaved: (v) => contact = contact?.copyWith(givenName: v), + ), + TextFormField( + initialValue: contact?.middleName ?? '', + decoration: const InputDecoration(labelText: 'Middle name'), + onSaved: (v) => contact = contact?.copyWith(middleName: v), + ), + TextFormField( + initialValue: contact?.familyName ?? '', + decoration: const InputDecoration(labelText: 'Last name'), + onSaved: (v) => contact = contact?.copyWith(familyName: v), + ), + TextFormField( + initialValue: contact?.prefix ?? '', + decoration: const InputDecoration(labelText: 'Prefix'), + onSaved: (v) => contact = contact?.copyWith(prefix: v), + ), + TextFormField( + initialValue: contact?.suffix ?? '', + decoration: const InputDecoration(labelText: 'Suffix'), + onSaved: (v) => contact = contact?.copyWith(suffix: v), + ), + TextFormField( + decoration: const InputDecoration(labelText: 'Phone'), + onSaved: (v) => contact = contact?.copyWith( + phones: [Contact$Field(label: 'mobile', value: v)], + ), + keyboardType: TextInputType.phone, + ), + TextFormField( + decoration: const InputDecoration(labelText: 'E-mail'), + onSaved: (v) => contact = contact?.copyWith( + emails: [Contact$Field(label: 'work', value: v)], + ), + keyboardType: TextInputType.emailAddress, + ), + TextFormField( + initialValue: contact?.company ?? '', + decoration: const InputDecoration(labelText: 'Company'), + onSaved: (v) => contact = contact?.copyWith(company: v), + ), + TextFormField( + initialValue: contact?.jobTitle ?? '', + decoration: const InputDecoration(labelText: 'Job'), + onSaved: (v) => contact = contact?.copyWith(jobTitle: v), + ), + TextFormField( + initialValue: address.street ?? '', + decoration: const InputDecoration(labelText: 'Street'), + onSaved: (v) => address = address.copyWith(street: v), + ), + TextFormField( + initialValue: address.city ?? '', + decoration: const InputDecoration(labelText: 'City'), + onSaved: (v) => address = address.copyWith(city: v), + ), + TextFormField( + initialValue: address.region ?? '', + decoration: const InputDecoration(labelText: 'Region'), + onSaved: (v) => address = address.copyWith(region: v), + ), + TextFormField( + initialValue: address.postcode ?? '', + decoration: const InputDecoration(labelText: 'Postal code'), + onSaved: (v) => address = address.copyWith(postcode: v), + ), + TextFormField( + initialValue: address.country ?? '', + decoration: const InputDecoration(labelText: 'Country'), + onSaved: (v) => address = address.copyWith(country: v), + ), + ], + ), + ), + ), + ); +} diff --git a/contactos_android/example/lib/src/screens/navite_contacts_picker_screen.dart b/contactos_android/example/lib/src/screens/navite_contacts_picker_screen.dart new file mode 100644 index 0000000..f2a0627 --- /dev/null +++ b/contactos_android/example/lib/src/screens/navite_contacts_picker_screen.dart @@ -0,0 +1,62 @@ +/* + * Author: Anton Ustinoff | + * Date: 25 November 2025 + */ + +import 'dart:developer'; + +import 'package:contactos_android/contactos_android.dart'; +import 'package:contactos_example/main.dart' show kAndroidLocalizedLabels; +import 'package:flutter/material.dart'; + +/// {@template navite_contacts_picker_screen} +/// NativeContactsPickerScreen widget. +/// {@endtemplate} +class NativeContactsPickerScreen extends StatefulWidget { + /// {@macro navite_contacts_picker_screen} + const NativeContactsPickerScreen({super.key}); + + @override + State createState() => + _NativeContactsPickerScreenState(); +} + +/// State for widget [NativeContactsPickerScreen]. +class _NativeContactsPickerScreenState + extends State { + Contact? _contact; + + @override + void initState() { + super.initState(); + } + + Future _pickContact() async { + try { + final contact = await ContactosPluginAndroid.instance + .openDeviceContactPicker( + androidLocalizedLabels: kAndroidLocalizedLabels, + ); + setState(() => _contact = contact); + } on Object catch (e) { + log(e.toString()); + } + } + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text('Contacts Picker Example')), + body: SafeArea( + child: Column( + children: [ + ElevatedButton( + onPressed: _pickContact, + child: const Text('Pick a contact'), + ), + if (_contact != null) + Text('Contact selected: ${_contact?.displayName}'), + ], + ), + ), + ); +} diff --git a/contactos_android/example/pubspec.lock b/contactos_android/example/pubspec.lock new file mode 100644 index 0000000..8a6ea2b --- /dev/null +++ b/contactos_android/example/pubspec.lock @@ -0,0 +1,360 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + url: "https://pub.dev" + source: hosted + version: "1.4.1" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + collection: + dependency: "direct main" + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + contactos_android: + dependency: "direct main" + description: + path: ".." + relative: true + source: path + version: "0.0.1" + contactos_platform_interface: + dependency: transitive + description: + name: contactos_platform_interface + sha256: "6fc797fa685c84ef7e1ec8fc8c6aae1f9f09274bcc5ea26e435781a4fa907628" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.dev" + source: hosted + version: "1.0.8" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_driver: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + fuchsia_remote_debug_protocol: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + integration_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + intl: + dependency: "direct main" + description: + name: intl + sha256: "910f85bce16fb5c6f614e117efa303e85a1731bb0081edf3604a2ae6e9a3cc91" + url: "https://pub.dev" + source: hosted + version: "0.17.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0 + url: "https://pub.dev" + source: hosted + version: "6.0.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" + url: "https://pub.dev" + source: hosted + version: "0.12.18" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + url: "https://pub.dev" + source: hosted + version: "0.13.0" + meta: + dependency: transitive + description: + name: meta + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + url: "https://pub.dev" + source: hosted + version: "1.17.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + sha256: bc917da36261b00137bbc8896bf1482169cd76f866282368948f032c8c1caae1 + url: "https://pub.dev" + source: hosted + version: "12.0.1" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6" + url: "https://pub.dev" + source: hosted + version: "13.0.1" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: f84a188e79a35c687c132a0a0556c254747a08561e99ab933f12f6ca71ef3c98 + url: "https://pub.dev" + source: hosted + version: "9.4.6" + permission_handler_html: + dependency: transitive + description: + name: permission_handler_html + sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24" + url: "https://pub.dev" + source: hosted + version: "0.1.3+5" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878 + url: "https://pub.dev" + source: hosted + version: "4.3.0" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" + url: "https://pub.dev" + source: hosted + version: "0.2.1" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + process: + dependency: transitive + description: + name: process + sha256: "107d8be718f120bbba9dcd1e95e3bd325b1b4a4f07db64154635ba03f2567a0d" + url: "https://pub.dev" + source: hosted + version: "5.0.3" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://pub.dev" + source: hosted + version: "1.10.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + sync_http: + dependency: transitive + description: + name: sync_http + sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961" + url: "https://pub.dev" + source: hosted + version: "0.3.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" + url: "https://pub.dev" + source: hosted + version: "0.7.9" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 + url: "https://pub.dev" + source: hosted + version: "15.0.0" + web: + dependency: transitive + description: + name: web + sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb + url: "https://pub.dev" + source: hosted + version: "1.1.0" + webdriver: + dependency: transitive + description: + name: webdriver + sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade" + url: "https://pub.dev" + source: hosted + version: "3.1.0" +sdks: + dart: ">=3.10.0 <4.0.0" + flutter: ">=3.38.3" diff --git a/contactos_android/example/pubspec.yaml b/contactos_android/example/pubspec.yaml new file mode 100644 index 0000000..8f146d9 --- /dev/null +++ b/contactos_android/example/pubspec.yaml @@ -0,0 +1,44 @@ +name: contactos_example +description: Testbed for the contactos plugin implementation for Android. +publish_to: 'none' +version: 0.0.1 + + +environment: + sdk: '>=3.10.0 <4.0.0' + flutter: ">=3.38.3" + + +dependencies: + flutter: + sdk: flutter + + contactos_android: + # When depending on this package from a real application you should use: + # contactos: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + + intl: any + collection: any + cupertino_icons: any + + permission_handler: ^12.0.1 + + +dev_dependencies: + # Integration tests for Flutter + integration_test: + sdk: flutter + # Unit & Widget tests for Flutter + flutter_test: + sdk: flutter + + # Linting + flutter_lints: ^6.0.0 + + +flutter: + uses-material-design: true diff --git a/contactos_android/example/test/widget_test.dart b/contactos_android/example/test/widget_test.dart new file mode 100644 index 0000000..38699e8 --- /dev/null +++ b/contactos_android/example/test/widget_test.dart @@ -0,0 +1,33 @@ +// This is a basic Flutter widget test. +// To perform an interaction with a widget in your test, +// use the WidgetTester utility that Flutter +// provides. For example, you can send tap and scroll gestures. +// You can also use WidgetTester to find child widgets in the widget tree, +// read text, and verify that the values of widget properties are correct. + +import 'package:contactos_example/src/screens/contacts_list_screen.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() => group( + 'Widget_tests -', + () => group( + 'ContactsListScreents -', + () => testWidgets('First test', (tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const ContactsListScreen()); + + // Verify that our counter starts at 0. + expect(find.text('0'), findsOneWidget); + expect(find.text('1'), findsNothing); + + // Tap the '+' icon and trigger a frame. + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); + + // Verify that our counter has incremented. + expect(find.text('0'), findsNothing); + expect(find.text('1'), findsOneWidget); + }), + ), + ); diff --git a/contactos_android/lib/contactos_android.dart b/contactos_android/lib/contactos_android.dart new file mode 100644 index 0000000..f6ac248 --- /dev/null +++ b/contactos_android/lib/contactos_android.dart @@ -0,0 +1,7 @@ +// Copyright 2025 Anton Ustinoff. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'package:contactos_platform_interface/contactos_platform_interface.dart'; + +export 'src/contactos_android.dart'; diff --git a/contactos_android/lib/src/contactos_android.dart b/contactos_android/lib/src/contactos_android.dart new file mode 100644 index 0000000..264250e --- /dev/null +++ b/contactos_android/lib/src/contactos_android.dart @@ -0,0 +1,161 @@ +// Copyright 2025 Anton Ustinoff. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:typed_data'; + +import 'package:contactos_platform_interface/contactos_platform_interface.dart'; +import 'package:flutter/foundation.dart' show visibleForOverriding; + +/// The Android implementation of [ContactosPlatform]. +/// +/// This class implements the `package:contactos` +/// functionality for iOS and macOS. +class ContactosPluginAndroid extends ContactosPlatform { + /// Creates a new plugin for iOS and macOS implementation instance. + ContactosPluginAndroid._({ + @visibleForOverriding MethodChannelContactos? channel, + }) : _channel = channel ?? MethodChannelContactos.instance; + + /// Returns an instance using a specified [MethodChannelContactos]. + factory ContactosPluginAndroid._instanceFor({ + @visibleForOverriding MethodChannelContactos? channel, + }) => + ContactosPluginAndroid._(channel: channel); + + /// Returns the default instance + /// of [ContactosPluginAndroid]. + static ContactosPluginAndroid get instance => _instance; + + /// Returns an instance using the default [ContactosPluginAndroid]. + static final ContactosPluginAndroid _instance = + ContactosPluginAndroid._instanceFor(); + + /// The channel used to interact with the platform side of the plugin. + final MethodChannelContactos _channel; + + /// Registers this class + /// as the default instance of [ContactosPla]. + static void registerWith() { + ContactosPlatform.instance = ContactosPluginAndroid.instance; + } + + /// Fetches all contacts, or when specified, the contacts with a name + /// matching [query] + @override + Future> getContacts({ + String? query, + bool withThumbnails = true, + bool photoHighResolution = true, + bool orderByGivenName = true, + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) => + _channel.getContacts( + query: query, + withThumbnails: withThumbnails, + photoHighResolution: photoHighResolution, + orderByGivenName: orderByGivenName, + iOSLocalizedLabels: iOSLocalizedLabels, + androidLocalizedLabels: androidLocalizedLabels, + ); + + /// Fetches all contacts, or when specified, the contacts with the phone + /// matching [phone] + @override + Future> getContactsForPhone( + String? phone, { + bool withThumbnails = true, + bool photoHighResolution = true, + bool orderByGivenName = true, + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) => + _channel.getContactsForPhone( + phone, + withThumbnails: withThumbnails, + photoHighResolution: photoHighResolution, + orderByGivenName: orderByGivenName, + iOSLocalizedLabels: iOSLocalizedLabels, + androidLocalizedLabels: androidLocalizedLabels, + ); + + /// Fetches all contacts, or when specified, the contacts with the email + /// matching [email] + /// Works only on iOS + @override + Future> getContactsForEmail( + String email, { + bool withThumbnails = true, + bool photoHighResolution = true, + bool orderByGivenName = true, + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) => + _channel.getContactsForEmail( + email, + withThumbnails: withThumbnails, + photoHighResolution: photoHighResolution, + orderByGivenName: orderByGivenName, + iOSLocalizedLabels: iOSLocalizedLabels, + androidLocalizedLabels: androidLocalizedLabels, + ); + + /// Loads the avatar for the given contact and returns it. If the user does + /// not have an avatar, then `null` is returned in that slot. Only implemented + /// on Android. + @override + Future getAvatar(Contact contact, {bool photoHighRes = true}) => + _channel.getAvatar(contact, photoHighRes: photoHighRes); + + /// Adds the [contact] to the device contact list + @override + Future addContact(Contact contact) => _channel.addContact(contact); + + /// Deletes the [contact] if it has a valid identifier + @override + Future deleteContact(Contact contact) => + _channel.deleteContact(contact); + + /// Updates the [contact] if it has a valid identifier + @override + Future updateContact(Contact contact) => + _channel.updateContact(contact); + + /// Opens the contact form with the fields prefilled with the values from the + @override + Future openContactForm({ + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) => + _channel.openContactForm( + iOSLocalizedLabels: iOSLocalizedLabels, + androidLocalizedLabels: androidLocalizedLabels, + ); + + /// Opens the contact form with the fields prefilled with the values from the + /// [contact] parameter + @override + Future openExistingContact( + Contact contact, { + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) => + _channel.openExistingContact( + contact, + iOSLocalizedLabels: iOSLocalizedLabels, + androidLocalizedLabels: androidLocalizedLabels, + ); + + /// Displays the device/native contact picker dialog + /// and returns the contact selected by the user + @override + Future openDeviceContactPicker({ + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) => + _channel.openDeviceContactPicker( + iOSLocalizedLabels: iOSLocalizedLabels, + androidLocalizedLabels: androidLocalizedLabels, + ); +} diff --git a/contactos_android/pubspec.lock b/contactos_android/pubspec.lock new file mode 100644 index 0000000..4acdebf --- /dev/null +++ b/contactos_android/pubspec.lock @@ -0,0 +1,276 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + url: "https://pub.dev" + source: hosted + version: "1.4.1" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + contactos_platform_interface: + dependency: "direct main" + description: + name: contactos_platform_interface + sha256: "6fc797fa685c84ef7e1ec8fc8c6aae1f9f09274bcc5ea26e435781a4fa907628" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_driver: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + fuchsia_remote_debug_protocol: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + integration_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0 + url: "https://pub.dev" + source: hosted + version: "6.0.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" + url: "https://pub.dev" + source: hosted + version: "0.12.18" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + url: "https://pub.dev" + source: hosted + version: "0.13.0" + meta: + dependency: transitive + description: + name: meta + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + url: "https://pub.dev" + source: hosted + version: "1.17.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + process: + dependency: transitive + description: + name: process + sha256: c6248e4526673988586e8c00bb22a49210c258dc91df5227d5da9748ecf79744 + url: "https://pub.dev" + source: hosted + version: "5.0.5" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://pub.dev" + source: hosted + version: "1.10.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + sync_http: + dependency: transitive + description: + name: sync_http + sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961" + url: "https://pub.dev" + source: hosted + version: "0.3.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" + url: "https://pub.dev" + source: hosted + version: "0.7.9" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + url: "https://pub.dev" + source: hosted + version: "15.0.2" + webdriver: + dependency: transitive + description: + name: webdriver + sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade" + url: "https://pub.dev" + source: hosted + version: "3.1.0" +sdks: + dart: ">=3.9.0-0 <4.0.0" + flutter: ">=3.29.3" diff --git a/contactos_android/pubspec.yaml b/contactos_android/pubspec.yaml new file mode 100644 index 0000000..4157d90 --- /dev/null +++ b/contactos_android/pubspec.yaml @@ -0,0 +1,59 @@ +name: contactos_android +description: Android implementation of the contactos plugin. +version: 0.0.1 + +# publish_to: none + +homepage: https://github.com/ziqq/contactos/contactos_android + +repository: https://github.com/ziqq/contactos/contactos_android + +issue_tracker: https://github.com/ziqq/contactos/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+contactos%22 + +funding: + - https://www.buymeacoffee.com/ziqq + - https://boosty.to/ziqq + +#screenshots: +# - description: 'Example of using the library to check app version.' +# path: example.png + +topics: + - contacts + + +environment: + sdk: '>=3.6.0 <4.0.0' + flutter: ">=3.29.0" + + +dependencies: + flutter: + sdk: flutter + + contactos_platform_interface: ^1.0.1 + # contactos_platform_interface: + # path: ../contactos_platform_interface + + +dev_dependencies: + # Integration tests for Flutter + integration_test: + sdk: flutter + # Unit & Widget tests for Flutter + flutter_test: + sdk: flutter + + # Linting + flutter_lints: ^6.0.0 + + +flutter: + plugin: + implements: contactos + platforms: + android: + package: flutter.plugins.contactos + pluginClass: ContactosPlugin + dartPluginClass: ContactosPluginAndroid + diff --git a/contactos_android/test/contactos_android_test.dart b/contactos_android/test/contactos_android_test.dart new file mode 100644 index 0000000..ebe7cf3 --- /dev/null +++ b/contactos_android/test/contactos_android_test.dart @@ -0,0 +1,273 @@ +// Copyright 2025 Anton Ustinoff. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:contactos_android/contactos_android.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('ContactosPluginAndroid -', () { + const channel = MethodChannel('github.com/ziqq/contactos'); + final log = []; + + setUp(() { + TestWidgetsFlutterBinding.ensureInitialized(); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (methodCall) async { + log.add(methodCall); + switch (methodCall.method) { + case 'getContacts': + case 'getContactsForEmail': + case 'getContactsForPhone': + return [ + {'identifier': 'id', 'displayName': 'Name'} + ]; + case 'openContactForm': + case 'openExistingContact': + return {'identifier': 'id', 'displayName': 'Name'}; + case 'openDeviceContactPicker': + return [ + {'identifier': 'id', 'displayName': 'Name'} + ]; + case 'getAvatar': + return Uint8List.fromList([0, 1, 2]); + default: + return null; + } + }); + log.clear(); + }); + + tearDown(log.clear); + + test('registerWith registers the instance', () { + ContactosPluginAndroid.registerWith(); + expect(ContactosPlatform.instance, isA()); + }); + + group('getContacts -', () { + test('calls method channel with correct arguments', () async { + await ContactosPluginAndroid.instance.getContacts( + query: 'query', + withThumbnails: false, + photoHighResolution: false, + orderByGivenName: false, + iOSLocalizedLabels: false, + androidLocalizedLabels: false, + ); + + expect(log, hasLength(1)); + expect( + log.first, + isMethodCall( + 'getContacts', + arguments: { + 'query': 'query', + 'withThumbnails': false, + 'photoHighResolution': false, + 'orderByGivenName': false, + 'iOSLocalizedLabels': false, + 'androidLocalizedLabels': false, + }, + ), + ); + }); + }); + + group('getContactsForPhone -', () { + test('calls method channel with correct arguments', () async { + await ContactosPluginAndroid.instance.getContactsForPhone( + '123', + withThumbnails: false, + photoHighResolution: false, + orderByGivenName: false, + iOSLocalizedLabels: false, + androidLocalizedLabels: false, + ); + + expect(log, hasLength(1)); + expect( + log.first, + isMethodCall( + 'getContactsForPhone', + arguments: { + 'phone': '123', + 'withThumbnails': false, + 'photoHighResolution': false, + 'orderByGivenName': false, + 'iOSLocalizedLabels': false, + 'androidLocalizedLabels': false, + }, + ), + ); + }); + }); + + group('getContactsForEmail -', () { + test('calls method channel with correct arguments', () async { + await ContactosPluginAndroid.instance.getContactsForEmail( + 'test@example.com', + withThumbnails: false, + photoHighResolution: false, + orderByGivenName: false, + iOSLocalizedLabels: false, + androidLocalizedLabels: false, + ); + + expect(log, hasLength(1)); + expect( + log.first, + isMethodCall( + 'getContactsForEmail', + arguments: { + 'email': 'test@example.com', + 'withThumbnails': false, + 'photoHighResolution': false, + 'orderByGivenName': false, + 'iOSLocalizedLabels': false, + 'androidLocalizedLabels': false, + }, + ), + ); + }); + }); + + group('getAvatar -', () { + test('calls method channel with correct arguments', () async { + const contact = Contact(identifier: 'id'); + await ContactosPluginAndroid.instance.getAvatar( + contact, + photoHighRes: false, + ); + + expect(log, hasLength(1)); + expect( + log.first, + isMethodCall( + 'getAvatar', + arguments: { + 'contact': contact.toJson(), + 'identifier': 'id', + 'photoHighResolution': false, + }, + ), + ); + }); + }); + + group('addContact -', () { + test('calls method channel with correct arguments', () async { + const contact = Contact(identifier: 'id'); + await ContactosPluginAndroid.instance.addContact(contact); + + expect(log, hasLength(1)); + expect( + log.first, + isMethodCall( + 'addContact', + arguments: contact.toJson(), + ), + ); + }); + }); + + group('deleteContact -', () { + test('calls method channel with correct arguments', () async { + const contact = Contact(identifier: 'id'); + await ContactosPluginAndroid.instance.deleteContact(contact); + + expect(log, hasLength(1)); + expect( + log.first, + isMethodCall( + 'deleteContact', + arguments: contact.toJson(), + ), + ); + }); + }); + + group('updateContact -', () { + test('calls method channel with correct arguments', () async { + const contact = Contact(identifier: 'id'); + await ContactosPluginAndroid.instance.updateContact(contact); + + expect(log, hasLength(1)); + expect( + log.first, + isMethodCall( + 'updateContact', + arguments: contact.toJson(), + ), + ); + }); + }); + + group('openContactForm -', () { + test('calls method channel with correct arguments', () async { + await ContactosPluginAndroid.instance.openContactForm( + iOSLocalizedLabels: false, + androidLocalizedLabels: false, + ); + + expect(log, hasLength(1)); + expect( + log.first, + isMethodCall( + 'openContactForm', + arguments: { + 'iOSLocalizedLabels': false, + 'androidLocalizedLabels': false, + }, + ), + ); + }); + }); + + group('openExistingContact -', () { + test('calls method channel with correct arguments', () async { + const contact = Contact(identifier: 'id'); + await ContactosPluginAndroid.instance.openExistingContact( + contact, + iOSLocalizedLabels: false, + androidLocalizedLabels: false, + ); + + expect(log, hasLength(1)); + expect( + log.first, + isMethodCall( + 'openExistingContact', + arguments: { + 'contact': contact.toJson(), + 'iOSLocalizedLabels': false, + 'androidLocalizedLabels': false, + }, + ), + ); + }); + }); + + group('openDeviceContactPicker -', () { + test('calls method channel with correct arguments', () async { + await ContactosPluginAndroid.instance.openDeviceContactPicker( + iOSLocalizedLabels: false, + androidLocalizedLabels: false, + ); + + expect(log, hasLength(1)); + expect( + log.first, + isMethodCall( + 'openDeviceContactPicker', + arguments: { + 'iOSLocalizedLabels': false, + 'androidLocalizedLabels': false, + }, + ), + ); + }); + }); + }); +} diff --git a/contactos_android/tool/tag.dart b/contactos_android/tool/tag.dart new file mode 100644 index 0000000..b2ccab2 --- /dev/null +++ b/contactos_android/tool/tag.dart @@ -0,0 +1,76 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:collection/collection.dart'; +import 'package:l/l.dart'; + +Future _runGitCommand(List args) async { + final result = await Process.run('git', args); + if (result.exitCode == 0) return; + l.e('Error running git ${args.join(" ")}: ${result.stderr}'); + exit(1); +} + +/// dart run tool/tag.dart +void main() => runZonedGuarded( + () async { + // Check if there are any uncommitted or unpushed changes + final statusResult = + await Process.run('git', ['status', '--porcelain']); + if ((statusResult.stdout as String).trim().isNotEmpty) { + l.e('There are uncommitted changes.'); + exit(1); + } + final aheadResult = await Process.run( + 'git', ['rev-list', '--count', '--left-only', '@{u}...HEAD']); + if ((aheadResult.stdout as String).trim() != '0') { + l.e('There are unpushed changes.'); + exit(1); + } + + // Fetch pubspec.yaml file to get the version + final pubspecFile = File('pubspec.yaml'); + if (!pubspecFile.existsSync()) { + l.w('File pubspec.yaml not found.'); + exit(1); + } + final pubspecContent = pubspecFile.readAsLinesSync(); + final versionLine = pubspecContent + .firstWhereOrNull((line) => line.trim().startsWith('version:')); + if (versionLine == null || versionLine.isEmpty) { + l.e('Version not found in pubspec.yaml.'); + exit(1); + } + final version = versionLine.split(':')[1].trim(); + l.i('Found version: $version'); + + // Validate version format + final versionParts = version.split('.'); + if (versionParts.length != 3 || + versionParts.any((e) => int.tryParse(e) == null)) { + l.e('Invalid version format: $version'); + exit(1); + } + + final tagName = 'v$version'; // Tag format: v1.2.3 + + // Check if the tag already exists + final tagResult = await Process.run('git', ['tag', '-l', tagName]); + if (tagResult.stdout?.toString().trim() == tagName) { + l.e('Tag $tagName already exists.'); + exit(1); + } + + // Create tag + await _runGitCommand(['tag', tagName]); + l.i('Tag $tagName created.'); + + // Push tag + await _runGitCommand(['push', 'origin', tagName]); + l.i('Tag $tagName pushed to remote repository.'); + }, + (e, st) { + l.e('Unexpected error: $e', st); + exit(1); + }, + ); diff --git a/contactos_foundation/.gitignore b/contactos_foundation/.gitignore new file mode 100644 index 0000000..9cfb36e --- /dev/null +++ b/contactos_foundation/.gitignore @@ -0,0 +1,82 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +**/build +**/doc/api/ +**/Flutter/ephemeral/ +**/ios/Flutter/.last_build_id +**/Pods/ +.dart_tool/ +.flutter-plugins-dependencies +.flutter-plugins +.pub-cache/ +.pub/ +.fvm/ + +# Platform-specific generated files +**/android/**/GeneratedPluginRegistrant.* +**/android/**/local.properties +**/android/**/.cxx/ +**/android/.kotlin +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +**/android/**/key.properties +**/*.keystore +**/*.jks +**/android/**/captures/ +**/android/**/.gradle +**/android/**/gradlew +**/android/**/gradle-wrapper.jar +**/android/**/gradlew.bat +**/ios/Flutter/Generated.xcconfig +**/ios/Flutter/flutter_export_environment.sh +**/ios/Runner/GeneratedPluginRegistrant.* +**/macos/Flutter/GeneratedPluginRegistrant.* +**/windows/flutter/generated_* +**/linux/flutter/generated_* + +# Xcode-related +**/dgph +**/xcuserdata/ + +# Pana +log.pana.json + +# Test +.coverage/ +coverage/ +reports/ +/test/**/*.json +/test/.test_coverage.dart +!/test/**/fixtures/*.json +integration_test/screenshots + +# Temp +.temp/ +temp/ +.tmp/ +tmp/ \ No newline at end of file diff --git a/contactos_foundation/.pubignore b/contactos_foundation/.pubignore new file mode 100644 index 0000000..7b5ee92 --- /dev/null +++ b/contactos_foundation/.pubignore @@ -0,0 +1,5 @@ +pubspec.lock +reports/ +coverage/ +build/ +example/ \ No newline at end of file diff --git a/contactos_foundation/CHANGELOG.md b/contactos_foundation/CHANGELOG.md new file mode 100644 index 0000000..ef51eaa --- /dev/null +++ b/contactos_foundation/CHANGELOG.md @@ -0,0 +1,7 @@ +# Changelog + +## 0.0.2 +- **CHANGED**: Package metadata + +## 0.0.1 +- **ADDED**: Initial release \ No newline at end of file diff --git a/contactos_foundation/LICENSE b/contactos_foundation/LICENSE new file mode 100644 index 0000000..4de1fa1 --- /dev/null +++ b/contactos_foundation/LICENSE @@ -0,0 +1,27 @@ +BSD 3-Clause License + +Copyright (c) 2025, Anton Ustinoff +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/contactos_foundation/Makefile b/contactos_foundation/Makefile new file mode 100644 index 0000000..ee4c505 --- /dev/null +++ b/contactos_foundation/Makefile @@ -0,0 +1,108 @@ +SHELL :=/bin/bash -e -o pipefail +PWD :=$(shell pwd) + +.DEFAULT_GOAL := all +.PHONY: all +all: ## build pipeline +all: format check test-unit publish-check + +.PHONY: ci +ci: ## CI build pipeline +ci: all + +.PHONY: precommit +precommit: ## validate the branch before commit +precommit: all + +.PHONY: help +help: ## Help dialog + @echo 'Usage: make ' + @echo '' + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' + +.PHONY: doctor +doctor: ## Check flutter doctor + @fvm flutter doctor + +.PHONY: version +version: ## Check flutter version + @fvm flutter --version + +.PHONY: format +format: ## Format code + @fvm dart format -l 80 lib test || (echo "Β―\_(ツ)_/Β― Format code error"; exit 1) + +.PHONY: fix +fix: format ## Fix code + @fvm dart fix --apply lib + +.PHONY: clean-cache +clean-cache: ## Clean the pub cache + @fvm flutter pub cache repair + +.PHONY: clean +clean: ## Clean flutter + @fvm flutter clean + +.PHONY: get +get: ## Get dependencies + @fvm flutter pub get || (echo "Β―\_(ツ)_/Β― Get contactos_foundation dependencies error"; exit 1) + +.PHONY: analyze +analyze: get format ## Analyze code + @fvm dart analyze --fatal-infos --fatal-warnings || (echo "Β―\_(ツ)_/Β― Analyze code error"; exit 1) + +.PHONY: check +check: analyze ## Check the code + @fvm dart pub global deactivate pana > /dev/null 2>&1 || true + @fvm dart pub global activate pana + @fvm dart pub global run pana --json > log.pana.json || (echo "Β―\_(ツ)_/Β― Pana analysis error"; exit 1) + +.PHONY: publish-check +publish-check: check ## Check the code before publish + @fvm dart pub publish --dry-run || (echo "Β―\_(ツ)_/Β― Publish check error"; exit 1) + +.PHONY: publish +publish: ## Publish package + @fvm dart pub publish --server=https://pub.dartlang.org || (echo "Β―\_(ツ)_/Β― Publish error"; exit 1) + +.PHONY: coverage +coverage: ## Runs get coverage + @lcov --summary coverage/lcov.info + +.PHONY: run-genhtml +run-genhtml: ## Runs generage coverage html + @genhtml coverage/lcov.info -o coverage/html + +.PHONY: test-unit +test-unit: ## Runs unit tests + @fvm flutter test --coverage test/contactos_foundation_test.dart || (echo "Β―\_(ツ)_/Β― Error while running test-unit"; exit 1) + @genhtml coverage/lcov.info --output=coverage -o coverage/html || (echo "Β―\_(ツ)_/Β― Error while running genhtml with coverage"; exit 2) + +.PHONY: tag +tag: ## Add a tag to the current commit + @fvm dart run tool/tag.dart + +.PHONY: tag-add +tag-add: ## Add TAG. E.g: make tag-add TAG=v1.0.0 + @if [ -z "$(TAG)" ]; then echo "Β―\_(ツ)_/Β― TAG is not set"; exit 1; fi + @echo "" + @echo "START ADDING TAG: $(TAG)" + @echo "" + @git tag $(TAG) + @git push origin $(TAG) + @echo "" + @echo "CREATED AND PUSHED TAG $(TAG)" + @echo "" + +.PHONY: tag-remove +tag-remove: ## Delete TAG. E.g: make tag-delete TAG=v1.0.0 + @if [ -z "$(TAG)" ]; then echo "Β―\_(ツ)_/Β― TAG is not set"; exit 1; fi + @echo "" + @echo "START REMOVING TAG: $(TAG)" + @echo "" + @git tag -d $(TAG) + @git push origin --delete $(TAG) + @echo "" + @echo "DELETED TAG $(TAG) LOCALLY AND REMOTELY" + @echo "" diff --git a/contactos_foundation/README.md b/contactos_foundation/README.md new file mode 100644 index 0000000..efb636b --- /dev/null +++ b/contactos_foundation/README.md @@ -0,0 +1,27 @@ +# contactos_foundation + +[![pub package](https://img.shields.io/pub/v/contactos_foundation.svg)](https://pub.dev/packages/contactos_foundation) +[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT) +[![style: flutter lints](https://img.shields.io/badge/style-flutter__lints-blue)](https://pub.dev/packages/flutter_lints) + +The iOS (Darwin) implementation of the [`contactos`](https://pub.dev/packages/contactos) plugin. + +## Usage + +This package is not meant to be used directly. It is automatically included in your app when you depend on the main [`contactos`](https://pub.dev/packages/contactos) package. + +For more information on how to use the `contactos` plugin, please see the [main package's README](https://github.com/ziqq/contactos/blob/main/contactos/README.md). + +## Contributing + +This is part of a federated plugin. Contributions to the platform-specific implementations are welcome. Please see the [main repository](https://github.com/ziqq/contactos) for more information on how to contribute. + + +## Maintainers + +[Anton Ustinoff (ziqq)](https://github.com/ziqq) + + +## License + +[MIT](https://github.com/ziqq/contactos/blob/main/LICENSE) diff --git a/contactos_foundation/analysis_options.yaml b/contactos_foundation/analysis_options.yaml new file mode 100644 index 0000000..ed15c99 --- /dev/null +++ b/contactos_foundation/analysis_options.yaml @@ -0,0 +1,235 @@ +include: package:flutter_lints/flutter.yaml + +analyzer: + exclude: + # Build + - "build/**" + # Tests + - "test/**.mocks.dart" + - ".test_coverage.dart" + - ".coverage/**" + - "coverage/**" + # Assets + - "assets/**" + # Generated + - "**/generated/**" + # - '**/lib/l10n/**.arb' + - "**.g.dart" + - "**.gr.dart" + - "**.gql.dart" + - "**.freezed.dart" + - "**.config.dart" + - "**.mocks.dart" + - "**.gen.dart" + - "**.pb.dart" + - "**.pbenum.dart" + - "**.pbjson.dart" + # Flutter Version Manager + - ".fvm/**" + # Tools + - ".dart_tool/**" + - "scripts/**" + - "tool/**" + # Platform + - "ios/**" + - "android/**" + - "web/**" + - "macos/**" + - "windows/**" + - "linux/**" + + # Enable the following options to enable strong mode. + language: + strict-casts: true + strict-raw-types: true + strict-inference: true + + errors: + # Allow having TODOs in the code + todo: ignore + + # Info + directives_ordering: info + always_declare_return_types: info + + # Warning + unsafe_html: warning + missing_return: warning + missing_required_param: warning + no_logic_in_create_state: warning + empty_catches: warning + + # Error + always_use_package_imports: error + avoid_relative_lib_imports: error + avoid_slow_async_io: error + avoid_types_as_parameter_names: error + valid_regexps: error + always_require_non_null_named_parameters: error + +linter: + rules: + # Public packages + public_member_api_docs: true + lines_longer_than_80_chars: true + + # Enabling rules + always_use_package_imports: true + avoid_relative_lib_imports: true + + # Disable rules + sort_pub_dependencies: false + prefer_relative_imports: false + prefer_final_locals: false + avoid_escaping_inner_quotes: false + curly_braces_in_flow_control_structures: false + + # Enabled + use_named_constants: true + unnecessary_constructor_name: true + sort_constructors_first: true + exhaustive_cases: true + sort_unnamed_constructors_first: true + type_literal_in_constant_pattern: true + always_put_required_named_parameters_first: true + avoid_annotating_with_dynamic: true + avoid_bool_literals_in_conditional_expressions: true + avoid_double_and_int_checks: true + avoid_field_initializers_in_const_classes: true + avoid_implementing_value_types: true + avoid_js_rounded_ints: true + avoid_print: true + avoid_renaming_method_parameters: true + avoid_returning_null_for_void: true + avoid_single_cascade_in_expression_statements: true + avoid_slow_async_io: true + avoid_unnecessary_containers: true + avoid_unused_constructor_parameters: true + avoid_void_async: true + await_only_futures: true + cancel_subscriptions: true + cascade_invocations: true + close_sinks: true + control_flow_in_finally: true + empty_statements: true + collection_methods_unrelated_type: true + join_return_with_assignment: true + leading_newlines_in_multiline_strings: true + literal_only_boolean_expressions: true + missing_whitespace_between_adjacent_strings: true + no_adjacent_strings_in_list: true + no_logic_in_create_state: true + no_runtimeType_toString: true + only_throw_errors: true + overridden_fields: true + package_names: true + package_prefixed_library_names: true + parameter_assignments: true + prefer_asserts_in_initializer_lists: true + prefer_asserts_with_message: true + prefer_const_constructors: true + prefer_const_constructors_in_immutables: true + prefer_const_declarations: true + prefer_const_literals_to_create_immutables: true + prefer_constructors_over_static_methods: true + prefer_expression_function_bodies: true + prefer_final_in_for_each: true + prefer_foreach: true + prefer_if_elements_to_conditional_expressions: true + prefer_inlined_adds: true + prefer_int_literals: true + prefer_is_not_operator: true + prefer_null_aware_operators: true + prefer_typing_uninitialized_variables: true + prefer_void_to_null: true + provide_deprecation_message: true + sized_box_for_whitespace: true + sort_child_properties_last: true + test_types_in_equals: true + throw_in_finally: true + unnecessary_null_aware_assignments: true + unnecessary_overrides: true + unnecessary_parenthesis: true + unnecessary_raw_strings: true + unnecessary_statements: true + unnecessary_string_escapes: true + unnecessary_string_interpolations: true + # unsafe_html: true + use_full_hex_values_for_flutter_colors: true + use_raw_strings: true + use_string_buffers: true + valid_regexps: true + void_checks: true + + # Pedantic 1.9.0 + always_declare_return_types: true + annotate_overrides: true + avoid_empty_else: true + avoid_init_to_null: true + avoid_null_checks_in_equality_operators: true + avoid_return_types_on_setters: true + avoid_shadowing_type_parameters: true + avoid_types_as_parameter_names: true + camel_case_extensions: true + empty_catches: true + empty_constructor_bodies: true + library_names: true + library_prefixes: true + no_duplicate_case_values: true + null_closures: true + omit_local_variable_types: true + prefer_adjacent_string_concatenation: true + prefer_collection_literals: true + prefer_conditional_assignment: true + prefer_contains: true + prefer_final_fields: true + prefer_for_elements_to_map_fromIterable: true + prefer_generic_function_type_aliases: true + prefer_if_null_operators: true + prefer_is_empty: true + prefer_is_not_empty: true + prefer_iterable_whereType: true + prefer_single_quotes: true + prefer_spread_collections: true + recursive_getters: true + slash_for_doc_comments: true + type_init_formals: true + unawaited_futures: true + unnecessary_const: true + unnecessary_new: true + unnecessary_null_in_if_null_operators: true + unnecessary_this: true + unrelated_type_equality_checks: true + use_function_type_syntax_for_parameters: true + use_rethrow_when_possible: true + + # Effective_dart 1.2.0 + camel_case_types: true + file_names: true + non_constant_identifier_names: true + constant_identifier_names: true + directives_ordering: true + # package_api_docs: true + implementation_imports: true + prefer_interpolation_to_compose_strings: true + unnecessary_brace_in_string_interps: true + avoid_function_literals_in_foreach_calls: true + prefer_function_declarations_over_variables: true + unnecessary_lambdas: true + unnecessary_getters_setters: true + prefer_initializing_formals: true + avoid_catches_without_on_clauses: true + avoid_catching_errors: true + use_to_and_as_if_applicable: true + one_member_abstracts: true + avoid_classes_with_only_static_members: true + prefer_mixin: true + use_setters_to_change_properties: true + avoid_setters_without_getters: true + avoid_returning_this: true + type_annotate_public_apis: true + avoid_types_on_closure_parameters: true + avoid_private_typedef_functions: true + avoid_positional_boolean_parameters: true + hash_and_equals: true + avoid_equals_and_hash_code_on_mutable_classes: true \ No newline at end of file diff --git a/contactos_foundation/dart_dependency_validator.yaml b/contactos_foundation/dart_dependency_validator.yaml new file mode 100644 index 0000000..06cc68f --- /dev/null +++ b/contactos_foundation/dart_dependency_validator.yaml @@ -0,0 +1,8 @@ +ignore: + - dart_code_metrics + - integration_test + - flutter_lints + - collection + +exclude: + - "example/**" diff --git a/contactos/ios/contactos.podspec b/contactos_foundation/darwin/contactos_foundation.podspec similarity index 61% rename from contactos/ios/contactos.podspec rename to contactos_foundation/darwin/contactos_foundation.podspec index 641697c..3a185c2 100644 --- a/contactos/ios/contactos.podspec +++ b/contactos_foundation/darwin/contactos_foundation.podspec @@ -2,28 +2,37 @@ # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html # Pod::Spec.new do |s| - s.name = 'contactos' - s.version = '1.0.6' + s.name = 'contactos_foundation' + s.version = '0.0.1' s.summary = "A Flutter plugin to retrieve and manage contacts on iOS devices" s.description = <<-DESC A Flutter plugin to retrieve and manage contacts on iOS devices. DESC - s.homepage = 'https://github.com/ziqq/contactos/contactos' + + s.homepage = 'https://github.com/ziqq/contactos/contactos_foundation' s.license = { :type => 'BSD', :file => '../LICENSE' } - s.author = { 'Anton Ustinoff' => 'a.a.ustinoff@gmail.com' } - s.source = { :http => 'https://github.com/ziqq/contactos/contactos' } - s.public_header_files = 'contactos/Sources/contactos/**/*.h' - s.source_files = 'contactos/Sources/contactos/**/*.{swift,m,h}' + s.author = 'Anton Ustinoff' + + # Use local path (plugin shipped from local packages directory for Flutter). Remote :http not needed. + s.source = { :path => '.' } + s.source_files = 'contactos_foundation/Sources/contactos_foundation/**/*.swift' + s.requires_arc = true + s.ios.dependency 'Flutter' s.osx.dependency 'FlutterMacOS' - s.ios.deployment_target = '12.0' - s.osx.deployment_target = '10.14' + + s.ios.deployment_target = '13.0' + s.osx.deployment_target = '10.15' + + s.ios.frameworks = 'UIKit' + s.osx.frameworks = 'AppKit' + + s.swift_version = '5.0' s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } s.xcconfig = { 'LIBRARY_SEARCH_PATHS' => '$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)/ $(SDKROOT)/usr/lib/swift', 'LD_RUNPATH_SEARCH_PATHS' => '/usr/lib/swift', } - s.swift_version = '5.0' s.resource_bundles = {'contactos_privacy' => ['contactos/Sources/contactos/Resources/PrivacyInfo.xcprivacy']} end diff --git a/contactos/ios/contactos/Package.swift b/contactos_foundation/darwin/contactos_foundation/Package.swift similarity index 100% rename from contactos/ios/contactos/Package.swift rename to contactos_foundation/darwin/contactos_foundation/Package.swift diff --git a/contactos/ios/contactos/Sources/contactos/ContactosPlugin.swift b/contactos_foundation/darwin/contactos_foundation/Sources/contactos_foundation/ContactosPlugin.swift similarity index 94% rename from contactos/ios/contactos/Sources/contactos/ContactosPlugin.swift rename to contactos_foundation/darwin/contactos_foundation/Sources/contactos_foundation/ContactosPlugin.swift index e0ede27..545d6f0 100644 --- a/contactos/ios/contactos/Sources/contactos/ContactosPlugin.swift +++ b/contactos_foundation/darwin/contactos_foundation/Sources/contactos_foundation/ContactosPlugin.swift @@ -4,26 +4,25 @@ import Contacts import ContactsUI @available(iOS 9.0, *) -public class SwiftContactosPlugin: NSObject, FlutterPlugin, CNContactViewControllerDelegate, CNContactPickerDelegate { +public class ContactosPlugin: NSObject, FlutterPlugin, CNContactViewControllerDelegate, CNContactPickerDelegate { private var result: FlutterResult? = nil private var localizedLabels: Bool = true - private let rootViewController: UIViewController static let FORM_OPERATION_CANCELED: Int = 1 static let FORM_COULD_NOT_BE_OPEN: Int = 2 public static func register(with registrar: FlutterPluginRegistrar) { let channel = FlutterMethodChannel(name: "github.com/ziqq/contactos", binaryMessenger: registrar.messenger()) - guard let rootViewController = UIApplication.shared.delegate?.window??.rootViewController else { - NSLog("ContactosPlugin: Failed to get rootViewController") - return - } - let instance = SwiftContactosPlugin(rootViewController) + let instance = ContactosPlugin() registrar.addMethodCallDelegate(instance, channel: channel) instance.preLoadContactView() } - init(_ rootViewController: UIViewController) { - self.rootViewController = rootViewController + override init() { + super.init() + } + + private func getRootViewController() -> UIViewController? { + return UIApplication.shared.delegate?.window??.rootViewController } public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { @@ -238,8 +237,9 @@ public class SwiftContactosPlugin: NSObject, FlutterPlugin, CNContactViewControl controller.delegate = self DispatchQueue.main.async { let navigation = UINavigationController .init(rootViewController: controller) - let viewController : UIViewController? = UIApplication.shared.delegate?.window??.rootViewController - viewController?.present(navigation, animated:true, completion: nil) + if let viewController = self.getRootViewController() { + viewController.present(navigation, animated:true, completion: nil) + } } return nil } @@ -253,9 +253,10 @@ public class SwiftContactosPlugin: NSObject, FlutterPlugin, CNContactViewControl @objc func cancelContactForm() { if let result = self.result { - let viewController : UIViewController? = UIApplication.shared.delegate?.window??.rootViewController - viewController?.dismiss(animated: true, completion: nil) - result(SwiftContactosPlugin.FORM_OPERATION_CANCELED) + if let viewController = self.getRootViewController() { + viewController.dismiss(animated: true, completion: nil) + } + result(ContactosPlugin.FORM_OPERATION_CANCELED) self.result = nil } } @@ -266,7 +267,7 @@ public class SwiftContactosPlugin: NSObject, FlutterPlugin, CNContactViewControl if let contact = contact { result(contactToDictionary(contact: contact, localizedLabels: localizedLabels)) } else { - result(SwiftContactosPlugin.FORM_OPERATION_CANCELED) + result(ContactosPlugin.FORM_OPERATION_CANCELED) } self.result = nil } @@ -277,7 +278,7 @@ public class SwiftContactosPlugin: NSObject, FlutterPlugin, CNContactViewControl do { // Check to make sure dictionary has an identifier guard let identifier = contact["identifier"] as? String else{ - result(SwiftContactosPlugin.FORM_COULD_NOT_BE_OPEN) + result(ContactosPlugin.FORM_COULD_NOT_BE_OPEN) return nil; } let backTitle = contact["backTitle"] as? String @@ -297,16 +298,18 @@ public class SwiftContactosPlugin: NSObject, FlutterPlugin, CNContactViewControl viewController.delegate = self DispatchQueue.main.async { let navigation = UINavigationController .init(rootViewController: viewController) - var currentViewController = UIApplication.shared.keyWindow?.rootViewController + var currentViewController = self.getRootViewController() while let nextView = currentViewController?.presentedViewController { currentViewController = nextView } - let activityIndicatorView = UIActivityIndicatorView.init(style: UIActivityIndicatorView.Style.gray) - activityIndicatorView.frame = (UIApplication.shared.keyWindow?.frame)! + let activityIndicatorView = UIActivityIndicatorView.init(style: .gray) + if let viewFrame = currentViewController?.view.frame { + activityIndicatorView.frame = viewFrame + } activityIndicatorView.startAnimating() activityIndicatorView.backgroundColor = UIColor.white navigation.view.addSubview(activityIndicatorView) - currentViewController!.present(navigation, animated: true, completion: nil) + currentViewController?.present(navigation, animated: true, completion: nil) DispatchQueue.main.asyncAfter(deadline: .now()+0.5 ){ activityIndicatorView.removeFromSuperview() @@ -315,7 +318,7 @@ public class SwiftContactosPlugin: NSObject, FlutterPlugin, CNContactViewControl return nil } catch { NSLog(error.localizedDescription) - result(SwiftContactosPlugin.FORM_COULD_NOT_BE_OPEN) + result(ContactosPlugin.FORM_COULD_NOT_BE_OPEN) return nil } } @@ -328,11 +331,13 @@ public class SwiftContactosPlugin: NSObject, FlutterPlugin, CNContactViewControl contactPicker.delegate = self //contactPicker!.displayedPropertyKeys = [CNContactPhoneNumbersKey]; DispatchQueue.main.async { - self.rootViewController.present(contactPicker, animated: true, completion: nil) + if let rootViewController = self.getRootViewController() { + rootViewController.present(contactPicker, animated: true, completion: nil) + } } } - //MARK:- CNContactPickerDelegate Method + // MARK:- CNContactPickerDelegate Method public func contactPicker(_ picker: CNContactPickerViewController, didSelect contact: CNContact) { if let result = self.result { @@ -343,7 +348,7 @@ public class SwiftContactosPlugin: NSObject, FlutterPlugin, CNContactViewControl public func contactPickerDidCancel(_ picker: CNContactPickerViewController) { if let result = self.result { - result(SwiftContactosPlugin.FORM_OPERATION_CANCELED) + result(ContactosPlugin.FORM_OPERATION_CANCELED) self.result = nil } } diff --git a/contactos/ios/contactos/Sources/contactos/Resources/PrivacyInfo.xcprivacy b/contactos_foundation/darwin/contactos_foundation/Sources/contactos_foundation/Resources/PrivacyInfo.xcprivacy similarity index 100% rename from contactos/ios/contactos/Sources/contactos/Resources/PrivacyInfo.xcprivacy rename to contactos_foundation/darwin/contactos_foundation/Sources/contactos_foundation/Resources/PrivacyInfo.xcprivacy diff --git a/contactos_foundation/darwin/contactos_foundation/Tests/.keep b/contactos_foundation/darwin/contactos_foundation/Tests/.keep new file mode 100644 index 0000000..e69de29 diff --git a/contactos_foundation/example/.gitignore b/contactos_foundation/example/.gitignore new file mode 100644 index 0000000..e96ef60 --- /dev/null +++ b/contactos_foundation/example/.gitignore @@ -0,0 +1,32 @@ +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/contactos_foundation/example/.metadata b/contactos_foundation/example/.metadata new file mode 100644 index 0000000..a700013 --- /dev/null +++ b/contactos_foundation/example/.metadata @@ -0,0 +1,8 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 8f65fec5f5f7d7afbb0965f4a44bdb330a28fb19 + channel: alpha diff --git a/contactos_foundation/example/README.md b/contactos_foundation/example/README.md new file mode 100644 index 0000000..8399213 --- /dev/null +++ b/contactos_foundation/example/README.md @@ -0,0 +1,8 @@ +# contactos_example + +Demonstrates how to use the contactos plugin. + +## Getting Started + +For help getting started with Flutter, view our online +[documentation](http://flutter.io/). diff --git a/contactos_foundation/example/ios/.gitignore b/contactos_foundation/example/ios/.gitignore new file mode 100644 index 0000000..14d6ec7 --- /dev/null +++ b/contactos_foundation/example/ios/.gitignore @@ -0,0 +1,43 @@ +.idea/ +.vagrant/ +.sconsign.dblite +.svn/ + +.DS_Store +*.swp +profile + +DerivedData/ +build/ +GeneratedPluginRegistrant.h +GeneratedPluginRegistrant.m + +*.pbxuser +*.mode1v3 +*.mode2v3 +*.perspectivev3 + +!default.pbxuser +!default.mode1v3 +!default.mode2v3 +!default.perspectivev3 + +xcuserdata + +*.moved-aside + +*.pyc +*sync/ +Icon? +.tags* + +/Flutter/app.flx +/Flutter/app.zip +/Flutter/App.framework +/Flutter/Flutter.framework +/Flutter/Generated.xcconfig +/Flutter/flutter_export_environment.sh +/ServiceDefinitions.json +/Flutter/Flutter.podspec + +Pods/ \ No newline at end of file diff --git a/contactos_foundation/example/ios/Flutter/AppFrameworkInfo.plist b/contactos_foundation/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..1f6b98f --- /dev/null +++ b/contactos_foundation/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,30 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + UIRequiredDeviceCapabilities + + arm64 + + MinimumOSVersion + 13.0 + + diff --git a/contactos_foundation/example/ios/Flutter/Debug.xcconfig b/contactos_foundation/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..e8efba1 --- /dev/null +++ b/contactos_foundation/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/contactos_foundation/example/ios/Flutter/Release.xcconfig b/contactos_foundation/example/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..399e934 --- /dev/null +++ b/contactos_foundation/example/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/contactos_foundation/example/ios/Podfile b/contactos_foundation/example/ios/Podfile new file mode 100644 index 0000000..268e8f9 --- /dev/null +++ b/contactos_foundation/example/ios/Podfile @@ -0,0 +1,49 @@ +# Uncomment this line to define a global platform for your project +platform :ios, '13.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + target.build_configurations.each do |config| + config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [ + '$(inherited)', + + ## dart: PermissionGroup.contacts + 'PERMISSION_CONTACTS=1', + ] + + end + end +end diff --git a/contactos_foundation/example/ios/Podfile.lock b/contactos_foundation/example/ios/Podfile.lock new file mode 100644 index 0000000..4a2dbe3 --- /dev/null +++ b/contactos_foundation/example/ios/Podfile.lock @@ -0,0 +1,35 @@ +PODS: + - contactos_foundation (0.0.1): + - Flutter + - FlutterMacOS + - Flutter (1.0.0) + - integration_test (0.0.1): + - Flutter + - permission_handler_apple (9.3.0): + - Flutter + +DEPENDENCIES: + - contactos_foundation (from `.symlinks/plugins/contactos_foundation/darwin`) + - Flutter (from `Flutter`) + - integration_test (from `.symlinks/plugins/integration_test/ios`) + - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) + +EXTERNAL SOURCES: + contactos_foundation: + :path: ".symlinks/plugins/contactos_foundation/darwin" + Flutter: + :path: Flutter + integration_test: + :path: ".symlinks/plugins/integration_test/ios" + permission_handler_apple: + :path: ".symlinks/plugins/permission_handler_apple/ios" + +SPEC CHECKSUMS: + contactos_foundation: 30cf4037c67f6003e6c2c35b61f748302e2632dd + Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 + integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573 + permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 + +PODFILE CHECKSUM: 142419311492affa1242e9eb36f2905ca1a37b07 + +COCOAPODS: 1.16.2 diff --git a/contactos_foundation/example/ios/Runner.xcodeproj/project.pbxproj b/contactos_foundation/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..872fc57 --- /dev/null +++ b/contactos_foundation/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,525 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB21CF90195004384FC /* Debug.xcconfig */; }; + 9740EEB51CF90195004384FC /* Generated.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB31CF90195004384FC /* Generated.xcconfig */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + D00F72E61F1A341EF236C241 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 376C65EDF2BBF4969C4BF795 /* Pods_Runner.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 376C65EDF2BBF4969C4BF795 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + DC2CD4E154FC564256406BCB /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + FADA5803582FD34ED02516C6 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D00F72E61F1A341EF236C241 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 0A611CA2056F00C87C8E2D3A /* Frameworks */ = { + isa = PBXGroup; + children = ( + 376C65EDF2BBF4969C4BF795 /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 6E34C24F55ECFA0978B626E3 /* Pods */ = { + isa = PBXGroup; + children = ( + FADA5803582FD34ED02516C6 /* Pods-Runner.debug.xcconfig */, + DC2CD4E154FC564256406BCB /* Pods-Runner.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 6E34C24F55ECFA0978B626E3 /* Pods */, + 0A611CA2056F00C87C8E2D3A /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 97C146F11CF9000F007C117D /* Supporting Files */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; + 97C146F11CF9000F007C117D /* Supporting Files */ = { + isa = PBXGroup; + children = ( + ); + name = "Supporting Files"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 0069A5BAC5F591E956309FC9 /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 7705EB8180C636DF7F0C2A3A /* [CP] Copy Pods Resources */, + AC8E7B761714740CD348D9B5 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = "The Chromium Authors"; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 0910; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + English, + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 9740EEB51CF90195004384FC /* Generated.xcconfig in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 0069A5BAC5F591E956309FC9 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 7705EB8180C636DF7F0C2A3A /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh", + "${PODS_CONFIGURATION_BUILD_DIR}/permission_handler_apple/permission_handler_apple_privacy.bundle", + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/permission_handler_apple_privacy.bundle", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; + AC8E7B761714740CD348D9B5 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/contactos_foundation/contactos_foundation.framework", + "${BUILT_PRODUCTS_DIR}/integration_test/integration_test.framework", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/contactos_foundation.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/integration_test.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ARCHS = arm64; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = ""; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = flutter.plugins.contactos.contactsServiceExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_SWIFT3_OBJC_INFERENCE = On; + SWIFT_VERSION = 4.0; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ARCHS = arm64; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = ""; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = flutter.plugins.contactos.contactsServiceExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_SWIFT3_OBJC_INFERENCE = On; + SWIFT_VERSION = 4.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/contactos_foundation/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/contactos_foundation/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/contactos_foundation/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/contactos_foundation/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/contactos_foundation/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/contactos_foundation/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/contactos_foundation/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/contactos_foundation/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..221227e --- /dev/null +++ b/contactos_foundation/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/contactos_foundation/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/contactos_foundation/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/contactos_foundation/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/contactos_foundation/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/contactos_foundation/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/contactos_foundation/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/contactos_foundation/example/ios/Runner/AppDelegate.swift b/contactos_foundation/example/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..b636303 --- /dev/null +++ b/contactos_foundation/example/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import UIKit +import Flutter + +@main +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d225b3c --- /dev/null +++ b/contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,121 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "idiom" : "ios-marketing", + "size" : "1024x1024", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..28c6bf0 Binary files /dev/null and b/contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..2ccbfd9 Binary files /dev/null and b/contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..f091b6b Binary files /dev/null and b/contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..4cde121 Binary files /dev/null and b/contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..d0ef06e Binary files /dev/null and b/contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..dcdc230 Binary files /dev/null and b/contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..2ccbfd9 Binary files /dev/null and b/contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..c8f9ed8 Binary files /dev/null and b/contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..a6d6b86 Binary files /dev/null and b/contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..a6d6b86 Binary files /dev/null and b/contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..75b2d16 Binary files /dev/null and b/contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..c4df70d Binary files /dev/null and b/contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..6a84f41 Binary files /dev/null and b/contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..d0e1f58 Binary files /dev/null and b/contactos_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/contactos_foundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/contactos_foundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/contactos_foundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/contactos_foundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/contactos_foundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/contactos_foundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/contactos_foundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/contactos_foundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/contactos_foundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/contactos_foundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/contactos_foundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/contactos_foundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/contactos_foundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/contactos_foundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/contactos_foundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +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. \ No newline at end of file diff --git a/contactos_foundation/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/contactos_foundation/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/contactos_foundation/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/contactos_foundation/example/ios/Runner/Base.lproj/Main.storyboard b/contactos_foundation/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/contactos_foundation/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/contactos_foundation/example/ios/Runner/Info.plist b/contactos_foundation/example/ios/Runner/Info.plist new file mode 100644 index 0000000..a61b535 --- /dev/null +++ b/contactos_foundation/example/ios/Runner/Info.plist @@ -0,0 +1,55 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + contactos_example + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + NSContactsUsageDescription + This app requires contacts access to function properly. + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + arm64 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/contactos_foundation/example/ios/Runner/Runner-Bridging-Header.h b/contactos_foundation/example/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..7335fdf --- /dev/null +++ b/contactos_foundation/example/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" \ No newline at end of file diff --git a/contactos_foundation/example/lib/main.dart b/contactos_foundation/example/lib/main.dart new file mode 100644 index 0000000..86291d3 --- /dev/null +++ b/contactos_foundation/example/lib/main.dart @@ -0,0 +1,99 @@ +import 'package:contactos_example/src/contacts_list_screen.dart'; +import 'package:contactos_example/src/navite_contacts_picker_screen.dart'; +import 'package:flutter/material.dart'; +import 'package:permission_handler/permission_handler.dart'; + +void main() => runApp(const ContactsExampleApp()); + +/// iOS only: Localized labels language setting +/// is equal to CFBundleDevelopmentRegion value (Info.plist) of the iOS project +/// Set iOSLocalizedLabels=false if you always want english labels +/// whatever is the CFBundleDevelopmentRegion value. +const iOSLocalizedLabels = false; + +/// {@template main} +/// Example app for the `contactos` plugin. +/// {@endtemplate} +class ContactsExampleApp extends StatelessWidget { + /// {@macro main} + const ContactsExampleApp({super.key}); + + @override + Widget build(BuildContext context) => MaterialApp( + home: const _HomePage(), + routes: { + '/add': (_) => const AddContactScreen(), + '/contacts-list': (_) => const ContactsListScreen(), + '/native-contacts-picker': (_) => const NativeContactsPickerScreen(), + }, + ); +} + +class _HomePage extends StatefulWidget { + const _HomePage({ + super.key, // ignore: unused_element_parameter + }); + + @override + State<_HomePage> createState() => __HomePageState(); +} + +class __HomePageState extends State<_HomePage> { + @override + void initState() { + super.initState(); + _askPermissions(); + } + + Future _askPermissions([String? routeName]) async { + final navigator = Navigator.of(context); + final permissionStatus = await _getContactPermission(); + if (permissionStatus == PermissionStatus.granted) { + if (routeName != null) await navigator.pushNamed(routeName); + } else { + _handleInvalidPermissions(permissionStatus); + } + } + + Future _getContactPermission() async { + final permission = await Permission.contacts.status; + if (permission != PermissionStatus.granted && + permission != PermissionStatus.permanentlyDenied) { + final permissionStatus = await Permission.contacts.request(); + return permissionStatus; + } else { + return permission; + } + } + + void _handleInvalidPermissions(PermissionStatus permissionStatus) { + Widget? content; + if (permissionStatus == PermissionStatus.denied) { + content = const Text('Access to contact data denied'); + } else if (permissionStatus == PermissionStatus.permanentlyDenied) { + content = const Text('Contact data not available on device'); + } + if (content == null) return; + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: content)); + } + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text('Contacts Plugin Example')), + body: SafeArea( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ElevatedButton( + child: const Text('Contacts list'), + onPressed: () => _askPermissions('/contacts-list'), + ), + ElevatedButton( + child: const Text('Native Contacts picker'), + onPressed: () => _askPermissions('/native-contacts-picker'), + ), + ], + ), + ), + ); +} diff --git a/contactos_foundation/example/lib/src/contacts_list_screen.dart b/contactos_foundation/example/lib/src/contacts_list_screen.dart new file mode 100644 index 0000000..566e465 --- /dev/null +++ b/contactos_foundation/example/lib/src/contacts_list_screen.dart @@ -0,0 +1,588 @@ +/* + * Author: Anton Ustinoff | + * Date: 25 November 2025 + */ + +import 'dart:developer'; + +import 'package:collection/collection.dart'; +import 'package:contactos_example/main.dart' show iOSLocalizedLabels; +import 'package:contactos_foundation/contactos_foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; + +/// {@template contacts_list_screen} +/// Contacts list screen widget. +/// {@endtemplate} +class ContactsListScreen extends StatefulWidget { + /// {@macro contacts_list_screen} + const ContactsListScreen({super.key}); + + @override + State createState() => _ContactsListPageState(); +} + +class _ContactsListPageState extends State { + List? _contacts; + + @override + void initState() { + super.initState(); + refreshContacts(); + } + + Future refreshContacts() async { + // final contacts = + // await ContactosPluginFoundation + // .instance + // .getContactsForPhone("8554964652"); + + // Load without thumbnails initially. + final contacts = await ContactosPluginFoundation.instance.getContacts( + iOSLocalizedLabels: iOSLocalizedLabels, + withThumbnails: false, + ); + + _contacts = contacts; + if (!mounted) return; + setState(() {}); + + // Lazy load thumbnails after rendering initial contacts. + for (var contact in _contacts ?? contacts) { + ContactosPluginFoundation.instance.getAvatar(contact).then((avatar) { + contact = contact.copyWith(avatar: avatar); + if (mounted) setState(() {}); + }).ignore(); + } + } + + Future updateContact() async { + var contact = _contacts?.firstWhereOrNull( + (c) => c.familyName?.startsWith('Ninja') ?? false, + ); + if (contact == null) return; + await ContactosPluginFoundation.instance.updateContact(contact); + await refreshContacts(); + } + + Future _openContactForm() async { + try { + var _ = await ContactosPluginFoundation.instance.openContactForm( + iOSLocalizedLabels: iOSLocalizedLabels, + ); + await refreshContacts(); + } on FormOperationException catch (e) { + switch (e.errorCode) { + case FormOperationErrorCode.unknown: + case FormOperationErrorCode.canceled: + case FormOperationErrorCode.couldNotBeOpen: + default: + log(e.toString()); + } + } + } + + void contactOnDeviceHasBeenUpdated(Contact contact) { + if (!mounted) return; + final id = _contacts?.indexWhere((c) => c.identifier == contact.identifier); + if (id == null || id < 0) return; + _contacts?[id] = contact; + setState(() {}); + } + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar( + title: const Text('Contacts Plugin Example'), + actions: [ + IconButton(icon: const Icon(Icons.create), onPressed: _openContactForm), + ], + ), + floatingActionButton: FloatingActionButton( + child: const Icon(Icons.add), + onPressed: () { + Navigator.of(context).pushNamed('/add').then((_) { + refreshContacts(); + }); + }, + ), + body: SafeArea( + child: _contacts != null + ? ListView.builder( + itemCount: _contacts?.length ?? 0, + itemBuilder: (context, index) { + final contact = _contacts?.elementAt(index); + if (contact == null) return const SizedBox.shrink(); + return ListTile( + onTap: () { + final route = MaterialPageRoute( + builder: (context) => _ContactDetailsPage( + contact, + onContactDeviceSave: contactOnDeviceHasBeenUpdated, + ), + ); + Navigator.of(context).push(route); + }, + leading: + (contact.avatar != null && + (contact.avatar?.length ?? 0) > 0) + ? CircleAvatar( + backgroundImage: MemoryImage(contact.avatar!), + ) + : CircleAvatar(child: Text(contact.initials())), + title: Text(contact.displayName ?? ''), + ); + }, + ) + : const Center(child: CircularProgressIndicator()), + ), + ); +} + +class _ContactDetailsPage extends StatefulWidget { + const _ContactDetailsPage( + this._contact, { + this.onContactDeviceSave, + super.key, // ignore: unused_element_parameter + }); + + final Contact _contact; + final void Function(Contact)? onContactDeviceSave; + + @override + State<_ContactDetailsPage> createState() => _ContactDetailsPageState(); +} + +class _ContactDetailsPageState extends State<_ContactDetailsPage> { + Future _openExistingContactOnDevice(BuildContext context) async { + try { + final contact = await ContactosPluginFoundation.instance + .openExistingContact( + widget._contact, + iOSLocalizedLabels: iOSLocalizedLabels, + ); + if (widget.onContactDeviceSave != null) { + widget.onContactDeviceSave?.call(contact); + } + if (!context.mounted) return; + Navigator.of(context).pop(); + } on FormOperationException catch (e) { + switch (e.errorCode) { + case FormOperationErrorCode.unknown: + case FormOperationErrorCode.canceled: + case FormOperationErrorCode.couldNotBeOpen: + default: + log(e.toString()); + } + } + } + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar( + title: Text(widget._contact.displayName ?? ''), + actions: [ + /* IconButton( + icon: Icon(Icons.share), + onPressed: () => shareVCFCard(context, contact: _contact), + ), */ + IconButton( + icon: const Icon(Icons.delete), + onPressed: () { + ContactosPluginFoundation.instance.deleteContact(widget._contact); + }, + ), + IconButton( + icon: const Icon(Icons.update), + onPressed: () => Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => + _UpdateContactsPage(contact: widget._contact), + ), + ), + ), + IconButton( + icon: const Icon(Icons.edit), + onPressed: () => _openExistingContactOnDevice(context), + ), + ], + ), + body: SafeArea( + child: ListView( + children: [ + ListTile( + title: const Text('Name'), + trailing: Text(widget._contact.givenName ?? ''), + ), + ListTile( + title: const Text('Middle name'), + trailing: Text(widget._contact.middleName ?? ''), + ), + ListTile( + title: const Text('Family name'), + trailing: Text(widget._contact.familyName ?? ''), + ), + ListTile( + title: const Text('Prefix'), + trailing: Text(widget._contact.prefix ?? ''), + ), + ListTile( + title: const Text('Suffix'), + trailing: Text(widget._contact.suffix ?? ''), + ), + ListTile( + title: const Text('Birthday'), + trailing: Text( + widget._contact.birthday != null + ? DateFormat('dd-MM-yyyy').format(widget._contact.birthday!) + : '', + ), + ), + ListTile( + title: const Text('Company'), + trailing: Text(widget._contact.company ?? ''), + ), + ListTile( + title: const Text('Job'), + trailing: Text(widget._contact.jobTitle ?? ''), + ), + ListTile( + title: const Text('Account Type'), + trailing: Text( + (widget._contact.androidAccountType != null) + ? widget._contact.androidAccountType.toString() + : '', + ), + ), + _AddressesTile(widget._contact.postalAddresses!), + _ItemsTile('Phones', widget._contact.phones!), + _ItemsTile('Emails', widget._contact.emails!), + ], + ), + ), + ); +} + +@immutable +class _AddressesTile extends StatelessWidget { + const _AddressesTile( + this._addresses, { + super.key, // ignore: unused_element_parameter + }); + + final List _addresses; + + @override + Widget build(BuildContext context) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const ListTile(title: Text('Addresses')), + Column( + children: [ + for (final a in _addresses) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + children: [ + ListTile( + title: const Text('Street'), + trailing: Text(a.street ?? ''), + ), + ListTile( + title: const Text('Postcode'), + trailing: Text(a.postcode ?? ''), + ), + ListTile( + title: const Text('City'), + trailing: Text(a.city ?? ''), + ), + ListTile( + title: const Text('Region'), + trailing: Text(a.region ?? ''), + ), + ListTile( + title: const Text('Country'), + trailing: Text(a.country ?? ''), + ), + ], + ), + ), + ], + ), + ], + ); +} + +class _ItemsTile extends StatelessWidget { + const _ItemsTile( + this._title, + this._items, { + super.key, // ignore: unused_element_parameter + }); + + final List _items; + final String _title; + + @override + Widget build(BuildContext context) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ListTile(title: Text(_title)), + Column( + children: [ + for (final i in _items) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: ListTile( + title: Text(i.label ?? ''), + trailing: Text(i.value ?? ''), + ), + ), + ], + ), + ], + ); +} + +/// {@template add_contact_screen} +/// Add contact screen. +/// {@endtemplate} +class AddContactScreen extends StatefulWidget { + /// {@macro add_contact_screen} + const AddContactScreen({ + super.key, // ignore: unused_element_parameter + }); + + @override + State createState() => _AddContactScreenState(); +} + +class _AddContactScreenState extends State { + final GlobalKey _formKey = GlobalKey(); + + Contact$PostalAddress address = const Contact$PostalAddress(label: 'Home'); + Contact contact = const Contact(); + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar( + title: const Text('Add a contact'), + actions: [ + TextButton( + onPressed: () { + _formKey.currentState?.save(); + final newContact = contact.copyWith(postalAddresses: [address]); + ContactosPluginFoundation.instance.addContact(newContact); + Navigator.of(context).pop(); + }, + child: const Icon(Icons.save, color: Colors.white), + ), + ], + ), + body: Container( + padding: const EdgeInsets.all(12), + child: Form( + key: _formKey, + child: ListView( + children: [ + TextFormField( + decoration: const InputDecoration(labelText: 'First name'), + onSaved: (v) => contact.copyWith(givenName: v), + ), + TextFormField( + decoration: const InputDecoration(labelText: 'Middle name'), + onSaved: (v) => contact.copyWith(middleName: v), + ), + TextFormField( + decoration: const InputDecoration(labelText: 'Last name'), + onSaved: (v) => contact = contact.copyWith(familyName: v), + ), + TextFormField( + decoration: const InputDecoration(labelText: 'Prefix'), + onSaved: (v) => contact = contact.copyWith(prefix: v), + ), + TextFormField( + decoration: const InputDecoration(labelText: 'Suffix'), + onSaved: (v) => contact = contact.copyWith(suffix: v), + ), + TextFormField( + decoration: const InputDecoration(labelText: 'Phone'), + onSaved: (v) => contact = contact.copyWith( + phones: [Contact$Field(label: 'mobile', value: v)], + ), + keyboardType: TextInputType.phone, + ), + TextFormField( + decoration: const InputDecoration(labelText: 'E-mail'), + onSaved: (v) => contact = contact.copyWith( + emails: [Contact$Field(label: 'work', value: v)], + ), + keyboardType: TextInputType.emailAddress, + ), + TextFormField( + decoration: const InputDecoration(labelText: 'Company'), + onSaved: (v) => contact = contact.copyWith(company: v), + ), + TextFormField( + decoration: const InputDecoration(labelText: 'Job'), + onSaved: (v) => contact = contact.copyWith(jobTitle: v), + ), + TextFormField( + decoration: const InputDecoration(labelText: 'Street'), + onSaved: (v) => address = address.copyWith(street: v), + ), + TextFormField( + decoration: const InputDecoration(labelText: 'City'), + onSaved: (v) => address = address.copyWith(city: v), + ), + TextFormField( + decoration: const InputDecoration(labelText: 'Region'), + onSaved: (v) => address = address.copyWith(region: v), + ), + TextFormField( + decoration: const InputDecoration(labelText: 'Postal code'), + onSaved: (v) => address = address.copyWith(postcode: v), + ), + TextFormField( + decoration: const InputDecoration(labelText: 'Country'), + onSaved: (v) => address = address.copyWith(country: v), + ), + ], + ), + ), + ), + ); +} + +class _UpdateContactsPage extends StatefulWidget { + const _UpdateContactsPage({ + required this.contact, + super.key, // ignore: unused_element_parameter + }); + + final Contact? contact; + + @override + State<_UpdateContactsPage> createState() => __UpdateContactsPageState(); +} + +class __UpdateContactsPageState extends State<_UpdateContactsPage> { + final GlobalKey _formKey = GlobalKey(); + + Contact$PostalAddress address = const Contact$PostalAddress(label: 'Home'); + Contact? contact; + + @override + void initState() { + super.initState(); + contact = widget.contact; + } + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar( + title: const Text('Update Contact'), + actions: [ + IconButton( + icon: const Icon(Icons.save, color: Colors.white), + onPressed: () async { + _formKey.currentState?.save(); + final navigator = Navigator.of(context); + final newContact = contact?.copyWith(postalAddresses: [address]); + if (newContact == null) return; + await ContactosPluginFoundation.instance.updateContact(newContact); + await navigator.pushReplacement( + MaterialPageRoute( + builder: (_) => const ContactsListScreen(), + ), + ); + }, + ), + ], + ), + body: Container( + padding: const EdgeInsets.all(12), + child: Form( + key: _formKey, + child: ListView( + children: [ + TextFormField( + initialValue: contact?.givenName ?? '', + decoration: const InputDecoration(labelText: 'First name'), + onSaved: (v) => contact = contact?.copyWith(givenName: v), + ), + TextFormField( + initialValue: contact?.middleName ?? '', + decoration: const InputDecoration(labelText: 'Middle name'), + onSaved: (v) => contact = contact?.copyWith(middleName: v), + ), + TextFormField( + initialValue: contact?.familyName ?? '', + decoration: const InputDecoration(labelText: 'Last name'), + onSaved: (v) => contact = contact?.copyWith(familyName: v), + ), + TextFormField( + initialValue: contact?.prefix ?? '', + decoration: const InputDecoration(labelText: 'Prefix'), + onSaved: (v) => contact = contact?.copyWith(prefix: v), + ), + TextFormField( + initialValue: contact?.suffix ?? '', + decoration: const InputDecoration(labelText: 'Suffix'), + onSaved: (v) => contact = contact?.copyWith(suffix: v), + ), + TextFormField( + decoration: const InputDecoration(labelText: 'Phone'), + onSaved: (v) => contact = contact?.copyWith( + phones: [Contact$Field(label: 'mobile', value: v)], + ), + keyboardType: TextInputType.phone, + ), + TextFormField( + decoration: const InputDecoration(labelText: 'E-mail'), + onSaved: (v) => contact = contact?.copyWith( + emails: [Contact$Field(label: 'work', value: v)], + ), + keyboardType: TextInputType.emailAddress, + ), + TextFormField( + initialValue: contact?.company ?? '', + decoration: const InputDecoration(labelText: 'Company'), + onSaved: (v) => contact = contact?.copyWith(company: v), + ), + TextFormField( + initialValue: contact?.jobTitle ?? '', + decoration: const InputDecoration(labelText: 'Job'), + onSaved: (v) => contact = contact?.copyWith(jobTitle: v), + ), + TextFormField( + initialValue: address.street ?? '', + decoration: const InputDecoration(labelText: 'Street'), + onSaved: (v) => address = address.copyWith(street: v), + ), + TextFormField( + initialValue: address.city ?? '', + decoration: const InputDecoration(labelText: 'City'), + onSaved: (v) => address = address.copyWith(city: v), + ), + TextFormField( + initialValue: address.region ?? '', + decoration: const InputDecoration(labelText: 'Region'), + onSaved: (v) => address = address.copyWith(region: v), + ), + TextFormField( + initialValue: address.postcode ?? '', + decoration: const InputDecoration(labelText: 'Postal code'), + onSaved: (v) => address = address.copyWith(postcode: v), + ), + TextFormField( + initialValue: address.country ?? '', + decoration: const InputDecoration(labelText: 'Country'), + onSaved: (v) => address = address.copyWith(country: v), + ), + ], + ), + ), + ), + ); +} diff --git a/contactos_foundation/example/lib/src/navite_contacts_picker_screen.dart b/contactos_foundation/example/lib/src/navite_contacts_picker_screen.dart new file mode 100644 index 0000000..d7513e9 --- /dev/null +++ b/contactos_foundation/example/lib/src/navite_contacts_picker_screen.dart @@ -0,0 +1,59 @@ +/* + * Author: Anton Ustinoff | + * Date: 25 November 2025 + */ + +import 'dart:developer'; + +import 'package:contactos_example/main.dart' show iOSLocalizedLabels; +import 'package:contactos_foundation/contactos_foundation.dart'; +import 'package:flutter/material.dart'; + +/// {@template navite_contacts_picker_screen} +/// NativeContactsPickerScreen widget. +/// {@endtemplate} +class NativeContactsPickerScreen extends StatefulWidget { + /// {@macro navite_contacts_picker_screen} + const NativeContactsPickerScreen({super.key}); + + @override + State createState() => + _NativeContactsPickerScreenState(); +} + +class _NativeContactsPickerScreenState + extends State { + Contact? _contact; + + @override + void initState() { + super.initState(); + } + + Future _pickContact() async { + try { + final contact = await ContactosPluginFoundation.instance + .openDeviceContactPicker(iOSLocalizedLabels: iOSLocalizedLabels); + setState(() => _contact = contact); + } on Object catch (e) { + log(e.toString()); + } + } + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text('Contacts Picker Example')), + body: SafeArea( + child: Column( + children: [ + ElevatedButton( + onPressed: _pickContact, + child: const Text('Pick a contact'), + ), + if (_contact != null) + Text('Contact selected: ${_contact?.displayName}'), + ], + ), + ), + ); +} diff --git a/contactos_foundation/example/pubspec.lock b/contactos_foundation/example/pubspec.lock new file mode 100644 index 0000000..fb39450 --- /dev/null +++ b/contactos_foundation/example/pubspec.lock @@ -0,0 +1,344 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + url: "https://pub.dev" + source: hosted + version: "1.4.1" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + collection: + dependency: "direct main" + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + contactos_foundation: + dependency: "direct main" + description: + path: ".." + relative: true + source: path + version: "0.0.2" + contactos_platform_interface: + dependency: transitive + description: + name: contactos_platform_interface + sha256: "6fc797fa685c84ef7e1ec8fc8c6aae1f9f09274bcc5ea26e435781a4fa907628" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.dev" + source: hosted + version: "1.0.8" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_driver: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + fuchsia_remote_debug_protocol: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + integration_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + intl: + dependency: "direct main" + description: + name: intl + sha256: "910f85bce16fb5c6f614e117efa303e85a1731bb0081edf3604a2ae6e9a3cc91" + url: "https://pub.dev" + source: hosted + version: "0.17.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + matcher: + dependency: transitive + description: + name: matcher + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" + url: "https://pub.dev" + source: hosted + version: "0.12.18" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + url: "https://pub.dev" + source: hosted + version: "0.13.0" + meta: + dependency: transitive + description: + name: meta + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + url: "https://pub.dev" + source: hosted + version: "1.17.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + sha256: "59adad729136f01ea9e35a48f5d1395e25cba6cea552249ddbe9cf950f5d7849" + url: "https://pub.dev" + source: hosted + version: "11.4.0" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: d3971dcdd76182a0c198c096b5db2f0884b0d4196723d21a866fc4cdea057ebc + url: "https://pub.dev" + source: hosted + version: "12.1.0" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: f84a188e79a35c687c132a0a0556c254747a08561e99ab933f12f6ca71ef3c98 + url: "https://pub.dev" + source: hosted + version: "9.4.6" + permission_handler_html: + dependency: transitive + description: + name: permission_handler_html + sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24" + url: "https://pub.dev" + source: hosted + version: "0.1.3+5" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878 + url: "https://pub.dev" + source: hosted + version: "4.3.0" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" + url: "https://pub.dev" + source: hosted + version: "0.2.1" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + process: + dependency: transitive + description: + name: process + sha256: "107d8be718f120bbba9dcd1e95e3bd325b1b4a4f07db64154635ba03f2567a0d" + url: "https://pub.dev" + source: hosted + version: "5.0.3" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://pub.dev" + source: hosted + version: "1.10.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + sync_http: + dependency: transitive + description: + name: sync_http + sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961" + url: "https://pub.dev" + source: hosted + version: "0.3.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" + url: "https://pub.dev" + source: hosted + version: "0.7.9" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 + url: "https://pub.dev" + source: hosted + version: "15.0.0" + web: + dependency: transitive + description: + name: web + sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb + url: "https://pub.dev" + source: hosted + version: "1.1.0" + webdriver: + dependency: transitive + description: + name: webdriver + sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade" + url: "https://pub.dev" + source: hosted + version: "3.1.0" +sdks: + dart: ">=3.10.0 <4.0.0" + flutter: ">=3.38.3" diff --git a/contactos_foundation/example/pubspec.yaml b/contactos_foundation/example/pubspec.yaml new file mode 100644 index 0000000..5199544 --- /dev/null +++ b/contactos_foundation/example/pubspec.yaml @@ -0,0 +1,34 @@ +name: contactos_example +description: Testbed for the contactos foundation implementation. +publish_to: none +version: 0.0.1 + + +environment: + sdk: '>=3.10.0 <4.0.0' + flutter: ">=3.38.3" + + +dependencies: + flutter: + sdk: flutter + + contactos_foundation: + path: ../ + + cupertino_icons: any + collection: any + intl: any + + permission_handler: ^11.4.0 + + +dev_dependencies: + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + + +flutter: + uses-material-design: true diff --git a/contactos_foundation/example/test/widget_test.dart b/contactos_foundation/example/test/widget_test.dart new file mode 100644 index 0000000..de20a2c --- /dev/null +++ b/contactos_foundation/example/test/widget_test.dart @@ -0,0 +1,33 @@ +// This is a basic Flutter widget test. +// To perform an interaction with a widget in your test, +// use the WidgetTester utility that Flutter +// provides. For example, you can send tap and scroll gestures. +// You can also use WidgetTester to find child widgets in the widget tree, +// read text, and verify that the values of widget properties are correct. + +import 'package:contactos_example/src/contacts_list_screen.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() => group( + 'Widget_tests -', + () => group( + 'ContactsListScreents -', + () => testWidgets('First test', (tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const ContactsListScreen()); + + // Verify that our counter starts at 0. + expect(find.text('0'), findsOneWidget); + expect(find.text('1'), findsNothing); + + // Tap the '+' icon and trigger a frame. + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); + + // Verify that our counter has incremented. + expect(find.text('0'), findsNothing); + expect(find.text('1'), findsOneWidget); + }), + ), +); diff --git a/contactos_foundation/lib/contactos_foundation.dart b/contactos_foundation/lib/contactos_foundation.dart new file mode 100644 index 0000000..3797a5a --- /dev/null +++ b/contactos_foundation/lib/contactos_foundation.dart @@ -0,0 +1,7 @@ +// Copyright 2025 Anton Ustinoff. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'package:contactos_platform_interface/contactos_platform_interface.dart'; + +export 'src/contactos_foundation.dart'; diff --git a/contactos_foundation/lib/src/contactos_foundation.dart b/contactos_foundation/lib/src/contactos_foundation.dart new file mode 100644 index 0000000..0eb3731 --- /dev/null +++ b/contactos_foundation/lib/src/contactos_foundation.dart @@ -0,0 +1,161 @@ +// Copyright 2025 Anton Ustinoff. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:typed_data'; + +import 'package:contactos_platform_interface/contactos_platform_interface.dart'; +import 'package:flutter/foundation.dart' show visibleForOverriding; + +/// The iOS and macOS implementation of [ContactosPlatform]. +/// +/// This class implements the `package:contactos` +/// functionality for iOS and macOS. +class ContactosPluginFoundation extends ContactosPlatform { + /// Creates a new plugin for iOS and macOS implementation instance. + ContactosPluginFoundation._({ + @visibleForOverriding MethodChannelContactos? channel, + }) : _channel = channel ?? MethodChannelContactos.instance; + + /// Returns an instance using a specified [MethodChannelContactos]. + factory ContactosPluginFoundation._instanceFor({ + @visibleForOverriding MethodChannelContactos? channel, + }) => + ContactosPluginFoundation._(channel: channel); + + /// Returns the default instance + /// of [ContactosPluginFoundation]. + static ContactosPluginFoundation get instance => _instance; + + /// Returns an instance using the default [ContactosPluginFoundation]. + static final ContactosPluginFoundation _instance = + ContactosPluginFoundation._instanceFor(); + + /// The channel used to interact with the platform side of the plugin. + final MethodChannelContactos _channel; + + /// Registers this class + /// as the default instance of [ContactosPla]. + static void registerWith() { + ContactosPlatform.instance = ContactosPluginFoundation.instance; + } + + /// Fetches all contacts, or when specified, the contacts with a name + /// matching [query] + @override + Future> getContacts({ + String? query, + bool withThumbnails = true, + bool photoHighResolution = true, + bool orderByGivenName = true, + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) => + _channel.getContacts( + query: query, + withThumbnails: withThumbnails, + photoHighResolution: photoHighResolution, + orderByGivenName: orderByGivenName, + iOSLocalizedLabels: iOSLocalizedLabels, + androidLocalizedLabels: androidLocalizedLabels, + ); + + /// Fetches all contacts, or when specified, the contacts with the phone + /// matching [phone] + @override + Future> getContactsForPhone( + String? phone, { + bool withThumbnails = true, + bool photoHighResolution = true, + bool orderByGivenName = true, + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) => + _channel.getContactsForPhone( + phone, + withThumbnails: withThumbnails, + photoHighResolution: photoHighResolution, + orderByGivenName: orderByGivenName, + iOSLocalizedLabels: iOSLocalizedLabels, + androidLocalizedLabels: androidLocalizedLabels, + ); + + /// Fetches all contacts, or when specified, the contacts with the email + /// matching [email] + /// Works only on iOS + @override + Future> getContactsForEmail( + String email, { + bool withThumbnails = true, + bool photoHighResolution = true, + bool orderByGivenName = true, + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) => + _channel.getContactsForEmail( + email, + withThumbnails: withThumbnails, + photoHighResolution: photoHighResolution, + orderByGivenName: orderByGivenName, + iOSLocalizedLabels: iOSLocalizedLabels, + androidLocalizedLabels: androidLocalizedLabels, + ); + + /// Loads the avatar for the given contact and returns it. If the user does + /// not have an avatar, then `null` is returned in that slot. Only implemented + /// on Android. + @override + Future getAvatar(Contact contact, {bool photoHighRes = true}) => + _channel.getAvatar(contact, photoHighRes: photoHighRes); + + /// Adds the [contact] to the device contact list + @override + Future addContact(Contact contact) => _channel.addContact(contact); + + /// Deletes the [contact] if it has a valid identifier + @override + Future deleteContact(Contact contact) => + _channel.deleteContact(contact); + + /// Updates the [contact] if it has a valid identifier + @override + Future updateContact(Contact contact) => + _channel.updateContact(contact); + + /// Opens the contact form with the fields prefilled with the values from the + @override + Future openContactForm({ + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) => + _channel.openContactForm( + iOSLocalizedLabels: iOSLocalizedLabels, + androidLocalizedLabels: androidLocalizedLabels, + ); + + /// Opens the contact form with the fields prefilled with the values from the + /// [contact] parameter + @override + Future openExistingContact( + Contact contact, { + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) => + _channel.openExistingContact( + contact, + iOSLocalizedLabels: iOSLocalizedLabels, + androidLocalizedLabels: androidLocalizedLabels, + ); + + /// Displays the device/native contact picker dialog + /// and returns the contact selected by the user + @override + Future openDeviceContactPicker({ + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) => + _channel.openDeviceContactPicker( + iOSLocalizedLabels: iOSLocalizedLabels, + androidLocalizedLabels: androidLocalizedLabels, + ); +} diff --git a/contactos_foundation/pubspec.lock b/contactos_foundation/pubspec.lock new file mode 100644 index 0000000..4acdebf --- /dev/null +++ b/contactos_foundation/pubspec.lock @@ -0,0 +1,276 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + url: "https://pub.dev" + source: hosted + version: "1.4.1" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + contactos_platform_interface: + dependency: "direct main" + description: + name: contactos_platform_interface + sha256: "6fc797fa685c84ef7e1ec8fc8c6aae1f9f09274bcc5ea26e435781a4fa907628" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_driver: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + fuchsia_remote_debug_protocol: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + integration_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0 + url: "https://pub.dev" + source: hosted + version: "6.0.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" + url: "https://pub.dev" + source: hosted + version: "0.12.18" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + url: "https://pub.dev" + source: hosted + version: "0.13.0" + meta: + dependency: transitive + description: + name: meta + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + url: "https://pub.dev" + source: hosted + version: "1.17.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + process: + dependency: transitive + description: + name: process + sha256: c6248e4526673988586e8c00bb22a49210c258dc91df5227d5da9748ecf79744 + url: "https://pub.dev" + source: hosted + version: "5.0.5" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://pub.dev" + source: hosted + version: "1.10.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + sync_http: + dependency: transitive + description: + name: sync_http + sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961" + url: "https://pub.dev" + source: hosted + version: "0.3.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" + url: "https://pub.dev" + source: hosted + version: "0.7.9" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + url: "https://pub.dev" + source: hosted + version: "15.0.2" + webdriver: + dependency: transitive + description: + name: webdriver + sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade" + url: "https://pub.dev" + source: hosted + version: "3.1.0" +sdks: + dart: ">=3.9.0-0 <4.0.0" + flutter: ">=3.29.3" diff --git a/contactos_foundation/pubspec.yaml b/contactos_foundation/pubspec.yaml new file mode 100644 index 0000000..411ba12 --- /dev/null +++ b/contactos_foundation/pubspec.yaml @@ -0,0 +1,62 @@ +name: contactos_foundation +description: iOS implementation of the contactos plugin. +version: 0.0.2 + +# publish_to: none + +homepage: https://github.com/ziqq/contactos/contactos_foundation + +repository: https://github.com/ziqq/contactos/contactos_foundation + +issue_tracker: https://github.com/ziqq/contactos/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+contactos%22 + +funding: + - https://www.buymeacoffee.com/ziqq + - https://boosty.to/ziqq + +#screenshots: +# - description: 'Example of using the library to check app version.' +# path: example.png + +topics: + - contacts + + +environment: + sdk: '>=3.6.0 <4.0.0' + flutter: ">=3.29.0" + + +dependencies: + flutter: + sdk: flutter + + contactos_platform_interface: ^1.0.1 + # contactos_platform_interface: + # path: ../contactos_platform_interface + + +dev_dependencies: + # Integration tests for Flutter + integration_test: + sdk: flutter + # Unit & Widget tests for Flutter + flutter_test: + sdk: flutter + + # Linting + flutter_lints: ^6.0.0 + + +flutter: + plugin: + platforms: + ios: + pluginClass: ContactosPlugin + dartPluginClass: ContactosPluginFoundation + sharedDarwinSource: true + # macos: + # pluginClass: ContactosPlugin + # dartPluginClass: ContactosPluginFoundation + # sharedDarwinSource: true + diff --git a/contactos_foundation/test/contactos_foundation_test.dart b/contactos_foundation/test/contactos_foundation_test.dart new file mode 100644 index 0000000..3bc3132 --- /dev/null +++ b/contactos_foundation/test/contactos_foundation_test.dart @@ -0,0 +1,273 @@ +// Copyright 2025 Anton Ustinoff. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:contactos_foundation/contactos_foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('ContactosPluginFoundation -', () { + const channel = MethodChannel('github.com/ziqq/contactos'); + final log = []; + + setUp(() { + TestWidgetsFlutterBinding.ensureInitialized(); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (methodCall) async { + log.add(methodCall); + switch (methodCall.method) { + case 'getContacts': + case 'getContactsForEmail': + case 'getContactsForPhone': + return [ + {'identifier': 'id', 'displayName': 'Name'} + ]; + case 'openContactForm': + case 'openExistingContact': + return {'identifier': 'id', 'displayName': 'Name'}; + case 'openDeviceContactPicker': + return [ + {'identifier': 'id', 'displayName': 'Name'} + ]; + case 'getAvatar': + return Uint8List.fromList([0, 1, 2]); + default: + return null; + } + }); + log.clear(); + }); + + tearDown(log.clear); + + test('registerWith registers the instance', () { + ContactosPluginFoundation.registerWith(); + expect(ContactosPlatform.instance, isA()); + }); + + group('getContacts -', () { + test('calls method channel with correct arguments', () async { + await ContactosPluginFoundation.instance.getContacts( + query: 'query', + withThumbnails: false, + photoHighResolution: false, + orderByGivenName: false, + iOSLocalizedLabels: false, + androidLocalizedLabels: false, + ); + + expect(log, hasLength(1)); + expect( + log.first, + isMethodCall( + 'getContacts', + arguments: { + 'query': 'query', + 'withThumbnails': false, + 'photoHighResolution': false, + 'orderByGivenName': false, + 'iOSLocalizedLabels': false, + 'androidLocalizedLabels': false, + }, + ), + ); + }); + }); + + group('getContactsForPhone -', () { + test('calls method channel with correct arguments', () async { + await ContactosPluginFoundation.instance.getContactsForPhone( + '123', + withThumbnails: false, + photoHighResolution: false, + orderByGivenName: false, + iOSLocalizedLabels: false, + androidLocalizedLabels: false, + ); + + expect(log, hasLength(1)); + expect( + log.first, + isMethodCall( + 'getContactsForPhone', + arguments: { + 'phone': '123', + 'withThumbnails': false, + 'photoHighResolution': false, + 'orderByGivenName': false, + 'iOSLocalizedLabels': false, + 'androidLocalizedLabels': false, + }, + ), + ); + }); + }); + + group('getContactsForEmail -', () { + test('calls method channel with correct arguments', () async { + await ContactosPluginFoundation.instance.getContactsForEmail( + 'test@example.com', + withThumbnails: false, + photoHighResolution: false, + orderByGivenName: false, + iOSLocalizedLabels: false, + androidLocalizedLabels: false, + ); + + expect(log, hasLength(1)); + expect( + log.first, + isMethodCall( + 'getContactsForEmail', + arguments: { + 'email': 'test@example.com', + 'withThumbnails': false, + 'photoHighResolution': false, + 'orderByGivenName': false, + 'iOSLocalizedLabels': false, + 'androidLocalizedLabels': false, + }, + ), + ); + }); + }); + + group('getAvatar -', () { + test('calls method channel with correct arguments', () async { + const contact = Contact(identifier: 'id'); + await ContactosPluginFoundation.instance.getAvatar( + contact, + photoHighRes: false, + ); + + expect(log, hasLength(1)); + expect( + log.first, + isMethodCall( + 'getAvatar', + arguments: { + 'contact': contact.toJson(), + 'identifier': 'id', + 'photoHighResolution': false, + }, + ), + ); + }); + }); + + group('addContact -', () { + test('calls method channel with correct arguments', () async { + const contact = Contact(identifier: 'id'); + await ContactosPluginFoundation.instance.addContact(contact); + + expect(log, hasLength(1)); + expect( + log.first, + isMethodCall( + 'addContact', + arguments: contact.toJson(), + ), + ); + }); + }); + + group('deleteContact -', () { + test('calls method channel with correct arguments', () async { + const contact = Contact(identifier: 'id'); + await ContactosPluginFoundation.instance.deleteContact(contact); + + expect(log, hasLength(1)); + expect( + log.first, + isMethodCall( + 'deleteContact', + arguments: contact.toJson(), + ), + ); + }); + }); + + group('updateContact -', () { + test('calls method channel with correct arguments', () async { + const contact = Contact(identifier: 'id'); + await ContactosPluginFoundation.instance.updateContact(contact); + + expect(log, hasLength(1)); + expect( + log.first, + isMethodCall( + 'updateContact', + arguments: contact.toJson(), + ), + ); + }); + }); + + group('openContactForm -', () { + test('calls method channel with correct arguments', () async { + await ContactosPluginFoundation.instance.openContactForm( + iOSLocalizedLabels: false, + androidLocalizedLabels: false, + ); + + expect(log, hasLength(1)); + expect( + log.first, + isMethodCall( + 'openContactForm', + arguments: { + 'iOSLocalizedLabels': false, + 'androidLocalizedLabels': false, + }, + ), + ); + }); + }); + + group('openExistingContact -', () { + test('calls method channel with correct arguments', () async { + const contact = Contact(identifier: 'id'); + await ContactosPluginFoundation.instance.openExistingContact( + contact, + iOSLocalizedLabels: false, + androidLocalizedLabels: false, + ); + + expect(log, hasLength(1)); + expect( + log.first, + isMethodCall( + 'openExistingContact', + arguments: { + 'contact': contact.toJson(), + 'iOSLocalizedLabels': false, + 'androidLocalizedLabels': false, + }, + ), + ); + }); + }); + + group('openDeviceContactPicker -', () { + test('calls method channel with correct arguments', () async { + await ContactosPluginFoundation.instance.openDeviceContactPicker( + iOSLocalizedLabels: false, + androidLocalizedLabels: false, + ); + + expect(log, hasLength(1)); + expect( + log.first, + isMethodCall( + 'openDeviceContactPicker', + arguments: { + 'iOSLocalizedLabels': false, + 'androidLocalizedLabels': false, + }, + ), + ); + }); + }); + }); +} diff --git a/contactos_foundation/tool/tag.dart b/contactos_foundation/tool/tag.dart new file mode 100644 index 0000000..b2ccab2 --- /dev/null +++ b/contactos_foundation/tool/tag.dart @@ -0,0 +1,76 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:collection/collection.dart'; +import 'package:l/l.dart'; + +Future _runGitCommand(List args) async { + final result = await Process.run('git', args); + if (result.exitCode == 0) return; + l.e('Error running git ${args.join(" ")}: ${result.stderr}'); + exit(1); +} + +/// dart run tool/tag.dart +void main() => runZonedGuarded( + () async { + // Check if there are any uncommitted or unpushed changes + final statusResult = + await Process.run('git', ['status', '--porcelain']); + if ((statusResult.stdout as String).trim().isNotEmpty) { + l.e('There are uncommitted changes.'); + exit(1); + } + final aheadResult = await Process.run( + 'git', ['rev-list', '--count', '--left-only', '@{u}...HEAD']); + if ((aheadResult.stdout as String).trim() != '0') { + l.e('There are unpushed changes.'); + exit(1); + } + + // Fetch pubspec.yaml file to get the version + final pubspecFile = File('pubspec.yaml'); + if (!pubspecFile.existsSync()) { + l.w('File pubspec.yaml not found.'); + exit(1); + } + final pubspecContent = pubspecFile.readAsLinesSync(); + final versionLine = pubspecContent + .firstWhereOrNull((line) => line.trim().startsWith('version:')); + if (versionLine == null || versionLine.isEmpty) { + l.e('Version not found in pubspec.yaml.'); + exit(1); + } + final version = versionLine.split(':')[1].trim(); + l.i('Found version: $version'); + + // Validate version format + final versionParts = version.split('.'); + if (versionParts.length != 3 || + versionParts.any((e) => int.tryParse(e) == null)) { + l.e('Invalid version format: $version'); + exit(1); + } + + final tagName = 'v$version'; // Tag format: v1.2.3 + + // Check if the tag already exists + final tagResult = await Process.run('git', ['tag', '-l', tagName]); + if (tagResult.stdout?.toString().trim() == tagName) { + l.e('Tag $tagName already exists.'); + exit(1); + } + + // Create tag + await _runGitCommand(['tag', tagName]); + l.i('Tag $tagName created.'); + + // Push tag + await _runGitCommand(['push', 'origin', tagName]); + l.i('Tag $tagName pushed to remote repository.'); + }, + (e, st) { + l.e('Unexpected error: $e', st); + exit(1); + }, + ); diff --git a/contactos_platform_interface/CHANGELOG.md b/contactos_platform_interface/CHANGELOG.md index 0ea42ac..edff57b 100644 --- a/contactos_platform_interface/CHANGELOG.md +++ b/contactos_platform_interface/CHANGELOG.md @@ -1,2 +1,9 @@ +# Changelog + +## 1.0.1 +- **ADDED**: `LICENSE` file to the package +- **ADDED**: Tests +- **CHANGED**: Bump dependencies + ## 1.0.0 - **ADDED**: Initial release \ No newline at end of file diff --git a/contactos_platform_interface/LICENSE b/contactos_platform_interface/LICENSE index 789dcc0..4de1fa1 100644 --- a/contactos_platform_interface/LICENSE +++ b/contactos_platform_interface/LICENSE @@ -1,18 +1,16 @@ BSD 3-Clause License -Copyright (c) 2025, Anton Ustinoff +Copyright (c) 2025, Anton Ustinoff All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: -1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. diff --git a/contactos_platform_interface/Makefile b/contactos_platform_interface/Makefile index 1439f93..d1032a7 100644 --- a/contactos_platform_interface/Makefile +++ b/contactos_platform_interface/Makefile @@ -50,16 +50,17 @@ get: ## Get dependencies .PHONY: analyze analyze: get format ## Analyze code - @fvm dart analyze --fatal-infos --fatal-warnings + @fvm dart analyze --fatal-infos --fatal-warnings || (echo "Β―\_(ツ)_/Β― Analyze code error"; exit 1) .PHONY: check check: analyze ## Check the code + @fvm dart pub global deactivate pana > /dev/null 2>&1 || true @fvm dart pub global activate pana - @pana --json --no-warning --line-length 80 > log.pana.json + @fvm dart pub global run pana --json > log.pana.json || (echo "Β―\_(ツ)_/Β― Pana analysis error"; exit 1) .PHONY: publish-check publish-check: check ## Check the code before publish - @fvm dart pub publish --dry-run + @fvm dart pub publish --dry-run || (echo "Β―\_(ツ)_/Β― Publish check error"; exit 1) .PHONY: publish publish: ## Publish package @@ -75,7 +76,7 @@ run-genhtml: ## Runs generage coverage html .PHONY: test-unit test-unit: ## Runs unit tests - @fvm flutter test --coverage || (echo "Β―\_(ツ)_/Β― Error while running test-unit"; exit 1) + @fvm flutter test --coverage test/contactos_platform_interface_test.dart || (echo "Β―\_(ツ)_/Β― Error while running test-unit"; exit 1) @genhtml coverage/lcov.info --output=coverage -o coverage/html || (echo "Β―\_(ツ)_/Β― Error while running genhtml with coverage"; exit 2) .PHONY: tag diff --git a/contactos_platform_interface/README.md b/contactos_platform_interface/README.md index 7b5cf2d..e94a35e 100644 --- a/contactos_platform_interface/README.md +++ b/contactos_platform_interface/README.md @@ -1,29 +1,51 @@ # contactos_platform_interface -A common platform interface for the [`contactos`][1] plugin. +[![pub package](https://img.shields.io/pub/v/contactos_platform_interface.svg)](https://pub.dev/packages/contactos_platform_interface) +[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT) +[![style: flutter lints](https://img.shields.io/badge/style-flutter__lints-blue)](https://pub.dev/packages/flutter_lints) + +A common platform interface for the [`contactos`](https://pub.dev/packages/contactos) plugin. This interface allows platform-specific implementations of the `contactos` plugin, as well as the plugin itself, to ensure they are supporting the same interface. -# Usage +## Architecture + +The `contactos` plugin uses the [federated plugin architecture](https://flutter.dev/go/federated-plugins). + +- **`contactos`**: The app-facing package that developers depend on. +- **`contactos_platform_interface`**: This package. It declares the interface that platform packages must implement. +- **`contactos_android`**, **`contactos_foundation`**: Platform-specific implementations. + +## Usage To implement a new platform-specific implementation of `contactos`, extend -[`ContactosPlatform`][2] with implementations that perform the platform-specific behaviors, -and when you register your plugin. +[`ContactosPlatform`](lib/src/contactos_platform_interface.dart) with implementations that perform the platform-specific behaviors. + +### Example + +```dart +class ContactosWindows extends ContactosPlatform { + static void registerWith() { + ContactosPlatform.instance = ContactosWindows(); + } + + @override + Future> getContacts({String? query, ...}) { + // Implementation for Windows + } + + // ... implement other methods +} +``` + -Please note that the plugin tooling only registers the native and/or Dart classes -listed in your package's `pubspec.yaml`, so if you intend to implement more than -one class, you will need to manually register the second class -(as can be seen in the Android and iOS implementations). +## Maintainers -# Note on breaking changes +[Anton Ustinoff (ziqq)](https://github.com/ziqq) -Strongly prefer non-breaking changes (such as adding a method to the interface) -over breaking changes for this package. -See https://flutter.dev/go/platform-interface-breaking-changes for a discussion -on why a less-clean interface is preferable to a breaking change. +## License -[1]: ../contactos -[2]: lib/contactos_platform_interface.dart \ No newline at end of file +[MIT](https://github.com/ziqq/contactos/blob/main/LICENSE) \ No newline at end of file diff --git a/contactos_platform_interface/lib/contactos_platform_interface.dart b/contactos_platform_interface/lib/contactos_platform_interface.dart index f6e2177..921c728 100644 --- a/contactos_platform_interface/lib/contactos_platform_interface.dart +++ b/contactos_platform_interface/lib/contactos_platform_interface.dart @@ -1,3 +1,7 @@ +// Copyright 2025 Anton Ustinoff. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + export 'src/contactos_platform_interface.dart'; export 'src/method_channel_contactos.dart'; export 'src/types.dart' hide JSON; diff --git a/contactos_platform_interface/lib/src/contactos_platform_interface.dart b/contactos_platform_interface/lib/src/contactos_platform_interface.dart index 93b4c17..14b3bce 100644 --- a/contactos_platform_interface/lib/src/contactos_platform_interface.dart +++ b/contactos_platform_interface/lib/src/contactos_platform_interface.dart @@ -1,3 +1,7 @@ +// Copyright 2025 Anton Ustinoff. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + import 'dart:typed_data'; import 'package:contactos_platform_interface/contactos_platform_interface.dart'; diff --git a/contactos_platform_interface/lib/src/method_channel_contactos.dart b/contactos_platform_interface/lib/src/method_channel_contactos.dart index 1cb691a..a6a7102 100644 --- a/contactos_platform_interface/lib/src/method_channel_contactos.dart +++ b/contactos_platform_interface/lib/src/method_channel_contactos.dart @@ -164,7 +164,7 @@ class MethodChannelContactos extends ContactosPlatform { // result contains either : // - an List of contacts containing 0 or 1 contact // - a FormOperationErrorCode value - if (result case List resultList) { + if (result case List resultList) { if (resultList.isEmpty) return null; result = resultList.first; } diff --git a/contactos_platform_interface/lib/src/types.dart b/contactos_platform_interface/lib/src/types.dart index d3dc628..7e89fb3 100644 --- a/contactos_platform_interface/lib/src/types.dart +++ b/contactos_platform_interface/lib/src/types.dart @@ -1,3 +1,7 @@ +// Copyright 2025 Anton Ustinoff. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + import 'dart:typed_data'; import 'package:collection/collection.dart'; @@ -522,13 +526,13 @@ class FormOperationException implements Exception { /// with a [FormOperationErrorCode.couldNotBeOpen] /// {@macro form_operation_exception} const factory FormOperationException.couldNotBeOpen() = - FormOperationException$Canceled; + FormOperationException$CouldNotBeOpen; /// Creates a [FormOperationException] /// with a [FormOperationErrorCode.unknown] /// {@macro form_operation_exception} const factory FormOperationException.unknown() = - FormOperationException$Canceled; + FormOperationException$Unknown; /// The error code associated with this exception final FormOperationErrorCode? errorCode; diff --git a/contactos_platform_interface/pubspec.lock b/contactos_platform_interface/pubspec.lock index 15b5e8e..7b98765 100644 --- a/contactos_platform_interface/pubspec.lock +++ b/contactos_platform_interface/pubspec.lock @@ -21,10 +21,10 @@ packages: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" clock: dependency: transitive description: @@ -58,10 +58,10 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "6.0.0" flutter_test: dependency: "direct dev" description: flutter @@ -71,66 +71,66 @@ packages: dependency: "direct dev" description: name: l - sha256: "4f4c5a88028cd052e0840e7420f57fbf2a32fb945c45355db7bacb70bfa94a9f" + sha256: "48db3024c2f74e1bc84b753b17d6754a066969c246de59505d6306f5154cbc86" url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "5.0.1" leak_tracker: dependency: transitive description: name: leak_tracker - sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" url: "https://pub.dev" source: hosted - version: "10.0.9" + version: "11.0.2" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" url: "https://pub.dev" source: hosted - version: "3.0.9" + version: "3.0.10" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" lints: dependency: transitive description: name: lints - sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 + sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0 url: "https://pub.dev" source: hosted - version: "5.1.1" + version: "6.0.0" matcher: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.18" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: "direct main" description: name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" path: dependency: transitive description: @@ -196,18 +196,18 @@ packages: dependency: transitive description: name: test_api - sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" url: "https://pub.dev" source: hosted - version: "0.7.4" + version: "0.7.9" vector_math: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" vm_service: dependency: transitive description: @@ -217,5 +217,5 @@ packages: source: hosted version: "15.0.0" sdks: - dart: ">=3.7.0-0 <4.0.0" + dart: ">=3.9.0-0 <4.0.0" flutter: ">=3.29.3" diff --git a/contactos_platform_interface/pubspec.yaml b/contactos_platform_interface/pubspec.yaml index c53f0d3..50715b6 100644 --- a/contactos_platform_interface/pubspec.yaml +++ b/contactos_platform_interface/pubspec.yaml @@ -1,31 +1,42 @@ name: contactos_platform_interface description: A common platform interface for the contactos plugin. +version: 1.0.1 + +homepage: https://github.com/ziqq/contactos/contactos_platform_interface + repository: https://github.com/ziqq/contactos/contactos_platform_interface + issue_tracker: https://github.com/ziqq/contactos/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+contactos%22 -version: 1.0.0 + +funding: + - https://www.buymeacoffee.com/ziqq + - https://boosty.to/ziqq + +topics: + - contacts + environment: sdk: '>=3.6.0 <4.0.0' flutter: ">=3.29.3" + dependencies: flutter: sdk: flutter - plugin_platform_interface: ^2.1.7 + plugin_platform_interface: ^2.1.8 # Utilities - collection: '>=1.19.0 <2.0.0' - meta: '>=1.15.0 <2.0.0' + collection: '^1.16.0' + meta: '^1.16.0' + dev_dependencies: flutter_test: sdk: flutter # Linting - flutter_lints: ^5.0.0 + flutter_lints: ^6.0.0 # Logging - l: ^5.0.0 - -topics: - - contacts + l: ^5.0.1 diff --git a/contactos_platform_interface/test/contactos_platform_interface_test.dart b/contactos_platform_interface/test/contactos_platform_interface_test.dart index 7a94afa..f0c2d32 100644 --- a/contactos_platform_interface/test/contactos_platform_interface_test.dart +++ b/contactos_platform_interface/test/contactos_platform_interface_test.dart @@ -1,222 +1,18 @@ -// Copyright 2025 Anton Ustinoff. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. +/* + * Author: Anton Ustinoff | + * Date: 26 November 2025 + */ -import 'dart:typed_data'; - -import 'package:contactos_platform_interface/contactos_platform_interface.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:plugin_platform_interface/plugin_platform_interface.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - group(ContactosPlatform, () { - test('disallows implementing interface', () { - expect( - () { - ContactosPlatform.instance = IllegalImplementation(); - }, - // In versions of `package:plugin_platform_interface` prior to fixing - // https://github.com/flutter/flutter/issues/109339, an attempt to - // implement a platform interface using `implements` would sometimes - // throw a `NoSuchMethodError` and other times throw an - // `AssertionError`. After the issue is fixed, an `AssertionError` will - // always be thrown. For the purpose of this test, we don't really care - // what exception is thrown, so just allow any exception. - throwsA(anything), - ); - }); +import 'src/contactos_platform_interface_test.dart' + as contactos_platform_interface_test; +import 'src/method_channel_contactos_test.dart' + as method_channel_contactos_test; +import 'src/types_test.dart' as types_test; - test('supports MockPlatformInterfaceMixin', () { - ContactosPlatform.instance = MockContactosPlatformImplementation(); +void main() => group('Unit_tests -', () { + contactos_platform_interface_test.main(); + method_channel_contactos_test.main(); + types_test.main(); }); - }); -} - -/// An implementation using `implements` that isn't a mock, which isn't allowed. -class IllegalImplementation implements ContactosPlatform { - // Intentionally declare self as not a mock to trigger the - // compliance check. - @override - bool get isMock => false; - - @override - Future addContact(Contact contact) { - throw UnimplementedError(); - } - - @override - Future deleteContact(Contact contact) { - throw UnimplementedError(); - } - - @override - Future getAvatar(Contact contact, {bool photoHighRes = true}) { - throw UnimplementedError(); - } - - @override - Future> getContacts({ - String? query, - bool withThumbnails = true, - bool photoHighResolution = true, - bool orderByGivenName = true, - bool iOSLocalizedLabels = true, - bool androidLocalizedLabels = true, - }) { - throw UnimplementedError(); - } - - @override - Future> getContactsForEmail( - String email, { - bool withThumbnails = true, - bool photoHighResolution = true, - bool orderByGivenName = true, - bool iOSLocalizedLabels = true, - bool androidLocalizedLabels = true, - }) { - throw UnimplementedError(); - } - - @override - Future> getContactsForPhone( - String? phone, { - bool withThumbnails = true, - bool photoHighResolution = true, - bool orderByGivenName = true, - bool iOSLocalizedLabels = true, - bool androidLocalizedLabels = true, - }) { - throw UnimplementedError(); - } - - @override - Future openContactForm({ - bool iOSLocalizedLabels = true, - bool androidLocalizedLabels = true, - }) { - throw UnimplementedError(); - } - - @override - Future openDeviceContactPicker({ - bool iOSLocalizedLabels = true, - bool androidLocalizedLabels = true, - }) { - throw UnimplementedError(); - } - - @override - Future openExistingContact( - Contact contact, { - bool iOSLocalizedLabels = true, - bool androidLocalizedLabels = true, - }) { - throw UnimplementedError(); - } - - @override - Future updateContact(Contact contact) { - throw UnimplementedError(); - } - - @override - ContactosPlatform delegateFor({required MethodChannelContactos channel}) { - throw UnimplementedError(); - } -} - -class MockContactosPlatformImplementation - with MockPlatformInterfaceMixin - implements ContactosPlatform { - @override - bool get isMock => false; - - @override - Future addContact(Contact contact) { - throw UnimplementedError(); - } - - @override - Future deleteContact(Contact contact) { - throw UnimplementedError(); - } - - @override - Future getAvatar(Contact contact, {bool photoHighRes = true}) { - throw UnimplementedError(); - } - - @override - Future> getContacts({ - String? query, - bool withThumbnails = true, - bool photoHighResolution = true, - bool orderByGivenName = true, - bool iOSLocalizedLabels = true, - bool androidLocalizedLabels = true, - }) { - throw UnimplementedError(); - } - - @override - Future> getContactsForEmail( - String email, { - bool withThumbnails = true, - bool photoHighResolution = true, - bool orderByGivenName = true, - bool iOSLocalizedLabels = true, - bool androidLocalizedLabels = true, - }) { - throw UnimplementedError(); - } - - @override - Future> getContactsForPhone( - String? phone, { - bool withThumbnails = true, - bool photoHighResolution = true, - bool orderByGivenName = true, - bool iOSLocalizedLabels = true, - bool androidLocalizedLabels = true, - }) { - throw UnimplementedError(); - } - - @override - Future openContactForm({ - bool iOSLocalizedLabels = true, - bool androidLocalizedLabels = true, - }) { - throw UnimplementedError(); - } - - @override - Future openDeviceContactPicker({ - bool iOSLocalizedLabels = true, - bool androidLocalizedLabels = true, - }) { - throw UnimplementedError(); - } - - @override - Future openExistingContact( - Contact contact, { - bool iOSLocalizedLabels = true, - bool androidLocalizedLabels = true, - }) { - throw UnimplementedError(); - } - - @override - Future updateContact(Contact contact) { - throw UnimplementedError(); - } - - @override - ContactosPlatform delegateFor({required MethodChannelContactos channel}) { - throw UnimplementedError(); - } -} diff --git a/contactos_platform_interface/test/method_channel_contactos_test.dart b/contactos_platform_interface/test/method_channel_contactos_test.dart deleted file mode 100644 index cf9cc72..0000000 --- a/contactos_platform_interface/test/method_channel_contactos_test.dart +++ /dev/null @@ -1,124 +0,0 @@ -import 'package:contactos_platform_interface/contactos_platform_interface.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - group('MethodChannelContactos -', () { - const channel = MethodChannel('github.com/ziqq/contactos'); - - late MethodChannelContactos contactos; - final log = []; - - setUp(() { - TestWidgetsFlutterBinding.ensureInitialized(); - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(channel, (methodCall) async { - log.add(methodCall); - switch (methodCall.method) { - case 'getContacts': - return [ - { - 'identifier': '1', - 'displayName': 'John Doe', - 'phones': [ - {'label': 'mobile', 'value': '+123456789'} - ], - 'emails': [ - {'label': 'work', 'value': 'johndoe@example.com'} - ], - } - ]; - case 'getContactsForEmail': - return [ - { - 'identifier': '2', - 'displayName': 'Jane Doe', - 'emails': [ - {'label': 'home', 'value': 'janedoe@example.com'} - ], - } - ]; - case 'getContactsForPhone': - return [ - { - 'identifier': '3', - 'displayName': 'Alice Smith', - 'phones': [ - {'label': 'home', 'value': '+987654321'} - ], - } - ]; - case 'addContact': - case 'deleteContact': - case 'updateContact': - return null; - case 'getAvatar': - return Uint8List.fromList([0, 1, 2, 3, 4, 5]); - default: - return null; - } - }); - - contactos = MethodChannelContactos.instance; - log.clear(); - }); - - tearDown(() async { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(channel, null); - }); - - test('getContacts should return list of contacts', () async { - final contacts = await contactos.getContacts(); - expect(contacts, isNotEmpty); - expect(contacts.first.identifier, '1'); - expect(contacts.first.displayName, 'John Doe'); - expect(contacts.first.phones?.first.value, '+123456789'); - expect(contacts.first.emails?.first.value, 'johndoe@example.com'); - expect(log.single.method, 'getContacts'); - }); - - test('getContactsForEmail should return list of contacts', () async { - final contacts = - await contactos.getContactsForEmail('janedoe@example.com'); - expect(contacts, isNotEmpty); - expect(contacts.first.identifier, '2'); - expect(contacts.first.displayName, 'Jane Doe'); - expect(contacts.first.emails?.first.value, 'janedoe@example.com'); - expect(log.single.method, 'getContactsForEmail'); - }); - - test('getContactsForPhone should return list of contacts', () async { - final contacts = await contactos.getContactsForPhone('+987654321'); - expect(contacts, isNotEmpty); - expect(contacts.first.identifier, '3'); - expect(contacts.first.displayName, 'Alice Smith'); - expect(contacts.first.phones?.first.value, '+987654321'); - expect(log.single.method, 'getContactsForPhone'); - }); - - test('getAvatar should return Uint8List', () async { - final avatar = await contactos.getAvatar(const Contact(identifier: '1')); - expect(avatar, isNotNull); - expect(avatar, isA()); - expect(log.single.method, 'getAvatar'); - }); - - test('addContact should call method channel', () async { - await contactos.addContact( - const Contact(identifier: '4', displayName: 'New Contact')); - expect(log.single.method, 'addContact'); - }); - - test('deleteContact should call method channel', () async { - await contactos.deleteContact(const Contact(identifier: '5')); - expect(log.single.method, 'deleteContact'); - }); - - test('updateContact should call method channel', () async { - await contactos.updateContact( - const Contact(identifier: '6', displayName: 'Updated Contact')); - expect(log.single.method, 'updateContact'); - }); - }); -} diff --git a/contactos_platform_interface/test/src/contactos_platform_interface_test.dart b/contactos_platform_interface/test/src/contactos_platform_interface_test.dart new file mode 100644 index 0000000..22ad176 --- /dev/null +++ b/contactos_platform_interface/test/src/contactos_platform_interface_test.dart @@ -0,0 +1,226 @@ +// Copyright 2025 Anton Ustinoff. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:typed_data'; + +import 'package:contactos_platform_interface/contactos_platform_interface.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('ContactosPlatform -', () { + setUp(TestWidgetsFlutterBinding.ensureInitialized); + + group('instance -', () { + test('is MethodChannelContactos by default', () { + expect(ContactosPlatform.instance, isA()); + }); + + test('cannot be implemented with `implements`', () { + expect( + () { + ContactosPlatform.instance = ImplementsContactosPlatform(); + }, + throwsA(isA()), + ); + }); + + test('can be extended', () { + ContactosPlatform.instance = ExtendsContactosPlatform(); + }); + + test('isMock returns false by default', () { + // ignore: deprecated_member_use_from_same_package + expect(ContactosPlatform.instance.isMock, isFalse); + }); + }); + + group('delegateFor', () { + test('throws UnimplementedError', () { + final platform = ExtendsContactosPlatform(); + ContactosPlatform.instance = platform; + + expect( + () => ContactosPlatform.instanceFor( + channel: MethodChannelContactos.instance, + ), + throwsUnimplementedError, + ); + }); + }); + }); +} + +class ImplementsContactosPlatform implements ContactosPlatform { + @override + Future addContact(Contact contact) { + throw UnimplementedError(); + } + + @override + ContactosPlatform delegateFor({required MethodChannelContactos channel}) { + throw UnimplementedError(); + } + + @override + Future deleteContact(Contact contact) { + throw UnimplementedError(); + } + + @override + Future getAvatar(Contact contact, {bool photoHighRes = true}) { + throw UnimplementedError(); + } + + @override + Future> getContacts({ + String? query, + bool withThumbnails = true, + bool photoHighResolution = true, + bool orderByGivenName = true, + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) { + throw UnimplementedError(); + } + + @override + Future> getContactsForEmail( + String email, { + bool withThumbnails = true, + bool photoHighResolution = true, + bool orderByGivenName = true, + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) { + throw UnimplementedError(); + } + + @override + Future> getContactsForPhone( + String? phone, { + bool withThumbnails = true, + bool photoHighResolution = true, + bool orderByGivenName = true, + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) { + throw UnimplementedError(); + } + + @override + bool get isMock => true; + + @override + Future openContactForm({ + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) { + throw UnimplementedError(); + } + + @override + Future openDeviceContactPicker({ + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) { + throw UnimplementedError(); + } + + @override + Future openExistingContact( + Contact contact, { + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) { + throw UnimplementedError(); + } + + @override + Future updateContact(Contact contact) { + throw UnimplementedError(); + } +} + +class ExtendsContactosPlatform extends ContactosPlatform { + @override + Future addContact(Contact contact) { + throw UnimplementedError(); + } + + @override + Future deleteContact(Contact contact) { + throw UnimplementedError(); + } + + @override + Future getAvatar(Contact contact, {bool photoHighRes = true}) { + throw UnimplementedError(); + } + + @override + Future> getContacts({ + String? query, + bool withThumbnails = true, + bool photoHighResolution = true, + bool orderByGivenName = true, + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) { + throw UnimplementedError(); + } + + @override + Future> getContactsForEmail( + String email, { + bool withThumbnails = true, + bool photoHighResolution = true, + bool orderByGivenName = true, + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) { + throw UnimplementedError(); + } + + @override + Future> getContactsForPhone( + String? phone, { + bool withThumbnails = true, + bool photoHighResolution = true, + bool orderByGivenName = true, + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) { + throw UnimplementedError(); + } + + @override + Future openContactForm({ + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) { + throw UnimplementedError(); + } + + @override + Future openDeviceContactPicker({ + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) { + throw UnimplementedError(); + } + + @override + Future openExistingContact( + Contact contact, { + bool iOSLocalizedLabels = true, + bool androidLocalizedLabels = true, + }) { + throw UnimplementedError(); + } + + @override + Future updateContact(Contact contact) { + throw UnimplementedError(); + } +} diff --git a/contactos_platform_interface/test/src/method_channel_contactos_test.dart b/contactos_platform_interface/test/src/method_channel_contactos_test.dart new file mode 100644 index 0000000..e41e9fb --- /dev/null +++ b/contactos_platform_interface/test/src/method_channel_contactos_test.dart @@ -0,0 +1,254 @@ +import 'package:contactos_platform_interface/contactos_platform_interface.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('MethodChannelContactos -', () { + const channel = MethodChannel('github.com/ziqq/contactos'); + + late MethodChannelContactos contactos; + final log = []; + + setUp(() { + TestWidgetsFlutterBinding.ensureInitialized(); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (methodCall) async { + log.add(methodCall); + switch (methodCall.method) { + case 'getContacts': + return [ + { + 'identifier': '1', + 'displayName': 'John Doe', + 'phones': [ + {'label': 'mobile', 'value': '+123456789'} + ], + 'emails': [ + {'label': 'work', 'value': 'johndoe@example.com'} + ], + } + ]; + case 'getContactsForEmail': + return [ + { + 'identifier': '2', + 'displayName': 'Jane Doe', + 'emails': [ + {'label': 'home', 'value': 'janedoe@example.com'} + ], + } + ]; + case 'getContactsForPhone': + return [ + { + 'identifier': '3', + 'displayName': 'Alice Smith', + 'phones': [ + {'label': 'home', 'value': '+987654321'} + ], + } + ]; + case 'addContact': + case 'deleteContact': + case 'updateContact': + return null; + case 'getAvatar': + return Uint8List.fromList([0, 1, 2, 3, 4, 5]); + default: + return null; + } + }); + + contactos = MethodChannelContactos.instance; + log.clear(); + }); + + tearDown(() async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, null); + }); + + group('getContacts -', () { + test('should return list of contacts', () async { + final contacts = await contactos.getContacts(); + expect(contacts, isNotEmpty); + expect(contacts.first.identifier, '1'); + expect(contacts.first.displayName, 'John Doe'); + expect(contacts.first.phones?.first.value, '+123456789'); + expect(contacts.first.emails?.first.value, 'johndoe@example.com'); + expect(log.single.method, 'getContacts'); + }); + }); + + group('getContactsForEmail -', () { + test('should return list of contacts', () async { + final contacts = + await contactos.getContactsForEmail('janedoe@example.com'); + expect(contacts, isNotEmpty); + expect(contacts.first.identifier, '2'); + expect(contacts.first.displayName, 'Jane Doe'); + expect(contacts.first.emails?.first.value, 'janedoe@example.com'); + expect(log.single.method, 'getContactsForEmail'); + }); + }); + + group('getContactsForPhone -', () { + test('should return list of contacts', () async { + final contacts = await contactos.getContactsForPhone('+987654321'); + expect(contacts, isNotEmpty); + expect(contacts.first.identifier, '3'); + expect(contacts.first.displayName, 'Alice Smith'); + expect(contacts.first.phones?.first.value, '+987654321'); + expect(log.single.method, 'getContactsForPhone'); + }); + + test('returns empty list if phone is null or empty', () async { + var contacts = await contactos.getContactsForPhone(null); + expect(contacts, isEmpty); + + contacts = await contactos.getContactsForPhone(''); + expect(contacts, isEmpty); + }); + }); + + group('getAvatar -', () { + test('should return Uint8List', () async { + final avatar = + await contactos.getAvatar(const Contact(identifier: '1')); + expect(avatar, isNotNull); + expect(avatar, isA()); + expect(log.single.method, 'getAvatar'); + }); + }); + + group('addContact -', () { + test('should call method channel', () async { + await contactos.addContact( + const Contact(identifier: '4', displayName: 'New Contact')); + expect(log.single.method, 'addContact'); + }); + }); + + group('deleteContact -', () { + test('should call method channel', () async { + await contactos.deleteContact(const Contact(identifier: '5')); + expect(log.single.method, 'deleteContact'); + }); + }); + + group('updateContact -', () { + test('should call method channel', () async { + await contactos.updateContact( + const Contact(identifier: '6', displayName: 'Updated Contact')); + expect(log.single.method, 'updateContact'); + }); + }); + + group('openContactForm -', () { + test('returns contact on success', () async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (methodCall) async { + if (methodCall.method == 'openContactForm') { + return {'identifier': 'new_id', 'displayName': 'New Contact'}; + } + return null; + }); + + final contact = await contactos.openContactForm(); + expect(contact.identifier, 'new_id'); + expect(contact.displayName, 'New Contact'); + }); + + test('throws canceled exception', () async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (methodCall) async { + if (methodCall.method == 'openContactForm') { + return 1; // Canceled code + } + return null; + }); + + expect( + () => contactos.openContactForm(), + throwsA(isA()), + ); + }); + + test('throws couldNotBeOpen exception', () async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (methodCall) async { + if (methodCall.method == 'openContactForm') { + return 2; // CouldNotBeOpen code + } + return null; + }); + + expect( + () => contactos.openContactForm(), + throwsA(isA()), + ); + }); + + test('throws unknown exception for other codes', () async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (methodCall) async { + if (methodCall.method == 'openContactForm') { + return 999; // Unknown code + } + return null; + }); + + expect( + () => contactos.openContactForm(), + throwsA(isA()), + ); + }); + }); + + group('openDeviceContactPicker -', () { + test('returns contact from list', () async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (methodCall) async { + if (methodCall.method == 'openDeviceContactPicker') { + return [ + {'identifier': 'picked_id', 'displayName': 'Picked Contact'} + ]; + } + return null; + }); + + final contact = await contactos.openDeviceContactPicker(); + expect(contact?.identifier, 'picked_id'); + }); + + test('returns null if list is empty', () async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (methodCall) async { + if (methodCall.method == 'openDeviceContactPicker') { + return []; + } + return null; + }); + + final contact = await contactos.openDeviceContactPicker(); + expect(contact, isNull); + }); + }); + + group('openExistingContact -', () { + test('returns updated contact', () async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (methodCall) async { + if (methodCall.method == 'openExistingContact') { + return {'identifier': 'id', 'displayName': 'Updated Name'}; + } + return null; + }); + + final contact = await contactos + .openExistingContact(const Contact(identifier: 'id')); + expect(contact.displayName, 'Updated Name'); + }); + }); + }); +} diff --git a/contactos_platform_interface/test/src/types_test.dart b/contactos_platform_interface/test/src/types_test.dart new file mode 100644 index 0000000..a88d722 --- /dev/null +++ b/contactos_platform_interface/test/src/types_test.dart @@ -0,0 +1,379 @@ +// Copyright 2025 Anton Ustinoff. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:typed_data'; + +import 'package:contactos_platform_interface/contactos_platform_interface.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('AndroidAccountType -', () { + group('fromString -', () { + test('returns correct enum', () { + expect( + AndroidAccountType.fromString('com.google'), + AndroidAccountType.google, + ); + expect( + AndroidAccountType.fromString('com.whatsapp'), + AndroidAccountType.whatsapp, + ); + expect( + AndroidAccountType.fromString('com.facebook'), + AndroidAccountType.facebook, + ); + expect( + AndroidAccountType.fromString('other'), + AndroidAccountType.other, + ); + expect(AndroidAccountType.fromString(null), null); + }); + }); + }); + + group('Contact -', () { + group('fromJson -', () { + test('parses correctly', () { + final json = { + 'identifier': 'id', + 'displayName': 'Display Name', + 'givenName': 'Given', + 'middleName': 'Middle', + 'prefix': 'Mr.', + 'suffix': 'Jr.', + 'familyName': 'Family', + 'company': 'Company', + 'jobTitle': 'Job', + 'androidAccountType': 'com.google', + 'androidAccountName': 'account', + 'emails': [ + {'label': 'work', 'value': 'email@example.com'} + ], + 'phones': [ + {'label': 'mobile', 'value': '1234567890'} + ], + 'postalAddresses': [ + { + 'label': 'home', + 'street': 'Street', + 'city': 'City', + 'postcode': '12345', + 'region': 'Region', + 'country': 'Country', + } + ], + 'avatar': [1, 2, 3], + 'birthday': '2000-01-01', + }; + + final contact = Contact.fromJson(json); + + expect(contact.identifier, 'id'); + expect(contact.displayName, 'Display Name'); + expect(contact.givenName, 'Given'); + expect(contact.middleName, 'Middle'); + expect(contact.prefix, 'Mr.'); + expect(contact.suffix, 'Jr.'); + expect(contact.familyName, 'Family'); + expect(contact.company, 'Company'); + expect(contact.jobTitle, 'Job'); + expect(contact.androidAccountType, AndroidAccountType.google); + expect(contact.androidAccountName, 'account'); + expect(contact.emails?.first.label, 'work'); + expect(contact.emails?.first.value, 'email@example.com'); + expect(contact.phones?.first.label, 'mobile'); + expect(contact.phones?.first.value, '1234567890'); + expect(contact.postalAddresses?.first.label, 'home'); + expect(contact.postalAddresses?.first.street, 'Street'); + expect(contact.avatar, Uint8List.fromList([1, 2, 3])); + expect(contact.birthday, DateTime(2000, 1, 1)); + }); + + test('handles invalid birthday', () { + final json = { + 'birthday': 'invalid', + }; + final contact = Contact.fromJson(json); + expect(contact.birthday, null); + }); + }); + + group('toJson -', () { + test('serializes correctly', () { + final contact = Contact( + identifier: 'id', + displayName: 'Display Name', + givenName: 'Given', + middleName: 'Middle', + prefix: 'Mr.', + suffix: 'Jr.', + familyName: 'Family', + company: 'Company', + jobTitle: 'Job', + androidAccountType: AndroidAccountType.google, + androidAccountTypeRaw: 'com.google', + androidAccountName: 'account', + emails: const [ + Contact$Field(label: 'work', value: 'email@example.com') + ], + phones: const [Contact$Field(label: 'mobile', value: '1234567890')], + postalAddresses: const [ + Contact$PostalAddress( + label: 'home', + street: 'Street', + city: 'City', + postcode: '12345', + region: 'Region', + country: 'Country', + ) + ], + avatar: Uint8List.fromList([1, 2, 3]), + birthday: DateTime(2000, 1, 1), + ); + + final json = contact.toJson(); + + expect(json['identifier'], 'id'); + expect(json['displayName'], 'Display Name'); + expect(json['birthday'], '2000-01-01'); + expect(json['emails'], isA>()); + expect(json['phones'], isA>()); + expect(json['postalAddresses'], isA>()); + }); + }); + + group('copyWith -', () { + test('works correctly', () { + const contact = Contact(identifier: 'id', givenName: 'Given'); + final copy = contact.copyWith(givenName: 'New Given'); + expect(copy.identifier, 'id'); + expect(copy.givenName, 'New Given'); + }); + + test('preserves values when arguments are null', () { + const contact = Contact(identifier: 'id', givenName: 'Given'); + final copy = contact.copyWith(identifier: 'newId'); + expect(copy.identifier, 'newId'); + expect(copy.givenName, 'Given'); + }); + }); + + group('initials -', () { + test('returns correct initials', () { + const contact1 = Contact(givenName: 'John', familyName: 'Doe'); + expect(contact1.initials(), 'JD'); + + const contact2 = Contact(givenName: 'John'); + expect(contact2.initials(), 'J'); + + const contact3 = Contact(familyName: 'Doe'); + expect(contact3.initials(), 'D'); + + const contact4 = Contact(); + expect(contact4.initials(), ''); + }); + }); + + group('operator +', () { + test('merges contacts', () { + const contact1 = Contact(givenName: 'John', emails: []); + const contact2 = Contact( + familyName: 'Doe', + emails: [Contact$Field(label: 'work', value: 'email')], + ); + final merged = contact1 + contact2; + expect(merged.givenName, 'John'); + expect(merged.familyName, 'Doe'); + expect(merged.emails?.length, 1); + }); + + test('merges contacts with existing lists', () { + const contact1 = Contact( + emails: [Contact$Field(label: 'home', value: 'home@email.com')], + ); + const contact2 = Contact( + emails: [Contact$Field(label: 'work', value: 'work@email.com')], + ); + final merged = contact1 + contact2; + expect(merged.emails?.length, 2); + }); + + test('merges contacts with null lists on left side', () { + const contact1 = Contact(givenName: 'A'); + const contact2 = Contact( + emails: [Contact$Field(label: 'a', value: 'a')], + phones: [Contact$Field(label: 'b', value: 'b')], + postalAddresses: [Contact$PostalAddress(street: 's')], + ); + final merged = contact1 + contact2; + expect(merged.emails?.length, 1); + expect(merged.phones?.length, 1); + expect(merged.postalAddresses?.length, 1); + }); + + test('merges contacts with non-null lists', () { + const contact1 = Contact( + phones: [Contact$Field(label: 'a', value: 'a')], + postalAddresses: [Contact$PostalAddress(street: 's1')], + ); + const contact2 = Contact( + phones: [Contact$Field(label: 'b', value: 'b')], + postalAddresses: [Contact$PostalAddress(street: 's2')], + ); + final merged = contact1 + contact2; + expect(merged.phones?.length, 2); + expect(merged.postalAddresses?.length, 2); + }); + }); + + group('equality and hashCode -', () { + test('works correctly', () { + const contact1 = Contact(identifier: 'id'); + const contact2 = Contact(identifier: 'id'); + const contact3 = Contact(identifier: 'other'); + + expect(contact1, contact2); + expect(contact1.hashCode, contact2.hashCode); + expect(contact1, isNot(contact3)); + }); + }); + }); + + group(r'Contact$PostalAddress -', () { + group('toString -', () { + test('returns formatted address', () { + const address = Contact$PostalAddress( + street: 'Street', + city: 'City', + country: 'Country', + ); + expect(address.toString(), 'Street, City, Country'); + }); + + test('returns formatted address with all fields', () { + const address = Contact$PostalAddress( + street: 'Street', + city: 'City', + region: 'Region', + postcode: '12345', + country: 'Country', + ); + expect(address.toString(), 'Street, City, Region 12345, Country'); + }); + + test('returns formatted address with missing fields', () { + const address1 = Contact$PostalAddress(city: 'City'); + expect(address1.toString(), 'City'); + + const address2 = Contact$PostalAddress(region: 'Region'); + expect(address2.toString(), 'Region'); + + const address3 = Contact$PostalAddress(postcode: '12345'); + expect(address3.toString(), '12345'); + + const address4 = Contact$PostalAddress(country: 'Country'); + expect(address4.toString(), 'Country'); + }); + + test('returns formatted address with combinations', () { + expect( + const Contact$PostalAddress(street: 'S', region: 'R').toString(), + 'S, R', + ); + expect( + const Contact$PostalAddress(street: 'S', postcode: 'P').toString(), + 'S P', + ); + expect( + const Contact$PostalAddress(street: 'S', country: 'C').toString(), + 'S, C', + ); + }); + }); + + group('equality and hashCode -', () { + test('works correctly', () { + const address1 = Contact$PostalAddress(street: 'Street'); + const address2 = Contact$PostalAddress(street: 'Street'); + const address3 = Contact$PostalAddress(street: 'Other'); + + expect(address1, address2); + expect(address1.hashCode, address2.hashCode); + expect(address1, isNot(address3)); + }); + }); + + group('copyWith -', () { + test('works correctly', () { + const address = Contact$PostalAddress(street: 'Street'); + final copy = address.copyWith(street: 'New Street'); + expect(copy.street, 'New Street'); + }); + + test('preserves values when arguments are null', () { + const address = Contact$PostalAddress(street: 'Street', city: 'City'); + final copy = address.copyWith(city: 'New City'); + expect(copy.street, 'Street'); + expect(copy.city, 'New City'); + }); + }); + }); + + group(r'Contact$Field -', () { + group('equality and hashCode -', () { + test('works correctly', () { + const field1 = Contact$Field(label: 'label', value: 'value'); + const field2 = Contact$Field(label: 'label', value: 'value'); + const field3 = Contact$Field(label: 'other', value: 'value'); + + expect(field1, field2); + expect(field1.hashCode, field2.hashCode); + expect(field1, isNot(field3)); + expect(field1, isNot('string')); + }); + }); + }); + + group('FormOperationException -', () { + group('toString -', () { + test('returns correct message', () { + const exception = FormOperationException.canceled(); + expect(exception.toString(), + 'FormOperationException: FormOperationErrorCode.canceled'); + }); + }); + + group('factories -', () { + test('create correct exceptions', () { + expect( + const FormOperationException.canceled(), + isA(), + ); + expect( + const FormOperationException.couldNotBeOpen(), + isA(), + ); + expect( + const FormOperationException.unknown(), + isA(), + ); + }); + + test('subclasses can be instantiated directly', () { + expect( + const FormOperationException$Canceled(), + isA(), + ); + expect( + const FormOperationException$CouldNotBeOpen(), + isA(), + ); + expect( + const FormOperationException$Unknown(), + isA(), + ); + }); + }); + }); +}