├── .cm └── gitstream.cm ├── .github ├── dependabot.yml └── workflows │ ├── dart.yml │ ├── pr-format.yml │ ├── release.yaml │ ├── test.yaml │ ├── update-android-sdk-version.yaml │ ├── update-ios-sdk-version.yaml │ └── update-sdk-versions.yaml ├── .gitignore ├── .metadata ├── .pubignore ├── CHANGELOG.md ├── DEVELOPMENT.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── android ├── .gitignore ├── build.gradle ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── settings.gradle └── src │ └── main │ ├── AndroidManifest.xml │ └── kotlin │ └── com │ └── devcycle │ └── devcycle_flutter_client_sdk │ └── DevCycleFlutterClientSdkPlugin.kt ├── example ├── .gitignore ├── README.md ├── analysis_options.yaml ├── android │ ├── .gitignore │ ├── app │ │ ├── build.gradle │ │ └── src │ │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── kotlin │ │ │ │ └── com │ │ │ │ │ └── example │ │ │ │ │ └── devcycle_flutter_client_sdk_example │ │ │ │ │ └── MainActivity.kt │ │ │ └── res │ │ │ │ ├── drawable-v21 │ │ │ │ └── launch_background.xml │ │ │ │ ├── drawable │ │ │ │ └── launch_background.xml │ │ │ │ ├── mipmap-hdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── values-night │ │ │ │ └── styles.xml │ │ │ │ └── values │ │ │ │ └── styles.xml │ │ │ └── profile │ │ │ └── AndroidManifest.xml │ ├── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ └── settings.gradle ├── ios │ ├── .gitignore │ ├── Flutter │ │ ├── AppFrameworkInfo.plist │ │ ├── Debug.xcconfig │ │ └── Release.xcconfig │ ├── Podfile │ ├── Podfile.lock │ ├── Runner.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ │ └── WorkspaceSettings.xcsettings │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ └── Runner │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ └── Icon-App-83.5x83.5@2x.png │ │ └── LaunchImage.imageset │ │ │ ├── Contents.json │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ └── README.md │ │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ │ ├── Info.plist │ │ └── Runner-Bridging-Header.h ├── lib │ └── main.dart ├── pubspec.yaml └── test │ └── widget_test.dart ├── ios ├── .gitignore ├── Assets │ └── .gitkeep ├── Classes │ ├── DevCycleFlutterClientSdkPlugin.h │ ├── DevCycleFlutterClientSdkPlugin.m │ └── SwiftDevCycleFlutterClientSdkPlugin.swift └── devcycle_flutter_client_sdk.podspec ├── lib ├── devcycle_event.dart ├── devcycle_flutter_client_sdk.dart ├── devcycle_flutter_client_sdk_method_channel.dart ├── devcycle_flutter_client_sdk_platform_interface.dart ├── devcycle_options.dart ├── devcycle_user.dart ├── dvc_feature.dart └── dvc_variable.dart ├── pubspec.yaml ├── scripts └── deploy.sh └── test ├── devcycle_flutter_client_sdk_method_channel_test.dart ├── devcycle_flutter_client_sdk_test.dart ├── dvc_event_test.dart ├── dvc_options_test.dart ├── dvc_user_test.dart ├── feature_test.dart └── variable_test.dart /.cm/gitstream.cm: -------------------------------------------------------------------------------- 1 | # -*- mode: yaml -*- 2 | 3 | manifest: 4 | version: 1.0 5 | 6 | # The `automations` section includes a list of automation that applies 7 | # to the repository in which gitStream is installed. Each automation has an 8 | # `if` key with a list of the necessary assertions, as well as a `run` key with a 9 | # list of all actions. All the listed assertions need to pass in order 10 | # for the following actions to be executed (there is AND relation between conditions). 11 | 12 | # Each automation under the `automations` section is independent of the others. 13 | # Every time a PR is opened or changed, the automation's conditions are evaluated (the `if`). 14 | # The actions under `run` are executed one by one if all the conditions pass. 15 | 16 | # Conditions consists of an expression, which are wrapped with double curly braces, and 17 | # includes a context variable like `files` and filter functions like `length`. Filters 18 | # functions are essentially functions that can be applied to context variables. They are 19 | # called with a pipe operator (|) and can take arguments. Read more on https://docs.gitstream.cm 20 | 21 | automations: 22 | # This is the name of the review automation. You can use whatever name, a meaningful name 23 | # will help to identify it in the future. Each automation name in this file should be unique. 24 | estimated_time_to_review: 25 | if: 26 | - true 27 | run: 28 | - action: add-label@v1 29 | args: 30 | label: "{{ calc.etr }} min review" 31 | color: {{ 'E94637' if (calc.etr >= 20) else ('FBBD10' if (calc.etr >= 5) else '36A853') }} 32 | 33 | safe_changes: 34 | # The `if` key has a list of conditions, each condition is specified as a Jinja expression 35 | # in a double curly braces. Expressions are evaluated by gitStream on a PR when triggered. 36 | if: 37 | # Given the PR code changes, check that only formatting changes were made 38 | - {{ is.formatting or is.docs or is.tests or is.dependabot }} 39 | # `run` key has a list of actions, which are executed one by one whenever the automation 40 | # conditions are met. 41 | run: 42 | # When the changes are validated as formatting only, you can help to speed up the review 43 | # by adding a label that marks it accordingly. 44 | - action: add-label@v1 45 | args: 46 | label: 'safe-changes' 47 | - action: approve@v1 48 | - action: merge@v1 49 | args: 50 | rebase_on_merge: true 51 | wait_for_all_checks: true 52 | 53 | 54 | calc: 55 | etr: {{ branch | estimatedReviewTime }} 56 | is: 57 | formatting: {{ source.diff.files | isFormattingChange }} 58 | docs: {{ files | allDocs }} 59 | tests: {{ files | allTests }} 60 | dependabot: {{ pr.author | includes(term='dependabot') }} 61 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | - package-ecosystem: gradle 8 | directory: "/android" 9 | schedule: 10 | interval: "weekly" 11 | reviewers: 12 | - "devcyclehq/engineering" 13 | -------------------------------------------------------------------------------- /.github/workflows/dart.yml: -------------------------------------------------------------------------------- 1 | name: Publish to pub.dev 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v[0-9]+.[0-9]+.[0-9]+*' 7 | 8 | jobs: 9 | publish: 10 | permissions: 11 | id-token: write 12 | uses: dart-lang/setup-dart/.github/workflows/publish.yml@v1 -------------------------------------------------------------------------------- /.github/workflows/pr-format.yml: -------------------------------------------------------------------------------- 1 | name: 'Semantic PR' 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - opened 7 | - edited 8 | - synchronize 9 | 10 | permissions: 11 | pull-requests: read 12 | 13 | jobs: 14 | main: 15 | name: Semantic PR title 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: amannn/action-semantic-pull-request@v5 19 | env: 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | permissions: 4 | contents: write 5 | 6 | on: 7 | workflow_dispatch: 8 | inputs: 9 | prerelease: 10 | description: "Prerelease" 11 | required: true 12 | type: boolean 13 | draft: 14 | description: "Draft" 15 | required: true 16 | type: boolean 17 | version-increment-type: 18 | description: "Which part of the version to increment:" 19 | required: true 20 | type: choice 21 | options: 22 | - major 23 | - minor 24 | - patch 25 | default: "patch" 26 | 27 | jobs: 28 | build: 29 | name: Release Flutter SDK 30 | runs-on: ubuntu-latest 31 | steps: 32 | - uses: actions/checkout@v4 33 | with: 34 | token: ${{ secrets.AUTOMATION_USER_TOKEN }} 35 | fetch-depth: 0 36 | 37 | - name: Install Flutter 38 | uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046 # v2.19.0 39 | with: 40 | flutter-version: "3.x" 41 | channel: "stable" 42 | 43 | - uses: DevCycleHQ/release-action/prepare-release@v2.3.0 44 | id: prepare-release 45 | with: 46 | github-token: ${{ secrets.AUTOMATION_USER_TOKEN }} 47 | prerelease: ${{ github.event.inputs.prerelease }} 48 | draft: ${{ github.event.inputs.draft }} 49 | version-increment-type: ${{ github.event.inputs.version-increment-type }} 50 | 51 | - name: Update version in code 52 | run: | 53 | sed -i -E "s/version:.*/version: \"${{steps.prepare-release.outputs.next-release-tag}}\"/" ./pubspec.yaml 54 | sed -i -E "s/s.version = '.*'/s.version = '${{steps.prepare-release.outputs.next-release-tag}}'/g" ios/devcycle_flutter_client_sdk.podspec 55 | sed -i -E "s/version '.*'/version '${{steps.prepare-release.outputs.next-release-tag}}'/g" android/build.gradle 56 | sed -i -E "s/# Change Log//g" CHANGELOG.md 57 | echo -e "# Change Log\n## [${{steps.prepare-release.outputs.next-release-tag}}] - "$(date "+%Y-%m-%d")"\n${{ steps.prepare-release.outputs.changelog }}" > update.md 58 | cat update.md CHANGELOG.md > CHANGELOG.md.tmp 59 | mv CHANGELOG.md.tmp CHANGELOG.md 60 | rm update.md 61 | 62 | - name: Commit and push 63 | if: inputs.draft != true 64 | run: | 65 | git config --global user.email "foundation-admin@devcycle.com" 66 | git config --global user.name "DevCycle Automation" 67 | git add . 68 | git commit -m "Release ${{steps.prepare-release.outputs.next-release-tag}}" 69 | git push origin HEAD:main 70 | 71 | - name: Install dependencies 72 | run: flutter pub get 73 | 74 | - name: Analyze 75 | run: flutter analyze 76 | 77 | - name: Run tests 78 | run: flutter test 79 | 80 | - uses: DevCycleHQ/release-action/create-release@v2.3.0 81 | id: create-release 82 | with: 83 | github-token: ${{ secrets.AUTOMATION_USER_TOKEN }} 84 | tag: ${{ steps.prepare-release.outputs.next-release-tag }} 85 | target: main 86 | prerelease: ${{ github.event.inputs.prerelease }} 87 | draft: ${{ github.event.inputs.draft }} 88 | changelog: ${{ steps.prepare-release.outputs.changelog }} 89 | 90 | - name: Display link to release 91 | run: | 92 | echo "::notice title=Release ID::${{ steps.create-release.outputs.release-id }}" 93 | echo "::notice title=Release URL::${{ steps.create-release.outputs.release-url }}" 94 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Flutter SDK Unit Tests 2 | permissions: 3 | contents: read 4 | 5 | # Leverages the Flutter GH Action at https://github.com/marketplace/actions/flutter-action 6 | 7 | on: 8 | pull_request: 9 | branches: [main] 10 | 11 | jobs: 12 | build: 13 | name: Unit Test 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout Project 17 | uses: actions/checkout@v4 18 | - name: Install and set Flutter version 19 | uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046 # v2.19.0 20 | with: 21 | flutter-version: "3.x" 22 | channel: "stable" 23 | - name: Install Flutter packages 24 | run: flutter pub get 25 | - name: Analyze 26 | run: flutter analyze 27 | - name: Run Unit Tests 28 | run: flutter test 29 | - name: Run Publish Dry Run 30 | run: flutter pub publish --dry-run 31 | -------------------------------------------------------------------------------- /.github/workflows/update-android-sdk-version.yaml: -------------------------------------------------------------------------------- 1 | name: Update Android SDK Version 2 | 3 | on: 4 | schedule: 5 | - cron: "0 9 * * MON" 6 | workflow_dispatch: 7 | workflow_call: 8 | inputs: 9 | target-version: 10 | description: "Specific Android SDK version to update to (optional)" 11 | required: false 12 | type: string 13 | 14 | permissions: 15 | contents: write 16 | pull-requests: write 17 | 18 | jobs: 19 | update-android-sdk: 20 | name: Update Android SDK Version 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: Checkout repository 24 | uses: actions/checkout@v4 25 | with: 26 | token: ${{ secrets.AUTOMATION_USER_TOKEN }} 27 | fetch-depth: 0 28 | 29 | - name: Fetch latest Android SDK version 30 | id: android-version 31 | run: | 32 | if [ -n "${{ inputs.target-version }}" ]; then 33 | TARGET_VERSION=$(echo "${{ inputs.target-version }}" | sed 's/^v//') 34 | echo "version=$TARGET_VERSION" >> $GITHUB_OUTPUT 35 | echo "Using specified Android SDK version: $TARGET_VERSION" 36 | else 37 | LATEST_ANDROID_VERSION=$(curl -s https://api.github.com/repos/DevCycleHQ/android-client-sdk/releases/latest | jq -r '.tag_name' | sed 's/^v//') 38 | echo "version=$LATEST_ANDROID_VERSION" >> $GITHUB_OUTPUT 39 | echo "Latest Android SDK version: $LATEST_ANDROID_VERSION" 40 | fi 41 | 42 | - name: Get current Android SDK version 43 | id: current-android 44 | run: | 45 | CURRENT_ANDROID=$(grep "android-client-sdk" android/build.gradle | sed -E 's/.*:([^"]+)".*/\1/') 46 | echo "version=$CURRENT_ANDROID" >> $GITHUB_OUTPUT 47 | echo "Current Android SDK version: $CURRENT_ANDROID" 48 | 49 | - name: Check if Android update is needed 50 | id: check-update 51 | run: | 52 | if [ "${{ steps.android-version.outputs.version }}" != "${{ steps.current-android.outputs.version }}" ]; then 53 | echo "update-needed=true" >> $GITHUB_OUTPUT 54 | echo "Android SDK update needed: ${{ steps.current-android.outputs.version }} -> ${{ steps.android-version.outputs.version }}" 55 | else 56 | echo "update-needed=false" >> $GITHUB_OUTPUT 57 | echo "Android SDK is already up to date" 58 | fi 59 | 60 | - name: Update Android SDK version 61 | if: steps.check-update.outputs.update-needed == 'true' 62 | run: | 63 | sed -i -E 's/(implementation\("com\.devcycle:android-client-sdk:)[^"]+("\))/\1${{ steps.android-version.outputs.version }}\2/' android/build.gradle 64 | echo "Updated Android SDK version to ${{ steps.android-version.outputs.version }}" 65 | 66 | - name: Setup Flutter 67 | if: steps.check-update.outputs.update-needed == 'true' 68 | uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046 # v2.19.0 69 | with: 70 | flutter-version: "3.x" 71 | channel: "stable" 72 | 73 | - name: Prepare Flutter 74 | if: steps.check-update.outputs.update-needed == 'true' 75 | run: | 76 | cd example 77 | flutter pub get 78 | 79 | - name: Setup Java for Gradle 80 | if: steps.check-update.outputs.update-needed == 'true' 81 | uses: actions/setup-java@v4 82 | with: 83 | distribution: "temurin" 84 | java-version: "17" 85 | 86 | - name: Update Android dependency resolution 87 | if: steps.check-update.outputs.update-needed == 'true' 88 | run: | 89 | cd example/android 90 | ./gradlew dependencies --configuration implementation > /dev/null 2>&1 || true 91 | echo "✅ Updated Android dependency resolution for version ${{ steps.android-version.outputs.version }}" 92 | 93 | - name: Create Pull Request 94 | if: steps.check-update.outputs.update-needed == 'true' 95 | uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8 96 | with: 97 | token: ${{ secrets.AUTOMATION_USER_TOKEN }} 98 | commit-message: | 99 | chore: update Android SDK version to ${{ steps.android-version.outputs.version }} 100 | 101 | - Android SDK: ${{ steps.current-android.outputs.version }} -> ${{ steps.android-version.outputs.version }} 102 | title: "chore: update Android SDK to v${{ steps.android-version.outputs.version }}" 103 | body: | 104 | ## Summary 105 | This PR updates the DevCycle Android SDK to the latest version. 106 | 107 | ## Changes 108 | - **Android SDK**: `${{ steps.current-android.outputs.version }}` → `${{ steps.android-version.outputs.version }}` 109 | 110 | ## Files Modified 111 | - `android/build.gradle` - Updated Android SDK dependency version 112 | - Android dependency resolution updated for example app 113 | 114 | ## Testing 115 | This PR will be automatically tested by the Flutter SDK Unit Tests workflow which validates: 116 | - Flutter package dependencies 117 | - Code analysis with `flutter analyze` 118 | - Unit tests with `flutter test` 119 | - Package publishing validation with `flutter pub publish --dry-run` 120 | 121 | ## Release Notes 122 | See the [Android SDK release notes](https://github.com/DevCycleHQ/android-client-sdk/releases/tag/v${{ steps.android-version.outputs.version }}) for details on what's new in this version. 123 | 124 | --- 125 | This PR was automatically generated by the Update Android SDK Version workflow. 126 | branch: update-android-sdk-${{ steps.android-version.outputs.version }} 127 | delete-branch: true 128 | base: main 129 | labels: | 130 | dependencies 131 | android 132 | automated-pr 133 | 134 | - name: Output results 135 | run: | 136 | if [ "${{ steps.check-update.outputs.update-needed }}" = "true" ]; then 137 | echo "::notice title=Android SDK Updated::Created PR to update Android SDK to v${{ steps.android-version.outputs.version }}" 138 | else 139 | echo "::notice title=No Updates::Android SDK is already up to date (v${{ steps.current-android.outputs.version }})" 140 | fi 141 | -------------------------------------------------------------------------------- /.github/workflows/update-ios-sdk-version.yaml: -------------------------------------------------------------------------------- 1 | name: Update iOS SDK Version 2 | 3 | on: 4 | schedule: 5 | - cron: "0 9 * * MON" 6 | workflow_dispatch: 7 | workflow_call: 8 | inputs: 9 | target-version: 10 | description: "Specific iOS SDK version to update to (optional)" 11 | required: false 12 | type: string 13 | 14 | permissions: 15 | contents: write 16 | pull-requests: write 17 | 18 | jobs: 19 | update-ios-sdk: 20 | name: Update iOS SDK Version 21 | runs-on: macos-latest 22 | steps: 23 | - name: Checkout repository 24 | uses: actions/checkout@v4 25 | with: 26 | token: ${{ secrets.AUTOMATION_USER_TOKEN }} 27 | fetch-depth: 0 28 | 29 | - name: Fetch latest iOS SDK version 30 | id: ios-version 31 | run: | 32 | if [ -n "${{ inputs.target-version }}" ]; then 33 | TARGET_VERSION=$(echo "${{ inputs.target-version }}" | sed 's/^v//') 34 | echo "version=$TARGET_VERSION" >> $GITHUB_OUTPUT 35 | echo "Using specified iOS SDK version: $TARGET_VERSION" 36 | else 37 | LATEST_IOS_VERSION=$(curl -s https://api.github.com/repos/DevCycleHQ/ios-client-sdk/releases/latest | jq -r '.tag_name' | sed 's/^v//') 38 | echo "version=$LATEST_IOS_VERSION" >> $GITHUB_OUTPUT 39 | echo "Latest iOS SDK version: $LATEST_IOS_VERSION" 40 | fi 41 | 42 | - name: Get current iOS SDK version 43 | id: current-ios 44 | run: | 45 | CURRENT_IOS=$(grep "s.dependency 'DevCycle'" ios/devcycle_flutter_client_sdk.podspec | sed -E "s/.*'DevCycle', '([^']+)'.*/\1/") 46 | echo "version=$CURRENT_IOS" >> $GITHUB_OUTPUT 47 | echo "Current iOS SDK version: $CURRENT_IOS" 48 | 49 | - name: Check if iOS update is needed 50 | id: check-update 51 | run: | 52 | if [ "${{ steps.ios-version.outputs.version }}" != "${{ steps.current-ios.outputs.version }}" ]; then 53 | echo "update-needed=true" >> $GITHUB_OUTPUT 54 | echo "iOS SDK update needed: ${{ steps.current-ios.outputs.version }} -> ${{ steps.ios-version.outputs.version }}" 55 | else 56 | echo "update-needed=false" >> $GITHUB_OUTPUT 57 | echo "iOS SDK is already up to date" 58 | fi 59 | 60 | - name: Update iOS SDK version 61 | if: steps.check-update.outputs.update-needed == 'true' 62 | run: | 63 | sed -i '' -E "s/(s\.dependency 'DevCycle', ')([^']+)(')/\1${{ steps.ios-version.outputs.version }}\3/" ios/devcycle_flutter_client_sdk.podspec 64 | echo "Updated iOS SDK version to ${{ steps.ios-version.outputs.version }}" 65 | 66 | - name: Setup Flutter 67 | if: steps.check-update.outputs.update-needed == 'true' 68 | uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046 # v2.19.0 69 | with: 70 | flutter-version: "3.x" 71 | channel: "stable" 72 | 73 | - name: Prepare Flutter 74 | if: steps.check-update.outputs.update-needed == 'true' 75 | run: | 76 | cd example 77 | flutter pub get 78 | 79 | - name: Update iOS dependency lock file 80 | if: steps.check-update.outputs.update-needed == 'true' 81 | run: | 82 | cd example/ios 83 | pod update DevCycle --repo-update 84 | echo "✅ Updated iOS Podfile.lock for DevCycle ${{ steps.ios-version.outputs.version }}" 85 | 86 | - name: Create Pull Request 87 | if: steps.check-update.outputs.update-needed == 'true' 88 | uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8 89 | with: 90 | token: ${{ secrets.AUTOMATION_USER_TOKEN }} 91 | commit-message: | 92 | chore: update iOS SDK version to ${{ steps.ios-version.outputs.version }} 93 | 94 | - iOS SDK: ${{ steps.current-ios.outputs.version }} -> ${{ steps.ios-version.outputs.version }} 95 | title: "chore: update iOS SDK to v${{ steps.ios-version.outputs.version }}" 96 | body: | 97 | ## Summary 98 | This PR updates the DevCycle iOS SDK to the latest version. 99 | 100 | ## Changes 101 | - **iOS SDK**: `${{ steps.current-ios.outputs.version }}` → `${{ steps.ios-version.outputs.version }}` 102 | 103 | ## Files Modified 104 | - `ios/devcycle_flutter_client_sdk.podspec` - Updated iOS SDK dependency version 105 | - `example/ios/Podfile.lock` - Updated iOS dependency lock file 106 | 107 | ## Testing 108 | This PR will be automatically tested by the Flutter SDK Unit Tests workflow which validates: 109 | - Flutter package dependencies 110 | - Code analysis with `flutter analyze` 111 | - Unit tests with `flutter test` 112 | - Package publishing validation with `flutter pub publish --dry-run` 113 | 114 | ## Release Notes 115 | See the [iOS SDK release notes](https://github.com/DevCycleHQ/ios-client-sdk/releases/tag/v${{ steps.ios-version.outputs.version }}) for details on what's new in this version. 116 | 117 | --- 118 | This PR was automatically generated by the Update iOS SDK Version workflow. 119 | branch: update-ios-sdk-${{ steps.ios-version.outputs.version }} 120 | delete-branch: true 121 | base: main 122 | labels: | 123 | dependencies 124 | ios 125 | automated-pr 126 | 127 | - name: Output results 128 | run: | 129 | if [ "${{ steps.check-update.outputs.update-needed }}" = "true" ]; then 130 | echo "::notice title=iOS SDK Updated::Created PR to update iOS SDK to v${{ steps.ios-version.outputs.version }}" 131 | else 132 | echo "::notice title=No Updates::iOS SDK is already up to date (v${{ steps.current-ios.outputs.version }})" 133 | fi 134 | -------------------------------------------------------------------------------- /.github/workflows/update-sdk-versions.yaml: -------------------------------------------------------------------------------- 1 | name: Update DevCycle SDK Versions 2 | 3 | on: 4 | schedule: 5 | - cron: "0 9 * * MON" 6 | workflow_dispatch: 7 | repository_dispatch: 8 | types: [update-dvc-sdks] 9 | 10 | permissions: 11 | contents: write 12 | pull-requests: write 13 | 14 | jobs: 15 | trigger-ios-update: 16 | name: Update iOS SDK 17 | uses: ./.github/workflows/update-ios-sdk-version.yaml 18 | secrets: inherit 19 | 20 | trigger-android-update: 21 | name: Update Android SDK 22 | uses: ./.github/workflows/update-android-sdk-version.yaml 23 | secrets: inherit 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | # Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. 26 | pubspec.lock 27 | **/doc/api/ 28 | .dart_tool/ 29 | .packages 30 | build/ 31 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled. 5 | 6 | version: 7 | revision: 135454af32477f815a7525073027a3ff9eff1bfd 8 | channel: stable 9 | 10 | project_type: plugin 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: 135454af32477f815a7525073027a3ff9eff1bfd 17 | base_revision: 135454af32477f815a7525073027a3ff9eff1bfd 18 | - platform: ios 19 | create_revision: 135454af32477f815a7525073027a3ff9eff1bfd 20 | base_revision: 135454af32477f815a7525073027a3ff9eff1bfd 21 | 22 | # User provided section 23 | 24 | # List of Local paths (relative to this file) that should be 25 | # ignored by the migrate tool. 26 | # 27 | # Files that are not part of the templates will be ignored by default. 28 | unmanaged_files: 29 | - 'lib/main.dart' 30 | - 'ios/Runner.xcodeproj/project.pbxproj' 31 | -------------------------------------------------------------------------------- /.pubignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | # Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. 26 | pubspec.lock 27 | **/doc/api/ 28 | .dart_tool/ 29 | .packages 30 | build/ 31 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | ## [1.10.0] - 2025-06-05 3 | ## Bug Fixes 4 | 5 | - fix: update DevCycle SDKs action by @jonathannorris in #169 6 | - fix: pod update command by @jonathannorris in #171 7 | - fix: actions permissions by @jonathannorris in #173 8 | - fix: update iOS SDK action by @jonathannorris in #175 9 | 10 | ## Other Changes 11 | 12 | - chore: add GH action script to update DevCycle iOS / Android SDKs by @jonathannorris in #168 13 | - chore: remove 'v' from version check by @jonathannorris in #170 14 | - chore: split DevCycle SDK Update workflows into two, run iOS workflow on mac by @jonathannorris in #172 15 | - chore: update Android SDK to v2.3.1 by @DevCycle-Automation in #174 16 | - chore: update iOS SDK to v1.20.0 by @DevCycle-Automation in #176 17 | 18 | 19 | 20 | ## Uncategorized 21 | 22 | 23 | 24 | ## [1.9.2] - 2025-06-04 25 | ## Bug Fixes 26 | 27 | - fix: add namespace to build gradle for newer versions of gradle by @jsalaber in #167 28 | 29 | ## Other Changes 30 | 31 | - chore: Use flutter/dart native OIDC by @JamieSinn in #163 32 | - chore: Potential fix for code scanning alert no. 1: Workflow does not contain permissions by @JamieSinn in #165 33 | - chore: Potential fix for code scanning alert no. 2: Workflow does not contain permissions by @JamieSinn in #164 34 | 35 | 36 | 37 | ## Uncategorized 38 | 39 | 40 | 41 | ## [1.9.1] - 2024-10-03 42 | ## Other Changes 43 | 44 | - chore: update logger, plugin_platform_interface, uuid and flutter_lints dependencies by @kaushalkapasi in #162 45 | 46 | 47 | 48 | ## Uncategorized 49 | 50 | 51 | 52 | ## [1.9.0] - 2024-09-20 53 | ## Bug Fixes 54 | 55 | - fix: onInitialization to wait for initialization to the complete and config received by @suthar26 in #160 56 | 57 | 58 | 59 | ## Uncategorized 60 | 61 | 62 | 63 | ## [1.8.4] - 2024-07-09 64 | ## Bug Fixes 65 | 66 | - fix: pass in type for swift to use the correct variable types by @jsalaber in #158 67 | 68 | 69 | 70 | ## Uncategorized 71 | 72 | 73 | 74 | ## [1.8.3] - 2024-04-30 75 | ## Bug Fixes 76 | 77 | - fix: Revert to previous release code by @JamieSinn in #149 78 | 79 | ## Other Changes 80 | 81 | - chore: release v2 - move all into one file again by @JamieSinn in #148 82 | - chore: update DevCycle iOS Client SDK to 1.17.1 by @kaushalkapasi in #150 83 | 84 | 85 | 86 | ## Uncategorized 87 | 88 | 89 | 90 | ## [1.8.3] - 2024-04-26 91 | ## Other Changes 92 | 93 | - chore: release v2 - move all into one file again by @JamieSinn in #148 94 | 95 | 96 | 97 | ## Uncategorized 98 | 99 | 100 | 101 | ## [1.8.2] - 2024-04-26 102 | ## Other Changes 103 | 104 | - chore: Enable flutter's native auto publishing based on github tags. by @JamieSinn in #147 105 | 106 | 107 | 108 | ## Uncategorized 109 | 110 | 111 | 112 | ## [1.8.1] - 2024-04-26 113 | ## Bug Fixes 114 | 115 | - fix: use raw value instead of returning internal enum class by @jsalaber in #146 116 | 117 | 118 | 119 | ## Uncategorized 120 | 121 | - Change dependabot to weekly by @JamieSinn in #144 122 | 123 | 124 | ## [1.8.1] - 2024-04-26 125 | ## Bug Fixes 126 | 127 | - fix: use raw value instead of returning internal enum class by @jsalaber in #146 128 | 129 | 130 | 131 | ## Uncategorized 132 | 133 | - Change dependabot to weekly by @JamieSinn in #144 134 | 135 | 136 | ## [1.8.0] - 2024-04-24 137 | ## Features 138 | 139 | - feat: add apiProxyUrl and eventsApiProxyUrl options to the DevCycle Options Builder by @kaushalkapasi in #139 140 | 141 | ## Bug Fixes 142 | 143 | - fix: update iOS options builder to use the right proxyURL builder options by @kaushalkapasi in #140 144 | - fix: update Android minSdkVeresion to 23 (Android 6.0 - Marshmallow) by @kaushalkapasi in #142 145 | 146 | ## Other Changes 147 | 148 | - chore: Update android example app by @kaushalkapasi in #141 149 | - chore: Fix release action by @kaushalkapasi in #143 150 | 151 | 152 | 153 | ## Uncategorized 154 | 155 | 156 | 157 | ## [1.7.5] - 2024-04-19 158 | ## Features 159 | 160 | - feat: add apiProxyUrl and eventsApiProxyUrl options to the DevCycle Options Builder by @kaushalkapasi in #139 161 | 162 | 163 | 164 | ## Uncategorized 165 | 166 | 167 | 168 | ## [1.7.4] - 2024-04-18 169 | 170 | 171 | ## Uncategorized 172 | 173 | - chore: Update DevCycle Deps by @kaushalkapasi in #137 174 | - chore: Bump com.android.tools.build:gradle from 8.3.1 to 8.3.2 in /android by @dependabot[bot] in #138 175 | 176 | 177 | ## [1.7.3] - 2023-12-19 178 | 179 | 180 | ## Uncategorized 181 | 182 | - Bump com.devcycle:android-client-sdk from 2.0.2 to 2.0.5 in /android by @dependabot[bot] in #129 183 | 184 | 185 | ## [1.7.2] - 2023-10-24 186 | 187 | 188 | ## Uncategorized 189 | 190 | - chore: dependabot by @JamieSinn in #116 191 | - chore(deps): actions/checkout from 2 to 4 by @dependabot[bot] in #120 192 | - chore(deps): subosito/flutter-action from 1 to 2 by @dependabot[bot] in #118 193 | - chore(deps): com.devcycle:android-client-sdk from 2.0.0 to 2.0.2 in /android by @dependabot[bot] in #119 194 | - chore(deps): com.android.tools.build:gradle from 7.1.2 to 8.1.2 in /android by @dependabot[bot] in #117 195 | - chore(deps): org.jetbrains.kotlin:kotlin-gradle-plugin from 1.6.10 to 1.9.10 in /android by @dependabot[bot] in #121 196 | 197 | 198 | ## [1.7.1] - 2023-08-09 199 | ## Bug Fixes 200 | 201 | - fix: release action name change by @JamieSinn in #107 202 | - fix: rename model files from dvc to devcycle by @jonathannorris in #110 203 | - fix: Update versions for all sub-projects and add the changelog script by @JamieSinn in #111 204 | 205 | ## Build System / CI 206 | 207 | - ci: revised how the changelog updates in the github action by @chris-hoefgen in #113 208 | - ci: fixed incorrect changelog filename in release action by @chris-hoefgen in #114 209 | - ci: another release action fix by @chris-hoefgen in #115 210 | 211 | 212 | 213 | ## Uncategorized 214 | 215 | - chore: update to use the tagged version of the release action instead of main by @chris-hoefgen in #112 216 | 217 | 218 | 219 | ## [1.7.0] - 2023-07-27 220 | 221 | - update Android to 2.0.0 222 | - update iOS to 1.14.0 223 | - External interfaces changed to use `DevCycle` over `DVC`. For example: `DVCClient` -> `DevCycleClient`, `DVCUser` -> `DevCycleUser`. 224 | Old interfaces are marked as deprecated. 225 | 226 | ## [1.6.10] - 2023-06-19 227 | 228 | - add logging disabling methods `disableCustomEventLogging()` and `disableAutomaticEventLogging()` 229 | 230 | ## [1.5.1] - 2023-05-26 231 | 232 | - bump Android version to 1.6.1 233 | 234 | ## [1.5.0] - 2023-05-25 235 | 236 | - bump Android version to 1.6.0 237 | 238 | ## [1.4.0] - 2023-05-16 239 | 240 | - Add `variableValue()` method to `DVCClient` to get the value of a variable for a user. 241 | - Update `variable()` method to return non-optional value, will use default value if variable is not found. 242 | 243 | ## [1.3.3] - 2023-05-09 244 | 245 | - Fix error handling, and update callbacks to pass errors as strings 246 | 247 | ## [1.3.2] - 2023-05-03 248 | 249 | - bump Android version to 1.4.4 250 | 251 | ## [1.3.1] - 2023-04-11 252 | 253 | - add builder method for disabling realtime updates using `.disableRealtimeUpdates()` 254 | 255 | ## [1.3.0] - 2023-03-24 256 | 257 | - update ios and android version to latest release 258 | - update support for flutter versions 259 | 260 | ## [1.2.0] - 2023-03-21 261 | 262 | - catchup with androids new function definitions 263 | 264 | ## [1.1.2] - 2023-03-17 265 | 266 | - bump iOS version to 1.10.0 267 | 268 | ## [1.1.1] - 2023-02-22 269 | 270 | - ignore deprecated usage in test 271 | 272 | ## [1.1.0] - 2023-02-22 273 | 274 | - rename `environmentKey` to `sdkKey` for consistency across SDKs 275 | - bump iOS version to 1.9.1 276 | - bump Android version to 1.4.0 277 | 278 | ## [1.0.2] - 2023-02-3 279 | 280 | - bump iOS version to 1.9.0 281 | 282 | ## [1.0.1] - 2023-02-1 283 | 284 | - bump Android version to 1.2.4 285 | 286 | ## [1.0.0] - 2023-01-16 287 | 288 | - First supported release of DevCycle Flutter Client SDK. Supports Android and iOS. 289 | 290 | ## [0.0.2] - 2023-01-16 291 | 292 | - bump iOS version 293 | 294 | ## [0.0.1] - 2023-01-13 295 | 296 | - Initial Publish 297 | -------------------------------------------------------------------------------- /DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | ## Development 2 | 3 | Build instructions 4 | ------------------ 5 | 6 | ## Prerequisites 7 | 8 | See the [Flutter install](https://flutter.dev/docs/get-started/install) page for setting up Flutter for building Android and iOS plugins. 9 | 10 | ### Android 11 | 12 | Open `example/android` in Android Studio. From there you can run the example app or develop the Android host by modifying `devcycle_flutter_client_sdk > java > com.devcycle.devcycle_flutter_client_sdk > DevCycleFlutterClientSdkPlugin.kt` 13 | 14 | ### iOS 15 | 16 | To run the example app, open `example/ios/Runner.xcworkspace` in xCode or use the `flutter` command line tool and run `flutter run` in the `/example` directory. Develop the iOS host by modifying `ios > Classes > SwiftDevCycleFlutterClientSdkPlugin.swift`. 17 | 18 | ## Testing 19 | 20 | To run the unit tests for the SDK, run `flutter test` in the SDK repo. 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 DevCycle.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DevCycle Flutter Client SDK 2 | 3 | The Flutter Client SDK for DevCycle! This SDK uses our Client SDK APIs to perform all user segmentation and bucketing for the SDK, providing fast response times using our globally distributed edge workers all around the world. 4 | The SDK is available as a package on Pub. It is also open source and can be viewed on GitHub. 5 | 6 | ## Supported Platforms 7 | 8 | This version of the DevCycle Flutter Client SDK supports a minimum of Flutter 2.5.0, iOS 13.7 and Android API Version 26. 9 | 10 | Other Flutter platforms are not currently supported by this SDK. 11 | 12 | ## Installation 13 | 14 | ### Flutter CLI 15 | 16 | The SDK can be installed into your Flutter project by running `flutter pub add devcycle_flutter_client_sdk`. 17 | 18 | ### Pub Spec 19 | 20 | The SDK can be installed into your Flutter project by adding the following to your `pubspec.yaml`: 21 | 22 | ```dart 23 | devcycle_flutter_client_sdk: ^1.7.0 24 | ``` 25 | 26 | Then, run `flutter pub get`. 27 | 28 | ## Usage 29 | 30 | To find usage documentation, check out our [docs](https://docs.devcycle.com/docs/sdk/client-side-sdks/flutter). 31 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:flutter_lints/flutter.yaml 2 | analyzer: 3 | exclude: [test/**] 4 | # Additional information about this file can be found at 5 | # https://dart.dev/guides/language/analysis-options 6 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle/ 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | .cxx 10 | 11 | gradlew 12 | gradlew.bat -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | group 'com.devcycle.devcycle_flutter_client_sdk' 2 | version '1.10.0' 3 | 4 | buildscript { 5 | ext.kotlin_version = '1.9.23' 6 | repositories { 7 | google() 8 | mavenCentral() 9 | } 10 | 11 | dependencies { 12 | classpath 'com.android.tools.build:gradle:8.3.2' 13 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 14 | } 15 | } 16 | 17 | allprojects { 18 | repositories { 19 | google() 20 | mavenCentral() 21 | } 22 | } 23 | 24 | apply plugin: 'com.android.library' 25 | apply plugin: 'kotlin-android' 26 | 27 | android { 28 | namespace "com.devcycle.devcycle_flutter_client_sdk" 29 | compileSdkVersion 31 30 | 31 | compileOptions { 32 | sourceCompatibility JavaVersion.VERSION_1_8 33 | targetCompatibility JavaVersion.VERSION_1_8 34 | } 35 | 36 | kotlinOptions { 37 | jvmTarget = '1.8' 38 | } 39 | 40 | sourceSets { 41 | main.java.srcDirs += 'src/main/kotlin' 42 | } 43 | 44 | defaultConfig { 45 | minSdkVersion 23 46 | } 47 | 48 | dependencies { 49 | implementation("com.devcycle:android-client-sdk:2.3.1") 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevCycleHQ/flutter-client-sdk/aa2a4366da774db6bd354f4c09076cceabecba83/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip 4 | networkTimeout=10000 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'devcycle_flutter_client_sdk' 2 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /android/src/main/kotlin/com/devcycle/devcycle_flutter_client_sdk/DevCycleFlutterClientSdkPlugin.kt: -------------------------------------------------------------------------------- 1 | package com.devcycle.devcycle_flutter_client_sdk 2 | 3 | import android.content.Context 4 | import android.os.Handler 5 | import android.os.Looper 6 | import androidx.annotation.NonNull 7 | import com.devcycle.sdk.android.api.* 8 | import com.devcycle.sdk.android.model.* 9 | import com.devcycle.sdk.android.util.LogLevel 10 | 11 | import kotlin.collections.* 12 | import io.flutter.embedding.engine.plugins.FlutterPlugin 13 | import io.flutter.plugin.common.MethodCall 14 | import io.flutter.plugin.common.MethodChannel 15 | import io.flutter.plugin.common.MethodChannel.MethodCallHandler 16 | import io.flutter.plugin.common.MethodChannel.Result 17 | import org.json.JSONArray 18 | import org.json.JSONObject 19 | import java.math.BigDecimal 20 | import java.text.SimpleDateFormat 21 | 22 | /** DevCycleFlutterClientSdkPlugin */ 23 | class DevCycleFlutterClientSdkPlugin: FlutterPlugin, MethodCallHandler { 24 | /// The MethodChannel that will the communication between Flutter and native Android 25 | /// 26 | /// This local reference serves to register the plugin with the Flutter Engine and unregister it 27 | /// when the Flutter Engine is detached from the Activity 28 | private lateinit var channel : MethodChannel 29 | private lateinit var context: Context 30 | private lateinit var client: DevCycleClient 31 | private var stringVariableUpdates = mutableMapOf>() 32 | private var numberVariableUpdates = mutableMapOf>() 33 | private var booleanVariableUpdates = mutableMapOf>() 34 | private var jsonArrayVariableUpdates = mutableMapOf>() 35 | private var jsonObjectVariableUpdates = mutableMapOf>() 36 | 37 | 38 | override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { 39 | context = flutterPluginBinding.applicationContext 40 | channel = MethodChannel(flutterPluginBinding.binaryMessenger, "devcycle_flutter_client_sdk") 41 | channel.setMethodCallHandler(this) 42 | } 43 | 44 | private fun callFlutter(method: String, arguments: Any?) { 45 | // invokeMethod must be called on main thread 46 | if (Looper.myLooper() == Looper.getMainLooper()) { 47 | channel.invokeMethod(method, arguments) 48 | } else { 49 | // Call ourselves on the main thread 50 | Handler(Looper.getMainLooper()).post { callFlutter(method, arguments) } 51 | } 52 | } 53 | 54 | override fun onMethodCall(@NonNull call: MethodCall, @NonNull res: Result) { 55 | val args = mutableMapOf() 56 | when (call.method) { 57 | "initializeDevCycle" -> { 58 | val codecOptions: Map? = call.argument("options") 59 | val logLevel = getLogLevelFromMap(codecOptions) 60 | val clientBuilder = DevCycleClient 61 | .builder() 62 | .withContext(context) 63 | .withSDKKey(call.argument("sdkKey")!!) 64 | .withUser(getUserFromMap(call.argument("user")!!)) 65 | .withOptions(getOptionsFromMap(codecOptions)) 66 | if (logLevel is LogLevel) { 67 | clientBuilder.withLogLevel(logLevel) 68 | } 69 | 70 | client = clientBuilder.build() 71 | 72 | client.onInitialized(object : DevCycleCallback { 73 | override fun onSuccess(result: String) { 74 | args["isInitialized"] = true 75 | callFlutter("clientInitialized", args) 76 | res.success(null) 77 | } 78 | 79 | override fun onError(t: Throwable) { 80 | args["error"] = t.message 81 | args["isInitialized"] = false 82 | callFlutter("clientInitialized", args) 83 | res.error("error", t.message, null) 84 | } 85 | }) 86 | } 87 | "identifyUser" -> { 88 | val user = getUserFromMap(call.argument("user")!!) 89 | args["callbackId"] = call.argument("callbackId") as String? 90 | val callback = object: DevCycleCallback> { 91 | override fun onSuccess(result: Map) { 92 | args["variables"] = variablesToMap(result) 93 | callFlutter("userIdentified", args) 94 | } 95 | override fun onError(t: Throwable) { 96 | args["error"] = t.message 97 | callFlutter("userIdentified", args) 98 | } 99 | } 100 | 101 | client.identifyUser(user, callback) 102 | } 103 | "resetUser" -> { 104 | args["callbackId"] = call.argument("callbackId") as String? 105 | val callback = object: DevCycleCallback> { 106 | override fun onSuccess(result: Map) { 107 | args["variables"] = variablesToMap(result) 108 | callFlutter("userReset", args) 109 | } 110 | override fun onError(t: Throwable) { 111 | args["error"] = t.message 112 | callFlutter("userReset", args) 113 | } 114 | } 115 | 116 | client.resetUser(callback) 117 | } 118 | "variable" -> { 119 | val key: String = call.argument("key")!! 120 | 121 | when (call.argument("defaultValue")) { 122 | is String -> { 123 | val defaultValue = call.argument("defaultValue") 124 | val variable = client.variable(key, defaultValue!!) 125 | watchForVariableUpdate(variable, stringVariableUpdates) 126 | res.success(variableToMap(variable)) 127 | } 128 | is Boolean -> { 129 | val defaultValue = call.argument("defaultValue") 130 | val variable = client.variable(key, defaultValue!!) 131 | watchForVariableUpdate(variable, booleanVariableUpdates) 132 | res.success(variableToMap(variable)) 133 | } 134 | is Number -> { 135 | val defaultValue = call.argument("defaultValue") 136 | val variable = client.variable(key, defaultValue!!) 137 | watchForVariableUpdate(variable, numberVariableUpdates) 138 | res.success(variableToMap(variable)) 139 | } 140 | is HashMap<*, *> -> { 141 | val defaultValue = call.argument>("defaultValue") 142 | val variable = client.variable(key, JSONObject(defaultValue)) 143 | watchForVariableUpdate(variable, jsonObjectVariableUpdates) 144 | res.success(variableToMap(variable)) 145 | } 146 | is List<*> -> { 147 | val defaultValue = call.argument>("defaultValue") 148 | val variable = client.variable(key, JSONArray(defaultValue)) 149 | watchForVariableUpdate(variable, jsonArrayVariableUpdates) 150 | res.success(variableToMap(variable)) 151 | } 152 | } 153 | } 154 | "allFeatures" -> { 155 | val features = featuresToMap(client.allFeatures()) 156 | res.success(features) 157 | } 158 | "allVariables" -> { 159 | val variables = variablesToMap(client.allVariables()) 160 | res.success(variables) 161 | } 162 | "track" -> { 163 | val event = getEventFromMap(call.argument("event")!!) 164 | client.track(event) 165 | } 166 | "flushEvents" -> { 167 | args["callbackId"] = call.argument("callbackId") as String? 168 | val callback = object: DevCycleCallback { 169 | override fun onSuccess(result: String) { 170 | args["result"] = result 171 | callFlutter("eventsFlushed", args) 172 | } 173 | 174 | override fun onError(t: Throwable) { 175 | args["error"] = t.message 176 | callFlutter("flushEvents", args) 177 | } 178 | } 179 | 180 | client.flushEvents(callback) 181 | } 182 | "getPlatformVersion" -> { 183 | res.success("Android ${android.os.Build.VERSION.RELEASE}") 184 | } 185 | else -> { 186 | res.notImplemented() 187 | } 188 | } 189 | } 190 | 191 | override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { 192 | channel.setMethodCallHandler(null) 193 | } 194 | 195 | private fun getUserFromMap(map: Map): DevCycleUser { 196 | val userBuilder = DevCycleUser.builder() 197 | 198 | val anonymous = map["anonymous"] as? Boolean 199 | if (anonymous is Boolean) userBuilder.withIsAnonymous(anonymous) 200 | 201 | val userId = map["userId"] as? String 202 | if (userId is String) { 203 | android.util.Log.e("init", map["name"].toString()); 204 | userBuilder.withUserId(userId) 205 | userBuilder.withIsAnonymous(false) 206 | } else userBuilder.withIsAnonymous(true) 207 | 208 | val email = map["email"] as? String 209 | if (email is String) userBuilder.withEmail(email) 210 | 211 | val name = map["name"] as? String 212 | if (name is String) userBuilder.withName(name) 213 | 214 | val country = map["country"] as? String 215 | if (country is String) userBuilder.withCountry(country) 216 | 217 | if (map["customData"] != null) { 218 | userBuilder.withCustomData(map["customData"] as Map) 219 | } 220 | 221 | if (map["privateCustomData"] != null) { 222 | userBuilder.withPrivateCustomData(map["privateCustomData"] as Map) 223 | } 224 | 225 | return userBuilder.build() 226 | } 227 | 228 | private fun getEventFromMap(map: Map): DevCycleEvent { 229 | val eventBuilder = DevCycleEvent.builder() 230 | 231 | val type = map["type"] as? String 232 | if (type is String) eventBuilder.withType(type) 233 | 234 | val target = map["target"] as? String 235 | if (target is String) eventBuilder.withTarget(target) 236 | 237 | val valueStr = map["value"] as? String 238 | if (valueStr != null && valueStr != "null") { 239 | try { 240 | val value = BigDecimal(valueStr) 241 | eventBuilder.withValue(value) 242 | } catch (e: NumberFormatException) { 243 | throw e 244 | } 245 | } 246 | 247 | val dateStr = map["date"] as? String 248 | if (dateStr is String) { 249 | val format = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") 250 | val date = format.parse(dateStr) 251 | eventBuilder.withDate(date) 252 | } 253 | 254 | 255 | if (map["metaData"] != null) { 256 | eventBuilder.withMetaData(map["metaData"] as Map) 257 | } 258 | 259 | return eventBuilder.build() 260 | } 261 | 262 | 263 | private fun getOptionsFromMap(map: Map?): DevCycleOptions { 264 | val builder = DevCycleOptions.builder() 265 | 266 | if (map != null) { 267 | val flushEventsIntervalMs = map["flushEventsIntervalMs"] as? Long 268 | if (flushEventsIntervalMs is Long) builder.flushEventsIntervalMs(flushEventsIntervalMs) 269 | 270 | val disableEventLogging = map["flushEventsIntervalMs"] as? Boolean 271 | if (disableEventLogging is Boolean) builder.disableEventLogging(disableEventLogging) 272 | 273 | val configCacheTTL = map["configCacheTTL"] as? Long 274 | if (configCacheTTL is Long) builder.configCacheTTL(configCacheTTL) 275 | 276 | val disableConfigCache = map["disableConfigCache"] as? Boolean 277 | if (disableConfigCache is Boolean) builder.disableConfigCache(disableConfigCache) 278 | 279 | val disableRealtimeUpdates = map["disableRealtimeUpdates"] as? Boolean 280 | if (disableRealtimeUpdates is Boolean) builder.disableRealtimeUpdates(disableRealtimeUpdates) 281 | 282 | val disableAutomaticEventLogging = map["disableAutomaticEventLogging"] as? Boolean 283 | if(disableAutomaticEventLogging is Boolean) builder.disableAutomaticEventLogging(disableAutomaticEventLogging) 284 | 285 | val disableCustomEventLogging = map["disableCustomEventLogging"] as? Boolean 286 | if(disableCustomEventLogging is Boolean) builder.disableCustomEventLogging(disableCustomEventLogging) 287 | 288 | val apiProxyUrl = map["apiProxyUrl"] as? String 289 | if(apiProxyUrl is String) builder.apiProxyUrl(apiProxyUrl) 290 | 291 | val eventsApiProxyUrl = map["eventsApiProxyUrl"] as? String 292 | if(eventsApiProxyUrl is String) builder.eventsApiProxyUrl(eventsApiProxyUrl) 293 | } 294 | return builder.build() 295 | } 296 | 297 | private fun getLogLevelFromMap(map: Map?): LogLevel? { 298 | val logLevelString = map?.get("logLevel") as String? 299 | val logLevelMap = mapOf( 300 | "debug" to LogLevel.DEBUG, 301 | "info" to LogLevel.INFO, 302 | "warn" to LogLevel.WARN, 303 | "error" to LogLevel.ERROR 304 | ) 305 | 306 | if (logLevelString in logLevelMap) { 307 | return logLevelMap[logLevelString] 308 | } 309 | 310 | return null 311 | } 312 | 313 | private fun featuresToMap(features: Map?): Map> { 314 | val map = mutableMapOf>() 315 | 316 | features?.forEach { (key, feature) -> 317 | map[key] = featureToMap(feature) 318 | } 319 | 320 | return map 321 | } 322 | 323 | private fun featureToMap(feature: Feature): Map { 324 | val featureAsMap = mutableMapOf() 325 | featureAsMap["id"] = feature.id 326 | featureAsMap["key"] = feature.key 327 | featureAsMap["type"] = feature.type.toString() 328 | featureAsMap["variation"] = feature.variation 329 | featureAsMap["evalReason"] = feature.evalReason 330 | featureAsMap["variationName"] = feature.variationName 331 | featureAsMap["variationKey"] = feature.variationKey 332 | 333 | return featureAsMap 334 | } 335 | 336 | private fun variablesToMap(variables: Map?): Map> { 337 | val map = mutableMapOf>() 338 | 339 | variables?.forEach { (key, variable) -> 340 | map[key] = variableToMap(variable) 341 | } 342 | 343 | return map 344 | } 345 | 346 | private fun variableToMap(variable: BaseConfigVariable): Map { 347 | val variableAsMap = mutableMapOf() 348 | var value = variable.value 349 | variableAsMap["id"] = variable.id 350 | variableAsMap["key"] = variable.key 351 | variableAsMap["type"] = variable.type.toString() 352 | variableAsMap["value"] = value 353 | variableAsMap["evalReason"] = variable.evalReason 354 | 355 | if (value is JSONObject) variableAsMap["value"] = value.toMap() 356 | if (value is JSONArray) variableAsMap["value"] = value.toMap() 357 | 358 | return variableAsMap 359 | } 360 | 361 | private fun variableToMap(variable: Variable): Map { 362 | val variableAsMap = mutableMapOf() 363 | var value = variable.value 364 | variableAsMap["id"] = variable.id 365 | variableAsMap["key"] = variable.key 366 | variableAsMap["type"] = variable.type.toString() 367 | variableAsMap["value"] = value 368 | variableAsMap["evalReason"] = variable.evalReason 369 | 370 | if (value is JSONObject) variableAsMap["value"] = value.toMap() 371 | if (value is JSONArray) variableAsMap["value"] = value.toMap() 372 | 373 | return variableAsMap 374 | } 375 | 376 | private fun watchForVariableUpdate(variable: Variable, variableUpdateMap: MutableMap>) { 377 | if (variable.key !in variableUpdateMap) { 378 | variable.onUpdate { result: Variable -> 379 | callFlutter("variableUpdated", variableToMap(result)) 380 | } 381 | variableUpdateMap[variable.key] = (variable) 382 | } 383 | } 384 | 385 | private fun JSONArray.toMap(): List<*> { 386 | val map = (0 until this.length()).associate { Pair(it.toString(), this[it]) } 387 | return JSONObject(map).toMap().values.toList() 388 | } 389 | 390 | private fun JSONObject.toMap(): Map = keys().asSequence().associateWith { 391 | when (val value = this[it]) 392 | { 393 | is JSONArray -> value.toMap() 394 | is JSONObject -> value.toMap() 395 | JSONObject.NULL -> null 396 | else -> value 397 | } 398 | } 399 | } -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | **/doc/api/ 26 | **/ios/Flutter/.last_build_id 27 | .dart_tool/ 28 | .flutter-plugins 29 | .flutter-plugins-dependencies 30 | .packages 31 | .pub-cache/ 32 | .pub/ 33 | /build/ 34 | 35 | # Symbolication related 36 | app.*.symbols 37 | 38 | # Obfuscation related 39 | app.*.map.json 40 | 41 | # Android Studio will place build artifacts here 42 | /android/app/debug 43 | /android/app/profile 44 | /android/app/release 45 | 46 | 47 | pubspec.lock 48 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # devcycle_flutter_client_sdk_example 2 | 3 | Demonstrates how to use the devcycle_flutter_client_sdk plugin. 4 | 5 | ## Getting Started 6 | 7 | This project is a starting point for a Flutter application. 8 | 9 | A few resources to get you started if this is your first Flutter project: 10 | 11 | - [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) 12 | - [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) 13 | 14 | For help getting started with Flutter development, view the 15 | [online documentation](https://docs.flutter.dev/), which offers tutorials, 16 | samples, guidance on mobile development, and a full API reference. 17 | 18 | ## Run iOS Example App 19 | 20 | To run the iOS Example App: 21 | - Get your Development Mobile SDK Key from the [DevCycle dashboard](app.devcycle.com). Replace `` in `example/main.dart` with the key. 22 | - navigate to the `cd example/ios` directory 23 | - Start an iOS Simulator: `flutter emulators --launch ios` 24 | - Then run `flutter run` to run the app on the simulator (you may need to run `pod update` first). 25 | 26 | ## Run Android Example App 27 | 28 | To run the Android Example App: 29 | - Get your Development Mobile SDK Key from the [DevCycle dashboard](app.devcycle.com). Replace `` in `example/main.dart` with the key. 30 | - navigate to the `cd example/android` directory 31 | - List your Android emulators using: `flutter emulators`, launch an Android emulator: `flutter emulators --launch Pixel_5_API_31` 32 | - Then run `flutter run` to run the app on the emulator. -------------------------------------------------------------------------------- /example/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at 17 | # https://dart-lang.github.io/linter/lints/index.html. 18 | # 19 | # Instead of disabling a lint rule for the entire project in the 20 | # section below, it can also be suppressed for a single line of code 21 | # or a specific dart file by using the `// ignore: name_of_lint` and 22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 23 | # producing the lint. 24 | rules: 25 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 26 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 27 | 28 | # Additional information about this file can be found at 29 | # https://dart.dev/guides/language/analysis-options 30 | -------------------------------------------------------------------------------- /example/android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /example/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.android.application" 3 | id "kotlin-android" 4 | id "dev.flutter.flutter-gradle-plugin" 5 | } 6 | 7 | def localProperties = new Properties() 8 | def localPropertiesFile = rootProject.file('local.properties') 9 | if (localPropertiesFile.exists()) { 10 | localPropertiesFile.withReader('UTF-8') { reader -> 11 | localProperties.load(reader) 12 | } 13 | } 14 | 15 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 16 | if (flutterVersionCode == null) { 17 | flutterVersionCode = '1' 18 | } 19 | 20 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 21 | if (flutterVersionName == null) { 22 | flutterVersionName = '1.0' 23 | } 24 | 25 | android { 26 | compileSdk 31 27 | // ndkVersion flutter.ndkVersion 28 | 29 | compileOptions { 30 | sourceCompatibility JavaVersion.VERSION_1_8 31 | targetCompatibility JavaVersion.VERSION_1_8 32 | } 33 | 34 | kotlinOptions { 35 | jvmTarget = '1.8' 36 | } 37 | 38 | sourceSets { 39 | main.java.srcDirs += 'src/main/kotlin' 40 | } 41 | 42 | defaultConfig { 43 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 44 | applicationId "com.example.devcycle_flutter_client_sdk_example" 45 | // You can update the following values to match your application needs. 46 | // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. 47 | minSdkVersion 23 48 | targetSdkVersion 31 49 | versionCode flutterVersionCode.toInteger() 50 | versionName flutterVersionName 51 | } 52 | 53 | buildTypes { 54 | release { 55 | // TODO: Add your own signing config for the release build. 56 | // Signing with the debug keys for now, so `flutter run --release` works. 57 | signingConfig signingConfigs.debug 58 | } 59 | } 60 | namespace 'com.example.devcycle_flutter_client_sdk_example' 61 | } 62 | 63 | flutter { 64 | source '../..' 65 | } 66 | 67 | dependencies { 68 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.10" 69 | } 70 | -------------------------------------------------------------------------------- /example/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 13 | 17 | 21 | 22 | 23 | 24 | 25 | 26 | 28 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /example/android/app/src/main/kotlin/com/example/devcycle_flutter_client_sdk_example/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.devcycle_flutter_client_sdk_example 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevCycleHQ/flutter-client-sdk/aa2a4366da774db6bd354f4c09076cceabecba83/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevCycleHQ/flutter-client-sdk/aa2a4366da774db6bd354f4c09076cceabecba83/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevCycleHQ/flutter-client-sdk/aa2a4366da774db6bd354f4c09076cceabecba83/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevCycleHQ/flutter-client-sdk/aa2a4366da774db6bd354f4c09076cceabecba83/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevCycleHQ/flutter-client-sdk/aa2a4366da774db6bd354f4c09076cceabecba83/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /example/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | allprojects { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | } 7 | 8 | rootProject.buildDir = '../build' 9 | subprojects { 10 | project.buildDir = "${rootProject.buildDir}/${project.name}" 11 | } 12 | subprojects { 13 | project.evaluationDependsOn(':app') 14 | } 15 | 16 | tasks.register("clean", Delete) { 17 | delete rootProject.buildDir 18 | } 19 | -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip 6 | -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | def flutterSdkPath = { 3 | def properties = new Properties() 4 | file("local.properties").withInputStream { properties.load(it) } 5 | def flutterSdkPath = properties.getProperty("flutter.sdk") 6 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 7 | return flutterSdkPath 8 | }() 9 | 10 | includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") 11 | 12 | repositories { 13 | google() 14 | mavenCentral() 15 | gradlePluginPortal() 16 | } 17 | } 18 | 19 | plugins { 20 | id "dev.flutter.flutter-plugin-loader" version "1.0.0" 21 | id "com.android.application" version '7.4.2' apply false 22 | id "org.jetbrains.kotlin.android" version "1.9.10" apply false 23 | } 24 | 25 | include ":app" 26 | -------------------------------------------------------------------------------- /example/ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /example/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 12.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | platform :ios, '12.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | end 36 | 37 | post_install do |installer| 38 | installer.pods_project.targets.each do |target| 39 | flutter_additional_ios_build_settings(target) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /example/ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - DevCycle (1.20.0): 3 | - LDSwiftEventSource (~> 3.3) 4 | - devcycle_flutter_client_sdk (1.9.2): 5 | - DevCycle (= 1.20.0) 6 | - Flutter 7 | - Flutter (1.0.0) 8 | - LDSwiftEventSource (3.3.0) 9 | 10 | DEPENDENCIES: 11 | - devcycle_flutter_client_sdk (from `.symlinks/plugins/devcycle_flutter_client_sdk/ios`) 12 | - Flutter (from `Flutter`) 13 | 14 | SPEC REPOS: 15 | trunk: 16 | - DevCycle 17 | - LDSwiftEventSource 18 | 19 | EXTERNAL SOURCES: 20 | devcycle_flutter_client_sdk: 21 | :path: ".symlinks/plugins/devcycle_flutter_client_sdk/ios" 22 | Flutter: 23 | :path: Flutter 24 | 25 | SPEC CHECKSUMS: 26 | DevCycle: 1d47bee02033fc73725a7336d57a6490c8b6cbb8 27 | devcycle_flutter_client_sdk: 83820fc1a50cc034337c5b4babcbd9a406b6abb0 28 | Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 29 | LDSwiftEventSource: b0826ca826ca890609a9833a05cae1f357fd3b99 30 | 31 | PODFILE CHECKSUM: 4e8f8b2be68aeea4c0d5beb6ff1e79fface1d048 32 | 33 | COCOAPODS: 1.16.2 34 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 54; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 11 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 12 | 430CA29F14BCDAF9409ECF6A /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8F824D47E0E582A68B2C36AE /* Pods_Runner.framework */; }; 13 | 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 14 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 15 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 16 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 17 | /* End PBXBuildFile section */ 18 | 19 | /* Begin PBXCopyFilesBuildPhase section */ 20 | 9705A1C41CF9048500538489 /* Embed Frameworks */ = { 21 | isa = PBXCopyFilesBuildPhase; 22 | buildActionMask = 2147483647; 23 | dstPath = ""; 24 | dstSubfolderSpec = 10; 25 | files = ( 26 | ); 27 | name = "Embed Frameworks"; 28 | runOnlyForDeploymentPostprocessing = 0; 29 | }; 30 | /* End PBXCopyFilesBuildPhase section */ 31 | 32 | /* Begin PBXFileReference section */ 33 | 10B4E7BBD93562E56E3C45C6 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 34 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 35 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 36 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 37 | 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 38 | 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 39 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 40 | 7BF58ED7A96F66311AA18EA5 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 41 | 8F824D47E0E582A68B2C36AE /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 42 | 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 43 | 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 44 | 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 45 | 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 46 | 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 47 | 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 48 | 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 49 | D819106D85C660AEC0608267 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 50 | /* End PBXFileReference section */ 51 | 52 | /* Begin PBXFrameworksBuildPhase section */ 53 | 97C146EB1CF9000F007C117D /* Frameworks */ = { 54 | isa = PBXFrameworksBuildPhase; 55 | buildActionMask = 2147483647; 56 | files = ( 57 | 430CA29F14BCDAF9409ECF6A /* Pods_Runner.framework in Frameworks */, 58 | ); 59 | runOnlyForDeploymentPostprocessing = 0; 60 | }; 61 | /* End PBXFrameworksBuildPhase section */ 62 | 63 | /* Begin PBXGroup section */ 64 | 9740EEB11CF90186004384FC /* Flutter */ = { 65 | isa = PBXGroup; 66 | children = ( 67 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 68 | 9740EEB21CF90195004384FC /* Debug.xcconfig */, 69 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 70 | 9740EEB31CF90195004384FC /* Generated.xcconfig */, 71 | ); 72 | name = Flutter; 73 | sourceTree = ""; 74 | }; 75 | 97C146E51CF9000F007C117D = { 76 | isa = PBXGroup; 77 | children = ( 78 | 9740EEB11CF90186004384FC /* Flutter */, 79 | 97C146F01CF9000F007C117D /* Runner */, 80 | 97C146EF1CF9000F007C117D /* Products */, 81 | C5BD6995F521DF53D5B9802A /* Pods */, 82 | ED8A8B5EB85FAEAE4CA60CED /* Frameworks */, 83 | ); 84 | sourceTree = ""; 85 | }; 86 | 97C146EF1CF9000F007C117D /* Products */ = { 87 | isa = PBXGroup; 88 | children = ( 89 | 97C146EE1CF9000F007C117D /* Runner.app */, 90 | ); 91 | name = Products; 92 | sourceTree = ""; 93 | }; 94 | 97C146F01CF9000F007C117D /* Runner */ = { 95 | isa = PBXGroup; 96 | children = ( 97 | 97C146FA1CF9000F007C117D /* Main.storyboard */, 98 | 97C146FD1CF9000F007C117D /* Assets.xcassets */, 99 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 100 | 97C147021CF9000F007C117D /* Info.plist */, 101 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 102 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 103 | 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 104 | 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, 105 | ); 106 | path = Runner; 107 | sourceTree = ""; 108 | }; 109 | C5BD6995F521DF53D5B9802A /* Pods */ = { 110 | isa = PBXGroup; 111 | children = ( 112 | 10B4E7BBD93562E56E3C45C6 /* Pods-Runner.debug.xcconfig */, 113 | D819106D85C660AEC0608267 /* Pods-Runner.release.xcconfig */, 114 | 7BF58ED7A96F66311AA18EA5 /* Pods-Runner.profile.xcconfig */, 115 | ); 116 | path = Pods; 117 | sourceTree = ""; 118 | }; 119 | ED8A8B5EB85FAEAE4CA60CED /* Frameworks */ = { 120 | isa = PBXGroup; 121 | children = ( 122 | 8F824D47E0E582A68B2C36AE /* Pods_Runner.framework */, 123 | ); 124 | name = Frameworks; 125 | sourceTree = ""; 126 | }; 127 | /* End PBXGroup section */ 128 | 129 | /* Begin PBXNativeTarget section */ 130 | 97C146ED1CF9000F007C117D /* Runner */ = { 131 | isa = PBXNativeTarget; 132 | buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; 133 | buildPhases = ( 134 | 48314DDA4B17BD8542B77ECC /* [CP] Check Pods Manifest.lock */, 135 | 9740EEB61CF901F6004384FC /* Run Script */, 136 | 97C146EA1CF9000F007C117D /* Sources */, 137 | 97C146EB1CF9000F007C117D /* Frameworks */, 138 | 97C146EC1CF9000F007C117D /* Resources */, 139 | 9705A1C41CF9048500538489 /* Embed Frameworks */, 140 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 141 | F2115E69E19B31622DE9EB78 /* [CP] Embed Pods Frameworks */, 142 | ); 143 | buildRules = ( 144 | ); 145 | dependencies = ( 146 | ); 147 | name = Runner; 148 | productName = Runner; 149 | productReference = 97C146EE1CF9000F007C117D /* Runner.app */; 150 | productType = "com.apple.product-type.application"; 151 | }; 152 | /* End PBXNativeTarget section */ 153 | 154 | /* Begin PBXProject section */ 155 | 97C146E61CF9000F007C117D /* Project object */ = { 156 | isa = PBXProject; 157 | attributes = { 158 | LastUpgradeCheck = 1300; 159 | ORGANIZATIONNAME = ""; 160 | TargetAttributes = { 161 | 97C146ED1CF9000F007C117D = { 162 | CreatedOnToolsVersion = 7.3.1; 163 | LastSwiftMigration = 1100; 164 | }; 165 | }; 166 | }; 167 | buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; 168 | compatibilityVersion = "Xcode 9.3"; 169 | developmentRegion = en; 170 | hasScannedForEncodings = 0; 171 | knownRegions = ( 172 | en, 173 | Base, 174 | ); 175 | mainGroup = 97C146E51CF9000F007C117D; 176 | productRefGroup = 97C146EF1CF9000F007C117D /* Products */; 177 | projectDirPath = ""; 178 | projectRoot = ""; 179 | targets = ( 180 | 97C146ED1CF9000F007C117D /* Runner */, 181 | ); 182 | }; 183 | /* End PBXProject section */ 184 | 185 | /* Begin PBXResourcesBuildPhase section */ 186 | 97C146EC1CF9000F007C117D /* Resources */ = { 187 | isa = PBXResourcesBuildPhase; 188 | buildActionMask = 2147483647; 189 | files = ( 190 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 191 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 192 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 193 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, 194 | ); 195 | runOnlyForDeploymentPostprocessing = 0; 196 | }; 197 | /* End PBXResourcesBuildPhase section */ 198 | 199 | /* Begin PBXShellScriptBuildPhase section */ 200 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { 201 | isa = PBXShellScriptBuildPhase; 202 | alwaysOutOfDate = 1; 203 | buildActionMask = 2147483647; 204 | files = ( 205 | ); 206 | inputPaths = ( 207 | "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", 208 | ); 209 | name = "Thin Binary"; 210 | outputPaths = ( 211 | ); 212 | runOnlyForDeploymentPostprocessing = 0; 213 | shellPath = /bin/sh; 214 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; 215 | }; 216 | 48314DDA4B17BD8542B77ECC /* [CP] Check Pods Manifest.lock */ = { 217 | isa = PBXShellScriptBuildPhase; 218 | buildActionMask = 2147483647; 219 | files = ( 220 | ); 221 | inputFileListPaths = ( 222 | ); 223 | inputPaths = ( 224 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 225 | "${PODS_ROOT}/Manifest.lock", 226 | ); 227 | name = "[CP] Check Pods Manifest.lock"; 228 | outputFileListPaths = ( 229 | ); 230 | outputPaths = ( 231 | "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", 232 | ); 233 | runOnlyForDeploymentPostprocessing = 0; 234 | shellPath = /bin/sh; 235 | 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"; 236 | showEnvVarsInLog = 0; 237 | }; 238 | 9740EEB61CF901F6004384FC /* Run Script */ = { 239 | isa = PBXShellScriptBuildPhase; 240 | alwaysOutOfDate = 1; 241 | buildActionMask = 2147483647; 242 | files = ( 243 | ); 244 | inputPaths = ( 245 | ); 246 | name = "Run Script"; 247 | outputPaths = ( 248 | ); 249 | runOnlyForDeploymentPostprocessing = 0; 250 | shellPath = /bin/sh; 251 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; 252 | }; 253 | F2115E69E19B31622DE9EB78 /* [CP] Embed Pods Frameworks */ = { 254 | isa = PBXShellScriptBuildPhase; 255 | buildActionMask = 2147483647; 256 | files = ( 257 | ); 258 | inputFileListPaths = ( 259 | "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", 260 | ); 261 | name = "[CP] Embed Pods Frameworks"; 262 | outputFileListPaths = ( 263 | "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", 264 | ); 265 | runOnlyForDeploymentPostprocessing = 0; 266 | shellPath = /bin/sh; 267 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; 268 | showEnvVarsInLog = 0; 269 | }; 270 | /* End PBXShellScriptBuildPhase section */ 271 | 272 | /* Begin PBXSourcesBuildPhase section */ 273 | 97C146EA1CF9000F007C117D /* Sources */ = { 274 | isa = PBXSourcesBuildPhase; 275 | buildActionMask = 2147483647; 276 | files = ( 277 | 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 278 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, 279 | ); 280 | runOnlyForDeploymentPostprocessing = 0; 281 | }; 282 | /* End PBXSourcesBuildPhase section */ 283 | 284 | /* Begin PBXVariantGroup section */ 285 | 97C146FA1CF9000F007C117D /* Main.storyboard */ = { 286 | isa = PBXVariantGroup; 287 | children = ( 288 | 97C146FB1CF9000F007C117D /* Base */, 289 | ); 290 | name = Main.storyboard; 291 | sourceTree = ""; 292 | }; 293 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { 294 | isa = PBXVariantGroup; 295 | children = ( 296 | 97C147001CF9000F007C117D /* Base */, 297 | ); 298 | name = LaunchScreen.storyboard; 299 | sourceTree = ""; 300 | }; 301 | /* End PBXVariantGroup section */ 302 | 303 | /* Begin XCBuildConfiguration section */ 304 | 249021D3217E4FDB00AE95B9 /* Profile */ = { 305 | isa = XCBuildConfiguration; 306 | buildSettings = { 307 | ALWAYS_SEARCH_USER_PATHS = NO; 308 | CLANG_ANALYZER_NONNULL = YES; 309 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 310 | CLANG_CXX_LIBRARY = "libc++"; 311 | CLANG_ENABLE_MODULES = YES; 312 | CLANG_ENABLE_OBJC_ARC = YES; 313 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 314 | CLANG_WARN_BOOL_CONVERSION = YES; 315 | CLANG_WARN_COMMA = YES; 316 | CLANG_WARN_CONSTANT_CONVERSION = YES; 317 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 318 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 319 | CLANG_WARN_EMPTY_BODY = YES; 320 | CLANG_WARN_ENUM_CONVERSION = YES; 321 | CLANG_WARN_INFINITE_RECURSION = YES; 322 | CLANG_WARN_INT_CONVERSION = YES; 323 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 324 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 325 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 326 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 327 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 328 | CLANG_WARN_STRICT_PROTOTYPES = YES; 329 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 330 | CLANG_WARN_UNREACHABLE_CODE = YES; 331 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 332 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 333 | COPY_PHASE_STRIP = NO; 334 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 335 | ENABLE_NS_ASSERTIONS = NO; 336 | ENABLE_STRICT_OBJC_MSGSEND = YES; 337 | GCC_C_LANGUAGE_STANDARD = gnu99; 338 | GCC_NO_COMMON_BLOCKS = YES; 339 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 340 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 341 | GCC_WARN_UNDECLARED_SELECTOR = YES; 342 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 343 | GCC_WARN_UNUSED_FUNCTION = YES; 344 | GCC_WARN_UNUSED_VARIABLE = YES; 345 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 346 | MTL_ENABLE_DEBUG_INFO = NO; 347 | SDKROOT = iphoneos; 348 | SUPPORTED_PLATFORMS = iphoneos; 349 | TARGETED_DEVICE_FAMILY = "1,2"; 350 | VALIDATE_PRODUCT = YES; 351 | }; 352 | name = Profile; 353 | }; 354 | 249021D4217E4FDB00AE95B9 /* Profile */ = { 355 | isa = XCBuildConfiguration; 356 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 357 | buildSettings = { 358 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 359 | CLANG_ENABLE_MODULES = YES; 360 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 361 | DEVELOPMENT_TEAM = ASZYP8H6QE; 362 | ENABLE_BITCODE = NO; 363 | INFOPLIST_FILE = Runner/Info.plist; 364 | LD_RUNPATH_SEARCH_PATHS = ( 365 | "$(inherited)", 366 | "@executable_path/Frameworks", 367 | ); 368 | PRODUCT_BUNDLE_IDENTIFIER = com.example.devcycleFlutterClientSdkExample; 369 | PRODUCT_NAME = "$(TARGET_NAME)"; 370 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 371 | SWIFT_VERSION = 5.0; 372 | VERSIONING_SYSTEM = "apple-generic"; 373 | }; 374 | name = Profile; 375 | }; 376 | 97C147031CF9000F007C117D /* Debug */ = { 377 | isa = XCBuildConfiguration; 378 | buildSettings = { 379 | ALWAYS_SEARCH_USER_PATHS = NO; 380 | CLANG_ANALYZER_NONNULL = YES; 381 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 382 | CLANG_CXX_LIBRARY = "libc++"; 383 | CLANG_ENABLE_MODULES = YES; 384 | CLANG_ENABLE_OBJC_ARC = YES; 385 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 386 | CLANG_WARN_BOOL_CONVERSION = YES; 387 | CLANG_WARN_COMMA = YES; 388 | CLANG_WARN_CONSTANT_CONVERSION = YES; 389 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 390 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 391 | CLANG_WARN_EMPTY_BODY = YES; 392 | CLANG_WARN_ENUM_CONVERSION = YES; 393 | CLANG_WARN_INFINITE_RECURSION = YES; 394 | CLANG_WARN_INT_CONVERSION = YES; 395 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 396 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 397 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 398 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 399 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 400 | CLANG_WARN_STRICT_PROTOTYPES = YES; 401 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 402 | CLANG_WARN_UNREACHABLE_CODE = YES; 403 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 404 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 405 | COPY_PHASE_STRIP = NO; 406 | DEBUG_INFORMATION_FORMAT = dwarf; 407 | ENABLE_STRICT_OBJC_MSGSEND = YES; 408 | ENABLE_TESTABILITY = YES; 409 | GCC_C_LANGUAGE_STANDARD = gnu99; 410 | GCC_DYNAMIC_NO_PIC = NO; 411 | GCC_NO_COMMON_BLOCKS = YES; 412 | GCC_OPTIMIZATION_LEVEL = 0; 413 | GCC_PREPROCESSOR_DEFINITIONS = ( 414 | "DEBUG=1", 415 | "$(inherited)", 416 | ); 417 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 418 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 419 | GCC_WARN_UNDECLARED_SELECTOR = YES; 420 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 421 | GCC_WARN_UNUSED_FUNCTION = YES; 422 | GCC_WARN_UNUSED_VARIABLE = YES; 423 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 424 | MTL_ENABLE_DEBUG_INFO = YES; 425 | ONLY_ACTIVE_ARCH = YES; 426 | SDKROOT = iphoneos; 427 | TARGETED_DEVICE_FAMILY = "1,2"; 428 | }; 429 | name = Debug; 430 | }; 431 | 97C147041CF9000F007C117D /* Release */ = { 432 | isa = XCBuildConfiguration; 433 | buildSettings = { 434 | ALWAYS_SEARCH_USER_PATHS = NO; 435 | CLANG_ANALYZER_NONNULL = YES; 436 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 437 | CLANG_CXX_LIBRARY = "libc++"; 438 | CLANG_ENABLE_MODULES = YES; 439 | CLANG_ENABLE_OBJC_ARC = YES; 440 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 441 | CLANG_WARN_BOOL_CONVERSION = YES; 442 | CLANG_WARN_COMMA = YES; 443 | CLANG_WARN_CONSTANT_CONVERSION = YES; 444 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 445 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 446 | CLANG_WARN_EMPTY_BODY = YES; 447 | CLANG_WARN_ENUM_CONVERSION = YES; 448 | CLANG_WARN_INFINITE_RECURSION = YES; 449 | CLANG_WARN_INT_CONVERSION = YES; 450 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 451 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 452 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 453 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 454 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 455 | CLANG_WARN_STRICT_PROTOTYPES = YES; 456 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 457 | CLANG_WARN_UNREACHABLE_CODE = YES; 458 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 459 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 460 | COPY_PHASE_STRIP = NO; 461 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 462 | ENABLE_NS_ASSERTIONS = NO; 463 | ENABLE_STRICT_OBJC_MSGSEND = YES; 464 | GCC_C_LANGUAGE_STANDARD = gnu99; 465 | GCC_NO_COMMON_BLOCKS = YES; 466 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 467 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 468 | GCC_WARN_UNDECLARED_SELECTOR = YES; 469 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 470 | GCC_WARN_UNUSED_FUNCTION = YES; 471 | GCC_WARN_UNUSED_VARIABLE = YES; 472 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 473 | MTL_ENABLE_DEBUG_INFO = NO; 474 | SDKROOT = iphoneos; 475 | SUPPORTED_PLATFORMS = iphoneos; 476 | SWIFT_COMPILATION_MODE = wholemodule; 477 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 478 | TARGETED_DEVICE_FAMILY = "1,2"; 479 | VALIDATE_PRODUCT = YES; 480 | }; 481 | name = Release; 482 | }; 483 | 97C147061CF9000F007C117D /* Debug */ = { 484 | isa = XCBuildConfiguration; 485 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; 486 | buildSettings = { 487 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 488 | CLANG_ENABLE_MODULES = YES; 489 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 490 | DEVELOPMENT_TEAM = ASZYP8H6QE; 491 | ENABLE_BITCODE = NO; 492 | INFOPLIST_FILE = Runner/Info.plist; 493 | LD_RUNPATH_SEARCH_PATHS = ( 494 | "$(inherited)", 495 | "@executable_path/Frameworks", 496 | ); 497 | PRODUCT_BUNDLE_IDENTIFIER = com.example.devcycleFlutterClientSdkExample; 498 | PRODUCT_NAME = "$(TARGET_NAME)"; 499 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 500 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 501 | SWIFT_VERSION = 5.0; 502 | VERSIONING_SYSTEM = "apple-generic"; 503 | }; 504 | name = Debug; 505 | }; 506 | 97C147071CF9000F007C117D /* Release */ = { 507 | isa = XCBuildConfiguration; 508 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 509 | buildSettings = { 510 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 511 | CLANG_ENABLE_MODULES = YES; 512 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 513 | DEVELOPMENT_TEAM = ASZYP8H6QE; 514 | ENABLE_BITCODE = NO; 515 | INFOPLIST_FILE = Runner/Info.plist; 516 | LD_RUNPATH_SEARCH_PATHS = ( 517 | "$(inherited)", 518 | "@executable_path/Frameworks", 519 | ); 520 | PRODUCT_BUNDLE_IDENTIFIER = com.example.devcycleFlutterClientSdkExample; 521 | PRODUCT_NAME = "$(TARGET_NAME)"; 522 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 523 | SWIFT_VERSION = 5.0; 524 | VERSIONING_SYSTEM = "apple-generic"; 525 | }; 526 | name = Release; 527 | }; 528 | /* End XCBuildConfiguration section */ 529 | 530 | /* Begin XCConfigurationList section */ 531 | 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { 532 | isa = XCConfigurationList; 533 | buildConfigurations = ( 534 | 97C147031CF9000F007C117D /* Debug */, 535 | 97C147041CF9000F007C117D /* Release */, 536 | 249021D3217E4FDB00AE95B9 /* Profile */, 537 | ); 538 | defaultConfigurationIsVisible = 0; 539 | defaultConfigurationName = Release; 540 | }; 541 | 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { 542 | isa = XCConfigurationList; 543 | buildConfigurations = ( 544 | 97C147061CF9000F007C117D /* Debug */, 545 | 97C147071CF9000F007C117D /* Release */, 546 | 249021D4217E4FDB00AE95B9 /* Profile */, 547 | ); 548 | defaultConfigurationIsVisible = 0; 549 | defaultConfigurationName = Release; 550 | }; 551 | /* End XCConfigurationList section */ 552 | }; 553 | rootObject = 97C146E61CF9000F007C117D /* Project object */; 554 | } 555 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevCycleHQ/flutter-client-sdk/aa2a4366da774db6bd354f4c09076cceabecba83/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevCycleHQ/flutter-client-sdk/aa2a4366da774db6bd354f4c09076cceabecba83/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevCycleHQ/flutter-client-sdk/aa2a4366da774db6bd354f4c09076cceabecba83/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevCycleHQ/flutter-client-sdk/aa2a4366da774db6bd354f4c09076cceabecba83/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevCycleHQ/flutter-client-sdk/aa2a4366da774db6bd354f4c09076cceabecba83/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevCycleHQ/flutter-client-sdk/aa2a4366da774db6bd354f4c09076cceabecba83/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevCycleHQ/flutter-client-sdk/aa2a4366da774db6bd354f4c09076cceabecba83/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevCycleHQ/flutter-client-sdk/aa2a4366da774db6bd354f4c09076cceabecba83/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevCycleHQ/flutter-client-sdk/aa2a4366da774db6bd354f4c09076cceabecba83/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevCycleHQ/flutter-client-sdk/aa2a4366da774db6bd354f4c09076cceabecba83/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevCycleHQ/flutter-client-sdk/aa2a4366da774db6bd354f4c09076cceabecba83/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevCycleHQ/flutter-client-sdk/aa2a4366da774db6bd354f4c09076cceabecba83/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevCycleHQ/flutter-client-sdk/aa2a4366da774db6bd354f4c09076cceabecba83/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevCycleHQ/flutter-client-sdk/aa2a4366da774db6bd354f4c09076cceabecba83/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevCycleHQ/flutter-client-sdk/aa2a4366da774db6bd354f4c09076cceabecba83/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevCycleHQ/flutter-client-sdk/aa2a4366da774db6bd354f4c09076cceabecba83/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevCycleHQ/flutter-client-sdk/aa2a4366da774db6bd354f4c09076cceabecba83/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevCycleHQ/flutter-client-sdk/aa2a4366da774db6bd354f4c09076cceabecba83/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /example/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | DevCycle Flutter Client Sdk 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | devcycle_flutter_client_sdk_example 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(FLUTTER_BUILD_NUMBER) 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | UIViewControllerBasedStatusBarAppearance 45 | 46 | CADisableMinimumFrameDurationOnPhone 47 | 48 | UIApplicationSupportsIndirectInputEvents 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /example/ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:logger/logger.dart'; 3 | import 'dart:async'; 4 | import 'dart:convert'; 5 | 6 | import 'package:flutter/services.dart'; 7 | import 'package:devcycle_flutter_client_sdk/devcycle_flutter_client_sdk.dart'; 8 | 9 | void main() { 10 | runApp(const MyApp()); 11 | } 12 | 13 | class MyApp extends StatefulWidget { 14 | const MyApp({super.key}); 15 | 16 | @override 17 | State createState() => _MyAppState(); 18 | } 19 | 20 | class _MyAppState extends State { 21 | final _logger = Logger(); 22 | String _platformVersion = 'Unknown'; 23 | String _displayValue = ''; 24 | String _variableValue = ''; 25 | bool _booleanValue = false; 26 | num _integerValue = 0; 27 | num _doubleValue = 0.0; 28 | var _jsonArrayValue = ''; 29 | var _jsonObjectValue = ''; 30 | 31 | var encodedJsonArray = ''' 32 | [ 33 | {"score": 40}, 34 | {"score": 80} 35 | ] 36 | '''; 37 | var encodedJsonObject = ''' 38 | { 39 | "score1": 40, 40 | "score2": 80 41 | } 42 | '''; 43 | 44 | final _devCycleClient = DevCycleClientBuilder() 45 | .sdkKey('') 46 | .user(DevCycleUserBuilder().userId('123').build()) 47 | .options(DevCycleOptionsBuilder().logLevel(LogLevel.debug).build()) 48 | .build(); 49 | 50 | @override 51 | void initState() { 52 | super.initState(); 53 | initPlatformState(); 54 | initVariable(); 55 | } 56 | 57 | // Platform messages are asynchronous, so we initialize in an async method. 58 | Future initPlatformState() async { 59 | String platformVersion; 60 | // Platform messages may fail, so we use a try/catch PlatformException. 61 | // We also handle the message potentially returning null. 62 | try { 63 | platformVersion = await _devCycleClient.getPlatformVersion() ?? 64 | 'Unknown platform version'; 65 | } on PlatformException { 66 | platformVersion = 'Failed to get platform version.'; 67 | } 68 | 69 | // If the widget was removed from the tree while the asynchronous platform 70 | // message was in flight, we want to discard the reply rather than calling 71 | // setState to update our non-existent appearance. 72 | if (!mounted) return; 73 | 74 | setState(() { 75 | _platformVersion = platformVersion; 76 | }); 77 | } 78 | 79 | Future initVariable() async { 80 | // Wait for client to initialize before fetching variables 81 | _devCycleClient.onInitialized(([error]) async { 82 | setState(() { 83 | _displayValue = error ?? 'DevCycle Client initialized'; 84 | }); 85 | 86 | final variable = 87 | await _devCycleClient.variable('string-variable', 'Default Value'); 88 | setState(() { 89 | _variableValue = variable.value; 90 | }); 91 | variable.onUpdate((updatedValue) { 92 | setState(() { 93 | _variableValue = updatedValue; 94 | }); 95 | }); 96 | 97 | final booleanVariable = 98 | await _devCycleClient.variable('boolean-variable', false); 99 | setState(() { 100 | _booleanValue = booleanVariable.value; 101 | }); 102 | booleanVariable.onUpdate((updatedValue) { 103 | setState(() { 104 | _booleanValue = updatedValue; 105 | }); 106 | }); 107 | 108 | final integerVariable = 109 | await _devCycleClient.variable('integer-variable', 188); 110 | setState(() { 111 | _integerValue = integerVariable.value; 112 | }); 113 | integerVariable.onUpdate((updatedValue) { 114 | setState(() { 115 | _integerValue = updatedValue; 116 | }); 117 | }); 118 | 119 | final doubleVariable = 120 | await _devCycleClient.variable('decimal-variable', 1.88); 121 | setState(() { 122 | _doubleValue = doubleVariable.value; 123 | }); 124 | doubleVariable.onUpdate((updatedValue) { 125 | setState(() { 126 | _doubleValue = updatedValue; 127 | }); 128 | }); 129 | 130 | final jsonArrayVariable = await _devCycleClient.variable( 131 | 'json-array-variable', jsonDecode(encodedJsonArray)); 132 | setState(() { 133 | _jsonArrayValue = jsonEncode(jsonArrayVariable.value); 134 | }); 135 | jsonArrayVariable.onUpdate((updatedValue) { 136 | setState(() { 137 | _jsonArrayValue = jsonEncode(updatedValue); 138 | }); 139 | }); 140 | 141 | final jsonObjectVariable = await _devCycleClient.variable( 142 | 'json-object-variable', jsonDecode(encodedJsonObject)); 143 | setState(() { 144 | _jsonObjectValue = jsonEncode(jsonObjectVariable.value); 145 | }); 146 | jsonObjectVariable.onUpdate((updatedValue) { 147 | setState(() { 148 | _jsonObjectValue = jsonEncode(updatedValue); 149 | }); 150 | }); 151 | }); 152 | } 153 | 154 | void resetUser() { 155 | _devCycleClient.resetUser(); 156 | setState(() { 157 | _displayValue = 'User reset!'; 158 | }); 159 | } 160 | 161 | void identifyUser() { 162 | DevCycleUser testUser = 163 | DevCycleUserBuilder().userId('test_user_123').build(); 164 | _devCycleClient.identifyUser(testUser, ((err, variables) { 165 | if (err != null) { 166 | setState(() { 167 | _displayValue = err; 168 | }); 169 | } else { 170 | _logger.d(variables.values 171 | .map((variable) => "${variable.key}: ${variable.value}") 172 | .toString()); 173 | } 174 | })); 175 | setState(() { 176 | _displayValue = 'Identified user: \n${testUser.toString()}'; 177 | }); 178 | } 179 | 180 | void identifyAnonUser() { 181 | DevCycleUser anonUser = DevCycleUserBuilder().isAnonymous(true).build(); 182 | _devCycleClient.identifyUser(anonUser); 183 | setState(() { 184 | _displayValue = 'Identified user: \n${anonUser.toString()}'; 185 | }); 186 | } 187 | 188 | void trackEvent() { 189 | DevCycleEvent event = DevCycleEventBuilder() 190 | .target('target-str') 191 | .type('flutter-test') 192 | .value(10.0) 193 | .metaData({'custom_key': 'value'}).build(); 194 | _devCycleClient.track(event); 195 | setState(() { 196 | _displayValue = 'Tracked event: \n${event.toString()}'; 197 | }); 198 | } 199 | 200 | void showAllFeatures() async { 201 | Map features = await _devCycleClient.allFeatures(); 202 | setState(() { 203 | _displayValue = 'All features: \n${features.keys.toString()}'; 204 | }); 205 | } 206 | 207 | void flushEvents() { 208 | _devCycleClient.flushEvents(([error]) { 209 | setState(() { 210 | _displayValue = error ?? 'Flushed events'; 211 | }); 212 | }); 213 | } 214 | 215 | void showAllVariables() async { 216 | Map variables = await _devCycleClient.allVariables(); 217 | setState(() { 218 | _displayValue = 219 | 'All variables: \n${variables.values.map((variable) => "${variable.key}: ${variable.value}").toString()}'; 220 | }); 221 | } 222 | 223 | @override 224 | Widget build(BuildContext context) { 225 | return MaterialApp( 226 | home: Scaffold( 227 | appBar: AppBar( 228 | title: const Text('Plugin example app'), 229 | ), 230 | body: Center( 231 | child: Column( 232 | mainAxisAlignment: MainAxisAlignment.center, 233 | children: [ 234 | Text("Value: $_variableValue"), 235 | Text("Value: $_booleanValue"), 236 | Text("Value: $_integerValue"), 237 | Text("Value: $_doubleValue"), 238 | Text("Value: $_jsonArrayValue"), 239 | Text("Value: $_jsonObjectValue"), 240 | Text('Running on: $_platformVersion\n'), 241 | Icon( 242 | Icons.star, 243 | color: _booleanValue ? Colors.blue[500] : Colors.red[500], 244 | ), 245 | _booleanValue 246 | ? const Icon( 247 | Icons.sentiment_very_satisfied, 248 | color: Colors.grey, 249 | ) 250 | : const Icon( 251 | Icons.sentiment_very_dissatisfied, 252 | color: Colors.grey, 253 | ), 254 | ElevatedButton( 255 | onPressed: showAllFeatures, child: const Text('All Features')), 256 | ElevatedButton( 257 | onPressed: showAllVariables, 258 | child: const Text('All Variables')), 259 | ElevatedButton( 260 | onPressed: identifyUser, child: const Text('Identify User')), 261 | ElevatedButton( 262 | onPressed: identifyAnonUser, 263 | child: const Text('Identify Anonymous User')), 264 | ElevatedButton( 265 | onPressed: resetUser, child: const Text('Reset User')), 266 | ElevatedButton( 267 | onPressed: trackEvent, child: const Text('Track Event')), 268 | ElevatedButton( 269 | onPressed: flushEvents, child: const Text('FlushEvents')), 270 | Text(_displayValue) 271 | ], 272 | )), 273 | ), 274 | ); 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: devcycle_flutter_client_sdk_example 2 | description: Demonstrates how to use the devcycle_flutter_client_sdk plugin. 3 | version: 0.0.1 4 | 5 | # The following line prevents the package from being accidentally published to 6 | # pub.dev using `flutter pub publish`. This is preferred for private packages. 7 | publish_to: "none" # Remove this line if you wish to publish to pub.dev 8 | 9 | environment: 10 | sdk: ">=2.17.0 <3.0.0" 11 | 12 | # Dependencies specify other packages that your package needs in order to work. 13 | # To automatically upgrade your package dependencies to the latest versions 14 | # consider running `flutter pub upgrade --major-versions`. Alternatively, 15 | # dependencies can be manually updated by changing the version numbers below to 16 | # the latest version available on pub.dev. To see which dependencies have newer 17 | # versions available, run `flutter pub outdated`. 18 | dependencies: 19 | flutter: 20 | sdk: flutter 21 | 22 | devcycle_flutter_client_sdk: 23 | # When depending on this package from a real application you should use: 24 | # devcycle_flutter_client_sdk: ^x.y.z 25 | # See https://dart.dev/tools/pub/dependencies#version-constraints 26 | # The example app is bundled with the plugin so we use a path dependency on 27 | # the parent directory to use the current plugin's version. 28 | path: ../ 29 | 30 | # The following adds the Cupertino Icons font to your application. 31 | # Use with the CupertinoIcons class for iOS style icons. 32 | cupertino_icons: ^1.0.2 33 | logger: ^2.4.0 34 | 35 | dev_dependencies: 36 | flutter_test: 37 | sdk: flutter 38 | 39 | # The "flutter_lints" package below contains a set of recommended lints to 40 | # encourage good coding practices. The lint set provided by the package is 41 | # activated in the `analysis_options.yaml` file located at the root of your 42 | # package. See that file for information about deactivating specific lint 43 | # rules and activating additional ones. 44 | flutter_lints: ^4.0.0 45 | 46 | # For information on the generic Dart part of this file, see the 47 | # following page: https://dart.dev/tools/pub/pubspec 48 | 49 | # The following section is specific to Flutter packages. 50 | flutter: 51 | # The following line ensures that the Material Icons font is 52 | # included with your application, so that you can use the icons in 53 | # the material Icons class. 54 | uses-material-design: true 55 | 56 | # To add assets to your application, add an assets section, like this: 57 | # assets: 58 | # - images/a_dot_burr.jpeg 59 | # - images/a_dot_ham.jpeg 60 | 61 | # An image asset can refer to one or more resolution-specific "variants", see 62 | # https://flutter.dev/assets-and-images/#resolution-aware 63 | 64 | # For details regarding adding assets from package dependencies, see 65 | # https://flutter.dev/assets-and-images/#from-packages 66 | 67 | # To add custom fonts to your application, add a fonts section here, 68 | # in this "flutter" section. Each entry in this list should have a 69 | # "family" key with the font family name, and a "fonts" key with a 70 | # list giving the asset and other descriptors for the font. For 71 | # example: 72 | # fonts: 73 | # - family: Schyler 74 | # fonts: 75 | # - asset: fonts/Schyler-Regular.ttf 76 | # - asset: fonts/Schyler-Italic.ttf 77 | # style: italic 78 | # - family: Trajan Pro 79 | # fonts: 80 | # - asset: fonts/TrajanPro.ttf 81 | # - asset: fonts/TrajanPro_Bold.ttf 82 | # weight: 700 83 | # 84 | # For details regarding fonts from package dependencies, 85 | # see https://flutter.dev/custom-fonts/#from-packages 86 | -------------------------------------------------------------------------------- /example/test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility in the flutter_test package. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | import 'package:devcycle_flutter_client_sdk_example/main.dart'; 12 | 13 | void main() { 14 | testWidgets('Verify Platform version', (WidgetTester tester) async { 15 | // Build our app and trigger a frame. 16 | await tester.pumpWidget(const MyApp()); 17 | 18 | // Verify that platform version is retrieved. 19 | expect( 20 | find.byWidgetPredicate( 21 | (Widget widget) => 22 | widget is Text && widget.data!.startsWith('Running on:'), 23 | ), 24 | findsOneWidget, 25 | ); 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vagrant/ 3 | .sconsign.dblite 4 | .svn/ 5 | 6 | .DS_Store 7 | *.swp 8 | profile 9 | 10 | DerivedData/ 11 | build/ 12 | GeneratedPluginRegistrant.h 13 | GeneratedPluginRegistrant.m 14 | 15 | .generated/ 16 | 17 | *.pbxuser 18 | *.mode1v3 19 | *.mode2v3 20 | *.perspectivev3 21 | 22 | !default.pbxuser 23 | !default.mode1v3 24 | !default.mode2v3 25 | !default.perspectivev3 26 | 27 | xcuserdata 28 | 29 | *.moved-aside 30 | 31 | *.pyc 32 | *sync/ 33 | Icon? 34 | .tags* 35 | 36 | /Flutter/Generated.xcconfig 37 | /Flutter/ephemeral/ 38 | /Flutter/flutter_export_environment.sh -------------------------------------------------------------------------------- /ios/Assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevCycleHQ/flutter-client-sdk/aa2a4366da774db6bd354f4c09076cceabecba83/ios/Assets/.gitkeep -------------------------------------------------------------------------------- /ios/Classes/DevCycleFlutterClientSdkPlugin.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface DevCycleFlutterClientSdkPlugin : NSObject 4 | @end 5 | -------------------------------------------------------------------------------- /ios/Classes/DevCycleFlutterClientSdkPlugin.m: -------------------------------------------------------------------------------- 1 | #import "DevCycleFlutterClientSdkPlugin.h" 2 | #if __has_include() 3 | #import 4 | #else 5 | // Support project import fallback if the generated compatibility header 6 | // is not copied when this plugin is created as a library. 7 | // https://forums.swift.org/t/swift-static-libraries-dont-copy-generated-objective-c-header/19816 8 | #import "devcycle_flutter_client_sdk-Swift.h" 9 | #endif 10 | 11 | @implementation DevCycleFlutterClientSdkPlugin 12 | + (void)registerWithRegistrar:(NSObject*)registrar { 13 | [SwiftDevCycleFlutterClientSdkPlugin registerWithRegistrar:registrar]; 14 | } 15 | @end 16 | -------------------------------------------------------------------------------- /ios/Classes/SwiftDevCycleFlutterClientSdkPlugin.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | 4 | import DevCycle 5 | 6 | public class SwiftDevCycleFlutterClientSdkPlugin: NSObject, FlutterPlugin { 7 | private var dvcClient: DVCClient? 8 | private let channel: FlutterMethodChannel 9 | private var variableUpdates: [String: Any] = [:] 10 | private let numberTypes = ["int", "double"] 11 | 12 | private init(channel: FlutterMethodChannel) { 13 | self.channel = channel 14 | } 15 | 16 | public static func register(with registrar: FlutterPluginRegistrar) { 17 | let channel = FlutterMethodChannel(name: "devcycle_flutter_client_sdk", binaryMessenger: registrar.messenger()) 18 | let instance = SwiftDevCycleFlutterClientSdkPlugin(channel: channel) 19 | registrar.addMethodCallDelegate(instance, channel: channel) 20 | } 21 | 22 | public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { 23 | var user: DVCUser? 24 | let args = call.arguments as? [String: Any] 25 | let callbackId = args?["callbackId"] as? String 26 | var callbackArgs: [String:Any] = [:] 27 | 28 | if let userArg = args?["user"] as? [String: Any] { 29 | user = getUserFromDict(dict: userArg) 30 | } 31 | 32 | switch call.method { 33 | case "initializeDevCycle": 34 | let sdkKey = args?["sdkKey"] as? String 35 | let options = args?["options"] as? [String: Any] 36 | if let dvcUser = user, let dvcKey = sdkKey { 37 | self.dvcClient = try? DVCClient.builder() 38 | .sdkKey(dvcKey) 39 | .user(dvcUser) 40 | .options(getOptionsFromDict(dict: options ?? [:] )) 41 | .build(onInitialized: { error in 42 | callbackArgs["isInitialized"] = true 43 | if (error != nil) { 44 | callbackArgs["error"] = "\(String(describing: error))" 45 | callbackArgs["isInitialized"] = false 46 | } 47 | self.channel.invokeMethod("clientInitialized", arguments: callbackArgs) 48 | }) 49 | } 50 | result(nil) 51 | case "identifyUser": 52 | if let dvcUser = user { 53 | try? self.dvcClient?.identifyUser(user: dvcUser, callback: { error, variables in 54 | callbackArgs["callbackId"] = callbackId 55 | if (error != nil) { 56 | callbackArgs["error"] = "\(String(describing: error))" 57 | } else { 58 | callbackArgs["variables"] = self.variablesToMap(variables: variables ?? [:]) 59 | } 60 | self.channel.invokeMethod("userIdentified", arguments: callbackArgs) 61 | }) 62 | } 63 | result(nil) 64 | case "resetUser": 65 | try? self.dvcClient?.resetUser(callback: { error, variables in 66 | callbackArgs["callbackId"] = callbackId 67 | if (error != nil) { 68 | callbackArgs["error"] = "\(String(describing: error))" 69 | } else { 70 | callbackArgs["variables"] = self.variablesToMap(variables: variables ?? [:]) 71 | } 72 | self.channel.invokeMethod("userReset", arguments: callbackArgs) 73 | }) 74 | result(nil) 75 | case "allFeatures": 76 | result(featuresToMap(features: self.dvcClient?.allFeatures() ?? [:])) 77 | case "allVariables": 78 | result(variablesToMap(variables: self.dvcClient?.allVariables() ?? [:])) 79 | case "getPlatformVersion": 80 | result("iOS " + UIDevice.current.systemVersion) 81 | case "variable": 82 | return getResultForVariable(args: args, callbackArgs: callbackArgs, result: result) 83 | case "track": 84 | if let event = args?["event"] as? [String: Any], let dvcEvent = getEventFromDict(dict: event) { 85 | self.dvcClient?.track(dvcEvent) 86 | } 87 | case "flushEvents": 88 | self.dvcClient?.flushEvents(callback: { error in 89 | callbackArgs["callbackId"] = callbackId 90 | if (error != nil) { 91 | callbackArgs["error"] = "\(String(describing: error))" 92 | } 93 | self.channel.invokeMethod("eventsFlushed", arguments: callbackArgs) 94 | }) 95 | result(nil) 96 | default: 97 | result(FlutterMethodNotImplemented) 98 | } 99 | } 100 | 101 | private func getResultForVariable(args: [String: Any]?, callbackArgs: [String:Any], result: @escaping FlutterResult) { 102 | guard 103 | let dvcClient = self.dvcClient, 104 | let varKey = args?["key"] as? String, 105 | let varDefaultValue = args?["defaultValue"], 106 | let type = args?["type"] as? String 107 | else { 108 | result(nil) 109 | return 110 | } 111 | 112 | if type == "String", let stringDefault = varDefaultValue as? String { 113 | let variable = dvcClient.variable(key: varKey, defaultValue: stringDefault) 114 | storeVariableInMemory(key: varKey, variable: variable, callbackArgs: callbackArgs) 115 | let codecVariable = dvcVariableToMap(variable: variable as DVCVariable) 116 | result(codecVariable) 117 | } else if type.contains("Map"), let jsonDefault = varDefaultValue as? Dictionary { 118 | let variable = dvcClient.variable(key: varKey, defaultValue: jsonDefault) 119 | storeVariableInMemory(key: varKey, variable: variable, callbackArgs: callbackArgs) 120 | let codecVariable = dvcVariableToMap(variable: variable as DVCVariable>) 121 | result(codecVariable) 122 | } else if numberTypes.contains(type), let numDefault = varDefaultValue as? Double { 123 | let variable = dvcClient.variable(key: varKey, defaultValue: numDefault) 124 | storeVariableInMemory(key: varKey, variable: variable, callbackArgs: callbackArgs) 125 | let codecVariable = dvcVariableToMap(variable: variable as DVCVariable) 126 | result(codecVariable) 127 | } else if type == "bool", let boolDefault = varDefaultValue as? Bool { 128 | let variable = dvcClient.variable(key: varKey, defaultValue: boolDefault) 129 | storeVariableInMemory(key: varKey, variable: variable, callbackArgs: callbackArgs) 130 | let codecVariable = dvcVariableToMap(variable: variable) 131 | result(codecVariable) 132 | } else { 133 | let variable = dvcClient.variable(key: varKey, defaultValue: varDefaultValue) 134 | result(dvcVariableToMap(variable: variable)) 135 | } 136 | } 137 | 138 | private func storeVariableInMemory(key: String, variable: DVCVariable, callbackArgs: [String:Any]) { 139 | if !self.variableUpdates.contains(where: { $0.key == key }) { 140 | variable.onUpdate(handler: { newValue in 141 | var newCallbackArgs = callbackArgs 142 | newCallbackArgs["key"] = variable.key 143 | newCallbackArgs["value"] = variable.value 144 | self.channel.invokeMethod("variableUpdated", arguments: callbackArgs) 145 | }) 146 | self.variableUpdates[variable.key] = variable 147 | } 148 | } 149 | 150 | private func getUserFromDict(dict: [String: Any?]) -> DVCUser? { 151 | let userBuilder = DVCUser.builder() 152 | 153 | if let userId = dict["userId"] as? String { 154 | userBuilder.userId(userId) 155 | } 156 | 157 | if let isAnonymous = dict["isAnonymous"] as? Bool { 158 | userBuilder.isAnonymous(isAnonymous) 159 | } 160 | 161 | if let email = dict["email"] as? String { 162 | userBuilder.email(email) 163 | } 164 | 165 | if let name = dict["name"] as? String { 166 | userBuilder.name(name) 167 | } 168 | 169 | if let country = dict["country"] as? String { 170 | userBuilder.country(country) 171 | } 172 | 173 | if let customData = dict["customData"] as? [String: Any] { 174 | userBuilder.customData(customData) 175 | } 176 | 177 | if let privateCustomData = dict["privateCustomData"] as? [String: Any] { 178 | userBuilder.privateCustomData(privateCustomData) 179 | } 180 | 181 | let user = try? userBuilder.build() 182 | return user 183 | } 184 | 185 | private func getOptionsFromDict(dict: [String: Any?]) -> DevCycleOptions { 186 | let optionsBuilder = DevCycleOptions.builder() 187 | 188 | if let flushEventsIntervalMs = dict["flushEventsIntervalMs"] as? Int { 189 | optionsBuilder.flushEventsIntervalMs(flushEventsIntervalMs) 190 | } 191 | 192 | if let disableEventLogging = dict["disableEventLogging"] as? Bool { 193 | optionsBuilder.disableEventLogging(disableEventLogging) 194 | } 195 | 196 | if let disableAutomaticEventLogging = dict["disableAutomaticEventLogging"] as? Bool { 197 | optionsBuilder.disableAutomaticEventLogging(disableAutomaticEventLogging) 198 | } 199 | 200 | if let disableCustomEventLogging = dict["disableCustomEventLogging"] as? Bool { 201 | optionsBuilder.disableCustomEventLogging(disableCustomEventLogging) 202 | } 203 | 204 | if let enableEdgeDB = dict["enableEdgeDB"] as? Bool { 205 | optionsBuilder.enableEdgeDB(enableEdgeDB) 206 | } 207 | 208 | if let configCacheTTL = dict["configCacheTTL"] as? Int { 209 | optionsBuilder.configCacheTTL(configCacheTTL) 210 | } 211 | 212 | if let disableConfigCache = dict["disableConfigCache"] as? Bool { 213 | optionsBuilder.disableConfigCache(disableConfigCache) 214 | } 215 | 216 | if let disableRealtimeUpdates = dict["disableRealtimeUpdates"] as? Bool { 217 | optionsBuilder.disableRealtimeUpdates(disableRealtimeUpdates) 218 | } 219 | 220 | let logLevelMap = [ 221 | "debug": LogLevel.debug, 222 | "info": LogLevel.info, 223 | "warn": LogLevel.warn, 224 | "error": LogLevel.error 225 | ] 226 | 227 | if let codecLogLevel = dict["logLevel"] as? String, let logLevel = logLevelMap[codecLogLevel] { 228 | optionsBuilder.logLevel(logLevel) 229 | } 230 | 231 | if let apiProxyUrl = dict["apiProxyUrl"] as? String { 232 | optionsBuilder.apiProxyURL(apiProxyUrl) 233 | } 234 | 235 | if let eventsApiProxyUrl = dict["eventsApiProxyUrl"] as? String { 236 | optionsBuilder.eventsApiProxyURL(eventsApiProxyUrl) 237 | } 238 | 239 | let options = optionsBuilder.build() 240 | return options 241 | } 242 | 243 | private func getEventFromDict(dict: [String: Any?]) -> DVCEvent? { 244 | let eventBuilder = DVCEvent.builder() 245 | 246 | if let type = dict["type"] as? String { 247 | eventBuilder.type(type) 248 | } 249 | 250 | if let target = dict["target"] as? String { 251 | eventBuilder.target(target) 252 | } 253 | 254 | let dateFormatter = ISO8601DateFormatter() 255 | dateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] 256 | if let dateString = dict["date"] as? String, let date = dateFormatter.date(from: dateString) { 257 | eventBuilder.clientDate(date) 258 | } 259 | 260 | if let value = dict["value"] as? String, let doubleValue = Double(value) { 261 | eventBuilder.value(doubleValue) 262 | } 263 | 264 | if let metaData = dict["metaData"] as? [String: Any] { 265 | eventBuilder.metaData(metaData) 266 | } 267 | 268 | let event = try? eventBuilder.build() 269 | return event 270 | } 271 | 272 | private func variablesToMap(variables: [String: Variable]) -> [String: Any] { 273 | var map: [String: Any] = [:] 274 | for (key, value) in variables { 275 | map[key] = variableToMap(variable: value) 276 | } 277 | return map 278 | } 279 | 280 | private func variableToMap(variable: Variable) -> [String: Any] { 281 | var map: [String: Any] = [:] 282 | map["id"] = variable._id 283 | map["key"] = variable.key 284 | map["type"] = variable.type.rawValue 285 | map["value"] = variable.value 286 | map["evalReason"] = variable.evalReason 287 | return map 288 | } 289 | 290 | private func dvcVariableToMap(variable: DVCVariable) -> [String: Any] { 291 | var map: [String: Any] = [:] 292 | map["key"] = variable.key 293 | map["type"] = variable.type?.rawValue 294 | map["value"] = variable.value 295 | map["evalReason"] = variable.evalReason 296 | map["isDefaulted"] = variable.isDefaulted 297 | return map 298 | } 299 | 300 | private func featuresToMap(features: [String: Feature]) -> [String: Any] { 301 | var map: [String: Any] = [:] 302 | for (key, value) in features { 303 | map[key] = featureToMap(feature: value) 304 | } 305 | return map 306 | } 307 | 308 | private func featureToMap(feature: Feature) -> [String: String] { 309 | var map: [String: String] = [:] 310 | map["id"] = feature._id 311 | map["key"] = feature.key 312 | map["type"] = feature.type 313 | map["variation"] = feature._variation 314 | map["evalReason"] = feature.evalReason 315 | map["variationKey"] = feature.variationKey 316 | map["variationName"] = feature.variationName 317 | return map 318 | } 319 | } 320 | -------------------------------------------------------------------------------- /ios/devcycle_flutter_client_sdk.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. 3 | # Run `pod lib lint devcycle_flutter_client_sdk.podspec` to validate before publishing. 4 | # 5 | Pod::Spec.new do |s| 6 | s.name = 'devcycle_flutter_client_sdk' 7 | s.version = '1.10.0' 8 | s.summary = 'DevCycle Flutter Client SDK plugin project.' 9 | s.description = <<-DESC 10 | A Flutter plugin to integrate with DevCycle Feature Flags. 11 | DESC 12 | s.homepage = 'https://devcycle.com/' 13 | s.license = { :file => '../LICENSE' } 14 | s.author = { 'DevCycle' => 'support@devcycle.com' } 15 | s.source = { :path => '.' } 16 | s.source_files = 'Classes/**/*' 17 | s.dependency 'Flutter' 18 | s.dependency 'DevCycle', '1.20.0' 19 | s.platform = :ios, '12.0' 20 | 21 | # Flutter.framework does not contain a i386 slice. 22 | s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } 23 | s.swift_version = '5.0' 24 | end 25 | -------------------------------------------------------------------------------- /lib/devcycle_event.dart: -------------------------------------------------------------------------------- 1 | class DevCycleEvent { 2 | final String? type; 3 | final String? target; 4 | final double? value; 5 | final DateTime? date; 6 | final Map? metaData; 7 | 8 | DevCycleEvent._builder(DevCycleEventBuilder builder) 9 | : type = builder._type, 10 | target = builder._target, 11 | value = builder._value, 12 | date = builder._date, 13 | metaData = builder._metaData; 14 | 15 | Map toCodec() { 16 | final Map result = {}; 17 | result['type'] = type; 18 | result['target'] = target; 19 | result['value'] = value.toString(); 20 | result['date'] = date?.toIso8601String(); 21 | result['metaData'] = metaData; 22 | return result; 23 | } 24 | 25 | @override 26 | String toString() { 27 | return 'DVCEvent{type: $type, target: $target, value: $value, date: $date, metaData: $metaData}'; 28 | } 29 | } 30 | 31 | @Deprecated('Use DevCycleEvent instead') 32 | typedef DVCEvent = DevCycleEvent; 33 | 34 | /// A builder for constructing [DevCycleEvent] objects. 35 | class DevCycleEventBuilder { 36 | String? _type; 37 | String? _target; 38 | double? _value; 39 | DateTime? _date; 40 | Map? _metaData; 41 | 42 | /// Custom event type 43 | DevCycleEventBuilder type(String type) { 44 | _type = type; 45 | return this; 46 | } 47 | 48 | /// Custom event target / subject of event. Contextual to event type 49 | DevCycleEventBuilder target(String target) { 50 | _target = target; 51 | return this; 52 | } 53 | 54 | /// Sets the user's email attribute. 55 | DevCycleEventBuilder value(double value) { 56 | _value = value; 57 | return this; 58 | } 59 | 60 | DevCycleEventBuilder date(DateTime date) { 61 | _date = date; 62 | return this; 63 | } 64 | 65 | DevCycleEventBuilder metaData(Map metaData) { 66 | _metaData = metaData; 67 | return this; 68 | } 69 | 70 | /// Constructs a [DevCycleEvent] instance from the values currently in the builder. 71 | DevCycleEvent build() { 72 | return DevCycleEvent._builder(this); 73 | } 74 | } 75 | 76 | @Deprecated('Use DevCycleEventBuilder instead') 77 | typedef DVCEventBuilder = DevCycleEventBuilder; -------------------------------------------------------------------------------- /lib/devcycle_flutter_client_sdk.dart: -------------------------------------------------------------------------------- 1 | import 'package:devcycle_flutter_client_sdk/devcycle_event.dart'; 2 | import 'package:flutter/services.dart'; 3 | import 'package:uuid/uuid.dart'; 4 | import 'package:logger/logger.dart'; 5 | import 'dart:async'; 6 | 7 | import 'devcycle_flutter_client_sdk_platform_interface.dart'; 8 | import 'devcycle_user.dart'; 9 | import 'devcycle_options.dart'; 10 | import 'dvc_variable.dart'; 11 | import 'dvc_feature.dart'; 12 | 13 | export 'devcycle_user.dart'; 14 | export 'devcycle_event.dart'; 15 | export 'devcycle_options.dart'; 16 | export 'dvc_feature.dart'; 17 | export 'dvc_variable.dart'; 18 | 19 | typedef ErrorCallback = void Function([String? error]); 20 | typedef VariablesCallback = void Function( 21 | String? error, Map variables); 22 | 23 | class DevCycleClient { 24 | static const _methodChannel = MethodChannel('devcycle_flutter_client_sdk'); 25 | 26 | static const _uuid = Uuid(); 27 | static final _logger = Logger(); 28 | 29 | Future? _clientReady; 30 | 31 | bool _isInitialized = false; 32 | Completer? _initializationCompleter; 33 | 34 | /// Callback triggered on client initialization 35 | ErrorCallback? _clientInitializedCallback; 36 | // Map of variable keys to a list of variable objects, used to update variable values 37 | final Map> _variableInstances = {}; 38 | // Map of callback IDs to user update callbacks. The ID is generated when identify is called 39 | final Map _identifyCallbacks = {}; 40 | // Map of callback IDs to user update callbacks. The ID is generated when reset is called 41 | final Map _resetCallbacks = {}; 42 | // Map of callback IDs to user update callbacks. The ID is generated when flushEvents is called 43 | final Map _eventCallbacks = {}; 44 | 45 | DevCycleClient._builder(DevCycleClientBuilder builder); 46 | 47 | _init(String sdkKey, DevCycleUser user, DevCycleOptions? options) { 48 | _methodChannel.setMethodCallHandler(_handleCallbacks); 49 | _clientReady = DevCycleFlutterClientSdkPlatform.instance 50 | .initializeDevCycle(sdkKey, user, options); 51 | } 52 | 53 | _trackVariable(DVCVariable? variable) { 54 | if (variable?.key == null) return; 55 | String key = variable?.key as String; 56 | _variableInstances[key] = _variableInstances[key] ?? []; 57 | _variableInstances[key]?.add(variable!); 58 | } 59 | 60 | Future _handleCallbacks(MethodCall call) async { 61 | final error = call.arguments['error']?.toString(); 62 | if (error != null) { 63 | _logger.e(error); 64 | } 65 | 66 | switch (call.method) { 67 | case 'clientInitialized': 68 | _isInitialized = call.arguments['isInitialized']; 69 | if (_clientInitializedCallback != null) { 70 | _clientInitializedCallback!(error); 71 | } 72 | // Complete the initialization 73 | if (_isInitialized) { 74 | _initializationCompleter?.complete(); 75 | } 76 | break; 77 | case 'variableUpdated': 78 | final key = call.arguments['key']; 79 | final updatedValue = call.arguments['value']; 80 | List variableInstances = _variableInstances[key] ?? []; 81 | for (final v in variableInstances) { 82 | if (v.callback != null) { 83 | v.value = updatedValue; 84 | v.callback!(updatedValue); 85 | } 86 | } 87 | break; 88 | case 'userIdentified': 89 | VariablesCallback? callback = 90 | _identifyCallbacks[call.arguments['callbackId']]; 91 | if (callback == null) { 92 | return; 93 | } 94 | if (error != null) { 95 | callback(error, {}); 96 | _identifyCallbacks.remove(call.arguments['callbackId']); 97 | return; 98 | } 99 | 100 | Map parsedVariables = {}; 101 | Map> variables = 102 | call.arguments['variables']; 103 | for (final entry in variables.entries) { 104 | parsedVariables[entry.key] = DVCVariable.fromCodec(entry.value); 105 | } 106 | callback(null, parsedVariables); 107 | _identifyCallbacks.remove(call.arguments['callbackId']); 108 | break; 109 | case 'userReset': 110 | VariablesCallback? callback = 111 | _resetCallbacks[call.arguments['callbackId']]; 112 | if (callback == null) { 113 | return; 114 | } 115 | if (error != null) { 116 | callback(error, {}); 117 | _resetCallbacks.remove(call.arguments['callbackId']); 118 | return; 119 | } 120 | Map parsedVariables = {}; 121 | Map> variables = 122 | call.arguments['variables']; 123 | for (final entry in variables.entries) { 124 | parsedVariables[entry.key] = DVCVariable.fromCodec(entry.value); 125 | } 126 | callback(null, parsedVariables); 127 | _resetCallbacks.remove(call.arguments['callbackId']); 128 | break; 129 | case 'eventsFlushed': 130 | ErrorCallback? callback = _eventCallbacks[call.arguments['callbackId']]; 131 | if (callback == null) { 132 | return; 133 | } 134 | callback(error); 135 | _eventCallbacks.remove(call.arguments['callbackId']); 136 | break; 137 | } 138 | } 139 | 140 | Future onInitialized(ErrorCallback callback) async { 141 | _clientInitializedCallback = callback; 142 | _initializationCompleter = Completer(); 143 | 144 | // Wait for the initialization to complete 145 | await _initializationCompleter?.future; 146 | 147 | return this; 148 | } 149 | 150 | Future getPlatformVersion() async { 151 | await _clientReady; 152 | return DevCycleFlutterClientSdkPlatform.instance.getPlatformVersion(); 153 | } 154 | 155 | Future identifyUser(DevCycleUser user, 156 | [VariablesCallback? callback]) async { 157 | await _clientReady; 158 | if (callback != null) { 159 | String callbackId = _uuid.v4(); 160 | _identifyCallbacks[callbackId] = callback; 161 | DevCycleFlutterClientSdkPlatform.instance.identifyUser(user, callbackId); 162 | } else { 163 | DevCycleFlutterClientSdkPlatform.instance.identifyUser(user); 164 | } 165 | } 166 | 167 | Future resetUser([VariablesCallback? callback]) async { 168 | await _clientReady; 169 | if (callback != null) { 170 | String callbackId = _uuid.v4(); 171 | _resetCallbacks[callbackId] = callback; 172 | DevCycleFlutterClientSdkPlatform.instance.resetUser(callbackId); 173 | } else { 174 | DevCycleFlutterClientSdkPlatform.instance.resetUser(); 175 | } 176 | } 177 | 178 | Future variableValue(String key, dynamic defaultValue) async { 179 | final variable = await this.variable(key, defaultValue); 180 | return variable.value; 181 | } 182 | 183 | Future variable(String key, dynamic defaultValue) async { 184 | await _clientReady; 185 | final variable = await DevCycleFlutterClientSdkPlatform.instance 186 | .variable(key, defaultValue); 187 | _trackVariable(variable); 188 | return variable; 189 | } 190 | 191 | Future> allFeatures() async { 192 | await _clientReady; 193 | final features = 194 | await DevCycleFlutterClientSdkPlatform.instance.allFeatures(); 195 | return features; 196 | } 197 | 198 | Future> allVariables() async { 199 | await _clientReady; 200 | final variables = 201 | await DevCycleFlutterClientSdkPlatform.instance.allVariables(); 202 | variables.values.forEach(_trackVariable); 203 | return variables; 204 | } 205 | 206 | Future track(DevCycleEvent event) async { 207 | await _clientReady; 208 | DevCycleFlutterClientSdkPlatform.instance.track(event); 209 | } 210 | 211 | Future flushEvents([ErrorCallback? callback]) async { 212 | await _clientReady; 213 | if (callback != null) { 214 | String callbackId = _uuid.v4(); 215 | _eventCallbacks[callbackId] = callback; 216 | DevCycleFlutterClientSdkPlatform.instance.flushEvents(callbackId); 217 | } else { 218 | DevCycleFlutterClientSdkPlatform.instance.flushEvents(); 219 | } 220 | } 221 | } 222 | 223 | @Deprecated('Use DevCycleClient instead') 224 | typedef DVCClient = DevCycleClient; 225 | 226 | class DevCycleClientBuilder { 227 | String? _sdkKey; 228 | DevCycleUser? _user; 229 | DevCycleOptions? _options; 230 | 231 | DevCycleClientBuilder sdkKey(String sdkKey) { 232 | _sdkKey = sdkKey; 233 | return this; 234 | } 235 | 236 | @Deprecated("Use sdkKey() method") 237 | DevCycleClientBuilder environmentKey(String environmentKey) { 238 | _sdkKey = environmentKey; 239 | return this; 240 | } 241 | 242 | DevCycleClientBuilder user(DevCycleUser user) { 243 | _user = user; 244 | return this; 245 | } 246 | 247 | DevCycleClientBuilder options(DevCycleOptions options) { 248 | _options = options; 249 | return this; 250 | } 251 | 252 | DevCycleClient build() { 253 | if (_sdkKey == null) throw Exception("SDK key must be set"); 254 | if (_user == null) throw Exception("User must be set"); 255 | DevCycleClient client = DevCycleClient._builder(this); 256 | client._init(_sdkKey!, _user!, _options); 257 | return client; 258 | } 259 | } 260 | 261 | @Deprecated('Use DevCycleClientBuilder instead') 262 | typedef DVCClientBuilder = DevCycleClientBuilder; 263 | -------------------------------------------------------------------------------- /lib/devcycle_flutter_client_sdk_method_channel.dart: -------------------------------------------------------------------------------- 1 | import 'package:devcycle_flutter_client_sdk/devcycle_flutter_client_sdk.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | import 'package:flutter/services.dart'; 4 | 5 | import 'devcycle_flutter_client_sdk_platform_interface.dart'; 6 | 7 | /// An implementation of [DevCycleFlutterClientSdkPlatform] that uses method channels. 8 | class MethodChannelDevCycleFlutterClientSdk 9 | extends DevCycleFlutterClientSdkPlatform { 10 | /// The method channel used to interact with the native platform. 11 | @visibleForTesting 12 | final methodChannel = const MethodChannel('devcycle_flutter_client_sdk'); 13 | 14 | @override 15 | Future getPlatformVersion() async { 16 | final version = 17 | await methodChannel.invokeMethod('getPlatformVersion'); 18 | return version; 19 | } 20 | 21 | @override 22 | Future initializeDevCycle( 23 | String sdkKey, DevCycleUser user, DevCycleOptions? options) async { 24 | Map codecUser = user.toCodec(); 25 | Map? codecOptions = options?.toCodec(); 26 | await methodChannel.invokeMethod('initializeDevCycle', 27 | {"sdkKey": sdkKey, "user": codecUser, "options": codecOptions}); 28 | } 29 | 30 | @override 31 | void identifyUser(DevCycleUser user, [String? callbackId]) { 32 | Map codecUser = user.toCodec(); 33 | methodChannel.invokeMethod( 34 | 'identifyUser', {"user": codecUser, "callbackId": callbackId}); 35 | } 36 | 37 | @override 38 | void resetUser([String? callbackId]) { 39 | methodChannel.invokeMethod('resetUser', {"callbackId": callbackId}); 40 | } 41 | 42 | @override 43 | Future variable(String key, dynamic defaultValue) async { 44 | final result = await methodChannel.invokeMethod('variable', { 45 | "key": key, 46 | "defaultValue": defaultValue, 47 | "type": defaultValue.runtimeType.toString() 48 | }) ?? 49 | {}; 50 | final map = Map.from(result); 51 | return map.isNotEmpty 52 | ? DVCVariable.fromCodec(map) 53 | : DVCVariable.fromDefault(key, defaultValue); 54 | } 55 | 56 | @override 57 | Future> allFeatures() async { 58 | final result = await methodChannel.invokeMethod('allFeatures') ?? {}; 59 | final map = Map.from(result); 60 | Map features = {}; 61 | 62 | map.forEach((key, value) { 63 | final codec = Map.from(value); 64 | features[key] = DVCFeature.fromCodec(codec); 65 | }); 66 | return features; 67 | } 68 | 69 | @override 70 | Future> allVariables() async { 71 | final result = await methodChannel.invokeMethod('allVariables') ?? {}; 72 | final map = Map.from(result); 73 | Map variables = {}; 74 | 75 | map.forEach((key, value) { 76 | final codec = Map.from(value); 77 | variables[key] = DVCVariable.fromCodec(codec); 78 | }); 79 | return variables; 80 | } 81 | 82 | @override 83 | void track(DevCycleEvent event) { 84 | Map codecEvent = event.toCodec(); 85 | methodChannel.invokeMethod('track', {"event": codecEvent}); 86 | } 87 | 88 | @override 89 | void flushEvents([String? callbackId]) { 90 | methodChannel.invokeMethod('flushEvents', {"callbackId": callbackId}); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /lib/devcycle_flutter_client_sdk_platform_interface.dart: -------------------------------------------------------------------------------- 1 | import 'package:devcycle_flutter_client_sdk/devcycle_flutter_client_sdk.dart'; 2 | import 'package:plugin_platform_interface/plugin_platform_interface.dart'; 3 | 4 | import 'devcycle_flutter_client_sdk_method_channel.dart'; 5 | 6 | abstract class DevCycleFlutterClientSdkPlatform extends PlatformInterface { 7 | /// Constructs a DevCycleFlutterClientSdkPlatform. 8 | DevCycleFlutterClientSdkPlatform() : super(token: _token); 9 | 10 | static final Object _token = Object(); 11 | 12 | static DevCycleFlutterClientSdkPlatform _instance = 13 | MethodChannelDevCycleFlutterClientSdk(); 14 | 15 | /// The default instance of [DevCycleFlutterClientSdkPlatform] to use. 16 | /// 17 | /// Defaults to [MethodChannelDevCycleFlutterClientSdk]. 18 | static DevCycleFlutterClientSdkPlatform get instance => _instance; 19 | 20 | /// Platform-specific implementations should set this with their own 21 | /// platform-specific class that extends [DevCycleFlutterClientSdkPlatform] when 22 | /// they register themselves. 23 | static set instance(DevCycleFlutterClientSdkPlatform instance) { 24 | PlatformInterface.verifyToken(instance, _token); 25 | _instance = instance; 26 | } 27 | 28 | Future getPlatformVersion() { 29 | throw UnimplementedError('platformVersion() has not been implemented.'); 30 | } 31 | 32 | Future initializeDevCycle( 33 | String sdkKey, 34 | DevCycleUser user, 35 | DevCycleOptions? options 36 | ) { 37 | throw UnimplementedError('initializeDevCycle() has not been implemented.'); 38 | } 39 | 40 | void identifyUser(DevCycleUser user, [String? callbackId]) { 41 | throw UnimplementedError('identifyUser() has not been implemented.'); 42 | } 43 | 44 | void resetUser([String? callbackId]) { 45 | throw UnimplementedError('resetUser() has not been implemented.'); 46 | } 47 | 48 | Future variable(String key, dynamic defaultValue) { 49 | throw UnimplementedError('variable() has not been implemented.'); 50 | } 51 | 52 | Future> allFeatures() { 53 | throw UnimplementedError('allFeatures() has not been implemented.'); 54 | } 55 | 56 | Future> allVariables() { 57 | throw UnimplementedError('allVariables() has not been implemented.'); 58 | } 59 | 60 | void track(DevCycleEvent event) { 61 | throw UnimplementedError('track() has not been implemented.'); 62 | } 63 | 64 | void flushEvents([String? callbackId]) { 65 | throw UnimplementedError('flushEvents() has not been implemented.'); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /lib/devcycle_options.dart: -------------------------------------------------------------------------------- 1 | enum LogLevel { debug, info, warn, error } 2 | 3 | class DevCycleOptions { 4 | final int? flushEventsIntervalMs; 5 | final bool? disableEventLogging; 6 | final bool? disableCustomEventLogging; 7 | final bool? disableAutomaticEventLogging; 8 | final bool? enableEdgeDB; 9 | final int? configCacheTTL; 10 | final bool? disableConfigCache; 11 | final bool? disableRealtimeUpdates; 12 | final LogLevel? logLevel; 13 | final String? apiProxyUrl; 14 | final String? eventsApiProxyUrl; 15 | 16 | DevCycleOptions._builder(DevCycleOptionsBuilder builder) 17 | : flushEventsIntervalMs = builder._flushEventsIntervalMs, 18 | disableEventLogging = builder._disableEventLogging, 19 | disableCustomEventLogging = builder._disableCustomEventLogging, 20 | disableAutomaticEventLogging = builder._disableAutomaticEventLogging, 21 | enableEdgeDB = builder._enableEdgeDB, 22 | configCacheTTL = builder._configCacheTTL, 23 | disableConfigCache = builder._disableConfigCache, 24 | disableRealtimeUpdates = builder._disableRealtimeUpdates, 25 | logLevel = builder._logLevel, 26 | apiProxyUrl = builder._apiProxyUrl, 27 | eventsApiProxyUrl = builder._eventsApiProxyUrl; 28 | 29 | Map toCodec() { 30 | final Map result = {}; 31 | if (flushEventsIntervalMs != null) { 32 | result['flushEventsIntervalMs'] = flushEventsIntervalMs; 33 | } 34 | if (disableEventLogging != null) { 35 | result['disableEventLogging'] = disableEventLogging; 36 | } 37 | if (disableCustomEventLogging != null) { 38 | result['disableCustomEventLogging'] = disableCustomEventLogging; 39 | } 40 | if (disableAutomaticEventLogging != null) { 41 | result['disableAutomaticEventLogging'] = disableAutomaticEventLogging; 42 | } 43 | if (enableEdgeDB != null) result['enableEdgeDB'] = enableEdgeDB; 44 | if (configCacheTTL != null) result['configCacheTTL'] = configCacheTTL; 45 | if (disableConfigCache != null) { 46 | result['disableConfigCache'] = disableConfigCache; 47 | } 48 | if (disableRealtimeUpdates != null) { 49 | result['disableRealtimeUpdates'] = disableRealtimeUpdates; 50 | } 51 | if (logLevel != null) { 52 | result['logLevel'] = logLevel?.toString().split('.').last; 53 | } 54 | if (apiProxyUrl != null) { 55 | result['apiProxyUrl'] = apiProxyUrl; 56 | } 57 | if (eventsApiProxyUrl != null) { 58 | result['eventsApiProxyUrl'] = eventsApiProxyUrl; 59 | } 60 | return result; 61 | } 62 | } 63 | 64 | @Deprecated('Use DevCycleOptions instead') 65 | typedef DVCOptions = DevCycleOptions; 66 | 67 | /// A builder for constructing [DevCycleOptions] objects. 68 | class DevCycleOptionsBuilder { 69 | int? _flushEventsIntervalMs; 70 | bool? _disableEventLogging; 71 | bool? _disableCustomEventLogging; 72 | bool? _disableAutomaticEventLogging; 73 | bool? _enableEdgeDB; 74 | int? _configCacheTTL; 75 | bool? _disableConfigCache; 76 | bool? _disableRealtimeUpdates; 77 | LogLevel? _logLevel; 78 | String? _apiProxyUrl; 79 | String? _eventsApiProxyUrl; 80 | 81 | DevCycleOptionsBuilder flushEventsIntervalMs(int flushEventsIntervalMs) { 82 | _flushEventsIntervalMs = flushEventsIntervalMs; 83 | return this; 84 | } 85 | 86 | @Deprecated( 87 | 'Use disableCustomEventLogging and disableAutomaticEventLogging instead') 88 | DevCycleOptionsBuilder disableEventLogging(bool disableEventLogging) { 89 | _disableEventLogging = disableEventLogging; 90 | return this; 91 | } 92 | 93 | DevCycleOptionsBuilder disableCustomEventLogging( 94 | bool disableCustomEventLogging) { 95 | _disableCustomEventLogging = disableCustomEventLogging; 96 | return this; 97 | } 98 | 99 | DevCycleOptionsBuilder disableAutomaticEventLogging( 100 | bool disableAutomaticEventLogging) { 101 | _disableAutomaticEventLogging = disableAutomaticEventLogging; 102 | return this; 103 | } 104 | 105 | DevCycleOptionsBuilder enableEdgeDB(bool enableEdgeDB) { 106 | _enableEdgeDB = enableEdgeDB; 107 | return this; 108 | } 109 | 110 | DevCycleOptionsBuilder configCacheTTL(int configCacheTTL) { 111 | _configCacheTTL = configCacheTTL; 112 | return this; 113 | } 114 | 115 | DevCycleOptionsBuilder disableConfigCache(bool disableConfigCache) { 116 | _disableConfigCache = disableConfigCache; 117 | return this; 118 | } 119 | 120 | DevCycleOptionsBuilder disableRealtimeUpdates(bool disableRealtimeUpdates) { 121 | _disableRealtimeUpdates = disableRealtimeUpdates; 122 | return this; 123 | } 124 | 125 | DevCycleOptionsBuilder logLevel(LogLevel logLevel) { 126 | _logLevel = logLevel; 127 | return this; 128 | } 129 | 130 | DevCycleOptionsBuilder apiProxyUrl(String apiProxyUrl) { 131 | _apiProxyUrl = apiProxyUrl; 132 | return this; 133 | } 134 | 135 | DevCycleOptionsBuilder eventsApiProxyUrl(String eventsApiProxyUrl) { 136 | _eventsApiProxyUrl = eventsApiProxyUrl; 137 | return this; 138 | } 139 | 140 | /// Constructs a [DevCycleOptions] instance from the values currently in the builder. 141 | DevCycleOptions build() { 142 | return DevCycleOptions._builder(this); 143 | } 144 | } 145 | 146 | @Deprecated('Use DevCycleOptionsBuilder instead') 147 | typedef DVCOptionsBuilder = DevCycleOptionsBuilder; 148 | -------------------------------------------------------------------------------- /lib/devcycle_user.dart: -------------------------------------------------------------------------------- 1 | class DevCycleUser { 2 | final String? userId; 3 | final bool? isAnonymous; 4 | final String? email; 5 | final String? name; 6 | final String? country; 7 | final Map? customData; 8 | final Map? privateCustomData; 9 | 10 | DevCycleUser._builder(DevCycleUserBuilder builder) 11 | : userId = builder._userId, 12 | isAnonymous = builder._isAnonymous, 13 | email = builder._email, 14 | name = builder._name, 15 | country = builder._country, 16 | customData = builder._customData, 17 | privateCustomData = builder._privateCustomData; 18 | 19 | Map toCodec() { 20 | final Map result = {}; 21 | if (userId != null) result['userId'] = userId; 22 | if (isAnonymous != null) result['isAnonymous'] = isAnonymous; 23 | if (email != null) result['email'] = email; 24 | if (name != null) result['name'] = name; 25 | if (country != null) result['country'] = country; 26 | if (customData != null) result['customData'] = customData; 27 | if (privateCustomData != null) { 28 | result['privateCustomData'] = privateCustomData; 29 | } 30 | return result; 31 | } 32 | 33 | @override 34 | String toString() { 35 | return 'DVCUser{userId: $userId, isAnonymous: $isAnonymous, email: $email, name: $name, country: $country, customData: $customData, privateCustomData: $privateCustomData}'; 36 | } 37 | } 38 | 39 | @Deprecated('Use DevCycleUser instead') 40 | typedef DVCUser = DevCycleUser; 41 | 42 | /// A builder for constructing [DevCycleUser] objects. 43 | class DevCycleUserBuilder { 44 | String? _userId; 45 | bool? _isAnonymous; 46 | String? _email; 47 | String? _name; 48 | String? _country; 49 | Map? _customData; 50 | Map? _privateCustomData; 51 | 52 | /// Sets whether the user is anonymous. 53 | DevCycleUserBuilder userId(String userId) { 54 | _userId = userId; 55 | return this; 56 | } 57 | 58 | /// Sets whether the user is anonymous. 59 | DevCycleUserBuilder isAnonymous(bool isAnonymous) { 60 | _isAnonymous = isAnonymous; 61 | return this; 62 | } 63 | 64 | /// Sets the user's email attribute. 65 | DevCycleUserBuilder email(String email) { 66 | _email = email; 67 | return this; 68 | } 69 | 70 | /// Sets the user's name attribute. 71 | DevCycleUserBuilder name(String name) { 72 | _name = name; 73 | return this; 74 | } 75 | 76 | /// Sets the user's country. 77 | DevCycleUserBuilder country(String country) { 78 | _country = country; 79 | return this; 80 | } 81 | 82 | DevCycleUserBuilder customData(Map customData) { 83 | _customData = customData; 84 | return this; 85 | } 86 | 87 | DevCycleUserBuilder privateCustomData(Map privateCustomData) { 88 | _privateCustomData = privateCustomData; 89 | return this; 90 | } 91 | 92 | /// Constructs a [DevCycleUser] instance from the values currently in the builder. 93 | DevCycleUser build() { 94 | return DevCycleUser._builder(this); 95 | } 96 | } 97 | 98 | @Deprecated('Use DevCycleUserBuilder instead') 99 | typedef DVCUserBuilder = DevCycleUserBuilder; 100 | -------------------------------------------------------------------------------- /lib/dvc_feature.dart: -------------------------------------------------------------------------------- 1 | import 'package:collection/collection.dart'; 2 | 3 | enum FeatureType { release, experiment, permission, ops } 4 | 5 | class DVCFeature { 6 | /// unique database id 7 | String? id; 8 | 9 | /// Unique key by Project, can be used in the SDK / API to reference by 'key' rather than _id. 10 | String? key; 11 | 12 | /// Feature type 13 | FeatureType? type; 14 | 15 | /// Bucketed feature variation ID 16 | String? variation; 17 | 18 | /// Evaluation reasoning 19 | String? evalReason; 20 | 21 | /// Variation name 22 | String? variationName; 23 | 24 | /// Variation key 25 | String? variationKey; 26 | 27 | static DVCFeature fromCodec(Map map) { 28 | DVCFeature feature = DVCFeature(); 29 | 30 | feature.id = map['id']; 31 | feature.key = map['key']; 32 | feature.type = FeatureType.values 33 | .firstWhereOrNull((e) => e.toString() == "FeatureType.${map['type']}"); 34 | feature.variation = map['variation']; 35 | feature.evalReason = map['evalReason']; 36 | feature.variationName = map['variationName']; 37 | feature.variationKey = map['variationKey']; 38 | 39 | return feature; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/dvc_variable.dart: -------------------------------------------------------------------------------- 1 | import 'package:collection/collection.dart'; 2 | 3 | enum VariableType { string, boolean, number, json } 4 | 5 | class DVCVariable { 6 | /// unique database id 7 | String? id; 8 | 9 | /// Unique key by Project, can be used in the SDK / API to reference by 'key' rather than _id. 10 | String key; 11 | 12 | /// Variable type 13 | VariableType? type; 14 | 15 | /// Variable value can be a string, number, boolean, or JSON 16 | T value; 17 | 18 | void Function(T value)? callback; 19 | 20 | DVCVariable({this.id, required this.key, this.type, required this.value}); 21 | 22 | static DVCVariable fromCodec(Map map) { 23 | String mapType = map['type'].toString().toLowerCase(); 24 | 25 | return DVCVariable( 26 | key: map['key'], 27 | value: map['value'], 28 | id: map['id'], 29 | type: VariableType.values 30 | .firstWhereOrNull((e) => e.toString() == "VariableType.$mapType") 31 | ); 32 | } 33 | 34 | static DVCVariable fromDefault(String key, dynamic defaultValue) { 35 | String mapType = defaultValue.runtimeType.toString().toLowerCase(); 36 | 37 | return DVCVariable( 38 | key: key, 39 | value: defaultValue, 40 | type: VariableType.values 41 | .firstWhereOrNull((e) => e.toString() == "VariableType.$mapType") 42 | ); 43 | } 44 | 45 | /// 46 | /// To be notified when Variable.value changes register a callback by calling this method. The 47 | /// callback will replace any previously registered callback. 48 | /// 49 | /// [callback] called with the updated variable 50 | /// 51 | onUpdate(void Function(T value) onUpdateCallback) { 52 | callback = onUpdateCallback; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: devcycle_flutter_client_sdk 2 | description: Official Flutter Client SDK for DevCycle. Supports iOS and Android. 3 | version: "1.10.0" 4 | homepage: https://www.devcycle.com 5 | repository: https://github.com/DevCycleHQ/flutter-client-sdk 6 | documentation: https://docs.devcycle.com/docs/sdk/client-side-sdks/flutter 7 | issue_tracker: https://github.com/DevCycleHQ/flutter-client-sdk/issues 8 | 9 | environment: 10 | sdk: ">=2.14.0 <4.0.0" 11 | flutter: ">=2.5.0" 12 | 13 | dependencies: 14 | collection: ^1.15.0 15 | flutter: 16 | sdk: flutter 17 | logger: ^2.4.0 18 | plugin_platform_interface: ^2.1.8 19 | uuid: ^4.4.2 20 | 21 | dev_dependencies: 22 | flutter_test: 23 | sdk: flutter 24 | flutter_lints: ^4.0.0 25 | 26 | platforms: 27 | android: 28 | ios: 29 | 30 | flutter: 31 | plugin: 32 | platforms: 33 | android: 34 | package: com.devcycle.devcycle_flutter_client_sdk 35 | pluginClass: DevCycleFlutterClientSdkPlugin 36 | ios: 37 | pluginClass: DevCycleFlutterClientSdkPlugin 38 | -------------------------------------------------------------------------------- /scripts/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | SHA="$(git rev-parse HEAD)" 3 | DEVCYCLE_PROD_SLEUTH_API_TOKEN="$(aws secretsmanager get-secret-value --secret-id=DEVCYCLE_PROD_SLEUTH_API_TOKEN | jq -r .SecretString )" 4 | 5 | # make sure we're able to track this deployment 6 | if [[ -z "$DEVCYCLE_PROD_SLEUTH_API_TOKEN" ]]; then 7 | echo "Sleuth.io deployment tracking token not found. Aborting." 8 | exit 1 9 | fi 10 | 11 | dart pub publish 12 | 13 | if [[ "$?" != 0 ]]; then 14 | echo "Publish failed. Aborting." 15 | exit 1 16 | fi 17 | 18 | curl -X POST \ 19 | -d api_key=$DEVCYCLE_PROD_SLEUTH_API_TOKEN \ 20 | -d environment=production \ 21 | -d sha=$SHA https://app.sleuth.io/api/1/deployments/taplytics/flutter-client-sdk/register_deploy -------------------------------------------------------------------------------- /test/devcycle_flutter_client_sdk_method_channel_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/services.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:devcycle_flutter_client_sdk/devcycle_flutter_client_sdk_method_channel.dart'; 4 | 5 | void main() { 6 | MethodChannelDevCycleFlutterClientSdk platform = 7 | MethodChannelDevCycleFlutterClientSdk(); 8 | const MethodChannel channel = MethodChannel('devcycle_flutter_client_sdk'); 9 | 10 | TestWidgetsFlutterBinding.ensureInitialized(); 11 | 12 | setUp(() { 13 | TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(channel, (call) async { 14 | return '42'; 15 | }); 16 | }); 17 | 18 | tearDown(() { 19 | TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(channel, null); 20 | }); 21 | 22 | test('getPlatformVersion', () async { 23 | expect(await platform.getPlatformVersion(), '42'); 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /test/devcycle_flutter_client_sdk_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/services.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:devcycle_flutter_client_sdk/devcycle_flutter_client_sdk.dart'; 4 | import 'package:devcycle_flutter_client_sdk/devcycle_flutter_client_sdk_platform_interface.dart'; 5 | import 'package:devcycle_flutter_client_sdk/devcycle_flutter_client_sdk_method_channel.dart'; 6 | 7 | void main() { 8 | final DevCycleFlutterClientSdkPlatform initialPlatform = 9 | DevCycleFlutterClientSdkPlatform.instance; 10 | MethodChannelDevCycleFlutterClientSdk platform = 11 | MethodChannelDevCycleFlutterClientSdk(); 12 | 13 | MethodCall? methodCall; 14 | handler(MethodCall mc) async { 15 | methodCall = mc; 16 | } 17 | 18 | TestWidgetsFlutterBinding.ensureInitialized(); 19 | TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger 20 | .setMockMethodCallHandler(platform.methodChannel, handler); 21 | 22 | DevCycleFlutterClientSdkPlatform.instance = platform; 23 | 24 | setUp(() { 25 | methodCall = null; 26 | }); 27 | 28 | test('$MethodChannelDevCycleFlutterClientSdk is the default instance', () { 29 | expect( 30 | initialPlatform, isInstanceOf()); 31 | }); 32 | 33 | group('initializeDevCycle', () { 34 | test('initializes with only required params', () async { 35 | DevCycleUser user = DevCycleUserBuilder().userId('user1').build(); 36 | DevCycleClientBuilder().user(user).sdkKey('123').build(); 37 | expect(methodCall?.method, 'initializeDevCycle'); 38 | expect(methodCall?.arguments, { 39 | "sdkKey": "123", 40 | "user": {"userId": "user1"}, 41 | "options": null 42 | }); 43 | }); 44 | 45 | test('check deprecated Builder classes', () async { 46 | DVCUser user = DVCUserBuilder().userId('user1').build(); 47 | DVCClientBuilder().user(user).sdkKey('123').build(); 48 | expect(methodCall?.method, 'initializeDevCycle'); 49 | expect(methodCall?.arguments, { 50 | "sdkKey": "123", 51 | "user": {"userId": "user1"}, 52 | "options": null 53 | }); 54 | }); 55 | 56 | test('initializes with environmentKey', () async { 57 | DevCycleUser user = DevCycleUserBuilder().userId('user1').build(); 58 | // ignore: deprecated_member_use_from_same_package 59 | DevCycleClientBuilder().user(user).environmentKey('123').build(); 60 | expect(methodCall?.method, 'initializeDevCycle'); 61 | expect(methodCall?.arguments, { 62 | "sdkKey": "123", 63 | "user": {"userId": "user1"}, 64 | "options": null 65 | }); 66 | }); 67 | 68 | test('initializes with all available params', () async { 69 | DevCycleUser user = DevCycleUserBuilder().userId('user1').build(); 70 | DevCycleOptions options = 71 | DVCOptionsBuilder().configCacheTTL(100).enableEdgeDB(true).build(); 72 | DevCycleClientBuilder().user(user).sdkKey('123').options(options).build(); 73 | expect(methodCall?.method, 'initializeDevCycle'); 74 | expect(methodCall?.arguments, { 75 | "sdkKey": "123", 76 | "user": {"userId": "user1"}, 77 | "options": {"configCacheTTL": 100, "enableEdgeDB": true} 78 | }); 79 | }); 80 | 81 | test('fails to initialize without an environment key', () async { 82 | DevCycleUser user = DevCycleUserBuilder().userId('123').build(); 83 | DevCycleClientBuilder clientBuilder = DevCycleClientBuilder().user(user); 84 | 85 | expect(clientBuilder.build, throwsException); 86 | }); 87 | 88 | test('fails to initialize without a user', () async { 89 | DevCycleClientBuilder clientBuilder = 90 | DevCycleClientBuilder().sdkKey('123'); 91 | 92 | expect(clientBuilder.build, throwsException); 93 | }); 94 | }); 95 | 96 | group('identifyUser', () { 97 | test('identify user correctly', () async { 98 | DevCycleUser user1 = DevCycleUserBuilder() 99 | .userId('user1') 100 | .isAnonymous(false) 101 | .email('test@example.com') 102 | .name('a b') 103 | .country('de') 104 | .build(); 105 | DevCycleUser user2 = DevCycleUserBuilder() 106 | .userId('user2') 107 | .isAnonymous(false) 108 | .email('test_2@example.com') 109 | .name('c d') 110 | .country('ca') 111 | .build(); 112 | 113 | DevCycleClient devcycleFlutterClientSdkPlugin = 114 | DevCycleClientBuilder().sdkKey('SDK_KEY').user(user1).build(); 115 | await devcycleFlutterClientSdkPlugin.identifyUser(user2); 116 | expect(methodCall?.method, 'identifyUser'); 117 | expect(methodCall?.arguments, { 118 | 'user': { 119 | 'userId': 'user2', 120 | 'isAnonymous': false, 121 | 'email': 'test_2@example.com', 122 | 'name': 'c d', 123 | 'country': 'ca' 124 | }, 125 | 'callbackId': null 126 | }); 127 | }); 128 | }); 129 | 130 | group('resetUser', () { 131 | test('reset user from client', () async { 132 | DevCycleUser user = DevCycleUserBuilder() 133 | .userId('user1') 134 | .isAnonymous(false) 135 | .email('test@example.com') 136 | .name('a b') 137 | .country('de') 138 | .build(); 139 | DevCycleClient devcycleFlutterClientSdkPlugin = 140 | DevCycleClientBuilder().sdkKey('SDK_KEY').user(user).build(); 141 | await devcycleFlutterClientSdkPlugin.resetUser(); 142 | expect(methodCall?.method, 'resetUser'); 143 | expect(methodCall?.arguments, {'callbackId': null}); 144 | }); 145 | }); 146 | 147 | group('variable', () { 148 | test('get variable', () async { 149 | DevCycleUser user = DevCycleUserBuilder().userId('user1').build(); 150 | DevCycleClient devcycleFlutterClientSdkPlugin = 151 | DevCycleClientBuilder().sdkKey('SDK_KEY').user(user).build(); 152 | await devcycleFlutterClientSdkPlugin.variable( 153 | 'test-key', 'default-value'); 154 | expect(methodCall?.method, 'variable'); 155 | expect(methodCall?.arguments, { 156 | 'key': 'test-key', 157 | 'defaultValue': 'default-value', 158 | 'type': 'String' 159 | }); 160 | }); 161 | 162 | test('get variable value', () async { 163 | DevCycleUser user = DevCycleUserBuilder().userId('user1').build(); 164 | DevCycleClient devcycleFlutterClientSdkPlugin = 165 | DevCycleClientBuilder().sdkKey('SDK_KEY').user(user).build(); 166 | await devcycleFlutterClientSdkPlugin.variableValue( 167 | 'test-key', 'default-value'); 168 | expect(methodCall?.method, 'variable'); 169 | expect(methodCall?.arguments, { 170 | 'key': 'test-key', 171 | 'defaultValue': 'default-value', 172 | 'type': 'String' 173 | }); 174 | }); 175 | }); 176 | 177 | group('allFeatures', () { 178 | test('fetch all features', () async { 179 | DevCycleUser user = DevCycleUserBuilder().userId('user1').build(); 180 | DevCycleClient devcycleFlutterClientSdkPlugin = 181 | DevCycleClientBuilder().user(user).sdkKey('123').build(); 182 | await devcycleFlutterClientSdkPlugin.allFeatures(); 183 | expect(methodCall?.method, 'allFeatures'); 184 | }); 185 | }); 186 | 187 | group('allVariables', () { 188 | test('fetch all variables', () async { 189 | DevCycleUser user = DevCycleUserBuilder().userId('user1').build(); 190 | DevCycleClient devcycleFlutterClientSdkPlugin = 191 | DevCycleClientBuilder().user(user).sdkKey('123').build(); 192 | await devcycleFlutterClientSdkPlugin.allVariables(); 193 | expect(methodCall?.method, 'allVariables'); 194 | }); 195 | }); 196 | group('track', () { 197 | test('track event with no properties', () async { 198 | DevCycleUser user = DevCycleUserBuilder().userId('user1').build(); 199 | DevCycleClient devcycleFlutterClientSdkPlugin = 200 | DevCycleClientBuilder().sdkKey('SDK_KEY').user(user).build(); 201 | DevCycleEvent event = DevCycleEventBuilder() 202 | .type('my event type') 203 | .target('my target') 204 | .build(); 205 | 206 | await devcycleFlutterClientSdkPlugin.track(event); 207 | expect(methodCall?.method, 'track'); 208 | expect(methodCall?.arguments, {'event': event.toCodec()}); 209 | }); 210 | 211 | test('track event with properties', () async { 212 | DevCycleUser user = DevCycleUserBuilder().userId('user1').build(); 213 | DevCycleClient devcycleFlutterClientSdkPlugin = 214 | DevCycleClientBuilder().sdkKey('SDK_KEY').user(user).build(); 215 | DevCycleEvent event = DevCycleEventBuilder() 216 | .type('my event type') 217 | .target('my target') 218 | .value(1) 219 | .metaData({"hello": "world"}).build(); 220 | 221 | await devcycleFlutterClientSdkPlugin.track(event); 222 | expect(methodCall?.method, 'track'); 223 | expect(methodCall?.arguments, {'event': event.toCodec()}); 224 | }); 225 | 226 | test('do not track event with disableCustomEventLogging', () async { 227 | DevCycleUser user = DevCycleUserBuilder().userId('user1').build(); 228 | DevCycleClient devcycleFlutterClientSdkPlugin = 229 | DevCycleClientBuilder().sdkKey('SDK_KEY').user(user).build(); 230 | DevCycleEvent event = DevCycleEventBuilder() 231 | .type('my event type') 232 | .target('my target') 233 | .value(1) 234 | .metaData({"hello": "world"}).build(); 235 | 236 | await devcycleFlutterClientSdkPlugin.track(event); 237 | expect(methodCall?.method, 'track'); 238 | expect(methodCall?.arguments, {'event': event.toCodec()}); 239 | }); 240 | }); 241 | 242 | group('flushEvents', () { 243 | test('flush events', () async { 244 | DevCycleUser user = DevCycleUserBuilder().userId('user1').build(); 245 | DevCycleClient devcycleFlutterClientSdkPlugin = 246 | DevCycleClientBuilder().sdkKey('SDK_KEY').user(user).build(); 247 | await devcycleFlutterClientSdkPlugin.flushEvents(); 248 | expect(methodCall?.method, 'flushEvents'); 249 | }); 250 | }); 251 | } 252 | -------------------------------------------------------------------------------- /test/dvc_event_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:devcycle_flutter_client_sdk/devcycle_flutter_client_sdk.dart'; 3 | 4 | void main() { 5 | test('builds event object', () { 6 | DevCycleEvent event = DevCycleEventBuilder() 7 | .type('my event type') 8 | .target('my target') 9 | .value(1) 10 | .date(DateTime.parse("2018-08-16T11:00:00.000Z")) 11 | .metaData({"hello": "world"}).build(); 12 | expect(event.type, equals('my event type')); 13 | expect(event.target, equals('my target')); 14 | expect(event.value, equals(1)); 15 | expect(event.date, equals(DateTime.parse("2018-08-16T11:00:00.000Z"))); 16 | expect(event.metaData, equals({"hello": "world"})); 17 | }); 18 | 19 | test('deprecated DVCEventBuilder', () { 20 | DVCEvent event = DVCEventBuilder() 21 | .type('my event type') 22 | .target('my target') 23 | .value(1) 24 | .date(DateTime.parse("2018-08-16T11:00:00.000Z")) 25 | .metaData({"hello": "world"}).build(); 26 | expect(event.type, equals('my event type')); 27 | expect(event.target, equals('my target')); 28 | expect(event.value, equals(1)); 29 | expect(event.date, equals(DateTime.parse("2018-08-16T11:00:00.000Z"))); 30 | expect(event.metaData, equals({"hello": "world"})); 31 | }); 32 | 33 | test('creates map from event object', () { 34 | DevCycleEvent event = DevCycleEventBuilder() 35 | .type('my event type') 36 | .target('my target') 37 | .value(1) 38 | .date(DateTime.parse("2018-08-16T11:00:00.000Z")) 39 | .metaData({"hello": "world"}).build(); 40 | Map eventMap = event.toCodec(); 41 | expect(eventMap['type'], equals('my event type')); 42 | expect(eventMap['target'], equals('my target')); 43 | expect(eventMap['value'], equals('1.0')); 44 | expect(eventMap['date'], equals("2018-08-16T11:00:00.000Z")); 45 | expect(eventMap['metaData'], equals({"hello": "world"})); 46 | }); 47 | } 48 | -------------------------------------------------------------------------------- /test/dvc_options_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:devcycle_flutter_client_sdk/devcycle_flutter_client_sdk.dart'; 3 | 4 | void main() { 5 | test('builds options object with all params', () { 6 | DevCycleOptions options = DevCycleOptionsBuilder() 7 | .flushEventsIntervalMs(100) 8 | .disableCustomEventLogging(true) 9 | .disableAutomaticEventLogging(false) 10 | .enableEdgeDB(false) 11 | .configCacheTTL(999) 12 | .disableConfigCache(false) 13 | .disableRealtimeUpdates(false) 14 | .logLevel(LogLevel.debug) 15 | .apiProxyUrl("http://api.test.com") 16 | .eventsApiProxyUrl("http://events.test.com") 17 | .build(); 18 | expect(options.flushEventsIntervalMs, equals(100)); 19 | expect(options.disableCustomEventLogging, isTrue); 20 | expect(options.disableAutomaticEventLogging, isFalse); 21 | expect(options.enableEdgeDB, isFalse); 22 | expect(options.configCacheTTL, equals(999)); 23 | expect(options.disableConfigCache, isFalse); 24 | expect(options.disableRealtimeUpdates, isFalse); 25 | expect(options.logLevel, equals(LogLevel.debug)); 26 | expect(options.apiProxyUrl, equals("http://api.test.com")); 27 | expect(options.eventsApiProxyUrl, equals("http://events.test.com")); 28 | }); 29 | 30 | test('deprecated DVCOptionsBuilder', () { 31 | DVCOptions options = DVCOptionsBuilder() 32 | .flushEventsIntervalMs(100) 33 | .disableCustomEventLogging(true) 34 | .disableAutomaticEventLogging(false) 35 | .enableEdgeDB(false) 36 | .configCacheTTL(999) 37 | .disableConfigCache(false) 38 | .disableRealtimeUpdates(false) 39 | .logLevel(LogLevel.debug) 40 | .apiProxyUrl("http://api.test.com") 41 | .eventsApiProxyUrl("http://events.test.com") 42 | .build(); 43 | expect(options.flushEventsIntervalMs, equals(100)); 44 | expect(options.disableCustomEventLogging, isTrue); 45 | expect(options.disableAutomaticEventLogging, isFalse); 46 | expect(options.enableEdgeDB, isFalse); 47 | expect(options.configCacheTTL, equals(999)); 48 | expect(options.disableConfigCache, isFalse); 49 | expect(options.disableRealtimeUpdates, isFalse); 50 | expect(options.logLevel, equals(LogLevel.debug)); 51 | expect(options.apiProxyUrl, equals("http://api.test.com")); 52 | expect(options.eventsApiProxyUrl, equals("http://events.test.com")); 53 | }); 54 | 55 | test('creates map from options object with all properties', () { 56 | DevCycleOptions options = DVCOptionsBuilder() 57 | .flushEventsIntervalMs(100) 58 | .disableAutomaticEventLogging(true) 59 | .disableCustomEventLogging(false) 60 | .enableEdgeDB(false) 61 | .configCacheTTL(999) 62 | .disableConfigCache(false) 63 | .logLevel(LogLevel.error) 64 | .apiProxyUrl("http://api.test.com") 65 | .eventsApiProxyUrl("http://events.test.com") 66 | .build(); 67 | Map codecOptions = options.toCodec(); 68 | expect(codecOptions['flushEventsIntervalMs'], equals(100)); 69 | expect(codecOptions['disableAutomaticEventLogging'], isTrue); 70 | expect(codecOptions['disableCustomEventLogging'], isFalse); 71 | expect(codecOptions['enableEdgeDB'], isFalse); 72 | expect(codecOptions['configCacheTTL'], equals(999)); 73 | expect(codecOptions['disableConfigCache'], isFalse); 74 | expect(codecOptions['logLevel'], equals('error')); 75 | expect(codecOptions['apiProxyUrl'], equals('http://api.test.com')); 76 | expect(codecOptions['eventsApiProxyUrl'], equals('http://events.test.com')); 77 | }); 78 | 79 | test('creates map from options object with only one property', () { 80 | DevCycleOptions options = 81 | DVCOptionsBuilder().flushEventsIntervalMs(100).build(); 82 | Map codecOptions = options.toCodec(); 83 | expect(codecOptions, {"flushEventsIntervalMs": 100}); 84 | }); 85 | } 86 | -------------------------------------------------------------------------------- /test/dvc_user_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:devcycle_flutter_client_sdk/devcycle_flutter_client_sdk.dart'; 3 | 4 | void main() { 5 | test('builds correct user object', () { 6 | DevCycleUser user = DevCycleUserBuilder() 7 | .userId('user1') 8 | .isAnonymous(false) 9 | .email('test@example.com') 10 | .name('a b') 11 | .country('de') 12 | .build(); 13 | expect(user.userId, equals('user1')); 14 | expect(user.isAnonymous, isFalse); 15 | expect(user.email, equals('test@example.com')); 16 | expect(user.name, equals('a b')); 17 | expect(user.country, equals('de')); 18 | }); 19 | 20 | test('deprecated DVCUserBuilder', () { 21 | DVCUser user = DVCUserBuilder() 22 | .userId('user1') 23 | .isAnonymous(false) 24 | .email('test@example.com') 25 | .name('a b') 26 | .country('de') 27 | .build(); 28 | expect(user.userId, equals('user1')); 29 | expect(user.isAnonymous, isFalse); 30 | expect(user.email, equals('test@example.com')); 31 | expect(user.name, equals('a b')); 32 | expect(user.country, equals('de')); 33 | }); 34 | 35 | test('creates map from user object', () { 36 | DevCycleUser user = DevCycleUserBuilder() 37 | .userId('user1') 38 | .isAnonymous(false) 39 | .email('test@example.com') 40 | .name('a b') 41 | .country('de') 42 | .build(); 43 | Map codecUser = user.toCodec(); 44 | expect(codecUser['userId'], equals('user1')); 45 | expect(codecUser['isAnonymous'], isFalse); 46 | expect(codecUser['email'], equals('test@example.com')); 47 | expect(codecUser['name'], equals('a b')); 48 | expect(codecUser['country'], equals('de')); 49 | }); 50 | } 51 | -------------------------------------------------------------------------------- /test/feature_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:devcycle_flutter_client_sdk/dvc_feature.dart'; 3 | 4 | void main() { 5 | test('builds feature object from a map', () { 6 | Map codecFeature = { 7 | "id": "feature1", 8 | "key": "feature-one", 9 | "type": "release", 10 | "variation": "variation1", 11 | "variationName": "Variation One", 12 | "variationKey": "var-one" 13 | }; 14 | DVCFeature feature = DVCFeature.fromCodec(codecFeature); 15 | expect(feature.id, equals('feature1')); 16 | expect(feature.key, equals('feature-one')); 17 | expect(feature.type.toString(), equals('FeatureType.release')); 18 | expect(feature.variation, equals('variation1')); 19 | expect(feature.variationName, equals('Variation One')); 20 | expect(feature.variationKey, equals('var-one')); 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /test/variable_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:devcycle_flutter_client_sdk/dvc_variable.dart'; 3 | 4 | void main() { 5 | test('builds string variable object from a map', () { 6 | Map codecVariable = { 7 | "id": "variable1", 8 | "key": "variable-one", 9 | "type": "String", 10 | "value": "hello world", 11 | "isDefaulted": false 12 | }; 13 | DVCVariable variable = DVCVariable.fromCodec(codecVariable); 14 | expect(variable.id, equals('variable1')); 15 | expect(variable.key, equals('variable-one')); 16 | expect(variable.type.toString(), equals('VariableType.string')); 17 | expect(variable.value, equals('hello world')); 18 | }); 19 | 20 | test('builds boolean variable object from a map', () { 21 | Map codecVariable = { 22 | "id": "variable1", 23 | "key": "variable-one", 24 | "type": "Boolean", 25 | "value": true 26 | }; 27 | DVCVariable variable = DVCVariable.fromCodec(codecVariable); 28 | expect(variable.id, equals('variable1')); 29 | expect(variable.key, equals('variable-one')); 30 | expect(variable.type.toString(), equals('VariableType.boolean')); 31 | expect(variable.value, equals(isTrue)); 32 | }); 33 | 34 | test('builds number variable object from a map', () { 35 | Map codecVariable = { 36 | "id": "variable1", 37 | "key": "variable-one", 38 | "type": "Number", 39 | "value": 100 40 | }; 41 | DVCVariable variable = DVCVariable.fromCodec(codecVariable); 42 | expect(variable.id, equals('variable1')); 43 | expect(variable.key, equals('variable-one')); 44 | expect(variable.type.toString(), equals('VariableType.number')); 45 | expect(variable.value, equals(100)); 46 | }); 47 | 48 | test('builds json variable object from a map', () { 49 | Map codecVariable = { 50 | "id": "variable1", 51 | "key": "variable-one", 52 | "type": "JSON", 53 | "value": {"hello": "world"} 54 | }; 55 | DVCVariable variable = DVCVariable.fromCodec(codecVariable); 56 | expect(variable.id, equals('variable1')); 57 | expect(variable.key, equals('variable-one')); 58 | expect(variable.type.toString(), equals('VariableType.json')); 59 | expect(variable.value, equals({"hello": "world"})); 60 | }); 61 | } 62 | --------------------------------------------------------------------------------