From 478d71e05e22d14f68f15653928fb0ea25ca920d Mon Sep 17 00:00:00 2001 From: Muhammad Rehan Date: Thu, 7 Nov 2024 23:16:48 +0500 Subject: [PATCH] chore: update sample app settings ui for data pipelines (#162) --- apps/amiapp_flutter/lib/src/app.dart | 2 +- apps/amiapp_flutter/lib/src/customer_io.dart | 12 +- apps/amiapp_flutter/lib/src/data/config.dart | 27 ++- .../lib/src/screens/settings.dart | 166 +++++++++--------- .../lib/src/utils/extensions.dart | 44 +++-- .../lib/src/widgets/settings_form_field.dart | 3 + 6 files changed, 140 insertions(+), 114 deletions(-) diff --git a/apps/amiapp_flutter/lib/src/app.dart b/apps/amiapp_flutter/lib/src/app.dart index 9ca6f17..4744fce 100644 --- a/apps/amiapp_flutter/lib/src/app.dart +++ b/apps/amiapp_flutter/lib/src/app.dart @@ -119,8 +119,8 @@ class _AmiAppState extends State { path: Screen.settings.path, builder: (context, state) => SettingsScreen( auth: _auth, + cdpApiKeyInitialValue: state.uri.queryParameters['cdp_api_key'], siteIdInitialValue: state.uri.queryParameters['site_id'], - apiKeyInitialValue: state.uri.queryParameters['api_key'], ), ), GoRoute( diff --git a/apps/amiapp_flutter/lib/src/customer_io.dart b/apps/amiapp_flutter/lib/src/customer_io.dart index 313854a..fb1b2d5 100644 --- a/apps/amiapp_flutter/lib/src/customer_io.dart +++ b/apps/amiapp_flutter/lib/src/customer_io.dart @@ -55,19 +55,19 @@ class CustomerIOSDK extends ChangeNotifier { } final InAppConfig? inAppConfig; - if (_sdkConfig?.migrationSiteId != null) { - inAppConfig = InAppConfig(siteId: _sdkConfig!.migrationSiteId ?? ''); + final migrationSiteId = _sdkConfig?.migrationSiteId; + if (migrationSiteId != null) { + inAppConfig = InAppConfig(siteId: migrationSiteId); } else { inAppConfig = null; } return CustomerIO.initialize( config: CustomerIOConfig( - cdpApiKey: '${_sdkConfig?.cdnHost}:${_sdkConfig?.cdpApiKey}', - migrationSiteId: _sdkConfig?.migrationSiteId, + cdpApiKey: _sdkConfig?.cdpApiKey ?? 'INVALID', + migrationSiteId: migrationSiteId, region: Region.us, logLevel: logLevel, - autoTrackDeviceAttributes: - _sdkConfig?.autoTrackDeviceAttributes, + autoTrackDeviceAttributes: _sdkConfig?.autoTrackDeviceAttributes, apiHost: _sdkConfig?.apiHost, cdnHost: _sdkConfig?.cdnHost, flushAt: _sdkConfig?.flushAt, diff --git a/apps/amiapp_flutter/lib/src/data/config.dart b/apps/amiapp_flutter/lib/src/data/config.dart index c162d60..d08b74b 100644 --- a/apps/amiapp_flutter/lib/src/data/config.dart +++ b/apps/amiapp_flutter/lib/src/data/config.dart @@ -33,9 +33,8 @@ class CustomerIOSDKConfig { PushConfig? pushConfig, }) : pushConfig = pushConfig ?? PushConfig(); - factory CustomerIOSDKConfig.fromEnv() => - CustomerIOSDKConfig( - cdpApiKey: dotenv.env[_PreferencesKey.cdpApiKey]!, + factory CustomerIOSDKConfig.fromEnv() => CustomerIOSDKConfig( + cdpApiKey: dotenv.env[_PreferencesKey.cdpApiKey] ?? 'INVALID', migrationSiteId: dotenv.env[_PreferencesKey.migrationSiteId], ); @@ -46,19 +45,19 @@ class CustomerIOSDKConfig { throw ArgumentError('cdpApiKey cannot be null'); } + final region = prefs.getString(_PreferencesKey.region) != null + ? Region.values.firstWhere( + (e) => e.name == prefs.getString(_PreferencesKey.region)) + : null; return CustomerIOSDKConfig( cdpApiKey: cdpApiKey, migrationSiteId: prefs.getString(_PreferencesKey.migrationSiteId), - region: prefs.getString(_PreferencesKey.region) != null - ? Region.values.firstWhere( - (e) => e.name == prefs.getString(_PreferencesKey.region)) - : null, - debugModeEnabled: prefs.getBool(_PreferencesKey.debugModeEnabled) != - false, - screenTrackingEnabled: prefs.getBool( - _PreferencesKey.screenTrackingEnabled) != false, + region: region, + debugModeEnabled: prefs.getBool(_PreferencesKey.debugModeEnabled), + screenTrackingEnabled: + prefs.getBool(_PreferencesKey.screenTrackingEnabled), autoTrackDeviceAttributes: - prefs.getBool(_PreferencesKey.autoTrackDeviceAttributes), + prefs.getBool(_PreferencesKey.autoTrackDeviceAttributes), apiHost: prefs.getString(_PreferencesKey.apiHost), cdnHost: prefs.getString(_PreferencesKey.cdnHost), flushAt: prefs.getInt(_PreferencesKey.flushAt), @@ -115,8 +114,8 @@ extension ConfigurationPreferencesExtensions on SharedPreferences { await setOrRemoveBool(_PreferencesKey.autoTrackDeviceAttributes, config.autoTrackDeviceAttributes); result = result && - await setOrRemoveBool( - _PreferencesKey.screenTrackingEnabled, config.screenTrackingEnabled); + await setOrRemoveBool(_PreferencesKey.screenTrackingEnabled, + config.screenTrackingEnabled); result = result && await setOrRemoveString(_PreferencesKey.apiHost, config.apiHost); result = result && diff --git a/apps/amiapp_flutter/lib/src/screens/settings.dart b/apps/amiapp_flutter/lib/src/screens/settings.dart index 30aab80..4da705a 100644 --- a/apps/amiapp_flutter/lib/src/screens/settings.dart +++ b/apps/amiapp_flutter/lib/src/screens/settings.dart @@ -15,13 +15,13 @@ import '../widgets/settings_form_field.dart'; class SettingsScreen extends StatefulWidget { final AmiAppAuth auth; + final String? cdpApiKeyInitialValue; final String? siteIdInitialValue; - final String? apiKeyInitialValue; const SettingsScreen({ required this.auth, + this.cdpApiKeyInitialValue, this.siteIdInitialValue, - this.apiKeyInitialValue, super.key, }); @@ -35,11 +35,12 @@ class _SettingsScreenState extends State { final _formKey = GlobalKey(); late final TextEditingController _deviceTokenValueController; - late final TextEditingController _trackingURLValueController; + late final TextEditingController _cdpApiKeyValueController; late final TextEditingController _siteIDValueController; - late final TextEditingController _apiKeyValueController; - late final TextEditingController _bqSecondsDelayValueController; - late final TextEditingController _bqMinNumberOfTasksValueController; + late final TextEditingController _apiHostValueController; + late final TextEditingController _cdnHostValueController; + late final TextEditingController _flushAtValueController; + late final TextEditingController _flushIntervalValueController; late bool _featureTrackScreens; late bool _featureTrackDeviceAttributes; @@ -52,19 +53,19 @@ class _SettingsScreenState extends State { final cioConfig = widget._customerIOSDK.sdkConfig; _deviceTokenValueController = TextEditingController(); - // _trackingURLValueController = - // TextEditingController(text: cioConfig?.trackingUrl); + _cdpApiKeyValueController = TextEditingController( + text: widget.cdpApiKeyInitialValue ?? cioConfig?.cdpApiKey); _siteIDValueController = TextEditingController( text: widget.siteIdInitialValue ?? cioConfig?.migrationSiteId); - _apiKeyValueController = TextEditingController( - text: widget.apiKeyInitialValue ?? cioConfig?.cdpApiKey); - // _bqSecondsDelayValueController = TextEditingController( - // text: cioConfig?.backgroundQueueSecondsDelay?.toTrimmedString()); - // _bqMinNumberOfTasksValueController = TextEditingController( - // text: cioConfig?.backgroundQueueMinNumOfTasks?.toString()); - // _featureTrackScreens = cioConfig?.screenTrackingEnabled ?? true; - // _featureTrackDeviceAttributes = - // cioConfig?.deviceAttributesTrackingEnabled ?? true; + _apiHostValueController = TextEditingController(text: cioConfig?.apiHost); + _cdnHostValueController = TextEditingController(text: cioConfig?.cdnHost); + _flushAtValueController = + TextEditingController(text: cioConfig?.flushAt?.toString()); + _flushIntervalValueController = TextEditingController( + text: cioConfig?.flushInterval?.toTrimmedString()); + _featureTrackScreens = cioConfig?.screenTrackingEnabled ?? true; + _featureTrackDeviceAttributes = + cioConfig?.autoTrackDeviceAttributes ?? true; _featureDebugMode = cioConfig?.debugModeEnabled ?? true; super.initState(); @@ -76,15 +77,14 @@ class _SettingsScreenState extends State { } final newConfig = CustomerIOSDKConfig( - migrationSiteId: _siteIDValueController.text.trim(), - cdpApiKey: _apiKeyValueController.text.trim(), - // trackingUrl: _trackingURLValueController.text.trim(), - // backgroundQueueSecondsDelay: - // _bqSecondsDelayValueController.text.trim().toDoubleOrNull(), - // backgroundQueueMinNumOfTasks: - // _bqMinNumberOfTasksValueController.text.trim().toIntOrNull(), + cdpApiKey: _cdpApiKeyValueController.text.trim(), + migrationSiteId: _siteIDValueController.text.trim().nullIfEmpty(), + apiHost: _apiHostValueController.text.trim().nullIfEmpty(), + cdnHost: _cdnHostValueController.text.trim().nullIfEmpty(), + flushAt: _flushAtValueController.text.trim().toIntOrNull(), + flushInterval: _flushIntervalValueController.text.trim().toIntOrNull(), screenTrackingEnabled: _featureTrackScreens, - // deviceAttributesTrackingEnabled: _featureTrackDeviceAttributes, + autoTrackDeviceAttributes: _featureTrackDeviceAttributes, debugModeEnabled: _featureDebugMode, ); widget._customerIOSDK.saveConfigToPreferences(newConfig).then((success) { @@ -109,16 +109,16 @@ class _SettingsScreenState extends State { } setState(() { + _cdpApiKeyValueController.text = defaultConfig.cdpApiKey; _siteIDValueController.text = defaultConfig.migrationSiteId ?? ''; - _apiKeyValueController.text = defaultConfig.cdpApiKey; - // _trackingURLValueController.text = defaultConfig.trackingUrl ?? ''; - // _bqSecondsDelayValueController.text = - // defaultConfig.backgroundQueueSecondsDelay?.toTrimmedString() ?? ''; - // _bqMinNumberOfTasksValueController.text = - // defaultConfig.backgroundQueueMinNumOfTasks?.toString() ?? ''; - // _featureTrackScreens = defaultConfig.screenTrackingEnabled; - // _featureTrackDeviceAttributes = - // defaultConfig.deviceAttributesTrackingEnabled; + _apiHostValueController.text = defaultConfig.apiHost ?? ''; + _cdnHostValueController.text = defaultConfig.cdnHost ?? ''; + _flushAtValueController.text = defaultConfig.flushAt?.toString() ?? ''; + _flushIntervalValueController.text = + defaultConfig.flushInterval?.toTrimmedString() ?? ''; + _featureTrackScreens = defaultConfig.screenTrackingEnabled ?? true; + _featureTrackDeviceAttributes = + defaultConfig.autoTrackDeviceAttributes ?? true; _featureDebugMode = defaultConfig.debugModeEnabled ?? true; _saveSettings(context); }); @@ -160,6 +160,7 @@ class _SettingsScreenState extends State { TextSettingsFormField( labelText: 'Device Token', semanticsLabel: 'Device Token Input', + hintText: 'Fetching...', valueController: _deviceTokenValueController, readOnly: true, suffixIcon: IconButton( @@ -178,20 +179,11 @@ class _SettingsScreenState extends State { }, ), ), - const SizedBox(height: 16), - TextSettingsFormField( - labelText: 'CIO Track URL', - semanticsLabel: 'Track URL Input', - valueController: _trackingURLValueController, - validator: (value) => value?.isValidUrl() != false - ? null - : 'Please enter formatted url e.g. https://tracking.cio/', - ), const SizedBox(height: 32), TextSettingsFormField( - labelText: 'Site Id', - semanticsLabel: 'Site ID Input', - valueController: _siteIDValueController, + labelText: 'CDP API Key', + semanticsLabel: 'CDP API Key Input', + valueController: _cdpApiKeyValueController, validator: (value) => value?.trim().isNotEmpty == true ? null @@ -199,32 +191,46 @@ class _SettingsScreenState extends State { ), const SizedBox(height: 16), TextSettingsFormField( - labelText: 'API Key', - semanticsLabel: 'API Key Input', - valueController: _apiKeyValueController, - validator: (value) => - value?.trim().isNotEmpty == true - ? null - : 'This field cannot be blank', + labelText: 'Site Id', + semanticsLabel: 'Site ID Input', + valueController: _siteIDValueController, ), const SizedBox(height: 32), TextSettingsFormField( - labelText: 'backgroundQueueSecondsDelay', - semanticsLabel: 'BQ Seconds Delay Input', - valueController: _bqSecondsDelayValueController, - keyboardType: const TextInputType.numberWithOptions( - decimal: true), + labelText: 'API Host', + semanticsLabel: 'API Host Input', + hintText: 'cdp.customer.io/v1', + valueController: _apiHostValueController, + validator: (value) => value?.isEmptyOrValidUrl() != false + ? null + : 'Please enter url e.g. cdp.customer.io/v1 (without https)', + ), + const SizedBox(height: 16), + TextSettingsFormField( + labelText: 'CDN Host', + semanticsLabel: 'CDN Host Input', + hintText: 'cdp.customer.io/v1', + valueController: _cdnHostValueController, + validator: (value) => value?.isEmptyOrValidUrl() != false + ? null + : 'Please enter url e.g. cdp.customer.io/v1 (without https)', + ), + const SizedBox(height: 32), + TextSettingsFormField( + labelText: 'Flush At', + semanticsLabel: 'BQ Min Number of Tasks Input', + hintText: '20', + valueController: _flushAtValueController, + keyboardType: TextInputType.number, validator: (value) { bool isBlank = value?.trim().isNotEmpty != true; - if (isBlank) { - return 'This field cannot be blank'; - } - - double minValue = 1.0; - bool isInvalid = - value?.isValidDouble(min: minValue) != true; - if (isInvalid) { - return 'The value must be greater than or equal to $minValue'; + if (!isBlank) { + int minValue = 1; + bool isInvalid = + value?.isValidInt(min: minValue) != true; + if (isInvalid) { + return 'The value must be greater than or equal to $minValue'; + } } return null; @@ -232,21 +238,21 @@ class _SettingsScreenState extends State { ), const SizedBox(height: 16), TextSettingsFormField( - labelText: 'backgroundQueueMinNumberOfTasks', - semanticsLabel: 'BQ Min Number of Tasks Input', - valueController: _bqMinNumberOfTasksValueController, - keyboardType: TextInputType.number, + labelText: 'Flush Interval', + semanticsLabel: 'BQ Seconds Delay Input', + hintText: '30', + valueController: _flushIntervalValueController, + keyboardType: const TextInputType.numberWithOptions( + decimal: true), validator: (value) { bool isBlank = value?.trim().isNotEmpty != true; - if (isBlank) { - return 'This field cannot be blank'; - } - - int minValue = 1; - bool isInvalid = - value?.isValidInt(min: minValue) != true; - if (isInvalid) { - return 'The value must be greater than or equal to $minValue'; + if (!isBlank) { + int minValue = 1; + bool isInvalid = + value?.isValidInt(min: minValue) != true; + if (isInvalid) { + return 'The value must be greater than or equal to $minValue'; + } } return null; diff --git a/apps/amiapp_flutter/lib/src/utils/extensions.dart b/apps/amiapp_flutter/lib/src/utils/extensions.dart index 1e5b085..c7dbcc9 100644 --- a/apps/amiapp_flutter/lib/src/utils/extensions.dart +++ b/apps/amiapp_flutter/lib/src/utils/extensions.dart @@ -32,6 +32,10 @@ extension AmiAppExtensions on BuildContext { extension AmiAppStringExtensions on String { bool equalsIgnoreCase(String? other) => toLowerCase() == other?.toLowerCase(); + String? nullIfEmpty() { + return isEmpty ? null : this; + } + int? toIntOrNull() { if (isNotEmpty) { return int.tryParse(this); @@ -58,23 +62,35 @@ extension AmiAppStringExtensions on String { } } - bool isValidUrl() { + bool isEmptyOrValidUrl() { String url = trim(); - // Empty text is not considered valid. + // Empty text is considered valid if (url.isEmpty) { + return true; + } + // If the URL contains a scheme, it is considered invalid + if (url.contains("://")) { return false; } - - // Currently only Android fails on URLs with empty host, still adding - // validation for all platforms to keep it consistent for app users - final Uri? uri = Uri.tryParse(url); + // Ensure the URL is prefixed with "https://" so that it can be parsed + final prefixedUrl = "https://$url"; + // If the URL is not parsable, it is considered invalid + final Uri? uri = Uri.tryParse(prefixedUrl); if (uri == null) { return false; } - // Valid URL with a host and http/https scheme - return uri.hasAuthority && - (uri.scheme == 'http' || uri.scheme == 'https') && - uri.path.endsWith("/"); + + // Check if the last character is alphanumeric + final isLastCharValid = RegExp(r'[a-zA-Z0-9]$').hasMatch(url); + + // Check validity conditions: + // - URL should not end with a slash + // - URL should contain a domain (e.g., cdp.customer.io) + // - URL should not contain a query or fragment + return isLastCharValid && + uri.host.contains('.') && + uri.query.isEmpty && + uri.fragment.isEmpty; } bool isValidInt({int? min, int? max}) { @@ -92,7 +108,7 @@ extension AmiAppStringExtensions on String { } } -extension AmiAppDoubleExtensions on double { +extension AmiAppIntExtensions on int { String? toTrimmedString() { if (this % 1.0 != 0.0) { return toString(); @@ -109,7 +125,9 @@ extension LocationExtensions on GoRouter { // https://flutter.dev/go/go-router-v9-breaking-changes String currentLocation() { final RouteMatch lastMatch = routerDelegate.currentConfiguration.last; - final RouteMatchList matchList = lastMatch is ImperativeRouteMatch ? lastMatch.matches : routerDelegate.currentConfiguration; + final RouteMatchList matchList = lastMatch is ImperativeRouteMatch + ? lastMatch.matches + : routerDelegate.currentConfiguration; return matchList.uri.toString(); } -} \ No newline at end of file +} diff --git a/apps/amiapp_flutter/lib/src/widgets/settings_form_field.dart b/apps/amiapp_flutter/lib/src/widgets/settings_form_field.dart index 6374911..79d068d 100644 --- a/apps/amiapp_flutter/lib/src/widgets/settings_form_field.dart +++ b/apps/amiapp_flutter/lib/src/widgets/settings_form_field.dart @@ -62,6 +62,9 @@ class TextSettingsFormField extends StatelessWidget { semanticsLabel: semanticsLabel, ), hintText: hintText, + hintStyle: TextStyle( + color: Theme.of(context).colorScheme.outline, + ), isDense: true, floatingLabelBehavior: floatingLabelBehavior, ),