diff --git a/ai_docs/agent/web_v2/agent_state.json b/ai_docs/agent/web_v2/agent_state.json
index 9c75e1e..298545b 100644
--- a/ai_docs/agent/web_v2/agent_state.json
+++ b/ai_docs/agent/web_v2/agent_state.json
@@ -1,7 +1,7 @@
{
"component": "web_v2",
- "current_task_id": "WEB-003",
- "iteration_count": 3,
+ "current_task_id": "WEB-001",
+ "iteration_count": 4,
"max_iterations": 10,
"started_at": "2025-11-21T00:31:28.302997+00:00",
"last_commit": null,
diff --git a/ai_docs/agent/web_v2/task_list.json b/ai_docs/agent/web_v2/task_list.json
index f62d306..fec45dd 100644
--- a/ai_docs/agent/web_v2/task_list.json
+++ b/ai_docs/agent/web_v2/task_list.json
@@ -279,4 +279,4 @@
"component": "web_v2"
}
]
-}
+}
\ No newline at end of file
diff --git a/mnemo_cards_web_v2/build/web/.last_build_id b/mnemo_cards_web_v2/build/web/.last_build_id
index b5c3e67..1629342 100644
--- a/mnemo_cards_web_v2/build/web/.last_build_id
+++ b/mnemo_cards_web_v2/build/web/.last_build_id
@@ -1 +1 @@
-cbc289e61044aa6e6f63e7abe894d47e
\ No newline at end of file
+1c4dc119e18bcf1ad36e5635fba58ce0
\ No newline at end of file
diff --git a/mnemo_cards_web_v2/build/web/flutter_bootstrap.js b/mnemo_cards_web_v2/build/web/flutter_bootstrap.js
index ba02e0f..af4b3c1 100644
--- a/mnemo_cards_web_v2/build/web/flutter_bootstrap.js
+++ b/mnemo_cards_web_v2/build/web/flutter_bootstrap.js
@@ -38,6 +38,6 @@ _flutter.buildConfig = {"engineRevision":"d3d45dcf251823c1769909cd43698d126db38d
_flutter.loader.load({
serviceWorkerSettings: {
- serviceWorkerVersion: "4289911251"
+ serviceWorkerVersion: "2237867940"
}
});
diff --git a/mnemo_cards_web_v2/build/web/index.html b/mnemo_cards_web_v2/build/web/index.html
index 1403f80..06747c7 100644
--- a/mnemo_cards_web_v2/build/web/index.html
+++ b/mnemo_cards_web_v2/build/web/index.html
@@ -1,10 +1,5 @@
-
-
-
-
-
+
+
+
+
+
+
+
+
diff --git a/mnemo_cards_web_v2/lib/utils/adsgram_stub.dart b/mnemo_cards_web_v2/lib/utils/adsgram_stub.dart
index 53d5270..6c316d0 100644
--- a/mnemo_cards_web_v2/lib/utils/adsgram_stub.dart
+++ b/mnemo_cards_web_v2/lib/utils/adsgram_stub.dart
@@ -52,12 +52,28 @@ class Adsgram {
/// Show a rewarded ad using the Adsgram JavaScript SDK
///
/// This method:
- /// 1. Sets up JavaScript callbacks for reward/error events
- /// 2. Calls the JavaScript showAdWithBlockId() function
- /// 3. Handles ad lifecycle events via callbacks
+ /// 1. Validates the ad configuration
+ /// 2. Checks SDK availability
+ /// 3. Sets up JavaScript callbacks for reward/error events
+ /// 4. Calls the JavaScript showAdWithBlockId() function
+ /// 5. Handles ad lifecycle events via callbacks
///
/// Throws [Exception] if SDK is not available or ad fails to show
+ /// Throws [ArgumentError] if ad configuration is invalid
Future showRewardedAd(AdsgramAd adConfig) async {
+ // Validate block ID
+ if (adConfig.blockId.trim().isEmpty) {
+ final error = 'Block ID is required and cannot be empty';
+ developer.log(error, name: 'Adsgram');
+ adConfig.onError?.call(error);
+ throw ArgumentError.value(
+ adConfig.blockId,
+ 'blockId',
+ error,
+ );
+ }
+
+ // Check SDK availability
if (!isAvailable) {
final error = 'Adsgram SDK not available. Make sure sad.min.js is loaded.';
developer.log(error, name: 'Adsgram');
@@ -65,11 +81,23 @@ class Adsgram {
throw Exception(error);
}
- if (adConfig.blockId.isEmpty) {
- final error = 'Block ID is required';
+ // Validate that JavaScript bridge functions exist
+ try {
+ final showAdFunction = js_util.getProperty(
+ js_util.globalThis,
+ 'showAdWithBlockId',
+ );
+ if (showAdFunction == null) {
+ final error = 'JavaScript bridge function showAdWithBlockId not found. Make sure foos.js is loaded.';
+ developer.log(error, name: 'Adsgram');
+ adConfig.onError?.call(error);
+ throw Exception(error);
+ }
+ } catch (e) {
+ final error = 'Failed to access JavaScript bridge: $e';
developer.log(error, name: 'Adsgram');
adConfig.onError?.call(error);
- throw ArgumentError(error);
+ throw Exception(error);
}
try {
@@ -131,6 +159,22 @@ class Adsgram {
});
// Register callbacks with JavaScript bridge
+ // Check if callback functions exist before calling
+ final setRewardCallback = js_util.getProperty(
+ js_util.globalThis,
+ 'setRewardCallback',
+ );
+ final setErrorCallback = js_util.getProperty(
+ js_util.globalThis,
+ 'setErrorCallback',
+ );
+
+ if (setRewardCallback == null || setErrorCallback == null) {
+ throw Exception(
+ 'JavaScript bridge callback functions not found. Make sure foos.js is loaded.',
+ );
+ }
+
js_util.callMethod(
js_util.globalThis,
'setRewardCallback',
diff --git a/mnemo_cards_web_v2/test/utils/adsgram_stub_test.dart b/mnemo_cards_web_v2/test/utils/adsgram_stub_test.dart
index 8bf98a3..b1cbaef 100644
--- a/mnemo_cards_web_v2/test/utils/adsgram_stub_test.dart
+++ b/mnemo_cards_web_v2/test/utils/adsgram_stub_test.dart
@@ -1,3 +1,4 @@
+@TestOn('browser') // These tests require JavaScript interop, so they must run on browser
import 'package:flutter_test/flutter_test.dart';
import 'package:mnemo_cards_web_v2/utils/adsgram_stub.dart';
@@ -40,9 +41,45 @@ void main() {
adConfig.onError?.call('test error');
expect(errorMessage, equals('test error'));
});
+
+ test('creates ad config with only reward callback', () {
+ bool rewardCalled = false;
+
+ final adConfig = AdsgramAd(
+ blockId: '12345',
+ rewardAmount: 1,
+ onReward: () {
+ rewardCalled = true;
+ },
+ );
+
+ expect(adConfig.onReward, isNotNull);
+ expect(adConfig.onError, isNull);
+
+ adConfig.onReward?.call();
+ expect(rewardCalled, isTrue);
+ });
+
+ test('creates ad config with only error callback', () {
+ String? errorMessage;
+
+ final adConfig = AdsgramAd(
+ blockId: '12345',
+ rewardAmount: 1,
+ onError: (error) {
+ errorMessage = error;
+ },
+ );
+
+ expect(adConfig.onReward, isNull);
+ expect(adConfig.onError, isNotNull);
+
+ adConfig.onError?.call('test error');
+ expect(errorMessage, equals('test error'));
+ });
});
- group('Adsgram', () {
+ group('Adsgram SDK Integration', () {
test('isAvailable returns false when SDK not loaded', () {
final adsgram = Adsgram.instance;
// In test environment, JavaScript interop won't have Adsgram SDK
@@ -50,18 +87,12 @@ void main() {
expect(adsgram.isAvailable, isFalse);
});
- test('showRewardedAd throws when SDK not available', () async {
+ test('showRewardedAd throws Exception when SDK not available', () async {
final adsgram = Adsgram.instance;
bool errorCallbackCalled = false;
String? errorMessage;
- const adConfig = AdsgramAd(
- blockId: '12345',
- rewardAmount: 1,
- onError: null,
- );
-
- final adConfigWithCallback = AdsgramAd(
+ final adConfig = AdsgramAd(
blockId: '12345',
rewardAmount: 1,
onError: (error) {
@@ -70,20 +101,11 @@ void main() {
},
);
- // Test without callback
+ // Should throw Exception (not ArgumentError) when SDK unavailable
expect(
() => adsgram.showRewardedAd(adConfig),
- throwsException,
+ throwsA(isA()),
);
-
- // Test with callback
- expect(
- () => adsgram.showRewardedAd(adConfigWithCallback),
- throwsException,
- );
-
- // Note: In actual test environment, callbacks may not be called
- // because JavaScript interop is not available
});
test('showRewardedAd throws ArgumentError when blockId is empty', () async {
@@ -104,21 +126,140 @@ void main() {
);
});
- test('showAdWithBlockId throws when SDK not available', () async {
+ test('showRewardedAd throws ArgumentError when blockId is whitespace only', () async {
+ final adsgram = Adsgram.instance;
+ bool errorCallbackCalled = false;
+
+ final adConfig = AdsgramAd(
+ blockId: ' ',
+ rewardAmount: 1,
+ onError: (error) {
+ errorCallbackCalled = true;
+ },
+ );
+
+ expect(
+ () => adsgram.showRewardedAd(adConfig),
+ throwsA(isA()),
+ );
+ });
+
+ test('showRewardedAd validates blockId before checking SDK', () async {
+ final adsgram = Adsgram.instance;
+
+ // Empty blockId should throw ArgumentError even if SDK is unavailable
+ final adConfig = AdsgramAd(
+ blockId: '',
+ rewardAmount: 1,
+ );
+
+ expect(
+ () => adsgram.showRewardedAd(adConfig),
+ throwsA(isA()),
+ );
+ });
+
+ test('showRewardedAd calls error callback when SDK unavailable', () async {
+ final adsgram = Adsgram.instance;
+ bool errorCallbackCalled = false;
+ String? errorMessage;
+
+ final adConfig = AdsgramAd(
+ blockId: '12345',
+ rewardAmount: 1,
+ onError: (error) {
+ errorCallbackCalled = true;
+ errorMessage = error;
+ },
+ );
+
+ try {
+ await adsgram.showRewardedAd(adConfig);
+ fail('Expected Exception to be thrown');
+ } catch (e) {
+ expect(e, isA());
+ // Note: In test environment, callbacks may not be called synchronously
+ // because JavaScript interop is not available
+ }
+ });
+
+ test('showRewardedAd calls error callback when blockId is invalid', () async {
+ final adsgram = Adsgram.instance;
+ bool errorCallbackCalled = false;
+ String? errorMessage;
+
+ final adConfig = AdsgramAd(
+ blockId: '',
+ rewardAmount: 1,
+ onError: (error) {
+ errorCallbackCalled = true;
+ errorMessage = error;
+ },
+ );
+
+ try {
+ await adsgram.showRewardedAd(adConfig);
+ fail('Expected ArgumentError to be thrown');
+ } catch (e) {
+ expect(e, isA());
+ }
+ });
+
+ test('showRewardedAd handles missing error callback gracefully', () async {
+ final adsgram = Adsgram.instance;
+
+ const adConfig = AdsgramAd(
+ blockId: '',
+ rewardAmount: 1,
+ onError: null,
+ );
+
+ // Should not throw when error callback is null
+ expect(
+ () => adsgram.showRewardedAd(adConfig),
+ throwsA(isA()),
+ );
+ });
+
+ test('showAdWithBlockId throws Exception when SDK not available', () async {
final adsgram = Adsgram.instance;
expect(
() => adsgram.showAdWithBlockId('12345'),
- throwsException,
+ throwsA(isA()),
);
});
- test('showAdWithBlockId throws when blockId is empty', () async {
+ test('showAdWithBlockId throws Exception when blockId is empty', () async {
final adsgram = Adsgram.instance;
expect(
() => adsgram.showAdWithBlockId(''),
- throwsException,
+ throwsA(isA()),
+ );
+ });
+
+ test('Adsgram instance is singleton', () {
+ final instance1 = Adsgram.instance;
+ final instance2 = Adsgram.instance;
+
+ expect(instance1, same(instance2));
+ });
+
+ test('showRewardedAd validates JavaScript bridge functions exist', () async {
+ final adsgram = Adsgram.instance;
+
+ // In test environment, JavaScript bridge won't be available
+ // This tests that we check for bridge functions before using them
+ final adConfig = AdsgramAd(
+ blockId: '12345',
+ rewardAmount: 1,
+ );
+
+ // Should throw Exception when bridge functions are missing
+ expect(
+ () => adsgram.showRewardedAd(adConfig),
+ throwsA(isA()),
);
});
});
diff --git a/mnemo_cards_web_v2/web/foos.js b/mnemo_cards_web_v2/web/foos.js
index 26005c6..c341be1 100644
--- a/mnemo_cards_web_v2/web/foos.js
+++ b/mnemo_cards_web_v2/web/foos.js
@@ -1,10 +1,28 @@
// Adsgram SDK integration for rewarded ads
// This file provides a bridge between Dart code and the Adsgram JavaScript SDK
-// Check if Adsgram SDK is loaded
-if (typeof window.Adsgram === 'undefined') {
- console.error('Adsgram SDK not loaded. Make sure sad.min.js is included in index.html');
-}
+// Wait for SDK to load if it's not immediately available
+(function() {
+ let sdkCheckAttempts = 0;
+ const maxCheckAttempts = 50; // Check for up to 5 seconds (50 * 100ms)
+
+ function checkSDK() {
+ if (typeof window.Adsgram !== 'undefined') {
+ console.log('Adsgram SDK loaded successfully');
+ return;
+ }
+
+ sdkCheckAttempts++;
+ if (sdkCheckAttempts < maxCheckAttempts) {
+ setTimeout(checkSDK, 100);
+ } else {
+ console.warn('Adsgram SDK not loaded after timeout. Make sure sad.min.js is included in index.html');
+ }
+ }
+
+ // Start checking after a short delay to allow script to load
+ setTimeout(checkSDK, 100);
+})();
// Store callbacks for ad lifecycle events
let rewardCallback = null;
@@ -18,17 +36,31 @@ const DEFAULT_BLOCK_ID = "16505";
* Initialize Adsgram ad controller with a specific block ID
* @param {string} blockId - The Adsgram block ID
* @returns {object} Adsgram ad controller instance
+ * @throws {Error} If SDK is not available or initialization fails
*/
function initAdController(blockId) {
if (typeof window.Adsgram === 'undefined') {
- throw new Error('Adsgram SDK not available');
+ const error = 'Adsgram SDK not available. Make sure sad.min.js is loaded.';
+ console.error(error);
+ throw new Error(error);
+ }
+
+ if (!blockId || typeof blockId !== 'string' || blockId.trim() === '') {
+ const error = 'Invalid block ID provided';
+ console.error(error);
+ throw new Error(error);
}
try {
- return window.Adsgram.init({ blockId: blockId });
+ const controller = window.Adsgram.init({ blockId: blockId });
+ if (!controller) {
+ throw new Error('Failed to initialize Adsgram controller: init returned null');
+ }
+ return controller;
} catch (error) {
- console.error('Failed to initialize Adsgram controller:', error);
- throw error;
+ const errorMessage = error?.message || error?.toString() || 'Unknown initialization error';
+ console.error('Failed to initialize Adsgram controller:', errorMessage);
+ throw new Error(`Failed to initialize Adsgram controller: ${errorMessage}`);
}
}
@@ -48,11 +80,30 @@ function showAd() {
* @returns {Promise} Promise that resolves when ad completes or rejects on error
*/
function showAdWithBlockId(blockId) {
- if (!blockId || typeof blockId !== 'string') {
+ // Validate block ID
+ if (!blockId || typeof blockId !== 'string' || blockId.trim() === '') {
const error = 'Invalid block ID provided';
console.error(error);
if (errorCallback && typeof errorCallback === 'function') {
- errorCallback(error);
+ try {
+ errorCallback(error);
+ } catch (callbackError) {
+ console.error('Error in error callback:', callbackError);
+ }
+ }
+ return Promise.reject(new Error(error));
+ }
+
+ // Check SDK availability
+ if (typeof window.Adsgram === 'undefined') {
+ const error = 'Adsgram SDK not available. Make sure sad.min.js is loaded.';
+ console.error(error);
+ if (errorCallback && typeof errorCallback === 'function') {
+ try {
+ errorCallback(error);
+ } catch (callbackError) {
+ console.error('Error in error callback:', callbackError);
+ }
}
return Promise.reject(new Error(error));
}
@@ -61,6 +112,19 @@ function showAdWithBlockId(blockId) {
// Initialize controller for this specific block
currentAdController = initAdController(blockId);
+ if (!currentAdController || typeof currentAdController.show !== 'function') {
+ const error = 'Invalid ad controller: show method not available';
+ console.error(error);
+ if (errorCallback && typeof errorCallback === 'function') {
+ try {
+ errorCallback(error);
+ } catch (callbackError) {
+ console.error('Error in error callback:', callbackError);
+ }
+ }
+ return Promise.reject(new Error(error));
+ }
+
// Show the ad
return currentAdController.show().then((result) => {
// Ad completed successfully - user watched till the end
@@ -72,6 +136,7 @@ function showAdWithBlockId(blockId) {
rewardCallback();
} catch (callbackError) {
console.error('Error in reward callback:', callbackError);
+ // Don't fail the promise if callback has an error
}
}
@@ -82,14 +147,20 @@ function showAdWithBlockId(blockId) {
// Format error message
let errorMessage = 'Ad failed';
- if (error && typeof error === 'object') {
- try {
- errorMessage = JSON.stringify(error);
- } catch (e) {
+ if (error) {
+ if (typeof error === 'object') {
+ if (error.message) {
+ errorMessage = error.message;
+ } else {
+ try {
+ errorMessage = JSON.stringify(error);
+ } catch (e) {
+ errorMessage = error.toString();
+ }
+ }
+ } else {
errorMessage = error.toString();
}
- } else if (error) {
- errorMessage = error.toString();
}
// Call error callback if set
@@ -105,7 +176,14 @@ function showAdWithBlockId(blockId) {
});
} catch (error) {
// Initialization or show() call failed
- const errorMessage = error ? error.toString() : 'Failed to show ad';
+ let errorMessage = 'Failed to show ad';
+ if (error) {
+ if (error.message) {
+ errorMessage = error.message;
+ } else {
+ errorMessage = error.toString();
+ }
+ }
console.error('Failed to show ad:', errorMessage);
if (errorCallback && typeof errorCallback === 'function') {
diff --git a/mnemo_cards_web_v2/web/index.html b/mnemo_cards_web_v2/web/index.html
index fc4d087..6732c68 100644
--- a/mnemo_cards_web_v2/web/index.html
+++ b/mnemo_cards_web_v2/web/index.html
@@ -1,10 +1,5 @@
-
-
-
-
-
+
+
+
+
+
+
+
+