Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions modules/ensemble/lib/action/action_invokable.dart
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ abstract class ActionInvokable with Invokable {
ActionType.disconnectSSE,
ActionType.openFaceCamera,
ActionType.executeAction,
ActionType.connectToWifi,
]);
}

Expand Down
97 changes: 97 additions & 0 deletions modules/ensemble/lib/action/wifi_action.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import 'package:ensemble/framework/action.dart';
import 'package:ensemble/framework/event.dart';
import 'package:ensemble/framework/scope.dart';
import 'package:ensemble/framework/stub/wifi_manager.dart';
import 'package:ensemble/screen_controller.dart';
import 'package:ensemble/util/utils.dart';
import 'package:ensemble_ts_interpreter/invokables/invokable.dart';
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';

class ConnectToWifiAction extends EnsembleAction {
ConnectToWifiAction({
super.initiator,
required this.ssid,
required this.password,
this.joinOnce,
this.rememberNetwork,
this.onSuccess,
this.onError,
});

final dynamic ssid;
final dynamic password;
final dynamic joinOnce;
final dynamic rememberNetwork;
final EnsembleAction? onSuccess;
final EnsembleAction? onError;

factory ConnectToWifiAction.fromYaml({Invokable? initiator, Map? payload}) {
if (payload == null || payload['ssid'] == null) {
throw Exception("connectToWifi requires 'ssid' parameter.");
}

return ConnectToWifiAction(
initiator: initiator,
ssid: payload['ssid'],
password: payload['password'] ?? '',
joinOnce: payload['joinOnce'],
rememberNetwork: payload['rememberNetwork'],
onSuccess:
EnsembleAction.from(payload['onSuccess'], initiator: initiator),
onError: EnsembleAction.from(payload['onError'], initiator: initiator),
);
}

@override
Future<void> execute(BuildContext context, ScopeManager scopeManager) async {
try {
final wifiManager = GetIt.I<WifiManager>();

final evaluatedSsid =
Utils.getString(scopeManager.dataContext.eval(ssid), fallback: '');
final evaluatedPassword =
Utils.getString(scopeManager.dataContext.eval(password), fallback: '');
final evaluatedJoinOnce =
Utils.optionalBool(scopeManager.dataContext.eval(joinOnce)) ?? false;
final evaluatedRememberNetwork =
Utils.optionalBool(scopeManager.dataContext.eval(rememberNetwork)) ??
true;

final result = await wifiManager.connect(
ssid: evaluatedSsid,
password: evaluatedPassword,
joinOnce: evaluatedJoinOnce,
rememberNetwork: evaluatedRememberNetwork,
);

if (result.success && onSuccess != null) {
await ScreenController().executeAction(
context,
onSuccess!,
event: EnsembleEvent(initiator, data: {
'status': result.status,
'message': result.message,
}),
);
} else if (!result.success && onError != null) {
await ScreenController().executeAction(
context,
onError!,
event: EnsembleEvent(initiator, error: result.message, data: {
'status': result.status,
'platformCode': result.platformCode,
}),
);
}
} catch (e) {
if (onError != null) {
await ScreenController().executeAction(
context,
onError!,
event: EnsembleEvent(initiator, error: e.toString()),
);
}
}
}
}
7 changes: 7 additions & 0 deletions modules/ensemble/lib/framework/action.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import 'package:ensemble/action/secure_storage.dart';
import 'package:ensemble/action/sign_in_out_action.dart';
import 'package:ensemble/action/sign_in_with_verification_code_actions.dart';
import 'package:ensemble/action/stripe_actions.dart';
import 'package:ensemble/action/wifi_action.dart';
import 'package:ensemble/action/toast_actions.dart';
import 'package:ensemble/action/take_screenshot.dart';
import 'package:ensemble/action/disable_hardware_navigation.dart';
Expand Down Expand Up @@ -1059,6 +1060,9 @@ enum ActionType {
// Stripe actions
initializeStripe,
showPaymentSheet,

// Wi-Fi actions
connectToWifi,
}

/// payload representing an Action to do (navigateToScreen, InvokeAPI, ..)
Expand Down Expand Up @@ -1318,6 +1322,9 @@ abstract class EnsembleAction {
} else if (actionType == ActionType.showPaymentSheet) {
return ShowPaymentSheetAction.fromYaml(
initiator: initiator, payload: payload);
} else if (actionType == ActionType.connectToWifi) {
return ConnectToWifiAction.fromYaml(
initiator: initiator, payload: payload);
} else {
throw LanguageError("Invalid action.",
recovery: "Make sure to use one of Ensemble-provided actions.");
Expand Down
37 changes: 37 additions & 0 deletions modules/ensemble/lib/framework/stub/wifi_manager.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import 'package:ensemble/framework/error_handling.dart';

abstract class WifiManager {
Future<WifiConnectResult> connect({
required String ssid,
required String password,
bool joinOnce = false,
bool rememberNetwork = true,
});
}

class WifiManagerStub implements WifiManager {
@override
Future<WifiConnectResult> connect({
required String ssid,
required String password,
bool joinOnce = false,
bool rememberNetwork = true,
}) {
throw ConfigError(
"Wi-Fi module is not enabled. Please review the Ensemble documentation.");
}
}

class WifiConnectResult {
final bool success;
final String status;
final String? message;
final String? platformCode;

const WifiConnectResult({
required this.success,
required this.status,
this.message,
this.platformCode,
});
}
10 changes: 10 additions & 0 deletions modules/ensemble/lib/module/wifi_module.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import 'package:ensemble/framework/stub/wifi_manager.dart';
import 'package:get_it/get_it.dart';

abstract class WifiModule {}

class WifiModuleStub implements WifiModule {
WifiModuleStub() {
GetIt.I.registerSingleton<WifiManager>(WifiManagerStub());
}
}
2 changes: 2 additions & 0 deletions modules/ensemble_wifi/lib/ensemble_wifi.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export 'package:ensemble_wifi/wifi_module.dart';
export 'package:ensemble_wifi/wifi_manager_impl.dart';
26 changes: 26 additions & 0 deletions modules/ensemble_wifi/lib/wifi_manager_impl.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import 'package:ensemble/framework/stub/wifi_manager.dart';
import 'package:smart_wifi_connect/smart_wifi_connect.dart';

class WifiManagerImpl implements WifiManager {
@override
Future<WifiConnectResult> connect({
required String ssid,
required String password,
bool joinOnce = false,
bool rememberNetwork = true,
}) async {
final result = await SmartWifiConnect.connect(
ssid: ssid,
password: password,
joinOnce: joinOnce,
rememberNetwork: rememberNetwork,
);

return WifiConnectResult(
success: result.success,
status: result.status.name,
message: result.message,
platformCode: result.platformCode,
);
}
}
10 changes: 10 additions & 0 deletions modules/ensemble_wifi/lib/wifi_module.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import 'package:ensemble/framework/stub/wifi_manager.dart';
import 'package:ensemble/module/wifi_module.dart';
import 'package:ensemble_wifi/wifi_manager_impl.dart';
import 'package:get_it/get_it.dart';

class WifiModuleImpl implements WifiModule {
WifiModuleImpl() {
GetIt.I.registerSingleton<WifiManager>(WifiManagerImpl());
}
}
28 changes: 28 additions & 0 deletions modules/ensemble_wifi/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: ensemble_wifi
description: Ensemble Wi-Fi connect module
version: 0.1.0
homepage: https://github.com/EnsembleUI/ensemble/tree/main/modules/ensemble_wifi

environment:
sdk: ">=3.5.0"
flutter: ">=3.24.0"

dependencies:
flutter:
sdk: flutter
ensemble:
git:
url: https://github.com/EnsembleUI/ensemble.git
ref: ensemble-v1.2.41
path: modules/ensemble
smart_wifi_connect:
git:
url: https://github.com/EnsembleUI/ensemble.git
ref: main
path: packages/smart_wifi_connect
get_it: ^8.0.0

dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
3 changes: 3 additions & 0 deletions packages/smart_wifi_connect/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## 0.0.1

* TODO: Describe initial release.
1 change: 1 addition & 0 deletions packages/smart_wifi_connect/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
TODO: Add your license here.
106 changes: 106 additions & 0 deletions packages/smart_wifi_connect/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# smart_wifi_connect

A lightweight Flutter plugin that allows apps to connect to a known Wi-Fi network using SSID and password.

## Features

- Connect to a Wi-Fi network by SSID and password
- Structured result with success/failure status
- Platform-appropriate APIs (iOS: `NEHotspotConfigurationManager`, Android: `WifiNetworkSpecifier`)
- No Wi-Fi scanning, no location inference, no background monitoring

## Usage

```dart
import 'package:smart_wifi_connect/smart_wifi_connect.dart';

final result = await SmartWifiConnect.connect(
ssid: 'MyNetwork',
password: 'MyPassword',
joinOnce: false,
rememberNetwork: true,
);

if (result.success) {
print('Connected!');
} else {
print('Failed: ${result.status} - ${result.message}');
}
```

## Parameters

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `ssid` | `String` | required | The Wi-Fi network name |
| `password` | `String` | required | The Wi-Fi password |
| `joinOnce` | `bool` | `false` | If true, the network is session-based (iOS only) |
| `rememberNetwork` | `bool` | `true` | If true, the device remembers the network |

## Status Values

| Status | Description |
|--------|-------------|
| `connected` | Successfully connected |
| `permissionDenied` | Required permissions were denied |
| `userCancelled` | User cancelled the connection prompt |
| `unsupported` | Platform does not support this feature |
| `invalidArguments` | Invalid parameters (e.g. empty SSID) |
| `failed` | Connection failed for another reason |

## Platform Setup

### Android

Add the following permissions to your app's `AndroidManifest.xml`:

```xml
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />

<!-- Android 13+ (API 33+): use NEARBY_WIFI_DEVICES instead of location -->
<uses-permission
android:name="android.permission.NEARBY_WIFI_DEVICES"
android:usesPermissionFlags="neverForLocation" />

<!-- Android 12 and below: location permission required for Wi-Fi APIs -->
<uses-permission
android:name="android.permission.ACCESS_FINE_LOCATION"
android:maxSdkVersion="32" />

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
```

**Why each permission is needed:**

- `CHANGE_WIFI_STATE` — Required to initiate Wi-Fi connections
- `NEARBY_WIFI_DEVICES` — Android 13+ replacement for location-based Wi-Fi access; `neverForLocation` flag ensures no location data is inferred
- `ACCESS_FINE_LOCATION` — Required on Android 12 and below for Wi-Fi connection APIs (capped at SDK 32)
- `INTERNET` / `ACCESS_NETWORK_STATE` — Required to use the connected network

**Minimum Android version:** API 29 (Android 10). On older devices, the plugin returns `unsupported`.

### iOS

Add the **Hotspot Configuration** capability to your app:

1. In Xcode, select your Runner target
2. Go to Signing & Capabilities
3. Click "+" and add **Hotspot Configuration**

This adds the entitlement:

```xml
<key>com.apple.developer.networking.hotspotconfiguration</key>
<true/>
```

iOS will show a native confirmation prompt when connecting. The plugin handles this and returns the appropriate result.

## Security & Privacy

- Wi-Fi passwords are never logged
- No Wi-Fi scanning is performed
- No location data is collected
- `neverForLocation` flag is used on Android 13+
- Permissions are requested only when `connect()` is called
Loading
Loading