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 @@ - - - - - + + + + + + + +