diff --git a/.github/workflows/starter-commands.yml b/.github/workflows/starter-commands.yml
new file mode 100644
index 000000000..e1dd7b058
--- /dev/null
+++ b/.github/workflows/starter-commands.yml
@@ -0,0 +1,49 @@
+name: Test Commands in Starter
+
+on:
+ pull_request:
+ branches:
+ - main
+
+jobs:
+ test-modules:
+ runs-on: ubuntu-latest
+ timeout-minutes: 20
+ defaults:
+ run:
+ working-directory: starter
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v2
+
+ - name: Set up Flutter SDK
+ uses: subosito/flutter-action@v2
+ with:
+ channel: 'stable'
+
+ - name: Set up Node.js
+ uses: actions/setup-node@v2
+
+ - name: Install dependencies
+ run: npm install
+
+ - name: Run hasCamera command
+ run: npm run hasCamera platform="ios,android" cameraDescription="Hello world"
+ continue-on-error: false
+
+ - name: Run hasFileManager command
+ run: npm run hasFileManager platform="ios,android" photoLibraryDescription="Hello" musicDescription="world"
+ continue-on-error: false
+
+ - name: Run hasContacts command
+ run: npm run hasContacts contactsDescription="Hello world" platform="ios,android"
+ continue-on-error: false
+
+ - name: Run hasConnect command
+ run: npm run hasConnect platform="ios,android" cameraDescription="Hello world" contactsDescription="Hello world"
+ continue-on-error: false
+
+ - name: Run hasLocation command
+ run: npm run hasLocation platform="ios,android" locationDescription="Hello world" alwaysUseLocationDescription="Hello world" inUseLocationDescription="Hello world"
+ continue-on-error: false
diff --git a/.gitignore b/.gitignore
index 1fa4d8c81..203cb02f4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,6 +8,8 @@ pubspec_overrides.yaml
/starter/ios/Flutter
starter/ios/Runner.xcodeproj/project.pbxproj
/node_modules
+starter/node_modules
+starter/package-lock.json
# FVM Version Cache
-.fvm/
\ No newline at end of file
+.fvm/
diff --git a/starter/android/build.gradle b/starter/android/build.gradle
index e90901803..fff94a155 100644
--- a/starter/android/build.gradle
+++ b/starter/android/build.gradle
@@ -20,10 +20,33 @@ allprojects {
rootProject.buildDir = '../build'
subprojects {
+ afterEvaluate { project ->
+ if (project.extensions.findByName("android") != null) {
+ Integer pluginCompileSdk = project.android.compileSdk
+ if (pluginCompileSdk != null && pluginCompileSdk < 34) {
+
+ def javaVersion = JavaVersion.VERSION_17
+ project.android {
+ compileSdk 34
+ if (namespace == null) {
+ namespace project.group
+ }
+ compileOptions {
+ sourceCompatibility javaVersion
+ targetCompatibility javaVersion
+ }
+ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
+ kotlinOptions {
+ jvmTarget = javaVersion.toString()
+ }
+ }
+ }
+ }
+ }
+ }
+
project.buildDir = "${rootProject.buildDir}/${project.name}"
-}
-subprojects {
- project.evaluationDependsOn(':app')
+ project.evaluationDependsOn(":app")
}
tasks.register("clean", Delete) {
diff --git a/starter/ios/Podfile b/starter/ios/Podfile
index 2dfd12a76..68bab5712 100644
--- a/starter/ios/Podfile
+++ b/starter/ios/Podfile
@@ -34,7 +34,7 @@ target 'Runner' do
use_modular_headers!
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
- pod 'FirebaseFirestore', :git => 'https://github.com/invertase/firestore-ios-sdk-frameworks.git', :tag => '11.4.0'
+ pod 'FirebaseFirestore', :git => 'https://github.com/invertase/firestore-ios-sdk-frameworks.git', :tag => '11.6.0'
target 'RunnerTests' do
inherit! :search_paths
end
diff --git a/starter/ios/Runner/Info.plist b/starter/ios/Runner/Info.plist
index 0b3eedd7f..5a5f1ed2b 100644
--- a/starter/ios/Runner/Info.plist
+++ b/starter/ios/Runner/Info.plist
@@ -71,5 +71,9 @@
filza
activator
+
+
+ ITSAppUsesNonExemptEncryption
+
diff --git a/starter/ios/Runner/Runner.entitlements b/starter/ios/Runner/Runner.entitlements
index c6140582f..96787e073 100644
--- a/starter/ios/Runner/Runner.entitlements
+++ b/starter/ios/Runner/Runner.entitlements
@@ -4,10 +4,10 @@
aps-environment
development
- com.apple.developer.applesignin
-
+
+
com.apple.developer.associated-domains
applinks:app.ensembleui.com
diff --git a/starter/lib/generated/ensemble_modules.dart b/starter/lib/generated/ensemble_modules.dart
index 264850d4a..a27c798ba 100644
--- a/starter/lib/generated/ensemble_modules.dart
+++ b/starter/lib/generated/ensemble_modules.dart
@@ -53,6 +53,10 @@ import 'package:get_it/get_it.dart';
// Uncomment to enable deeplink services
// import 'package:ensemble_deeplink/deferred_link_manager.dart';
+// Uncomment to enable push notifications services or Firebase Analytics
+// import 'package:flutter/foundation.dart';
+// import 'dart:io';
+
/// TODO: This class should be generated to enable selected Services
class EnsembleModules {
static final EnsembleModules _instance = EnsembleModules._internal();
@@ -168,13 +172,13 @@ class EnsembleModules {
if (enableChat) {
// Uncomment to enable ensemble chat
- // GetIt.I.registerSingleton(EnsembleChatImpl());
+ // GetIt.I.registerSingleton(EnsembleChatImpl.build(null));
} else {
GetIt.I.registerSingleton(const EnsembleChatStub());
}
if (useFirebaseAnalytics) {
//uncomment to enable firebase analytics
- //GetIt.I.registerSingleton(FirebaseAnalyticsProvider());
+ // GetIt.I.registerSingleton(FirebaseAnalyticsProvider());
} else {
GetIt.I.registerSingleton(LogProviderStub());
}
@@ -194,8 +198,7 @@ class EnsembleModules {
}
if (useBluetooth) {
-
- //GetIt.I.registerSingleton(BluetoothManagerImpl());
+ //GetIt.I.registerSingleton(BluetoothManagerImpl());
} else {
GetIt.I.registerSingleton(BluetoothManagerStub());
}
diff --git a/starter/package.json b/starter/package.json
new file mode 100644
index 000000000..c52baf2eb
--- /dev/null
+++ b/starter/package.json
@@ -0,0 +1,46 @@
+{
+ "name": "starter",
+ "version": "1.0.0",
+ "description": "This starter project enables running and deploying Ensemble-powered Apps across iOS, Android, and Web (other platforms are not yet fully supported). It also includes examples on how to integrate Ensemble pages into your existing Flutter App.",
+ "main": "index.js",
+ "directories": {
+ "lib": "lib",
+ "test": "test"
+ },
+ "scripts": {
+ "build": "tsc",
+ "enable": "ts-node src/dart_runner.ts enable",
+ "hasCamera": "ts-node src/dart_runner.ts camera",
+ "hasFileManager": "ts-node src/dart_runner.ts file_manager",
+ "hasContacts": "ts-node src/dart_runner.ts contacts",
+ "hasConnect": "ts-node src/dart_runner.ts plaid_connect",
+ "hasLocation": "ts-node src/dart_runner.ts location",
+ "hasDeeplink": "ts-node src/dart_runner.ts deeplink",
+ "hasFirebaseAnalytics": "ts-node src/dart_runner.ts firebase_analytics",
+ "hasMoengage": "ts-node src/dart_runner.ts moengage",
+ "hasNotification": "ts-node src/dart_runner.ts notification",
+ "hasBracket": "ts-node src/dart_runner.ts bracket",
+ "hasNetworkInfo": "ts-node src/dart_runner.ts network_info",
+ "hasChat": "ts-node src/dart_runner.ts ai_chat",
+ "hasAuth": "ts-node src/dart_runner.ts auth",
+ "hasBluetooth": "ts-node src/dart_runner.ts bluetooth",
+ "hasBiometric": "ts-node src/dart_runner.ts biometric",
+ "qrCodeEnabled": "ts-node src/dart_runner.ts qr_code",
+ "hasGoogleMaps": "ts-node src/dart_runner.ts google_maps",
+ "generate_keystore": "ts-node src/dart_runner.ts generateKeystore"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "dependencies": {
+ "inquirer": "^12.0.0",
+ "prompts": "^2.4.2"
+ },
+ "devDependencies": {
+ "@types/inquirer": "^9.0.7",
+ "@types/node": "^22.10.2",
+ "@types/prompts": "^2.4.9",
+ "ts-node": "^10.9.2",
+ "typescript": "^5.7.2"
+ }
+}
diff --git a/starter/pubspec.yaml b/starter/pubspec.yaml
index 7febc0bb0..9125823ed 100644
--- a/starter/pubspec.yaml
+++ b/starter/pubspec.yaml
@@ -2,7 +2,7 @@ name: ensemble_starter
description: Ensemble Starter project in Flutter
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
-publish_to: "none" # Remove this line if you wish to publish to pub.dev
+publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
@@ -103,12 +103,10 @@ dependencies:
# Uncomment to enable firebase analytics
# ensemble_firebase_analytics:
-# git:
-# url: https://github.com/EnsembleUI/ensemble.git
-# ref: main
-# path: modules/firebase_analytics
-
-
+ # git:
+ # url: https://github.com/EnsembleUI/ensemble.git
+ # ref: main
+ # path: modules/firebase_analytics
# Uncomment to enable ensemble chat widget
# ensemble_chat:
@@ -124,14 +122,12 @@ dependencies:
# ref: main
# path: modules/bracket
-
# Uncomment to enable NetworkInfo
-# ensemble_network_info:
-# git:
-# url: https://github.com/EnsembleUI/ensemble.git
-# ref: main
-# path: modules/ensemble_network_info
-
+ # ensemble_network_info:
+ # git:
+ # url: https://github.com/EnsembleUI/ensemble.git
+ # ref: main
+ # path: modules/ensemble_network_info
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
diff --git a/starter/scripts/constants.dart b/starter/scripts/constants.dart
new file mode 100644
index 000000000..21ea56664
--- /dev/null
+++ b/starter/scripts/constants.dart
@@ -0,0 +1,14 @@
+const firebaseProguardRules = '''
+# Keep Google Play Services classes
+-keep class com.google.android.gms.** { *; }
+-keep interface com.google.android.gms.** { *; }
+-dontwarn com.google.android.gms.**
+
+# Keep Firebase-related classes
+-keep class com.google.firebase.** { *; }
+-dontwarn com.google.firebase.**
+
+# Keep other necessary classes for Flutter
+-keep class io.flutter.** { *; }
+-dontwarn io.flutter.**
+''';
diff --git a/starter/scripts/firebase_performance.dart b/starter/scripts/firebase_performance.dart
new file mode 100644
index 000000000..4d048f07d
--- /dev/null
+++ b/starter/scripts/firebase_performance.dart
@@ -0,0 +1,82 @@
+import 'dart:io';
+
+import 'utils.dart';
+import 'utils/firebase_utils.dart';
+
+// Adds the Firebase Performance SDK to the project
+void main(List arguments) {
+ List platforms = getPlatforms(arguments);
+
+ try {
+ addDependency('firebase_performance', '^0.10.0+11');
+
+ if (platforms.contains('android')) {
+ addClasspathDependency(
+ "classpath 'com.google.firebase:perf-plugin:1.4.2'");
+ addPluginDependency("apply plugin: 'com.google.firebase.firebase-perf'");
+ }
+
+ // Configure iOS-specific settings
+ if (platforms.contains('ios')) {
+ addPod('FirebasePerformance');
+ }
+
+ print(
+ 'Firebase Performance SDK enabled successfully for ${platforms.join(', ')}! 🎉');
+ exit(0);
+ } catch (e) {
+ print('Starter Error: $e');
+ exit(1);
+ }
+}
+
+void addPod(String pod) {
+ final podfile = File('ios/Podfile');
+ if (!podfile.existsSync()) {
+ throw 'ios/Podfile not found';
+ }
+
+ final lines = podfile.readAsLinesSync();
+ final newLines = [];
+ bool added = false;
+
+ for (var line in lines) {
+ newLines.add(line);
+ if (line.contains('use_frameworks!')) {
+ newLines.add(" pod '$pod'");
+ added = true;
+ }
+ }
+
+ if (!added) {
+ throw 'use_frameworks! not found in ios/Podfile';
+ }
+
+ podfile.writeAsStringSync(newLines.join('\n'));
+}
+
+void addDependency(String dependency, String version) {
+ final pubspec = File(pubspecFilePath);
+ if (!pubspec.existsSync()) {
+ throw 'pubspec.yaml not found';
+ }
+
+ final content = pubspec.readAsStringSync();
+ final dependenciesSection =
+ RegExp(r'dependencies:', multiLine: true).firstMatch(content);
+
+ if (dependenciesSection == null) {
+ throw 'dependencies section not found in pubspec.yaml';
+ }
+
+ final dependencyPattern = RegExp(r'\s+$dependency:\s+\S+');
+ if (dependencyPattern.hasMatch(content)) {
+ return;
+ }
+ final newContent = content.replaceFirst(
+ dependenciesSection.group(0)!,
+ '${dependenciesSection.group(0)}\n $dependency: $version\n',
+ );
+
+ pubspec.writeAsStringSync(newContent);
+}
diff --git a/starter/scripts/generate_keystore.dart b/starter/scripts/generate_keystore.dart
new file mode 100644
index 000000000..fcaebcfa4
--- /dev/null
+++ b/starter/scripts/generate_keystore.dart
@@ -0,0 +1,86 @@
+import 'dart:io';
+import 'utils.dart';
+
+void main(List arguments) async {
+ try {
+ // Parse the arguments
+ String? storePassword = getArgumentValue(arguments, 'storePassword');
+ String? keyPassword = getArgumentValue(arguments, 'keyPassword');
+ String? keyAlias = getArgumentValue(arguments, 'keyAlias');
+
+ if (storePassword == null || keyPassword == null || keyAlias == null) {
+ throw Exception(
+ 'Missing required arguments. Usage: npm run generate_keystore storePassword= keyPassword= keyAlias=');
+ }
+
+ // Ensure passwords are at least 6 characters long to avoid issues with keytool
+ if (storePassword.length < 6 || keyPassword.length < 6) {
+ throw Exception(
+ 'storePassword and keyPassword must be at least 6 characters long.');
+ }
+
+ // Define paths for the keystore and key.properties files
+ String androidAppDir = Directory.current.path + '/android/app';
+ String androidDir = Directory.current.path + '/android';
+ String keystorePath = '$androidAppDir/keystore.jks';
+ String keyPropertiesPath = '$androidDir/key.properties';
+
+ // Check if the keystore file already exists
+ if (File(keystorePath).existsSync()) {
+ print('Keystore already exists at $keystorePath');
+ exit(0);
+ }
+
+ // Ensure that the directories exist
+ Directory(androidAppDir).createSync(recursive: true);
+ Directory(androidDir).createSync(recursive: true);
+
+ // Generate the keystore using the keytool command
+ String command = 'keytool';
+ List args = [
+ '-genkey',
+ '-v',
+ '-keystore',
+ keystorePath,
+ '-alias',
+ keyAlias,
+ '-keyalg',
+ 'RSA',
+ '-keysize',
+ '2048',
+ '-validity',
+ '9125',
+ '-storepass',
+ storePassword,
+ '-keypass',
+ keyPassword,
+ '-dname',
+ 'CN=Unknown, OU=Unknown, O=Unknown, L=Unknown, S=Unknown, C=US'
+ ];
+
+ ProcessResult result = await Process.run(command, args);
+
+ if (result.exitCode != 0) {
+ throw Exception(
+ 'Error generating keystore. Exit code: ${result.exitCode}\nError: ${result.stderr}');
+ }
+
+ print(result.stdout);
+
+ // Create the key.properties file with the keystore configuration
+ String keyPropertiesContent = '''
+storePassword=$storePassword
+keyPassword=$keyPassword
+keyAlias=$keyAlias
+storeFile=keystore.jks
+''';
+
+ File keyPropertiesFile = File(keyPropertiesPath);
+ keyPropertiesFile.writeAsStringSync(keyPropertiesContent.trim());
+
+ print('Keystore generated successfully!');
+ } catch (e) {
+ stderr.writeln('An error occurred: $e');
+ exit(1);
+ }
+}
diff --git a/starter/scripts/modules/enable_auth.dart b/starter/scripts/modules/enable_auth.dart
new file mode 100644
index 000000000..412743aa4
--- /dev/null
+++ b/starter/scripts/modules/enable_auth.dart
@@ -0,0 +1,154 @@
+import 'dart:io';
+
+import '../constants.dart';
+import '../utils.dart';
+import '../utils/firebase_utils.dart';
+import '../utils/proguard_utils.dart';
+
+void main(List arguments) async {
+ List platforms = getPlatforms(arguments);
+
+ // Extract client ID values from the arguments
+ String iOSClientId = getArgumentValue(arguments, 'googleIOSClientId') ?? '';
+ String androidClientId =
+ getArgumentValue(arguments, 'googleAndroidClientId') ?? '';
+ String webClientId = getArgumentValue(arguments, 'googleWebClientId') ?? '';
+ String serverClientId =
+ getArgumentValue(arguments, 'googleServerClientId') ?? '';
+
+ String? ensembleVersion = getArgumentValue(arguments, 'ensemble_version');
+
+ final statements = {
+ 'moduleStatements': [
+ "import 'package:ensemble_auth/auth_module.dart';",
+ 'GetIt.I.registerSingleton(AuthModuleImpl());',
+ ],
+ 'useStatements': [
+ 'static const useAuth = true;',
+ ],
+ };
+
+ final pubspecDependencies = [
+ {
+ 'statement': '''
+ensemble_auth:
+ git:
+ url: https://github.com/EnsembleUI/ensemble.git
+ ref: ${await packageVersion(version: ensembleVersion)}
+ path: modules/auth''',
+ 'regex':
+ r'#\s*ensemble_auth:\s*\n\s*#\s*git:\s*\n\s*#\s*url:\s*https:\/\/github\.com\/EnsembleUI\/ensemble\.git\s*\n\s*#\s*ref:\s*main\s*\n\s*#\s*path:\s*modules\/auth',
+ }
+ ];
+
+ try {
+ // Update the ensemble_modules.dart file
+ updateEnsembleModules(
+ statements['moduleStatements'],
+ statements['useStatements'],
+ );
+
+ // Update the pubspec.yaml file
+ updatePubspec(pubspecDependencies);
+
+ // Update the auth module configuration in ensemble-config.yaml
+ updateAuthConfig(iOSClientId, androidClientId, webClientId, serverClientId);
+ updateFirebaseConfig(platforms, arguments);
+ if (platforms.contains('android')) {
+ createProguardRules(firebaseProguardRules);
+ addClasspathDependency(
+ "classpath 'com.google.gms:google-services:4.3.15'");
+ addPluginDependency("apply plugin: 'com.google.gms.google-services'");
+ addSettingsPluginDependency(
+ 'id "com.google.gms.google-services" version "4.3.15" apply false');
+ addImplementationDependency(
+ "implementation platform('com.google.firebase:firebase-bom:32.7.0')");
+ }
+
+ // Update the iOS Info.plist
+ if (platforms.contains('ios') && iOSClientId.isNotEmpty) {
+ updateInfoPlist(iOSClientId);
+ }
+
+ if (platforms.contains('web') && webClientId.isNotEmpty) {
+ updateHtmlFile('',
+ '',
+ removalPattern:
+ r'');
+ }
+
+ print(
+ 'Auth module enabled and configuration updated successfully for ${platforms.join(', ')}! 🎉');
+ exit(0);
+ } catch (e) {
+ print('Starter Error: $e');
+ exit(1);
+ }
+}
+
+void updateAuthConfig(String iOSClientId, String androidClientId,
+ String webClientId, String serverClientId) {
+ try {
+ final file = File(ensembleConfigFilePath);
+ if (!file.existsSync()) {
+ throw Exception('Config file not found.');
+ }
+
+ String content = file.readAsStringSync();
+
+ // Define a map of client IDs and their corresponding config keys
+ final clientIds = {
+ 'iOSClientId': iOSClientId,
+ 'androidClientId': androidClientId,
+ 'webClientId': webClientId,
+ 'serverClientId': serverClientId,
+ };
+
+ // Replace each client ID if it's not empty
+ clientIds.forEach((key, value) {
+ if (value.isNotEmpty) {
+ content = content.replaceAllMapped(
+ RegExp('$key:\\s*.*', multiLine: true),
+ (match) => '$key: $value',
+ );
+ } else {
+ content = content.replaceAllMapped(
+ RegExp('$key:\\s*.*', multiLine: true),
+ (match) => '',
+ );
+ }
+ });
+
+ file.writeAsStringSync(content);
+ } catch (e) {
+ throw Exception(
+ 'Failed to update auth configuration in ensemble-config.yaml: $e');
+ }
+}
+
+void updateInfoPlist(String iOSClientId) {
+ try {
+ final file = File(iosInfoPlistFilePath);
+ if (!file.existsSync()) {
+ throw Exception('Info.plist file not found.');
+ }
+
+ String content = file.readAsStringSync();
+
+ final cleanedClientId =
+ iOSClientId.replaceAll('.apps.googleusercontent.com', '');
+
+ final reversedClientId = 'com.googleusercontent.apps.$cleanedClientId';
+
+ // Replace the current iOS client ID in the Info.plist file
+ content = content.replaceAllMapped(
+ RegExp(
+ r'com\.googleusercontent\.apps\.\d+-[a-zA-Z0-9]+'),
+ (match) => '$reversedClientId',
+ );
+
+ file.writeAsStringSync(content);
+ } catch (e) {
+ throw Exception('Failed to update Info.plist: $e');
+ }
+}
diff --git a/starter/scripts/modules/enable_biometric.dart b/starter/scripts/modules/enable_biometric.dart
new file mode 100644
index 000000000..f320f3906
--- /dev/null
+++ b/starter/scripts/modules/enable_biometric.dart
@@ -0,0 +1,68 @@
+import 'dart:io';
+
+import '../utils.dart';
+
+void main(List arguments) async {
+ List platforms = getPlatforms(arguments);
+
+ final androidPermissions = [
+ '',
+ ];
+
+ final iOSPermissions = [
+ {
+ 'key': 'faceIdDescription',
+ 'value': 'NSFaceIDUsageDescription',
+ }
+ ];
+
+ try {
+ // Add the biometric permission to AndroidManifest.xml
+ if (platforms.contains('android')) {
+ updateAndroidPermissions(permissions: androidPermissions);
+
+ // Update MainActivity to extend FlutterFragmentActivity
+ final mainActivityUpdates = {
+ 'kotlin': [
+ {
+ 'pattern':
+ r'import\s+io\.flutter\.embedding\.android\.FlutterActivity',
+ 'replacement':
+ 'import io.flutter.embedding.android.FlutterFragmentActivity'
+ },
+ {
+ 'pattern': r'class\s+MainActivity\s*:\s*FlutterActivity\(\)',
+ 'replacement': 'class MainActivity: FlutterFragmentActivity()'
+ }
+ ],
+ 'java': [
+ {
+ 'pattern':
+ r'import\s+io\.flutter\.embedding\.android\.FlutterActivity;',
+ 'replacement':
+ 'import io.flutter.embedding.android.FlutterFragmentActivity;'
+ },
+ {
+ 'pattern':
+ r'public\s+class\s+MainActivity\s+extends\s+FlutterActivity',
+ 'replacement':
+ 'public class MainActivity extends FlutterFragmentActivity'
+ }
+ ]
+ };
+
+ updateMainActivity(mainActivityUpdates);
+ }
+
+ // Add Face ID usage description to Info.plist for iOS
+ if (platforms.contains('ios')) {
+ updateIOSPermissions(iOSPermissions, arguments);
+ }
+
+ print('Biometric enabled successfully for ${platforms.join(', ')}! 🎉');
+ exit(0);
+ } catch (e) {
+ print('Starter Error: $e');
+ exit(1);
+ }
+}
diff --git a/starter/scripts/modules/enable_bluetooth.dart b/starter/scripts/modules/enable_bluetooth.dart
new file mode 100644
index 000000000..b0d0f95ce
--- /dev/null
+++ b/starter/scripts/modules/enable_bluetooth.dart
@@ -0,0 +1,98 @@
+import 'dart:io';
+
+import '../utils.dart';
+
+void main(List arguments) async {
+ List platforms = getPlatforms(arguments);
+ String? ensembleVersion = getArgumentValue(arguments, 'ensemble_version');
+
+ final statements = {
+ 'moduleStatements': [
+ "import 'package:ensemble_bluetooth/ensemble_bluetooth.dart';",
+ 'GetIt.I.registerSingleton(BluetoothManagerImpl());',
+ ],
+ 'useStatements': [
+ 'static const useBluetooth = true;',
+ ],
+ };
+
+ final pubspecDependencies = [
+ {
+ 'statement': '''
+ensemble_bluetooth:
+ git:
+ url: https://github.com/EnsembleUI/ensemble.git
+ ref: ${await packageVersion(version: ensembleVersion)}
+ path: modules/ensemble_bluetooth''',
+ 'regex':
+ r'#\s*ensemble_bluetooth:\s*\n\s*#\s*git:\s*\n\s*#\s*url:\s*https:\/\/github\.com\/EnsembleUI\/ensemble\.git\s*\n\s*#\s*ref:\s*main\s*\n\s*#\s*path:\s*modules\/ensemble_bluetooth',
+ }
+ ];
+
+ final androidPermissions = [
+ // New Android 12 Bluetooth permissions
+ '',
+ '',
+ // Legacy permissions for Android 11 or lower
+ '',
+ '',
+ // Tell Play Store app uses Bluetooth LE
+ '',
+ ];
+
+ final iOSPermissions = [
+ {
+ 'key': 'bluetoothDescription',
+ 'value': 'NSBluetoothAlwaysUsageDescription',
+ },
+ {
+ 'key': 'bluetoothPeripheralDescription',
+ 'value': 'NSBluetoothPeripheralUsageDescription',
+ }
+ ];
+
+ final iOSAdditionalSettings = [
+ {
+ 'key': 'UIBackgroundModes',
+ 'value': ['bluetooth-central', 'bluetooth-peripheral'],
+ 'isArray': true,
+ },
+ {
+ 'key': 'NSBluetoothServices',
+ 'value': ['180A', '180F', '1812'],
+ 'isArray': true,
+ }
+ ];
+
+ try {
+ // Update the ensemble_modules.dart file
+ updateEnsembleModules(
+ statements['moduleStatements'],
+ statements['useStatements'],
+ );
+
+ // Update the pubspec.yaml file
+ updatePubspec(pubspecDependencies);
+
+ // Add required permissions to AndroidManifest.xml
+ if (platforms.contains('android')) {
+ updateAndroidPermissions(permissions: androidPermissions);
+ }
+
+ // Add required permissions to Info.plist for iOS
+ if (platforms.contains('ios')) {
+ updateIOSPermissions(
+ iOSPermissions,
+ arguments,
+ additionalSettings: iOSAdditionalSettings,
+ );
+ }
+
+ print(
+ 'Bluetooth module enabled successfully for ${platforms.join(', ')}! 🎉');
+ exit(0);
+ } catch (e) {
+ print('Starter Error: $e');
+ exit(1);
+ }
+}
diff --git a/starter/scripts/modules/enable_bracket.dart b/starter/scripts/modules/enable_bracket.dart
new file mode 100644
index 000000000..c51746042
--- /dev/null
+++ b/starter/scripts/modules/enable_bracket.dart
@@ -0,0 +1,49 @@
+import 'dart:io';
+
+import '../utils.dart';
+
+void main(List arguments) async {
+ List platforms = getPlatforms(arguments);
+ String? ensembleVersion = getArgumentValue(arguments, 'ensemble_version');
+
+ final statements = {
+ 'moduleStatements': [
+ "import 'package:ensemble_bracket/ensemble_bracket.dart';",
+ 'GetIt.I.registerSingleton(EnsembleBracketImpl.build());',
+ ],
+ 'useStatements': [
+ 'static const useBracket = true;',
+ ],
+ };
+
+ final pubspecDependencies = [
+ {
+ 'statement': '''
+ensemble_bracket:
+ git:
+ url: https://github.com/EnsembleUI/ensemble.git
+ ref: ${await packageVersion(version: ensembleVersion)}
+ path: modules/bracket''',
+ 'regex':
+ r'#\s*ensemble_bracket:\s*\n\s*#\s*git:\s*\n\s*#\s*url:\s*https:\/\/github\.com\/EnsembleUI\/ensemble\.git\s*\n\s*#\s*ref:\s*main\s*\n\s*#\s*path:\s*modules\/bracket',
+ }
+ ];
+
+ try {
+ // Update the ensemble_modules.dart file
+ updateEnsembleModules(
+ statements['moduleStatements'],
+ statements['useStatements'],
+ );
+
+ // Update the pubspec.yaml file
+ updatePubspec(pubspecDependencies);
+
+ print(
+ 'Bracket module enabled successfully for ${platforms.join(', ')}! 🎉');
+ exit(0);
+ } catch (e) {
+ print('Starter Error: $e');
+ exit(1);
+ }
+}
diff --git a/starter/scripts/modules/enable_camera.dart b/starter/scripts/modules/enable_camera.dart
new file mode 100644
index 000000000..d33eb7ef7
--- /dev/null
+++ b/starter/scripts/modules/enable_camera.dart
@@ -0,0 +1,67 @@
+import 'dart:io';
+
+import '../utils.dart';
+
+Future main(List arguments) async {
+ List platforms = getPlatforms(arguments);
+ String? ensembleVersion = getArgumentValue(arguments, 'ensemble_version');
+
+ final cameraStatements = {
+ 'moduleStatements': [
+ "import 'package:ensemble_camera/camera_manager.dart';",
+ "GetIt.I.registerSingleton(CameraManagerImpl());",
+ ],
+ 'useStatements': [
+ 'static const useCamera = true;',
+ ],
+ };
+
+ final pubspecDependencies = [
+ {
+ 'statement': '''
+ensemble_camera:
+ git:
+ url: https://github.com/EnsembleUI/ensemble.git
+ ref: ${await packageVersion(version: ensembleVersion)}
+ path: modules/camera''',
+ 'regex':
+ r'#\s*ensemble_camera:\s*\n\s*#\s*git:\s*\n\s*#\s*url:\s*https:\/\/github\.com\/EnsembleUI\/ensemble\.git\s*\n\s*#\s*ref:\s*main\s*\n\s*#\s*path:\s*modules\/camera',
+ }
+ ];
+
+ final iOSPermissions = [
+ {
+ 'key': 'cameraDescription',
+ 'value': 'NSCameraUsageDescription',
+ }
+ ];
+
+ try {
+ // Update the ensemble_modules.dart file
+ updateEnsembleModules(
+ cameraStatements['moduleStatements'],
+ cameraStatements['useStatements'],
+ );
+
+ // Update the pubspec.yaml file
+ updatePubspec(pubspecDependencies);
+
+ // Add the camera permissions to AndroidManifest.xml
+ if (platforms.contains('android')) {
+ updateAndroidPermissions(permissions: [
+ ''
+ ]);
+ }
+
+ // Add the camera usage description to the iOS Info.plist file
+ if (platforms.contains('ios')) {
+ updateIOSPermissions(iOSPermissions, arguments);
+ }
+
+ print('Camera module enabled successfully for ${platforms.join(', ')}! 🎉');
+ exit(0);
+ } catch (e) {
+ print('Starter Error: $e');
+ exit(1);
+ }
+}
diff --git a/starter/scripts/modules/enable_chat.dart b/starter/scripts/modules/enable_chat.dart
new file mode 100644
index 000000000..d28b9a25a
--- /dev/null
+++ b/starter/scripts/modules/enable_chat.dart
@@ -0,0 +1,48 @@
+import 'dart:io';
+
+import '../utils.dart';
+
+void main(List arguments) async {
+ List platforms = getPlatforms(arguments);
+ String? ensembleVersion = getArgumentValue(arguments, 'ensemble_version');
+
+ final statements = {
+ 'moduleStatements': [
+ "import 'package:ensemble_chat/ensemble_chat.dart';",
+ 'GetIt.I.registerSingleton(EnsembleChatImpl.build(null));',
+ ],
+ 'useStatements': [
+ 'static const enableChat = true;',
+ ],
+ };
+
+ final pubspecDependencies = [
+ {
+ 'statement': '''
+ensemble_chat:
+ git:
+ url: https://github.com/EnsembleUI/ensemble.git
+ ref: ${await packageVersion(version: ensembleVersion)}
+ path: modules/chat''',
+ 'regex':
+ r'#\s*ensemble_chat:\s*\n\s*#\s*git:\s*\n\s*#\s*url:\s*https:\/\/github\.com\/EnsembleUI\/ensemble\.git\s*\n\s*#\s*ref:\s*main\s*\n\s*#\s*path:\s*modules\/chat',
+ }
+ ];
+
+ try {
+ // Update the ensemble_modules.dart file
+ updateEnsembleModules(
+ statements['moduleStatements'],
+ statements['useStatements'],
+ );
+
+ // Update the pubspec.yaml file
+ updatePubspec(pubspecDependencies);
+
+ print('Chat module enabled successfully for ${platforms.join(', ')}! 🎉');
+ exit(0);
+ } catch (e) {
+ print('Starter Error: $e');
+ exit(1);
+ }
+}
diff --git a/starter/scripts/modules/enable_connect.dart b/starter/scripts/modules/enable_connect.dart
new file mode 100644
index 000000000..63e306923
--- /dev/null
+++ b/starter/scripts/modules/enable_connect.dart
@@ -0,0 +1,100 @@
+import 'dart:io';
+
+import '../utils.dart';
+import '../utils/firebase_utils.dart';
+
+void main(List arguments) async {
+ List platforms = getPlatforms(arguments);
+ String? ensembleVersion = getArgumentValue(arguments, 'ensemble_version');
+
+ final statements = {
+ 'moduleStatements': [
+ "import 'package:ensemble_connect/plaid_link/plaid_link_manager.dart';",
+ "GetIt.I.registerSingleton(PlaidLinkManagerImpl());",
+ ],
+ 'useStatements': [
+ 'static const useConnect = true;',
+ ],
+ };
+
+ final pubspecDependencies = [
+ {
+ 'statement': '''
+ensemble_connect:
+ git:
+ url: https://github.com/EnsembleUI/ensemble.git
+ ref: ${await packageVersion(version: ensembleVersion)}
+ path: modules/connect''',
+ 'regex':
+ r'#\s*ensemble_connect:\s*\n\s*#\s*git:\s*\n\s*#\s*url:\s*https:\/\/github\.com\/EnsembleUI\/ensemble\.git\s*\n\s*#\s*ref:\s*main\s*\n\s*#\s*path:\s*modules\/connect',
+ }
+ ];
+
+ final iOSPermissions = [
+ {
+ 'key': 'cameraDescription',
+ 'value': 'NSCameraUsageDescription',
+ }
+ ];
+
+ try {
+ // Update the ensemble_modules.dart file
+ updateEnsembleModules(
+ statements['moduleStatements'],
+ statements['useStatements'],
+ );
+
+ updatePubspec(pubspecDependencies);
+
+ if (platforms.contains('android')) {
+ addImplementationDependency(
+ "implementation 'org.openjsse:openjsse:1.1.10'");
+ addImplementationDependency(
+ "implementation 'org.conscrypt:conscrypt-android:2.5.2'");
+ updateBuildGradle(minifyEnabled: false, shrinkResources: false);
+ }
+
+ if (platforms.contains('ios')) {
+ updateIOSPermissions(iOSPermissions, arguments);
+ }
+
+ // Add the ',
+ removalPattern:
+ r'https://cdn\.plaid\.com/link/v2/stable/link-initialize\.js',
+ );
+ }
+
+ print(
+ 'Connect module enabled successfully for ${platforms.join(', ')}! 🎉');
+ exit(0);
+ } catch (e) {
+ print('Starter Error: $e');
+ exit(1);
+ }
+}
+
+void updateBuildGradle(
+ {bool minifyEnabled = false, bool shrinkResources = false}) {
+ final buildGradleFile = File('android/app/build.gradle');
+ String content = buildGradleFile.readAsStringSync();
+
+ if (!content.contains('minifyEnabled')) {
+ content = content.replaceAllMapped(
+ RegExp(r'buildTypes\s*{[^}]*release\s*{', multiLine: true),
+ (match) =>
+ "buildTypes {\n release {\n minifyEnabled $minifyEnabled");
+ }
+
+ if (!content.contains('shrinkResources')) {
+ content = content.replaceAllMapped(
+ RegExp(r'buildTypes\s*{[^}]*release\s*{', multiLine: true),
+ (match) =>
+ "buildTypes {\n release {\n shrinkResources $shrinkResources");
+ }
+
+ buildGradleFile.writeAsStringSync(content);
+}
diff --git a/starter/scripts/modules/enable_contacts.dart b/starter/scripts/modules/enable_contacts.dart
new file mode 100644
index 000000000..6471ae32a
--- /dev/null
+++ b/starter/scripts/modules/enable_contacts.dart
@@ -0,0 +1,68 @@
+import 'dart:io';
+import '../utils.dart';
+
+void main(List arguments) async {
+ List platforms = getPlatforms(arguments);
+ String? ensembleVersion = getArgumentValue(arguments, 'ensemble_version');
+
+ final contactsStatements = {
+ 'moduleStatements': [
+ "import 'package:ensemble_contacts/contact_manager.dart';",
+ "GetIt.I.registerSingleton(ContactManagerImpl());"
+ ],
+ 'useStatements': ["static const useContacts = true;"],
+ };
+
+ final pubspecDependencies = [
+ {
+ 'statement': '''
+ensemble_contacts:
+ git:
+ url: https://github.com/EnsembleUI/ensemble.git
+ ref: ${await packageVersion(version: ensembleVersion)}
+ path: modules/contacts''',
+ 'regex':
+ r'#\s*ensemble_contacts:\s*\n\s*#\s*git:\s*\n\s*#\s*url:\s*https:\/\/github\.com\/EnsembleUI\/ensemble\.git\s*\n\s*#\s*ref:\s*main\s*\n\s*#\s*path:\s*modules\/contacts',
+ }
+ ];
+
+ final androidPermissions = [
+ '',
+ '',
+ ];
+
+ final iOSPermissions = [
+ {
+ 'key': 'contactsDescription',
+ 'value': 'NSContactsUsageDescription',
+ }
+ ];
+
+ try {
+ // Update the ensemble_modules.dart file
+ updateEnsembleModules(
+ contactsStatements['moduleStatements'],
+ contactsStatements['useStatements'],
+ );
+
+ // Update the pubspec.yaml file
+ updatePubspec(pubspecDependencies);
+
+ // Add the contacts permissions to AndroidManifest.xml
+ if (platforms.contains('android')) {
+ updateAndroidPermissions(permissions: androidPermissions);
+ }
+
+ // Add the contacts usage description to the iOS Info.plist file
+ if (platforms.contains('ios')) {
+ updateIOSPermissions(iOSPermissions, arguments);
+ }
+
+ print(
+ 'Contacts module enabled successfully for ${platforms.join(', ')}! 🎉');
+ exit(0);
+ } catch (e) {
+ print('Starter Error: $e');
+ exit(1);
+ }
+}
diff --git a/starter/scripts/modules/enable_deeplink.dart b/starter/scripts/modules/enable_deeplink.dart
new file mode 100644
index 000000000..1b45b5c22
--- /dev/null
+++ b/starter/scripts/modules/enable_deeplink.dart
@@ -0,0 +1,147 @@
+import 'dart:io';
+import '../utils.dart';
+import '../utils/deeplink_utils.dart';
+
+void main(List arguments) async {
+ List platforms = getPlatforms(arguments);
+ String? ensembleVersion = getArgumentValue(arguments, 'ensemble_version');
+
+ String? branchIOLiveKey =
+ getArgumentValue(arguments, 'branchIOLiveKey', required: true);
+ String? branchIOTestKey =
+ getArgumentValue(arguments, 'branchIOTestKey', required: true);
+ bool useTestKey =
+ getArgumentValue(arguments, 'branchIOUseTestKey')?.toLowerCase() ==
+ 'true';
+ String? scheme =
+ getArgumentValue(arguments, 'branchIOScheme', required: true);
+ List links =
+ getArgumentValue(arguments, 'branchIOLinks')?.split(',') ?? [];
+
+ if (branchIOLiveKey == null ||
+ branchIOLiveKey.isEmpty ||
+ branchIOTestKey == null ||
+ branchIOTestKey.isEmpty) {
+ print(
+ 'Error: Missing branchIOLiveKey argument. Usage: npm run useDeeplink branchIOLiveKey= branchIOTestKey= branchIOUseTestKey= branchIOScheme= branchIOLinks=');
+ exit(1);
+ }
+
+ final statements = {
+ 'moduleStatements': [
+ "import 'package:ensemble_deeplink/deferred_link_manager.dart';",
+ 'GetIt.I.registerSingleton(DeferredLinkManagerImpl());',
+ ],
+ 'useStatements': [
+ 'static const useDeeplink = true;',
+ ],
+ };
+
+ final pubspecDependencies = [
+ {
+ 'statement': '''
+ensemble_deeplink:
+ git:
+ url: https://github.com/EnsembleUI/ensemble.git
+ ref: ${await packageVersion(version: ensembleVersion)}
+ path: modules/deeplink''',
+ 'regex':
+ r'#\s*ensemble_deeplink:\s*\n\s*#\s*git:\s*\n\s*#\s*url:\s*https:\/\/github\.com\/EnsembleUI\/ensemble\.git\s*\n\s*#\s*ref:\s*main\s*\n\s*#\s*path:\s*modules\/deeplink',
+ }
+ ];
+
+ // Prepare Branch.io initialization script for web
+ final branchScript = '''
+''';
+
+ try {
+ // Update the ensemble_modules.dart file
+ updateEnsembleModules(
+ statements['moduleStatements'],
+ statements['useStatements'],
+ );
+
+ // Update the pubspec.yaml file
+ updatePubspec(pubspecDependencies);
+
+ // Inject the Branch.io script for the web platform
+ if (platforms.contains('web')) {
+ updateHtmlFile(
+ '',
+ branchScript,
+ );
+ }
+
+ updatePropertiesFile('branchTestKey', branchIOTestKey);
+ updatePropertiesFile('branchLiveKey', branchIOLiveKey);
+
+ // Modify AndroidManifest.xml for deep linking
+ if (platforms.contains('android')) {
+ final branchMetaData = [
+ '',
+ '',
+ '',
+ ];
+ updateAndroidPermissions(metaData: branchMetaData);
+ updateAndroidManifestWithDeeplink(
+ scheme: scheme ?? '',
+ links: links,
+ );
+ }
+
+ // Modify Info.plist for deep linking on iOS
+ if (platforms.contains('ios')) {
+ addPermissionDescriptionToInfoPlist(
+ 'branch_universal_link_domains',
+ links,
+ isArray: true,
+ );
+
+ addPermissionDescriptionToInfoPlist(
+ 'branch_key',
+ {
+ 'live': branchIOLiveKey,
+ 'test': branchIOTestKey,
+ },
+ isDict: true,
+ );
+
+ addBlockAboveLineInInfoPlist(
+ scheme ?? '',
+ '',
+ );
+
+ updateRunnerEntitlements(
+ module: 'deeplink',
+ deeplinkLinks: links,
+ );
+ }
+
+ print(
+ 'Deeplink module enabled successfully for ${platforms.join(', ')}! 🎉');
+ exit(0);
+ } catch (e) {
+ print('Starter Error: $e');
+ exit(1);
+ }
+}
diff --git a/starter/scripts/modules/enable_files.dart b/starter/scripts/modules/enable_files.dart
new file mode 100644
index 000000000..48ca15de9
--- /dev/null
+++ b/starter/scripts/modules/enable_files.dart
@@ -0,0 +1,95 @@
+import 'dart:io';
+import '../utils.dart';
+
+void main(List arguments) async {
+ List platforms = getPlatforms(arguments);
+ String? ensembleVersion = getArgumentValue(arguments, 'ensemble_version');
+
+ final fileManagerStatements = {
+ 'moduleStatements': [
+ "import 'package:ensemble_file_manager/file_manager.dart';",
+ "GetIt.I.registerSingleton(FileManagerImpl());"
+ ],
+ 'useStatements': ["static const useFiles = true;"],
+ };
+
+ final pubspecDependencies = [
+ {
+ 'statement': '''
+ensemble_file_manager:
+ git:
+ url: https://github.com/EnsembleUI/ensemble.git
+ ref: ${await packageVersion(version: ensembleVersion)}
+ path: modules/file_manager''',
+ 'regex':
+ r'#\s*ensemble_file_manager:\s*\n\s*#\s*git:\s*\n\s*#\s*url:\s*https:\/\/github\.com\/EnsembleUI\/ensemble\.git\s*\n\s*#\s*ref:\s*main\s*\n\s*#\s*path:\s*modules\/file_manager',
+ }
+ ];
+
+ final androidPermissions = [
+ '',
+ '',
+ ];
+
+ final iOSPermissions = [
+ {
+ 'key': 'photoLibraryDescription',
+ 'value': 'NSPhotoLibraryUsageDescription',
+ },
+ {
+ 'key': 'musicDescription',
+ 'value': 'NSAppleMusicUsageDescription',
+ }
+ ];
+
+ final iOSAdditionalSettings = [
+ {
+ 'key': 'UIBackgroundModes',
+ 'value': ['fetch', 'remote-notification'],
+ 'isArray': true,
+ },
+ {
+ 'key': 'UISupportsDocumentBrowser',
+ 'value': true,
+ 'isBoolean': true,
+ },
+ {
+ 'key': 'LSSupportsOpeningDocumentsInPlace',
+ 'value': true,
+ 'isBoolean': true,
+ },
+ ];
+
+ try {
+ // Update the ensemble_modules.dart file
+ updateEnsembleModules(
+ fileManagerStatements['moduleStatements'],
+ fileManagerStatements['useStatements'],
+ );
+
+ // Update the pubspec.yaml file
+ updatePubspec(pubspecDependencies);
+
+ // Add the storage permissions to AndroidManifest.xml
+ if (platforms.contains('android')) {
+ updateAndroidPermissions(permissions: androidPermissions);
+ }
+
+ // Add the required keys and descriptions to the Info.plist file for iOS
+ if (platforms.contains('ios')) {
+ updateIOSPermissions(
+ iOSPermissions,
+ arguments,
+ additionalSettings: iOSAdditionalSettings,
+ );
+ }
+
+ // Success message
+ print(
+ 'File manager module enabled successfully for ${platforms.join(', ')}! 🎉');
+ exit(0);
+ } catch (e) {
+ print('Starter Error: $e');
+ exit(1);
+ }
+}
diff --git a/starter/scripts/modules/enable_firebase_analytics.dart b/starter/scripts/modules/enable_firebase_analytics.dart
new file mode 100644
index 000000000..7e423734d
--- /dev/null
+++ b/starter/scripts/modules/enable_firebase_analytics.dart
@@ -0,0 +1,71 @@
+import 'dart:io';
+
+import '../utils.dart';
+import '../utils/firebase_utils.dart';
+
+void main(List arguments) async {
+ List platforms = getPlatforms(arguments);
+ String? ensembleVersion = getArgumentValue(arguments, 'ensemble_version');
+ String enableConsoleLogs =
+ getArgumentValue(arguments, 'enableConsoleLogs') ?? 'true';
+
+ final statements = {
+ 'moduleStatements': [
+ "import 'package:ensemble_firebase_analytics/firebase_analytics.dart';",
+ "GetIt.I.registerSingleton(FirebaseAnalyticsProvider());",
+ "import 'dart:io';",
+ "import 'package:flutter/foundation.dart';",
+ ],
+ 'useStatements': [
+ 'static const useFirebaseAnalytics = true;',
+ ],
+ };
+
+ final pubspecDependencies = [
+ {
+ 'statement': '''
+ensemble_firebase_analytics:
+ git:
+ url: https://github.com/EnsembleUI/ensemble.git
+ ref: ${await packageVersion(version: ensembleVersion)}
+ path: modules/firebase_analytics''',
+ 'regex':
+ r'#\s*ensemble_firebase_analytics:\s*\n\s*#\s*git:\s*\n\s*#\s*url:\s*https:\/\/github\.com\/EnsembleUI\/ensemble\.git\s*\n\s*#\s*ref:\s*main\s*\n\s*#\s*path:\s*modules\/firebase_analytics',
+ }
+ ];
+
+ try {
+ // Update the ensemble_modules.dart file
+ updateEnsembleModules(
+ statements['moduleStatements'],
+ statements['useStatements'],
+ );
+
+ // Generate Firebase configuration based on platform
+ updateFirebaseInitialization(platforms, arguments);
+ updateFirebaseConfig(platforms, arguments);
+ updateAnalyticsConfig(enableConsoleLogs);
+
+ if (platforms.contains('android')) {
+ addClasspathDependency(
+ "classpath 'com.google.gms:google-services:4.3.15'");
+ addPluginDependency("apply plugin: 'com.google.gms.google-services'");
+ addImplementationDependency(
+ "implementation 'com.google.firebase:firebase-analytics'");
+ addImplementationDependency(
+ "implementation platform('com.google.firebase:firebase-bom:32.7.0')");
+ addSettingsPluginDependency(
+ 'id "com.google.gms.google-services" version "4.3.15" apply false');
+ }
+
+ // Update the pubspec.yaml file
+ updatePubspec(pubspecDependencies);
+
+ print(
+ 'Firebase Analytics module enabled successfully for ${platforms.join(', ')}! 🎉');
+ exit(0);
+ } catch (e) {
+ print('Starter Error: $e');
+ exit(1);
+ }
+}
diff --git a/starter/scripts/modules/enable_google_maps.dart b/starter/scripts/modules/enable_google_maps.dart
new file mode 100644
index 000000000..1257e7710
--- /dev/null
+++ b/starter/scripts/modules/enable_google_maps.dart
@@ -0,0 +1,40 @@
+import 'dart:io';
+import '../utils.dart';
+
+void main(List arguments) async {
+ List platforms = getPlatforms(arguments);
+
+ String? googleMapsApiKeyAndroid = getArgumentValue(
+ arguments, 'androidGoogleMapsApiKey',
+ required: platforms.contains('android'));
+ String? googleMapsApiKeyIOS = getArgumentValue(
+ arguments, 'iOSGoogleMapsApiKey',
+ required: platforms.contains('ios'));
+ String? googleMapsApiKeyWeb = getArgumentValue(
+ arguments,
+ 'webGoogleMapsApiKey',
+ required: platforms.contains('web'),
+ );
+
+ try {
+ if (platforms.contains('android') && googleMapsApiKeyAndroid != null) {
+ updatePropertiesFile('googleMapsAPIKey', googleMapsApiKeyAndroid);
+ }
+ if (platforms.contains('ios') && googleMapsApiKeyIOS != null) {
+ updateAppDelegateForGoogleMaps(googleMapsApiKeyIOS);
+ }
+
+ if (platforms.contains('web') && googleMapsApiKeyWeb != null) {
+ updateHtmlFile('',
+ '',
+ removalPattern: r'https://maps\.googleapis\.com/maps/api/js\?key=.*');
+ }
+
+ print(
+ 'Google Maps module enabled successfully for ${platforms.join(', ')}! 🎉');
+ exit(0);
+ } catch (e) {
+ print('Starter Error: $e');
+ exit(1);
+ }
+}
diff --git a/starter/scripts/modules/enable_location.dart b/starter/scripts/modules/enable_location.dart
new file mode 100644
index 000000000..80bd4a279
--- /dev/null
+++ b/starter/scripts/modules/enable_location.dart
@@ -0,0 +1,77 @@
+import 'dart:io';
+import '../utils.dart';
+
+void main(List arguments) async {
+ List platforms = getPlatforms(arguments);
+ String? ensembleVersion = getArgumentValue(arguments, 'ensemble_version');
+
+ final statements = {
+ 'moduleStatements': [
+ "import 'package:ensemble_location/location_module.dart';",
+ "GetIt.I.registerSingleton(LocationModuleImpl());",
+ ],
+ 'useStatements': [
+ 'static const useLocation = true;',
+ ],
+ };
+
+ final pubspecDependencies = [
+ {
+ 'statement': '''
+ensemble_location:
+ git:
+ url: https://github.com/EnsembleUI/ensemble.git
+ ref: ${await packageVersion(version: ensembleVersion)}
+ path: modules/location''',
+ 'regex':
+ r'#\s*ensemble_location:\s*\n\s*#\s*git:\s*\n\s*#\s*url:\s*https:\/\/github\.com\/EnsembleUI\/ensemble\.git\s*\n\s*#\s*ref:\s*main\s*\n\s*#\s*path:\s*modules\/location',
+ }
+ ];
+
+ final androidPermissions = [
+ '',
+ ];
+
+ final iOSPermissions = [
+ {
+ 'key': 'inUseLocationDescription',
+ 'value': 'NSLocationWhenInUseUsageDescription',
+ },
+ {
+ 'key': 'alwaysUseLocationDescription',
+ 'value': 'NSLocationAlwaysUsageDescription',
+ },
+ {
+ 'key': 'locationDescription',
+ 'value': 'NSLocationAlwaysAndWhenInUseUsageDescription'
+ }
+ ];
+
+ try {
+ // Update the ensemble_modules.dart file
+ updateEnsembleModules(
+ statements['moduleStatements'],
+ statements['useStatements'],
+ );
+
+ // Update the pubspec.yaml file
+ updatePubspec(pubspecDependencies);
+
+ // Add the location permissions to AndroidManifest.xml
+ if (platforms.contains('android')) {
+ updateAndroidPermissions(permissions: androidPermissions);
+ }
+
+ // Add the location usage description to the iOS Info.plist file
+ if (platforms.contains('ios')) {
+ updateIOSPermissions(iOSPermissions, arguments);
+ }
+
+ print(
+ 'Location module enabled successfully for ${platforms.join(', ')}! 🎉');
+ exit(0);
+ } catch (e) {
+ print('Starter Error: $e');
+ exit(1);
+ }
+}
diff --git a/starter/scripts/modules/enable_moengage.dart b/starter/scripts/modules/enable_moengage.dart
new file mode 100644
index 000000000..e8cd7fbe7
--- /dev/null
+++ b/starter/scripts/modules/enable_moengage.dart
@@ -0,0 +1,337 @@
+import 'dart:io';
+import '../utils.dart';
+import '../utils/firebase_utils.dart';
+
+void main(List arguments) async {
+ try {
+ // Parse and validate arguments
+ List platforms = getPlatforms(arguments);
+ String? ensembleVersion = getArgumentValue(arguments, 'ensemble_version');
+ String moengageAppId =
+ getArgumentValue(arguments, 'moengage_workspace_id', required: true) ??
+ '';
+ String enableConsoleLogs =
+ getArgumentValue(arguments, 'enableConsoleLogs') ?? 'true';
+
+ final statements = {
+ 'moduleStatements': [
+ "import 'package:ensemble_moengage/moengage.dart';",
+ 'GetIt.I.registerSingleton(MoEngageImpl());',
+ "import 'dart:io';",
+ "import 'package:flutter/foundation.dart';",
+ ],
+ 'useStatements': [
+ 'static const useMoEngage = true;',
+ ],
+ };
+
+ // Update Firebase configuration
+ updateFirebaseInitialization(platforms, arguments);
+ updateFirebaseConfig(platforms, arguments);
+
+ // Update the ensemble_modules.dart file
+ updateEnsembleModules(
+ statements['moduleStatements'],
+ statements['useStatements'],
+ );
+
+ // Update pubspec.yaml
+ final pubspecDependencies = [
+ {
+ 'statement': '''
+ensemble_moengage:
+ git:
+ url: https://github.com/EnsembleUI/ensemble.git
+ ref: ${await packageVersion(version: ensembleVersion)}
+ path: modules/moengage''',
+ 'regex':
+ r'#\s*ensemble_moengage:\s*\n\s*#\s*git:\s*\n\s*#\s*url:\s*https:\/\/github\.com\/EnsembleUI\/ensemble\.git\s*\n\s*#\s*ref:\s*main\s*\n\s*#\s*path:\s*modules\/moengage',
+ }
+ ];
+ updatePubspec(pubspecDependencies);
+
+ // Update MoEngage module registration with workspaceId and logs
+ String modulesContent = readFileContent(ensembleModulesFilePath);
+ modulesContent = modulesContent.replaceAll(
+ 'GetIt.I.registerSingleton(MoEngageImpl());',
+ 'GetIt.I.registerSingleton(MoEngageImpl(workspaceId: \'$moengageAppId\', enableLogs: $enableConsoleLogs));');
+ writeFileContent(ensembleModulesFilePath, modulesContent);
+
+ // Update ensemble.properties file
+ updatePropertiesFile("moengageWorkspaceId", moengageAppId);
+
+ // Platform specific updates
+ if (platforms.contains('android')) {
+ await _updateAndroidConfiguration(arguments);
+ }
+
+ if (platforms.contains('ios')) {
+ // Get all updates
+ final updates = [
+ getMoEngageImportUpdate(),
+ getMoEngageInitUpdate(moengageAppId),
+ getMoEngageFunctionsUpdate(),
+ ];
+
+ updateAppDelegate(updates);
+ }
+
+ print(
+ 'MoEngage module enabled successfully for ${platforms.join(', ')}! 🎉');
+ exit(0);
+ } catch (e) {
+ print('Starter Error: $e');
+ exit(1);
+ }
+}
+
+Future _updateAndroidConfiguration(List arguments) async {
+ final packageId = getPropertyValue('appId');
+ final kotlinPath = getKotlinPath(packageId);
+
+ // Create Kotlin files
+ await _createKotlinFiles(kotlinPath, packageId);
+
+ // update gradle files
+ _updateGradleFile();
+
+ // Update AndroidManifest.xml
+ await modifyAndroidManifest(permissions: [
+ '',
+ '',
+ ''
+ ], applicationAttributes: {
+ 'android:name': '.MyApplication'
+ }, intentFilters: [
+ {
+ 'identifier': '',
+ 'content': '''
+
+
+
+
+
+
+ '''
+ }
+ ], services: [
+ {
+ 'identifier': 'com.moengage.firebase.MoEFireBaseMessagingService',
+ 'content': '''
+
+
+
+
+ '''
+ }
+ ], activities: [
+ {
+ 'identifier': 'com.moengage.pushbase.activities.PushTracker',
+ 'content': '''
+'''
+ }
+ ]);
+}
+
+Future _createKotlinFiles(String kotlinPath, String packageId) async {
+ await createKotlinFile('$kotlinPath/CustomPushListener.kt',
+ _getCustomPushListenerContent(packageId));
+
+ await createKotlinFile(
+ '$kotlinPath/MyApplication.kt', _getMyApplicationContent(packageId));
+
+ await createKotlinFile(
+ '$kotlinPath/MainActivity.kt', _getMainActivityContent(packageId));
+}
+
+Future _updateGradleFile() async {
+ // Add MoEngage specific dependencies
+ addImplementationDependency(
+ "implementation platform('com.google.firebase:firebase-bom:32.7.0')");
+ addImplementationDependency(
+ "implementation 'com.moengage:moe-android-sdk:12.8.01'");
+ addImplementationDependency(
+ "implementation 'com.google.firebase:firebase-messaging:23.4.1'");
+ addImplementationDependency(
+ "implementation 'androidx.lifecycle:lifecycle-process:2.7.0'");
+ addImplementationDependency("implementation 'androidx.core:core:1.6.0'");
+ addImplementationDependency(
+ "implementation 'androidx.appcompat:appcompat:1.3.1'");
+ addImplementationDependency(
+ "implementation 'com.github.bumptech.glide:glide:4.9.0'");
+}
+
+// Kotlin file content templates
+String _getCustomPushListenerContent(String packageId) => '''
+package $packageId
+
+import android.app.Activity
+import android.os.Bundle
+import com.moengage.core.internal.logger.Logger
+import com.moengage.core.model.AccountMeta
+import com.moengage.plugin.base.push.PluginPushCallback
+
+class CustomPushListener(accountMeta: AccountMeta) : PluginPushCallback(accountMeta) {
+ private val tag = "CustomPushListener"
+
+ override fun onNotificationClick(activity: Activity, payload: Bundle): Boolean {
+ Logger.print { "\$tag onNotificationClick() : " }
+ return super.onNotificationClick(activity, payload)
+ }
+}''';
+
+String _getMyApplicationContent(String packageId) => '''
+package $packageId
+
+import com.moengage.core.DataCenter
+import com.moengage.core.LogLevel
+import com.moengage.core.MoEngage
+import com.moengage.core.config.LogConfig
+import com.moengage.core.config.FcmConfig
+import com.moengage.core.config.MoEngageEnvironmentConfig
+import com.moengage.core.config.NotificationConfig
+import com.moengage.core.config.PushKitConfig
+import com.moengage.core.model.AccountMeta
+import com.moengage.core.model.SdkState
+import com.moengage.core.model.environment.MoEngageEnvironment
+import com.moengage.flutter.MoEInitializer
+import com.moengage.pushbase.MoEPushHelper
+import android.app.Application
+
+class MyApplication : Application() {
+ override fun onCreate() {
+ super.onCreate()
+
+ val moEngage = MoEngage.Builder(this, BuildConfig.MOENGAGE_WORKSPACE_ID, DataCenter.DATA_CENTER_1)
+ .configureFcm(FcmConfig(true))
+ .configurePushKit(PushKitConfig(true))
+ .configureMoEngageEnvironment(MoEngageEnvironmentConfig(MoEngageEnvironment.DEFAULT))
+ .configureNotificationMetaData(
+ NotificationConfig(
+ R.mipmap.ic_launcher,
+ R.mipmap.ic_launcher,
+ notificationColor = -1,
+ isMultipleNotificationInDrawerEnabled = false,
+ isBuildingBackStackEnabled = true,
+ isLargeIconDisplayEnabled = true
+ )
+ )
+
+ MoEInitializer.initialiseDefaultInstance(this, moEngage)
+ }
+}''';
+
+String _getMainActivityContent(String packageId) => '''
+package $packageId
+
+import android.content.Intent
+import android.content.res.Configuration
+import android.os.Bundle
+import android.util.Log
+import com.moengage.flutter.MoEFlutterHelper
+import io.flutter.embedding.android.FlutterActivity
+import io.flutter.embedding.engine.FlutterEngine
+
+class MainActivity : FlutterActivity() {
+ private val TAG = "MainActivity"
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ processIntent(intent)
+ }
+
+ override fun onConfigurationChanged(newConfig: Configuration) {
+ super.onConfigurationChanged(newConfig)
+ Log.d(TAG, "onConfigurationChanged(): \${newConfig.orientation}")
+ MoEFlutterHelper.getInstance().onConfigurationChanged()
+ }
+
+ override fun onNewIntent(intent: Intent) {
+ super.onNewIntent(intent)
+ processIntent(intent)
+ }
+
+ private fun processIntent(intent: Intent?) {
+ if (intent == null) return
+ Log.d(TAG, "processIntent(): \${intent.data}")
+ }
+
+ override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
+ super.configureFlutterEngine(flutterEngine)
+ }
+}''';
+
+// Get MoEngage import pattern updates
+Map getMoEngageImportUpdate() {
+ return {
+ 'pattern': r'import\s+UIKit\s*\nimport\s+Flutter\s*\n',
+ 'replacement': '''import UIKit
+import Flutter
+import moengage_flutter_ios
+import MoEngageSDK
+import MoEngageInApps
+import MoEngageMessaging
+
+'''
+ };
+}
+
+// Get MoEngage initialization pattern updates
+Map getMoEngageInitUpdate(String moengageAppId) {
+ return {
+ 'pattern':
+ r'if\s+#available\(iOS\s+10\.0,\s*\*\)\s*{\s*\n\s*UNUserNotificationCenter\.current\(\)\.delegate\s*=\s*self\s+as\s+UNUserNotificationCenterDelegate\s*\n\s*}',
+ 'replacement': '''// MoEngage initialization
+ let sdkConfig = MoEngageSDKConfig(withAppID: "$moengageAppId")
+ sdkConfig.appGroupID = "group.com.alphadevs.MoEngage.NotificationServices"
+ sdkConfig.consoleLogConfig = MoEngageConsoleLogConfig(isLoggingEnabled: true, loglevel: .verbose)
+
+ MoEngageSDKCore.sharedInstance.enableAllLogs()
+ MoEngageInitializer.sharedInstance.initializeDefaultInstance(sdkConfig, launchOptions: launchOptions)
+
+ if #available(iOS 10.0, *) {
+ UNUserNotificationCenter.current().delegate = self as UNUserNotificationCenterDelegate
+ }'''
+ };
+}
+
+// Get MoEngage functions pattern updates
+Map getMoEngageFunctionsUpdate() {
+ return {
+ 'pattern':
+ r'override\s+func\s+application\(\s*_\s+app:\s*UIApplication,\s*open\s+url:\s*URL,\s*options:\s*\[UIApplication\.OpenURLOptionsKey\s*:\s*Any\]\s*=\s*\[:\]\)\s*->\s*Bool\s*{\s*\n\s*//\s*Calling\s+flutter\s+method\s*"urlOpened"\s*from\s*iOS\s*\n\s*methodChannel\?\.invokeMethod\("urlOpened",\s*arguments:\s*url\.absoluteString\)\s*\n\s*return\s+true\s*\n\s*}\s*\n}',
+ 'replacement':
+ '''override func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
+ // Calling flutter method "urlOpened" from iOS
+ methodChannel?.invokeMethod("urlOpened", arguments: url.absoluteString)
+ return true
+ }
+
+ // MoEngage notification handling functions
+ override func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
+ MoEngageSDKMessaging.sharedInstance.setPushToken(deviceToken)
+ }
+
+ override func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
+ completionHandler([.alert, .sound])
+ }
+
+ override func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
+ MoEngageSDKMessaging.sharedInstance.userNotificationCenter(center, didReceive: response)
+ completionHandler()
+ }
+
+ override func application(_ application: UIApplication, willContinueUserActivityWithType userActivityType: String) -> Bool {
+ print("Opening Universal link", userActivityType)
+ return false
+ }
+}'''
+ };
+}
\ No newline at end of file
diff --git a/starter/scripts/modules/enable_network_info.dart b/starter/scripts/modules/enable_network_info.dart
new file mode 100644
index 000000000..b2658912a
--- /dev/null
+++ b/starter/scripts/modules/enable_network_info.dart
@@ -0,0 +1,88 @@
+import 'dart:io';
+
+import '../utils.dart';
+
+void main(List arguments) async {
+ List platforms = getPlatforms(arguments);
+ String? ensembleVersion = getArgumentValue(arguments, 'ensemble_version');
+ String? preciseLocationDescription =
+ getArgumentValue(arguments, 'preciseLocationDescription');
+
+ final statements = {
+ 'moduleStatements': [
+ "import 'package:ensemble_network_info/network_info.dart';",
+ 'GetIt.I.registerSingleton(NetworkInfoImpl());',
+ ],
+ 'useStatements': [
+ 'static const useNetworkInfo = true;',
+ ],
+ };
+
+ final pubspecDependencies = [
+ {
+ 'statement': '''
+ensemble_network_info:
+ git:
+ url: https://github.com/EnsembleUI/ensemble.git
+ ref: ${await packageVersion(version: ensembleVersion)}
+ path: modules/ensemble_network_info''',
+ 'regex':
+ r'#\s*ensemble_network_info:\s*\n\s*#\s*git:\s*\n\s*#\s*url:\s*https:\/\/github\.com\/EnsembleUI\/ensemble\.git\s*\n\s*#\s*ref:\s*main\s*\n\s*#\s*path:\s*modules\/ensemble_network_info',
+ }
+ ];
+
+ final androidPermissions = [
+ '',
+ ''
+ ];
+
+ final iOSPermissions = [
+ {
+ 'key': 'inUseLocationDescription',
+ 'value': 'NSLocationWhenInUseUsageDescription',
+ },
+ {
+ 'key': 'alwaysUseLocationDescription',
+ 'value': 'NSLocationAlwaysAndWhenInUseUsageDescription',
+ },
+ ];
+
+ try {
+ // Update the ensemble_modules.dart file
+ updateEnsembleModules(
+ statements['moduleStatements'],
+ statements['useStatements'],
+ );
+
+ // Update the pubspec.yaml file
+ updatePubspec(pubspecDependencies);
+
+ // Add required permissions to AndroidManifest.xml
+ if (platforms.contains('android')) {
+ updateAndroidPermissions(permissions: androidPermissions);
+ }
+
+ // Add required permissions to Info.plist
+ if (platforms.contains('ios')) {
+ if (preciseLocationDescription == null ||
+ preciseLocationDescription.isEmpty) {
+ print("Error: Precise location description is missing.");
+ exit(1);
+ }
+ updateIOSPermissions(iOSPermissions, arguments);
+ addPermissionDescriptionToInfoPlist(
+ 'NSLocationTemporaryUsageDescriptionDictionary',
+ {'PreciseLocation': preciseLocationDescription},
+ isDict: true,
+ );
+ updateRunnerEntitlements(module: 'networkInfo');
+ }
+
+ print(
+ 'Network Info module enabled successfully for ${platforms.join(', ')}! 🎉');
+ exit(0);
+ } catch (e) {
+ print('Starter Error: $e');
+ exit(1);
+ }
+}
diff --git a/starter/scripts/modules/enable_notifications.dart b/starter/scripts/modules/enable_notifications.dart
new file mode 100644
index 000000000..db2b6c80e
--- /dev/null
+++ b/starter/scripts/modules/enable_notifications.dart
@@ -0,0 +1,68 @@
+import 'dart:io';
+
+import '../utils.dart';
+import '../utils/firebase_utils.dart';
+
+void main(List arguments) {
+ List platforms = getPlatforms(arguments);
+
+ final statements = {
+ 'moduleStatements': [
+ "import 'package:flutter/foundation.dart';",
+ "import 'dart:io';",
+ ],
+ 'useStatements': [
+ 'static const useNotifications = true;',
+ ],
+ };
+
+ final androidPermissions = [
+ '',
+ ];
+
+ const notificationsMetaData = [
+ ''
+ ];
+
+ try {
+ // Update the ensemble_modules.dart file
+ updateEnsembleModules(
+ statements['moduleStatements'],
+ statements['useStatements'],
+ );
+
+ // Generate Firebase configuration based on platform
+ updateFirebaseInitialization(platforms, arguments);
+ updateFirebaseConfig(platforms, arguments);
+
+ if (platforms.contains('android')) {
+ addClasspathDependency(
+ "classpath 'com.google.gms:google-services:4.3.15'");
+ addPluginDependency("apply plugin: 'com.google.gms.google-services'");
+ addSettingsPluginDependency(
+ 'id "com.google.gms.google-services" version "4.3.15" apply false');
+ addImplementationDependency(
+ "implementation platform('com.google.firebase:firebase-bom:32.7.0')");
+ }
+
+ // Configure Android-specific settings
+ if (platforms.contains('android')) {
+ updateAndroidPermissions(
+ permissions: androidPermissions, metaData: notificationsMetaData);
+ }
+
+ // Configure iOS-specific settings
+ if (platforms.contains('ios')) {
+ updateRunnerEntitlements(
+ module: 'notifications',
+ );
+ }
+
+ print(
+ 'Notifications module enabled successfully for ${platforms.join(', ')}! 🎉');
+ exit(0);
+ } catch (e) {
+ print('Starter Error: $e');
+ exit(1);
+ }
+}
diff --git a/starter/scripts/modules/enable_qr_code.dart b/starter/scripts/modules/enable_qr_code.dart
new file mode 100644
index 000000000..553fd23d3
--- /dev/null
+++ b/starter/scripts/modules/enable_qr_code.dart
@@ -0,0 +1,28 @@
+import 'dart:io';
+
+import '../utils.dart';
+
+Future main(List arguments) async {
+ List platforms = getPlatforms(arguments);
+ final cameraStatements = {
+ 'moduleStatements': [
+ "import 'package:ensemble_camera/qr_code_scanner.dart';",
+ "GetIt.I.registerSingleton(",
+ " EnsembleQRCodeScannerImpl.build(EnsembleQRCodeScannerController()));",
+ ],
+ };
+
+ try {
+ // Update the ensemble_modules.dart file
+ updateEnsembleModules(
+ cameraStatements['moduleStatements'],
+ cameraStatements['useStatements'],
+ );
+
+ print('QR module enabled successfully for ${platforms.join(', ')}! 🎉');
+ exit(0);
+ } catch (e) {
+ print('Starter Error: $e');
+ exit(1);
+ }
+}
diff --git a/starter/scripts/utils.dart b/starter/scripts/utils.dart
new file mode 100644
index 000000000..4c78e1276
--- /dev/null
+++ b/starter/scripts/utils.dart
@@ -0,0 +1,789 @@
+import 'dart:io';
+
+const String ensembleModulesFilePath = 'lib/generated/ensemble_modules.dart';
+const String pubspecFilePath = 'pubspec.yaml';
+const String androidManifestFilePath =
+ 'android/app/src/main/AndroidManifest.xml';
+const String iosInfoPlistFilePath = 'ios/Runner/Info.plist';
+const String webIndexFilePath = 'web/index.html';
+const String ensemblePropertiesFilePath = 'ensemble/ensemble.properties';
+const String ensembleConfigFilePath = 'ensemble/ensemble-config.yaml';
+const String appDelegatePath = 'ios/Runner/AppDelegate.swift';
+const String runnerEntitlementsPath = 'ios/Runner/Runner.entitlements';
+const String androidBuildGradleFilePath = 'android/build.gradle';
+const String androidAppBuildGradleFilePath = 'android/app/build.gradle';
+const String androidSettingsGradleFilePath = 'android/settings.gradle';
+const String proguardRulesFilePath = 'android/app/proguard-rules.pro';
+
+// To read file content
+String readFileContent(String filePath) {
+ File file = File(filePath);
+ if (!file.existsSync()) {
+ throw Exception('$filePath not found.');
+ }
+ return file.readAsStringSync();
+}
+
+// Helper function to parse individual arguments in key=value format
+String? getArgumentValue(List arguments, String key,
+ {bool required = false}) {
+ for (var arg in arguments) {
+ final parts = arg.split('=');
+ if (parts.length == 2 && parts[0] == key) {
+ return parts[1];
+ }
+ }
+
+ if (required) {
+ throw Exception('Missing required argument: $key');
+ }
+
+ return null;
+}
+
+// Process platforms argument, defaulting to ['ios', 'android', 'web'] if not specified
+List getPlatforms(List arguments,
+ {List defaultPlatforms = const ['ios', 'android', 'web']}) {
+ String? platformArg = getArgumentValue(arguments, 'platform');
+ if (platformArg != null && platformArg.isNotEmpty) {
+ return platformArg.split(',').map((platform) => platform.trim()).toList();
+ }
+ return defaultPlatforms;
+}
+
+// To update content using regex
+String updateContent(String content, String regexPattern, String replacement) {
+ final RegExp regex = RegExp(regexPattern);
+ if (!regex.hasMatch(content) && !content.contains(replacement)) {
+ throw Exception('Pattern not found: $regexPattern');
+ }
+ return content.replaceAllMapped(regex, (match) => replacement);
+}
+
+// To write updated content to file
+void writeFileContent(String filePath, String content) {
+ File file = File(filePath);
+ file.writeAsStringSync(content);
+}
+
+// Add permission descriptions to Info.plist
+void addPermissionDescriptionToInfoPlist(String key, dynamic description,
+ {bool isArray = false, bool isBoolean = false, bool isDict = false}) {
+ File plistFile = File(iosInfoPlistFilePath);
+ if (!plistFile.existsSync()) {
+ throw Exception('Error: File does not exist at $iosInfoPlistFilePath');
+ }
+
+ String plistContent = plistFile.readAsStringSync();
+ bool updated = false;
+
+ if (plistContent.contains('$key')) {
+ if (!isArray && !isBoolean && !isDict) {
+ RegExp regex = RegExp('$key\\s*[^<]*');
+ String replacement = '$key\n $description';
+ plistContent = plistContent.replaceAll(regex, replacement);
+ updated = true;
+ } else if (isBoolean) {
+ RegExp regex = RegExp('$key\\s*<(true|false)/>');
+ String replacement =
+ '$key\n <${description ? 'true' : 'false'}/>';
+ plistContent = plistContent.replaceAll(regex, replacement);
+ updated = true;
+ } else if (isArray) {
+ RegExp regex =
+ RegExp('$key\\s*(.*?)', dotAll: true);
+ String arrayValues = (description as List)
+ .map((item) => ' $item')
+ .join('\n');
+ String replacement =
+ '$key\n \n$arrayValues\n ';
+ plistContent = plistContent.replaceAll(regex, replacement);
+ updated = true;
+ } else if (isDict) {
+ RegExp regex =
+ RegExp('$key\\s*(.*?)', dotAll: true);
+ String dictValues = (description as Map)
+ .entries
+ .map((entry) =>
+ ' ${entry.key}\n ${entry.value}')
+ .join('\n');
+ String replacement =
+ '$key\n \n$dictValues\n ';
+ plistContent = plistContent.replaceAll(regex, replacement);
+ updated = true;
+ }
+ }
+
+ if (!updated) {
+ // Find the closing tag to insert before
+ final dictEndIndex = plistContent.lastIndexOf('');
+ if (dictEndIndex != -1) {
+ String toInsert;
+ if (isArray) {
+ String arrayValues = (description as List)
+ .map((item) => ' $item')
+ .join('\n');
+ toInsert =
+ ' $key\n \n$arrayValues\n \n';
+ } else if (isBoolean) {
+ toInsert =
+ ' $key\n <${description ? 'true' : 'false'}/>\n';
+ } else if (isDict) {
+ String dictValues = (description as Map)
+ .entries
+ .map((entry) =>
+ ' ${entry.key}\n ${entry.value}')
+ .join('\n');
+ toInsert =
+ ' $key\n \n$dictValues\n \n';
+ } else {
+ toInsert = ' $key\n $description\n';
+ }
+
+ plistContent =
+ plistContent.replaceRange(dictEndIndex, dictEndIndex, toInsert);
+ updated = true;
+ }
+ }
+
+ if (!updated) {
+ throw Exception('Failed to update Info.plist with $key');
+ }
+
+ plistFile.writeAsStringSync(plistContent);
+}
+
+// Convert a string to a regex pattern
+String toRegexPattern(String statement, {bool isBoolean = false}) {
+ if (isBoolean) {
+ // For boolean statements like 'static const useCamera = true;'
+ final prefix = statement.split('=')[0].trim();
+ return RegExp.escape(prefix) + r'\s*=\s*(true|false);';
+ } else {
+ // For code statements like imports or registrations that may be commented out
+ String escapedStatement = RegExp.escape(statement);
+ return r'\/\/\s*' + escapedStatement.replaceAll(' ', r'\s+');
+ }
+}
+
+// Update ensemble_modules.dart file
+void updateEnsembleModules(
+ List? codeStatements, List? useStatements) {
+ String content = readFileContent(ensembleModulesFilePath);
+
+ // Process code statements (imports and register statements)
+ if (codeStatements != null && codeStatements.isNotEmpty) {
+ for (var statement in codeStatements) {
+ String regexPattern = toRegexPattern(statement);
+ content = updateContent(content, regexPattern, statement);
+ }
+ }
+
+ // Process use statements (e.g., static const useCamera = true)
+ if (useStatements != null && useStatements.isNotEmpty) {
+ for (var statement in useStatements) {
+ String regexPattern = toRegexPattern(statement, isBoolean: true);
+ content = updateContent(content, regexPattern, statement);
+ }
+ }
+
+ writeFileContent(ensembleModulesFilePath, content);
+}
+
+// Update pubspec.yaml file and throw error if content is not updated
+void updatePubspec(List