diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..0ecd8f6 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,36 @@ +name: CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: '3.27.0' + channel: 'stable' + + - name: Install dependencies + run: flutter pub get + + - name: Analyze code + run: flutter analyze + + # - name: Run tests + # run: flutter test + + - name: Check formatting + run: dart format --set-exit-if-changed . + + - name: Check publish readiness + run: flutter pub publish --dry-run diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..a445b75 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,56 @@ +name: Publish to pub.dev + +on: + release: + types: [published] + +jobs: + publish: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: '3.27.0' + channel: 'stable' + + - name: Verify version consistency + run: | + # Extract version from pubspec.yaml + PUBSPEC_VERSION=$(grep '^version: ' pubspec.yaml | sed 's/version: //') + # Remove 'v' prefix from tag if present + TAG_VERSION=${GITHUB_REF_NAME#v} + + echo "Tag version: $TAG_VERSION" + echo "Pubspec version: $PUBSPEC_VERSION" + + if [ "$TAG_VERSION" != "$PUBSPEC_VERSION" ]; then + echo "Error: Tag version ($TAG_VERSION) does not match pubspec.yaml version ($PUBSPEC_VERSION)" + echo "Please ensure your tag matches the version in pubspec.yaml" + exit 1 + fi + + echo "✓ Version check passed!" + + - name: Install dependencies + run: flutter pub get + + - name: Setup Dart credentials + run: | + mkdir -p ~/.pub-cache + echo '${{ secrets.PUB_CREDENTIALS }}' > ~/.pub-cache/credentials.json + + - name: Verify package scores + run: | + echo "Running package verification..." + flutter pub publish --dry-run + + - name: Publish to pub.dev + run: | + # Using --force because this runs in CI without user interaction + # The dry-run above ensures the package is valid before publishing + flutter pub publish --force diff --git a/CHANGELOG.md b/CHANGELOG.md index 41cc7d8..4a01c54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,39 @@ -## 0.0.1 +# Changelog -* TODO: Describe initial release. +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [0.1.0] - 2024-02-12 + +### Added + +- Initial release of Vapi Flutter SDK +- Unified mobile and web support +- Real-time voice conversation capabilities +- Assistant configuration and customization +- Event-based communication system +- Support for custom messages and interruptions +- Comprehensive error handling +- Platform-specific optimizations for mobile (iOS/Android) and web + +### Platform Support + +- iOS: Microphone permissions and audio session handling +- Android: Required permissions for audio recording +- Web: Dynamic SDK loading via CDN + +### Dependencies + +- Mobile: daily_flutter for WebRTC support +- Web: Vapi Web SDK loaded dynamically +- Cross-platform HTTP client support + +## [0.2.0] - 2025-06-25 + +### Added + +- Updated the dependencies of daily_flutter and others to latest version diff --git a/README.md b/README.md index f587675..3637e22 100644 --- a/README.md +++ b/README.md @@ -226,4 +226,14 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -``` \ No newline at end of file +``` + +## Contributing + +### Development Setup + +For detailed instructions on setting up your development environment, please see [SETUP.md](SETUP.md). + +### Releasing + +For information on how to release new versions to pub.dev, please see [RELEASE.md](RELEASE.md). diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 0000000..7660f24 --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,92 @@ +# Releasing to pub.dev + +This guide explains how to release new versions of the Vapi Flutter SDK to pub.dev using GitHub releases. + +## Initial Setup (One-time) + +### 1. Get pub.dev credentials + +```bash +./scripts/get_pub_credentials.sh +``` + +If not logged in yet: `flutter pub login` + +### 2. Add to GitHub Secrets + +1. Go to Settings → Secrets and variables → Actions +2. Add new secret: `PUB_CREDENTIALS` +3. Paste the JSON content from step 1 + +## Release Process + +### 1. Update version in pubspec.yaml + +Follow semantic versioning: + +- **MAJOR** (1.0.0 → 2.0.0): Breaking changes +- **MINOR** (0.1.0 → 0.2.0): New features, improvements, bug fixes +- **PATCH** (0.1.0 → 0.1.1): Bug fixes only + +```yaml +version: 0.2.0 # Update accordingly +``` + +### 2. Update CHANGELOG.md + +```markdown +## [0.2.0] - 2025-06-25 + +### Added +- New feature X + +### Fixed +- Bug where Z happened + +### Changed +- Improved performance of B + +### Breaking Changes # For major versions only +- Renamed method `oldName()` to `newName()` +``` + +### 3. Commit and push + +```bash +git add pubspec.yaml CHANGELOG.md +git commit -m "chore: bump version to 0.2.0" +git push origin main +``` + +### 4. Create GitHub Release + +1. Go to Releases → Create a new release +2. Create tag: `v0.2.0` (with 'v' prefix) +3. Title: `v0.2.0` +4. Copy notes from CHANGELOG.md +5. Publish release + +The GitHub Action will automatically publish to pub.dev. + +### 5. Monitor + +Check the Actions tab for the "Publish to pub.dev" workflow status. + +## Troubleshooting + +- **Tests failing**: Fix and create new release +- **Credentials issue**: Verify `PUB_CREDENTIALS` secret +- **Version conflict**: Ensure version bump in pubspec.yaml +- **Manual fallback**: `flutter pub publish` + +## Best Practices + +1. Test locally: `flutter test` and `flutter pub publish --dry-run` +2. Follow semantic versioning strictly +3. Keep CHANGELOG updated +4. Always release from main branch +5. Use consistent tag format: `vX.Y.Z` + +## Version Examples + +- `0.1.0` diff --git a/SETUP.md b/SETUP.md new file mode 100644 index 0000000..fd1030c --- /dev/null +++ b/SETUP.md @@ -0,0 +1,225 @@ +# Flutter Development Environment Setup for macOS + +This guide provides step-by-step instructions for setting up a Flutter development environment on macOS without using Android Studio. + +## Prerequisites + +- macOS with Homebrew installed +- Xcode installed from the App Store +- Terminal access + +## 1. Install Flutter + +```bash +brew install flutter +``` + +Verify the installation: + +```bash +flutter doctor +``` + +## 2. Android Development Setup (Without Android Studio) + +### Install Android SDK Command Line Tools + +```bash +brew install android-commandlinetools +``` + +### Install Required Android Components + +```bash +# Install platform tools, Android SDK, and build tools +sdkmanager --install "platform-tools" "platforms;android-34" "build-tools;34.0.0" + +# Verify installed components +sdkmanager --list_installed +``` + +### Configure Android SDK Path + +Create a symlink for Android SDK: + +```bash +ln -s /opt/homebrew/share/android-commandlinetools ~/Library/Android/sdk +``` + +Set up environment variables in your shell profile (~/.zshrc or ~/.bash_profile): + +```bash +export ANDROID_HOME=$HOME/Library/Android/sdk +export PATH="$ANDROID_HOME/platform-tools:$ANDROID_HOME/cmdline-tools/latest/bin:$PATH" +``` + +### Accept Android Licenses + +```bash +flutter doctor --android-licenses +``` + +### Install Java (Required for Android builds) + +```bash +brew install openjdk@17 +flutter config --jdk-dir /opt/homebrew/opt/openjdk@17 +``` + +Verify Java installation: + +```bash +flutter doctor -v | grep -A5 "Java" +``` + +## 3. iOS Development Setup + +### Configure Xcode + +```bash +# Switch to Xcode developer tools +sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer + +# Run Xcode first launch setup +sudo xcodebuild -runFirstLaunch +``` + +### Install CocoaPods + +```bash +brew install cocoapods +pod setup +pod --version +``` + +## 4. Project Setup + +Navigate to the Vapi Flutter SDK directory: + +```bash +# Install dependencies for the main project +flutter pub get + +# Install dependencies for the example project +cd example +flutter pub get +``` + +## 5. Running the Project + +### Check Available Devices + +```bash +flutter devices +flutter emulators +``` + +### macOS + +```bash +flutter run -d macos +# or build +flutter build macos +``` + +### iOS Simulator + +```bash +# List available simulators +xcrun simctl list devices available + +# Open iOS Simulator +open -a Simulator + +# Run on specific device +flutter run -d "iPhone 16 Plus" +``` + +### Android Emulator + +#### Create an Android Emulator + +```bash +# Download Android system image +sdkmanager --install "platforms;android-36" "system-images;android-36;google_apis_playstore;arm64-v8a" + +# Create emulator +avdmanager create avd -n android36_emulator -k "system-images;android-36;google_apis_playstore;arm64-v8a" -d pixel_7_pro + +# List available emulators +avdmanager list avd +``` + +#### Run Android Emulator + +```bash +# Launch emulator +flutter emulators --launch android36_emulator + +# Run app on emulator +flutter devices # Note the device ID (e.g., emulator-5554) +flutter run -d emulator-5554 + +# Check connected devices +adb devices +``` + +### Web (Note: Limited Support) + +```bash +flutter run -d chrome +``` + +**⚠️ Important:** The Vapi SDK has limited web support because the daily_flutter dependency uses native code (FFI) that's not available in web browsers. + +## 6. Building the Project + +### Android APK + +```bash +cd example +flutter clean +flutter build apk --debug +``` + +**macOS Gatekeeper Fix:** If the build fails due to macOS Gatekeeper blocking the gen_snapshot binary: + +```bash +find /opt/homebrew/Caskroom/flutter/ -name gen_snapshot -exec sudo xattr -rd com.apple.quarantine {} \; +``` + +## 7. Useful Commands + +### Git Operations + +```bash +# Switch to a pull request branch +gh pr checkout +``` + +### Cleanup Commands + +```bash +# Delete an emulator +avdmanager delete avd -n + +# Uninstall system images +sdkmanager --uninstall "system-images;android-34;google_apis_playstore;arm64-v8a" +``` + +## Troubleshooting + +1. **Flutter Doctor Issues**: Run `flutter doctor -v` for detailed diagnostics +2. **Android License Issues**: Re-run `flutter doctor --android-licenses` and accept all +3. **Xcode Issues**: Ensure Xcode is properly installed and command line tools are configured +4. **Build Failures**: Try `flutter clean` before rebuilding + +## Next Steps + +After setup is complete, you can: + +- Run the example app on various platforms +- Modify the code and test hot reload +- Build release versions for distribution + +For more information, refer to the [Flutter documentation](https://flutter.dev/docs). diff --git a/example/lib/main.dart b/example/lib/main.dart index 626054b..9aaf375 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -18,10 +18,10 @@ class _MyAppState extends State { String buttonText = 'Start Call'; bool isLoading = false; bool isCallStarted = false; - + VapiClient? vapiClient; VapiCall? currentCall; - + // Controllers for text fields final TextEditingController _publicKeyController = TextEditingController(); final TextEditingController _assistantIdController = TextEditingController(); @@ -67,8 +67,9 @@ class _MyAppState extends State { if (!isCallStarted) { // Start a new call using assistant ID - final call = await vapiClient!.start(assistantId: _assistantIdController.text.trim()); - + final call = await vapiClient! + .start(assistantId: _assistantIdController.text.trim()); + currentCall = call; call.onEvent.listen(_handleCallEvents); } else { @@ -81,7 +82,7 @@ class _MyAppState extends State { buttonText = 'Start Call'; isLoading = false; }); - + // Show error dialog if (mounted) { showDialog( diff --git a/lib/src/platform/mobile/vapi_mobile_call.dart b/lib/src/platform/mobile/vapi_mobile_call.dart index 16b39ee..c661851 100644 --- a/lib/src/platform/mobile/vapi_mobile_call.dart +++ b/lib/src/platform/mobile/vapi_mobile_call.dart @@ -12,41 +12,41 @@ import '../../shared/vapi_call_monitor.dart'; import '../../shared/vapi_call_transport.dart'; /// Represents an active voice AI call session. -/// +/// /// This class encapsulates a single call and provides methods to interact /// with the assistant, manage audio settings, and handle call lifecycle events. -/// +/// /// Example usage: /// ```dart /// final vapiClient = VapiClient('your-public-key'); -/// +/// /// // Start a call and return immediately /// final call = await vapiClient.start(assistantId: 'assistant-id'); -/// +/// /// // Or start a call and wait until the assistant is actively listening /// final activeCall = await vapiClient.start( /// assistantId: 'assistant-id', /// waitUntilActive: true, /// ); -/// +/// /// // Access call information /// print('Call ID: ${call.id}'); /// print('Assistant ID: ${call.assistantId}'); /// print('Status: ${call.status}'); -/// +/// /// // Listen to events /// call.onEvent.listen((event) { /// print('Event: ${event.label}'); /// }); -/// +/// /// // Send a message during the call /// await call.send({'message': 'Hello'}); -/// +/// /// // Check if call is still active /// if (call.status == VapiCallStatus.active) { /// await call.stop(); // This will trigger a 'call-end' event /// } -/// +/// /// // Clean up resources /// call.dispose(); /// ``` @@ -61,13 +61,13 @@ class VapiMobileCall implements VapiCall { final Completer _activeCallCompleter = Completer(); /// Stream of Vapi events that occur during this call's lifecycle - /// + /// /// Events include: /// - `call-start`: When the assistant connects and starts listening /// - `call-end`: When the call ends (automatically emitted for all termination scenarios) /// - `call-error`: When an error occurs during the call /// - `message`: When a message is received from the assistant - /// + /// /// The `call-end` event is guaranteed to be emitted whenever the call terminates, /// regardless of whether it was ended by calling [stop], network disconnection, /// or any other termination cause. @@ -78,9 +78,9 @@ class VapiMobileCall implements VapiCall { VapiCallStatus _status = VapiCallStatus.starting; /// The current status of this call - /// - /// Starts as [VapiCallStatus.starting], progresses to [VapiCallStatus.active] - /// when the assistant connects, and becomes [VapiCallStatus.ended] when the + /// + /// Starts as [VapiCallStatus.starting], progresses to [VapiCallStatus.active] + /// when the assistant connects, and becomes [VapiCallStatus.ended] when the /// call terminates. Once ended, no operations can be performed on this call instance. @override VapiCallStatus get status => _status; @@ -89,45 +89,45 @@ class VapiMobileCall implements VapiCall { /// Unique identifier for this call. @override final String id; - + /// Organization ID associated with this call @override final String orgId; - + /// Timestamp when this call was created @override final DateTime createdAt; - + /// Timestamp when this call was last updated @override final DateTime updatedAt; - + /// Type of the call (e.g., "webCall") @override final String type; - + /// Monitor configuration for this call @override final VapiCallMonitor monitor; - + /// Transport configuration for this call @override final VapiCallTransport transport; - + /// Web call URL for joining the call @override final String webCallUrl; - + /// ID of the assistant handling this call @override final String assistantId; - + /// Assistant configuration overrides for this call @override final Map assistantOverrides; /// Creates a new VapiCall instance - /// + /// /// This constructor is typically called internally by [VapiClient.start] VapiMobileCall._( this._call, @@ -146,11 +146,11 @@ class VapiMobileCall implements VapiCall { } /// Factory method to create a VapiCall with an initialized client and API response data - /// + /// /// This method parses the API response, sets up the call client and joins the call. /// If [waitUntilActive] is true, it will wait until the assistant starts listening static Future create( - CallClient callClient, + CallClient callClient, Map apiResponse, { bool waitUntilActive = false, }) async { @@ -160,12 +160,15 @@ class VapiMobileCall implements VapiCall { final createdAt = DateTime.parse(apiResponse['createdAt'] as String); final updatedAt = DateTime.parse(apiResponse['updatedAt'] as String); final type = apiResponse['type'] as String; - final monitor = VapiCallMonitor.fromJson(apiResponse['monitor'] as Map); - final transport = VapiCallTransport.fromJson(apiResponse['transport'] as Map); + final monitor = VapiCallMonitor.fromJson( + apiResponse['monitor'] as Map); + final transport = VapiCallTransport.fromJson( + apiResponse['transport'] as Map); final webCallUrl = apiResponse['webCallUrl'] as String; final assistantId = apiResponse['assistantId'] as String; - final assistantOverrides = Map.from(apiResponse['assistantOverrides'] as Map); - + final assistantOverrides = Map.from( + apiResponse['assistantOverrides'] as Map); + final call = VapiMobileCall._( callClient, id, @@ -179,14 +182,14 @@ class VapiMobileCall implements VapiCall { assistantId, assistantOverrides, ); - + await call._joinCall(webCallUrl); - + // Wait for the call to become active if requested if (waitUntilActive) { await call._activeCallCompleter.future; } - + return call; } @@ -232,31 +235,27 @@ class VapiMobileCall implements VapiCall { /// Joins the call using the provided web call URL Future _joinCall(String webCallUrl) async { const clientSettings = ClientSettingsUpdate.set( - inputs: InputSettingsUpdate.set( - microphone: MicrophoneInputSettingsUpdate.set( - isEnabled: BoolUpdate.set(true) - ), - camera: CameraInputSettingsUpdate.set( - isEnabled: BoolUpdate.set(false) - ), - ) - ); + inputs: InputSettingsUpdate.set( + microphone: + MicrophoneInputSettingsUpdate.set(isEnabled: BoolUpdate.set(true)), + camera: CameraInputSettingsUpdate.set(isEnabled: BoolUpdate.set(false)), + )); await _call .join( - url: Uri.parse(webCallUrl), - clientSettings: clientSettings, - ) + url: Uri.parse(webCallUrl), + clientSettings: clientSettings, + ) .catchError((e) { throw VapiStartCallException(e); }); } /// Sends a message to the assistant during this call - /// + /// /// [message] can be any serializable object that will be JSON encoded /// and sent to the assistant. - /// + /// /// Throws [VapiCallEndedException] if this call has ended. @override Future send(dynamic message) async { @@ -267,7 +266,7 @@ class VapiMobileCall implements VapiCall { } /// Handles incoming app messages from the assistant - /// + /// /// Parses JSON messages and emits appropriate events. void _onAppMessage(String msg) { try { @@ -275,11 +274,11 @@ class VapiMobileCall implements VapiCall { if (parsedMessage == "listening") { _status = VapiCallStatus.active; - + if (!_activeCallCompleter.isCompleted) { _activeCallCompleter.complete(); } - + _streamController.add(const VapiEvent("call-start")); return; } @@ -297,16 +296,15 @@ class VapiMobileCall implements VapiCall { _streamController.add(VapiEvent("message", messageMap)); } catch (parseError) { - print('parseError: $parseError'); - // Silently ignore parse errors + // Silently ignore parse errors for non-JSON messages } } /// Stops this call and leaves the session - /// + /// /// After calling this method, the call will be marked as ended and /// no further operations can be performed on this call instance. - /// + /// /// Throws [VapiCallEndedException] if this call has already ended. @override Future stop() async { @@ -317,9 +315,9 @@ class VapiMobileCall implements VapiCall { } /// Mutes or unmutes the microphone during this call - /// + /// /// [muted] - true to mute the microphone, false to unmute. - /// + /// /// Throws [VapiCallEndedException] if this call has ended. @override void setMuted(bool muted) { @@ -334,7 +332,7 @@ class VapiMobileCall implements VapiCall { } /// Returns true if the microphone is currently muted - /// + /// /// Throws [VapiCallEndedException] if this call has ended. @override bool get isMuted { @@ -345,14 +343,14 @@ class VapiMobileCall implements VapiCall { } /// Sets the audio output device for this call using Vapi's audio device enum - /// + /// /// [device] - The VapiAudioDevice to use for audio output. /// Available options: /// - [VapiAudioDevice.speakerphone] - Use the device's speakerphone /// - [VapiAudioDevice.wired] - Use wired headphones/earphones /// - [VapiAudioDevice.earpiece] - Use the device's earpiece /// - [VapiAudioDevice.bluetooth] - Use connected Bluetooth audio device - /// + /// /// Throws [VapiCallEndedException] if this call has ended. @override void setAudioDevice({VapiAudioDevice? device}) { @@ -371,10 +369,10 @@ class VapiMobileCall implements VapiCall { } /// Disposes of resources used by this VapiCall instance - /// + /// /// Call this method when you're done using the VapiCall instance to /// clean up the event stream and prevent memory leaks. - /// + /// /// Note: This will also dispose of the underlying call client. @override void dispose() { diff --git a/lib/src/platform/mobile/vapi_mobile_client.dart b/lib/src/platform/mobile/vapi_mobile_client.dart index b14725d..a3afc42 100644 --- a/lib/src/platform/mobile/vapi_mobile_client.dart +++ b/lib/src/platform/mobile/vapi_mobile_client.dart @@ -7,13 +7,13 @@ import '../../vapi_client_interface.dart'; import '../../vapi_call_interface.dart'; import '../../shared/exceptions.dart'; import '../../shared/assistant_config.dart'; -import 'vapi_mobile_call.dart'; +import 'vapi_mobile_call.dart'; /// Mobile-specific implementation of the Vapi client. -/// +/// /// This implementation uses the Daily.co WebRTC SDK for real-time communication /// and handles mobile-specific concerns like permissions and audio device management. -/// +/// /// Features: /// - Automatic microphone permission handling /// - WebRTC-based real-time communication @@ -27,7 +27,7 @@ class VapiMobileClient implements VapiClientInterface { final String apiBaseUrl; /// Creates a new mobile Vapi client. - /// + /// /// [publicKey] is required for API authentication. /// [apiBaseUrl] defaults to the production Vapi API. VapiMobileClient({ @@ -59,21 +59,18 @@ class VapiMobileClient implements VapiClientInterface { assistantOverrides: assistantOverrides, ); - final client = await _createClientWithRetries(clientCreationTimeoutDuration); + final client = + await _createClientWithRetries(clientCreationTimeoutDuration); try { - return await VapiMobileCall.create( - client, - apiResponse, - waitUntilActive: waitUntilActive - ); + return await VapiMobileCall.create(client, apiResponse, + waitUntilActive: waitUntilActive); } catch (e) { client.dispose(); rethrow; } } - @override void dispose() { // Mobile client doesn't hold persistent resources @@ -81,16 +78,16 @@ class VapiMobileClient implements VapiClientInterface { } /// Requests microphone permission from the user. - /// + /// /// On mobile platforms, microphone access requires explicit user permission. /// This method handles the permission request flow and guides users to /// app settings if permission is permanently denied. Future _requestMicrophonePermission() async { var microphoneStatus = await Permission.microphone.request(); - + if (microphoneStatus.isDenied) { microphoneStatus = await Permission.microphone.request(); - + if (microphoneStatus.isPermanentlyDenied) { await openAppSettings(); return; @@ -99,7 +96,7 @@ class VapiMobileClient implements VapiClientInterface { } /// Creates a call on Vapi servers and returns the full API response. - /// + /// /// This method handles the HTTP request to create a new call session /// and validates the response before returning. Future> _createVapiCall({ @@ -114,14 +111,13 @@ class VapiMobileClient implements VapiClientInterface { }; final assistantConfig = AssistantConfig( - assistantId: assistantId, - assistant: assistant, - assistantOverrides: assistantOverrides - ); + assistantId: assistantId, + assistant: assistant, + assistantOverrides: assistantOverrides); final response = await http.post( - url, - headers: headers, + url, + headers: headers, body: jsonEncode(assistantConfig.createRequestBody()), ); @@ -138,7 +134,7 @@ class VapiMobileClient implements VapiClientInterface { } /// Creates a Daily CallClient with retry logic. - /// + /// /// Network conditions and device states can cause client creation to fail. /// This method implements exponential backoff retry logic to handle /// transient failures gracefully. @@ -149,7 +145,8 @@ class VapiMobileClient implements VapiClientInterface { for (int attempt = 1; attempt <= maxRetries; attempt++) { try { - final client = await _createClientWithTimeout(clientCreationTimeoutDuration); + final client = + await _createClientWithTimeout(clientCreationTimeoutDuration); return client; } catch (error) { if (attempt >= maxRetries) { @@ -163,7 +160,7 @@ class VapiMobileClient implements VapiClientInterface { } /// Creates a CallClient with a timeout. - /// + /// /// Client creation can hang in poor network conditions. /// This method ensures creation fails fast if it takes too long. Future _createClientWithTimeout(Duration timeout) async { @@ -181,11 +178,11 @@ class VapiMobileClient implements VapiClientInterface { throw VapiStartCallException(error); } } -} +} -/// Common interface for retrieving the implementation +/// Common interface for retrieving the implementation /// so conditional imports can be used. -/// +/// /// [publicKey] is required for API authentication. /// [apiBaseUrl] defaults to the production Vapi API. getImplementation({ diff --git a/lib/src/platform/web/vapi_js_interop.dart b/lib/src/platform/web/vapi_js_interop.dart index 90c712d..0820411 100644 --- a/lib/src/platform/web/vapi_js_interop.dart +++ b/lib/src/platform/web/vapi_js_interop.dart @@ -1,7 +1,7 @@ import 'dart:js_interop'; -/// Extension type wrapper for the JavaScript Vapi instance -/// +/// Extension type wrapper for the JavaScript Vapi instance +/// /// This provides Dart bindings for the @vapi-ai/web package @JS("VapiEsModule.default") extension type VapiJs._(JSObject _) implements JSObject { @@ -9,7 +9,8 @@ extension type VapiJs._(JSObject _) implements JSObject { external factory VapiJs(String apiKey, [JSAny? apiBaseUrl]); /// Start a call with assistant configuration - external JSPromise start(JSAny assistantConfig, [JSObject? overrides]); + external JSPromise start(JSAny assistantConfig, + [JSObject? overrides]); /// Stop the current call external void stop(); diff --git a/lib/src/platform/web/vapi_web_call.dart b/lib/src/platform/web/vapi_web_call.dart index f702498..18a3756 100644 --- a/lib/src/platform/web/vapi_web_call.dart +++ b/lib/src/platform/web/vapi_web_call.dart @@ -13,10 +13,10 @@ import '../../shared/vapi_call_transport.dart'; import 'vapi_js_interop.dart'; /// Web-specific implementation of a Vapi call using the Vapi Web SDK. -/// +/// /// This implementation handles real-time voice communication in web browsers /// using the Vapi Web SDK's built-in WebRTC capabilities. -/// +/// /// Features: /// - Browser-native WebRTC communication /// - Automatic permission handling @@ -53,45 +53,45 @@ class VapiWebCall implements VapiCall { /// Unique identifier for this call. @override final String id; - + /// Organization ID associated with this call. @override final String orgId; - + /// Timestamp when this call was created. @override final DateTime createdAt; - + /// Timestamp when this call was last updated. @override final DateTime updatedAt; - + /// Type of the call (e.g., "webCall"). @override final String type; - + /// Monitor configuration for this call. @override final VapiCallMonitor monitor; - + /// Transport configuration for this call. @override final VapiCallTransport transport; - + /// Web call URL for joining the call. @override final String webCallUrl; - + /// ID of the assistant handling this call. @override final String assistantId; - + /// Assistant configuration overrides for this call. @override final Map assistantOverrides; /// Creates a new VapiWebCall instance. - /// + /// /// This constructor is typically called internally by [VapiWebClient.start]. VapiWebCall._( this._vapiJs, @@ -110,7 +110,7 @@ class VapiWebCall implements VapiCall { } /// Factory method to create a VapiWebCall with JavaScript call data. - /// + /// /// This method parses the JavaScript call object, sets up event listeners, /// and optionally waits for the call to become active. static Future create( @@ -118,20 +118,23 @@ class VapiWebCall implements VapiCall { AssistantConfig assistantConfig, { bool waitUntilActive = false, }) async { - - vapiJs.on("listening", (JSAny? data) { - print("here"); - }.toJS); + // Listen for the 'listening' event to track connection status + vapiJs.on( + "listening", + (JSAny? data) { + // Connection established + } + .toJS); final assistantValue = assistantConfig.getAssistantValue(); final jsAssistantValue = assistantValue as JSAny; late final JSObject jsCallData; try { - jsCallData = await vapiJs.start( - jsAssistantValue, - assistantConfig.assistantOverrides.jsify() as JSObject - ).toDart; + jsCallData = await vapiJs + .start(jsAssistantValue, + assistantConfig.assistantOverrides.jsify() as JSObject) + .toDart; } catch (e) { // nothing we can do here apparently the future completes with an // error (see next line) and to catch it we have to wrap the await. @@ -142,7 +145,7 @@ class VapiWebCall implements VapiCall { final callDataTmp = jsCallData.dartify() as Map; final callData = Map.from(callDataTmp); - + final id = callData['id']; final orgId = callData['orgId']; final createdAt = DateTime.tryParse(callData['createdAt'])!; @@ -154,7 +157,8 @@ class VapiWebCall implements VapiCall { final transport = VapiCallTransport.fromJson(transportTmp); final webCallUrl = callData['webCallUrl']; final assistantId = callData['assistantId']; - final assistantOverrides = Map.from(callData['assistantOverrides']); + final assistantOverrides = + Map.from(callData['assistantOverrides']); final call = VapiWebCall._( vapiJs, @@ -177,7 +181,7 @@ class VapiWebCall implements VapiCall { } /// This method manages the call lifecycle state. - void _eventListener(String event, [JSAny? data]) { + void _eventListener(String event, [JSAny? data]) { switch (event) { case 'call-start': _status = VapiCallStatus.active; @@ -199,7 +203,8 @@ class VapiWebCall implements VapiCall { break; case 'daily-participant-updated': - final updatedParticipant = Map.from(data.dartify() as Map); + final updatedParticipant = + Map.from(data.dartify() as Map); _emit(VapiEvent('daily-participant-updated', updatedParticipant)); break; @@ -209,12 +214,14 @@ class VapiWebCall implements VapiCall { break; case 'message': - final message = Map.from(data.dartify() as Map); + final message = + Map.from(data.dartify() as Map); _emit(VapiEvent('message', message)); break; case 'error': - final error = Map.from(data.dartify() as Map); + final error = + Map.from(data.dartify() as Map); _emit(VapiEvent('error', error)); break; } @@ -235,7 +242,8 @@ class VapiWebCall implements VapiCall { /// Sets up event listeners for the Vapi Web SDK. void _setupEventListeners() { - _vapiJs.on('daily-participant-updated', _wrapEventListener('daily-participant-updated')); + _vapiJs.on('daily-participant-updated', + _wrapEventListener('daily-participant-updated')); _vapiJs.on('call-start', _wrapEventListener('call-start')); _vapiJs.on('call-end', _wrapEventListener('call-end')); _vapiJs.on('speech-start', _wrapEventListener('speech-start')); @@ -280,34 +288,36 @@ class VapiWebCall implements VapiCall { if (_status == VapiCallStatus.ended || _isDisposed) { throw const VapiCallEndedException(); } - + try { _vapiJs.setMuted(muted); _isMuted = muted; } catch (e) { - throw VapiException('Failed to set mute state - unknown error occurred', e); + throw VapiException( + 'Failed to set mute state - unknown error occurred', e); } } /// Sets the audio output device for this call using Vapi's audio device enum - /// + /// /// Audio device management is typically handled by the browser /// This is a no-op on web platforms as audio routing is managed /// by the browser's media system and user preferences @override void setAudioDevice({VapiAudioDevice? device}) { - throw UnimplementedError('Audio device management is typically handled by the browser - this is a no-op on web platforms'); + throw UnimplementedError( + 'Audio device management is typically handled by the browser - this is a no-op on web platforms'); } @override void dispose() { if (_isDisposed) return; - + _isDisposed = true; _status = VapiCallStatus.ended; - + _streamController.close(); - + if (!_activeCallCompleter.isCompleted) { _activeCallCompleter.completeError(const VapiCallEndedException()); } diff --git a/lib/src/platform/web/vapi_web_client.dart b/lib/src/platform/web/vapi_web_client.dart index 0b15a25..5932b2c 100644 --- a/lib/src/platform/web/vapi_web_client.dart +++ b/lib/src/platform/web/vapi_web_client.dart @@ -13,10 +13,10 @@ import 'vapi_web_call.dart'; import 'vapi_js_interop.dart'; /// Web-specific implementation of the Vapi client. -/// +/// /// This implementation uses the Vapi Web SDK (@vapi-ai/web) through JavaScript interop /// for browser-based real-time communication. -/// +/// /// Features: /// - Browser-native WebRTC through Vapi Web SDK /// - Automatic browser permission handling @@ -44,16 +44,17 @@ class VapiWebClient implements VapiClientInterface { /// Called by Flutter's web plugin registrant at startup. static void registerWith(Registrar registrar) { if (!_scriptInjected) { - final cdnUrlJs = '$_cdnUrl'.toJS; + final cdnUrlJs = _cdnUrl.toJS; final modulePromise = importModule(cdnUrlJs).toDart; - + modulePromise.then((module) { final esModule = module.getProperty('default'.toJS); final moduleName = 'VapiEsModule'.toJS; globalContext.setProperty(moduleName, esModule); _scriptLoadedCompleter.complete(); }).catchError((e) { - final error = VapiClientCreationError('Failed to load Vapi Web SDK: $e'); + final error = + VapiClientCreationError('Failed to load Vapi Web SDK: $e'); _scriptLoadedCompleter.completeError(error); }); @@ -62,7 +63,7 @@ class VapiWebClient implements VapiClientInterface { } /// Creates a new web Vapi client. - /// + /// /// [publicKey] is required for API authentication. /// [apiBaseUrl] is not used in web implementation as the Vapi Web SDK /// handles API communication internally. @@ -75,7 +76,8 @@ class VapiWebClient implements VapiClientInterface { } if (!_scriptInjected || !_scriptLoadedCompleter.isCompleted) { - throw VapiClientCreationError('Vapi Web SDK script not loaded - injection status: $_scriptInjected, completion status: ${_scriptLoadedCompleter.isCompleted}'); + throw VapiClientCreationError( + 'Vapi Web SDK script not loaded - injection status: $_scriptInjected, completion status: ${_scriptLoadedCompleter.isCompleted}'); } try { @@ -94,10 +96,9 @@ class VapiWebClient implements VapiClientInterface { bool waitUntilActive = false, }) async { final assistantConfig = AssistantConfig( - assistantId: assistantId, - assistant: assistant, - assistantOverrides: assistantOverrides - ); + assistantId: assistantId, + assistant: assistant, + assistantOverrides: assistantOverrides); try { return VapiWebCall.create( @@ -121,11 +122,11 @@ class VapiWebClient implements VapiClientInterface { // Nothing we can do here } } -} +} -/// Common interface for retrieving the implementation +/// Common interface for retrieving the implementation /// so conditional imports can be used. -/// +/// /// [publicKey] is required for API authentication. /// [apiBaseUrl] defaults to the production Vapi API. getImplementation({ diff --git a/lib/src/shared/errors.dart b/lib/src/shared/errors.dart index f529570..44702af 100644 --- a/lib/src/shared/errors.dart +++ b/lib/src/shared/errors.dart @@ -27,7 +27,7 @@ class VapiError extends Error { } } -/// Error thrown when the Vapi client cannot be created +/// Error thrown when the Vapi client cannot be created /// /// This is most likely due to platform specific issues class VapiClientCreationError extends VapiError { diff --git a/lib/src/shared/exceptions.dart b/lib/src/shared/exceptions.dart index 1498c13..4107912 100644 --- a/lib/src/shared/exceptions.dart +++ b/lib/src/shared/exceptions.dart @@ -1,13 +1,13 @@ /// Base exception class for all Vapi-related errors. -/// +/// /// This exception provides a consistent interface for handling errors /// that occur during Vapi operations. class VapiException implements Exception { /// A human-readable description of the error. final String message; - + /// Optional additional details about the error. - /// + /// /// This field may contain error objects, stack traces, or other /// debugging information depending on the specific error type. final dynamic details; @@ -16,46 +16,49 @@ class VapiException implements Exception { const VapiException(this.message, [this.details]); @override - String toString() => 'VapiException: $message${details != null ? '\nDetails: $details' : ''}'; + String toString() => + 'VapiException: $message${details != null ? '\nDetails: $details' : ''}'; } /// Thrown when neither assistantId nor assistant is provided to the start method. -/// +/// /// At least one of these parameters must be provided to start a call: /// - `assistantId`: ID of a pre-configured assistant /// - `assistant`: Inline assistant configuration object class VapiMissingAssistantException extends VapiException { - const VapiMissingAssistantException() : super('Either assistantId or assistant must be provided'); + const VapiMissingAssistantException() + : super('Either assistantId or assistant must be provided'); } /// Thrown when the client is configured with invalid parameters. -/// +/// /// This includes cases like empty public keys, invalid URLs, or /// other configuration errors that prevent proper operation. class VapiConfigurationException extends VapiException { - const VapiConfigurationException(String message) : super(message); + const VapiConfigurationException(super.message); } /// Thrown when the call client fails to start or join the call session. -/// +/// /// This can happen due to network issues, invalid call URLs, or /// other connectivity problems. class VapiStartCallException extends VapiException { - const VapiStartCallException([dynamic details]) : super('Failed to start call', details); + const VapiStartCallException([dynamic details]) + : super('Failed to start call', details); } /// Thrown when attempting to start a call while another call is already in progress. -/// +/// /// Only one call can be active at a time. Stop the current call before starting a new one. class VapiCallInProgressException extends VapiException { const VapiCallInProgressException() : super('Call already in progress'); } /// Thrown when attempting to perform an operation on a call that has ended. -/// +/// /// This exception is thrown when calling methods like [VapiCall.send], [VapiCall.stop], /// [VapiCall.setMuted], [VapiCall.isMuted], or [VapiCall.setVapiAudioDevice] on /// a call that is no longer active. class VapiCallEndedException extends VapiException { const VapiCallEndedException() : super('Call has ended'); -} +} diff --git a/lib/src/shared/shared.dart b/lib/src/shared/shared.dart index a9da7aa..10bac02 100644 --- a/lib/src/shared/shared.dart +++ b/lib/src/shared/shared.dart @@ -1,5 +1,5 @@ /// Vapi Flutter SDK Shared Classes -/// +/// /// This library exports all the shared classes used by the Vapi Flutter SDK, /// including events, exceptions, enums, and interfaces. library shared; diff --git a/lib/src/shared/vapi_audio_device.dart b/lib/src/shared/vapi_audio_device.dart index 286bc0f..19461b1 100644 --- a/lib/src/shared/vapi_audio_device.dart +++ b/lib/src/shared/vapi_audio_device.dart @@ -1,39 +1,39 @@ /// Available audio output devices for Vapi calls. -/// +/// /// This enum provides a platform-agnostic way to specify which audio /// output device should be used during a call, without requiring /// direct dependency on the daily_flutter package. -/// +/// /// Example usage: /// ```dart /// // Switch to speakerphone during a call /// vapi.setVapiAudioDevice(device: VapiAudioDevice.speakerphone); -/// +/// /// // Switch to Bluetooth headset /// vapi.setVapiAudioDevice(device: VapiAudioDevice.bluetooth); /// ``` enum VapiAudioDevice { /// Use the device's built-in speakerphone. - /// + /// /// This routes audio through the device's main speaker, /// allowing hands-free operation. speakerphone, - + /// Use wired headphones or earphones. - /// + /// /// This routes audio through connected wired audio devices /// like headphones, earphones, or aux cables. wired, - + /// Use the device's earpiece speaker. - /// + /// /// This routes audio through the small speaker typically /// used for phone calls, providing private audio output. earpiece, - + /// Use connected Bluetooth audio devices. - /// + /// /// This routes audio through paired Bluetooth headphones, /// earbuds, or other Bluetooth audio devices. bluetooth, -} +} diff --git a/lib/src/shared/vapi_call_monitor.dart b/lib/src/shared/vapi_call_monitor.dart index 71ad735..0c84966 100644 --- a/lib/src/shared/vapi_call_monitor.dart +++ b/lib/src/shared/vapi_call_monitor.dart @@ -1,10 +1,10 @@ /// Represents the monitor configuration for a Vapi call. -/// +/// /// Contains URLs for monitoring and controlling the call. class VapiCallMonitor { /// WebSocket URL for listening to call events. final String listenUrl; - + /// HTTP URL for controlling the call. final String controlUrl; diff --git a/lib/src/shared/vapi_call_status.dart b/lib/src/shared/vapi_call_status.dart index b36188c..9273f77 100644 --- a/lib/src/shared/vapi_call_status.dart +++ b/lib/src/shared/vapi_call_status.dart @@ -1,5 +1,5 @@ /// Represents the current status of a Vapi call. -/// +/// /// The call progresses through these states: /// - [starting]: Call is being initialized and connecting /// - [active]: Call is connected and ready for interaction @@ -7,10 +7,10 @@ enum VapiCallStatus { /// Call is being initialized and connecting to the assistant. starting, - + /// Call is connected and ready for interaction with the assistant. active, - + /// Call has been terminated or disconnected. ended, -} +} diff --git a/lib/src/shared/vapi_call_transport.dart b/lib/src/shared/vapi_call_transport.dart index fbba4da..27ddedf 100644 --- a/lib/src/shared/vapi_call_transport.dart +++ b/lib/src/shared/vapi_call_transport.dart @@ -1,13 +1,13 @@ /// Represents the transport configuration for a Vapi call. -/// +/// /// Contains information about the call transport provider and settings. class VapiCallTransport { /// The transport provider (e.g., "daily"). final String provider; - + /// Whether assistant video is enabled for this call. final bool assistantVideoEnabled; - + /// The call URL for the transport provider. final String callUrl; diff --git a/lib/src/shared/vapi_event.dart b/lib/src/shared/vapi_event.dart index dadf70e..f07b730 100644 --- a/lib/src/shared/vapi_event.dart +++ b/lib/src/shared/vapi_event.dart @@ -1,14 +1,14 @@ /// Represents an event that occurs during the Vapi call lifecycle. -/// +/// /// Events are emitted by the Vapi client to inform your application /// about important state changes and messages during a call. -/// +/// /// Common event types include: /// - `call-start`: Emitted when the assistant connects and starts listening /// - `call-end`: Emitted when the call ends /// - `call-error`: Emitted when an error occurs during call setup /// - `message`: Emitted when a message is received from the assistant -/// +/// /// Example usage: /// ```dart /// vapi.onEvent.listen((event) { @@ -27,13 +27,13 @@ /// ``` class VapiEvent { /// The type/label of the event that occurred. - /// + /// /// This string identifies what kind of event happened and helps /// your application respond appropriately. final String label; - + /// Optional data associated with the event. - /// + /// /// The type and structure of this value depends on the event type: /// - For `message` events, this contains the parsed message data /// - For error events, this may contain error details @@ -44,11 +44,10 @@ class VapiEvent { const VapiEvent(this.label, [this.value]); /// Creates a VapiEvent from a message received during a call. - /// + /// /// This factory method is used to create message events from incoming /// data during a call session. factory VapiEvent.fromMessage(Map message) { return VapiEvent('message', message); } } - \ No newline at end of file diff --git a/lib/src/vapi_call_interface.dart b/lib/src/vapi_call_interface.dart index 7be32a6..3b448a4 100644 --- a/lib/src/vapi_call_interface.dart +++ b/lib/src/vapi_call_interface.dart @@ -6,62 +6,62 @@ import 'shared/vapi_call_monitor.dart'; import 'shared/vapi_call_transport.dart'; /// Abstract interface defining the contract for Vapi call implementations. -/// +/// /// This interface ensures consistent call management behavior across different /// platforms while allowing platform-specific optimizations and features. -/// +/// /// Each platform implementation handles the underlying communication /// (WebRTC, WebSocket, etc.) while exposing a unified API. abstract interface class VapiCall { // Core call metadata - available on all platforms - + /// Unique identifier for this call session. String get id; - + /// ID of the assistant handling this call. String get assistantId; /// Assistant configuration overrides for this call. Map get assistantOverrides; - + /// Organization ID associated with this call. String get orgId; - + /// Timestamp when this call was created. DateTime get createdAt; - + /// Timestamp when this call was last updated. DateTime get updatedAt; - + /// Current status of the call (starting, active, ended). VapiCallStatus get status; /// Type of the call (e.g., "webCall"). String get type; - + /// Monitor configuration for this call. VapiCallMonitor get monitor; - + /// Transport configuration for this call. VapiCallTransport get transport; - + /// Web call URL for joining the call. String get webCallUrl; // Audio control functionality - + /// Whether the user's microphone is currently muted. bool get isMuted; - + /// Mutes or unmutes the user's microphone. - /// + /// /// [muted] - true to mute, false to unmute void setMuted(bool muted); - + // Core call functionality - + /// Stream of events that occur during the call lifecycle. - /// + /// /// Events include: /// - `call-start`: When the assistant connects and starts listening /// - `call-end`: When the call terminates (any reason) @@ -70,37 +70,37 @@ abstract interface class VapiCall { /// - `volume-level`: Real-time volume updates /// - `error`: Error notifications Stream get onEvent; - + /// Sends a message to the assistant during the call. - /// + /// /// [message] should contain appropriate `type`, `role`, and `content` fields. /// Common message types include `add-message` for injecting system messages. - /// + /// /// Throws [VapiException] if the message cannot be sent. Future send(Map message); - + /// Stops the call and terminates the session. - /// + /// /// This triggers a `call-end` event and releases call resources. /// After calling stop, no further operations can be performed on this call. Future stop(); - + /// Releases resources associated with this call. - /// + /// /// Should be called after the call ends to prevent memory leaks. /// This is separate from [stop] to allow cleanup of ended calls. void dispose(); - + // Platform-specific functionality (may be no-op on some platforms) - + /// Sets the audio device for the call. - /// + /// /// This is primarily used on mobile platforms where multiple audio /// devices (speaker, earpiece, bluetooth) are available. - /// + /// /// On web platforms, this may be a no-op as audio routing is /// typically handled by the browser. - /// + /// /// [device] - The audio device to use for the call void setAudioDevice({VapiAudioDevice? device}); } diff --git a/lib/src/vapi_client.dart b/lib/src/vapi_client.dart index 19916a1..6f9ba7e 100644 --- a/lib/src/vapi_client.dart +++ b/lib/src/vapi_client.dart @@ -6,42 +6,41 @@ import 'shared/exceptions.dart'; // Import implementations directly - tree shaking will handle unused code -import 'platform/mobile/vapi_mobile_client.dart' - if (dart.library.js_interop) 'platform/web/vapi_web_client.dart'; +import 'platform/mobile/vapi_mobile_client.dart' + if (dart.library.js_interop) 'platform/web/vapi_web_client.dart'; /// Factory class for creating platform-specific Vapi clients. -/// +/// /// This class serves as the main entry point for the Vapi SDK. /// It automatically selects the appropriate implementation based on the platform. -/// +/// /// Example usage: /// ```dart /// final client = VapiClient('your-public-key'); -/// +/// /// // Start a call /// final call = await client.start(assistantId: 'assistant-id'); -/// +/// /// // Listen to events /// call.onEvent.listen((event) { /// print('Event: ${event.label}'); /// }); -/// +/// /// // Clean up /// call.dispose(); /// client.dispose(); /// ``` class VapiClient implements VapiClientInterface { - /// Whether the underlying platform has been initialized. - /// - /// This is useful to check if the client is ready to be used. - /// For example, when a [VapiClientCreationError] is thrown, + /// + /// This is useful to check if the client is ready to be used. + /// For example, when a [VapiClientCreationError] is thrown, /// the platform might not been initialized yet. - /// - /// Note: This completer can also complete with [VapiClientCreationError] + /// + /// Note: This completer can also complete with [VapiClientCreationError] /// if there is an error initializing the platform SDK. static Completer get platformInitialized => getPlatformInitialized(); - + /// The platform-specific implementation final VapiClientInterface _implementation; @@ -49,13 +48,13 @@ class VapiClient implements VapiClientInterface { VapiClient._(this._implementation); /// Creates a new Vapi client instance. - /// + /// /// The appropriate implementation (web or mobile) is automatically selected /// based on the platform. - /// + /// /// [publicKey] is required for API authentication. /// [apiBaseUrl] is optional and defaults to the production Vapi API. - /// + /// /// Throws [VapiConfigurationException] if the public key is invalid. factory VapiClient( String publicKey, { @@ -67,7 +66,7 @@ class VapiClient implements VapiClientInterface { return VapiClient._( getImplementation( - publicKey: publicKey, + publicKey: publicKey, apiBaseUrl: apiBaseUrl, ), ); @@ -82,17 +81,17 @@ class VapiClient implements VapiClientInterface { String get apiBaseUrl => _implementation.apiBaseUrl; /// Starts a voice AI call with the specified assistant. - /// + /// /// Either [assistantId] or [assistant] must be provided: /// - [assistantId]: ID of a pre-configured assistant /// - [assistant]: Inline assistant configuration object - /// + /// /// [assistantOverrides] allows you to override assistant settings for this call. /// [waitUntilActive] determines whether to wait until the call is active before returning. /// When true, the method will wait for the assistant to start listening before returning. - /// + /// /// Returns a [VapiCall] instance that can be used to interact with the call. - /// + /// /// Throws: /// - [VapiMissingAssistantException] if neither assistantId nor assistant is provided /// - [VapiJoinFailedException] if joining the call fails @@ -117,7 +116,7 @@ class VapiClient implements VapiClientInterface { } /// Disposes of any resources held by the client. - /// + /// /// This should be called when the client is no longer needed. @override void dispose() { @@ -125,7 +124,7 @@ class VapiClient implements VapiClientInterface { } /// Returns a string representation of the client. - /// + /// /// This is useful for debugging and logging. @override String toString() { diff --git a/lib/src/vapi_client_interface.dart b/lib/src/vapi_client_interface.dart index 0ed0c3e..8b61333 100644 --- a/lib/src/vapi_client_interface.dart +++ b/lib/src/vapi_client_interface.dart @@ -3,36 +3,35 @@ import 'vapi_call_interface.dart'; const String defaultApiBaseUrl = 'https://api.vapi.ai'; /// Abstract interface defining the contract for Vapi client implementations. -/// +/// /// This interface ensures consistent behavior across different platforms /// (mobile, web, desktop) while allowing platform-specific optimizations. -/// +/// /// All platform-specific implementations must conform to this interface, /// guaranteeing a unified API experience for developers. abstract interface class VapiClientInterface { - /// The public API key used for authentication with Vapi services. - /// + /// /// This key is provided by Vapi and identifies your application. String get publicKey; /// The base URL for the Vapi API. - /// + /// /// Defaults to the production Vapi API but can be overridden for /// testing or custom deployments. String get apiBaseUrl; /// Starts a voice AI call with the specified configuration. - /// + /// /// Either [assistantId] or [assistant] must be provided: /// - [assistantId]: ID of a pre-configured assistant from your Vapi dashboard /// - [assistant]: Inline assistant configuration for ephemeral assistants - /// + /// /// [assistantOverrides] allows overriding assistant settings or setting template variables. /// [waitUntilActive] determines whether to wait until the call becomes active before returning. - /// + /// /// Returns a [VapiCall] instance for interacting with the call. - /// + /// /// Throws [VapiException] if the call cannot be started. Future start({ String? assistantId, @@ -43,7 +42,7 @@ abstract interface class VapiClientInterface { }); /// Releases any resources held by this client. - /// + /// /// Should be called when the client is no longer needed to prevent /// memory leaks and ensure proper cleanup. void dispose(); diff --git a/lib/vapi.dart b/lib/vapi.dart index 8a6ab62..344da9f 100644 --- a/lib/vapi.dart +++ b/lib/vapi.dart @@ -1,22 +1,22 @@ /// Vapi Flutter SDK with unified mobile and web support -/// +/// /// A Flutter SDK for integrating with the Vapi voice AI platform. -/// +/// /// This library provides a simple and intuitive interface for adding /// voice AI capabilities to your Flutter applications across multiple platforms. -/// +/// /// ## Getting Started -/// +/// /// 1. Create a VapiClient instance with your public API key: /// ```dart /// final vapiClient = VapiClient('your-public-key-here'); /// ``` -/// +/// /// 2. Start a call and get a VapiCall instance: /// ```dart /// final call = await vapiClient.start(assistantId: 'your-assistant-id'); /// ``` -/// +/// /// 3. Access call information: /// ```dart /// print('Call ID: ${call.id}'); @@ -24,7 +24,7 @@ /// print('Created at: ${call.createdAt}'); /// print('Status: ${call.status}'); /// ``` -/// +/// /// 4. Listen for events on the call: /// ```dart /// call.onEvent.listen((event) { @@ -41,43 +41,43 @@ /// } /// }); /// ``` -/// +/// /// 5. Interact during the call: /// ```dart /// // Send a message /// await call.send({'type': 'user-message', 'message': 'Hello'}); -/// +/// /// // Mute/unmute /// call.setMuted(true); -/// +/// /// // Change audio device (mobile only) /// if (call is VapiMobileCall) { /// call.setAudioDevice(device: VapiAudioDevice.speakerphone); /// } -/// +/// /// // Check if call is still active /// if (call.status == VapiCallStatus.active) { /// // Call is still ongoing /// } /// ``` -/// +/// /// 6. End the call and clean up: /// ```dart /// await call.stop(); /// call.dispose(); /// client.dispose(); /// ``` -/// +/// /// ## Architecture -/// +/// /// The SDK uses a clean factory pattern with platform-specific implementations: -/// +/// /// - **VapiClient**: Factory class that creates platform-appropriate implementations /// - **VapiClientInterface**: Abstract interface for all client implementations /// - **VapiCallInterface**: Abstract interface for all call implementations /// - **VapiMobileClient/VapiMobileCall**: Mobile implementation using Daily.co WebRTC /// - **VapiWebClient/VapiWebCall**: Web implementation using Vapi Web SDK (coming soon) -/// +/// /// This architecture allows for: /// - Automatic platform detection and selection /// - Platform-specific optimizations @@ -85,9 +85,9 @@ /// - Easy addition of new platforms /// - Type-safe platform-specific features /// - Consistent API across platforms -/// +/// /// ## Available Types -/// +/// /// The SDK exports the following types for your use: /// - [VapiClient] - The main factory class for creating platform-appropriate clients /// - [VapiClientInterface] - Interface for client implementations diff --git a/scripts/get_pub_credentials.sh b/scripts/get_pub_credentials.sh new file mode 100755 index 0000000..39216cb --- /dev/null +++ b/scripts/get_pub_credentials.sh @@ -0,0 +1,62 @@ +#!/bin/bash + +# Define credential locations +MACOS_CREDS="$HOME/Library/Application Support/dart/pub-credentials.json" +LINUX_CREDS="$HOME/.pub-cache/credentials.json" + +# Function to display credentials +display_credentials() { + local creds_file=$1 + local location_name=$2 + + echo "Found credentials at $location_name location" + echo "Your pub.dev credentials:" + echo "=========================" + cat "$creds_file" + echo "" + echo "=========================" + echo "" + echo "Copy the ENTIRE content above (including the curly braces)" + echo "and add it as a GitHub secret named 'PUB_CREDENTIALS'" +} + +# Function to find and display credentials +find_credentials() { + if [ -f "$MACOS_CREDS" ]; then + display_credentials "$MACOS_CREDS" "macOS" + return 0 + elif [ -f "$LINUX_CREDS" ]; then + display_credentials "$LINUX_CREDS" "Linux/default" + return 0 + else + return 1 + fi +} + +# Main script +echo "This script helps you obtain your pub.dev credentials for GitHub Actions" +echo "=========================================================================" +echo "" + +# Try to find existing credentials +if find_credentials; then + exit 0 +fi + +# No credentials found, run login +echo "Credentials not found. Running 'flutter pub login' now..." +echo "" +flutter pub login + +# Check again after login +echo "" +echo "Checking for credentials after login..." +echo "" + +if find_credentials; then + exit 0 +else + echo "ERROR: Could not find credentials after login. Something went wrong." + echo "Please try running 'flutter pub login' manually." + exit 1 +fi