feat(web_v2): Complete Ads Reward Flow - Integrate Adsgram SDK

Task ID: WEB-001
Priority: high

Changes:

Completed by: AI Agent
Duration: 877383ms
This commit is contained in:
AI Agent 2025-11-21 09:57:19 +00:00
parent 19f0b952c0
commit 3de4932ae2
9 changed files with 335 additions and 62 deletions

View file

@ -1,7 +1,7 @@
{ {
"component": "web_v2", "component": "web_v2",
"current_task_id": "WEB-003", "current_task_id": "WEB-001",
"iteration_count": 3, "iteration_count": 4,
"max_iterations": 10, "max_iterations": 10,
"started_at": "2025-11-21T00:31:28.302997+00:00", "started_at": "2025-11-21T00:31:28.302997+00:00",
"last_commit": null, "last_commit": null,

View file

@ -1 +1 @@
cbc289e61044aa6e6f63e7abe894d47e 1c4dc119e18bcf1ad36e5635fba58ce0

View file

@ -38,6 +38,6 @@ _flutter.buildConfig = {"engineRevision":"d3d45dcf251823c1769909cd43698d126db38d
_flutter.loader.load({ _flutter.loader.load({
serviceWorkerSettings: { serviceWorkerSettings: {
serviceWorkerVersion: "4289911251" serviceWorkerVersion: "2237867940"
} }
}); });

View file

@ -1,10 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head>
<script src="https://telegram.org/js/telegram-web-app.js" defer=""></script>
<script src="https://sad.adsgram.ai/js/sad.min.js"></script>
<script src="foos.js"></script>
</head>
<head> <head>
<!-- <!--
If you are serving your web app in a path other than the root, change the If you are serving your web app in a path other than the root, change the
@ -37,6 +32,16 @@
<title>mnemo_cards_web_v2</title> <title>mnemo_cards_web_v2</title>
<link rel="manifest" href="manifest.json"> <link rel="manifest" href="manifest.json">
<!-- External SDKs -->
<!-- Telegram Web App SDK -->
<script src="https://telegram.org/js/telegram-web-app.js" defer></script>
<!-- Adsgram SDK for rewarded ads - must load before foos.js -->
<script src="https://sad.adsgram.ai/js/sad.min.js"></script>
<!-- JavaScript bridge for Adsgram SDK integration -->
<script src="foos.js"></script>
</head> </head>
<body> <body>
<script src="flutter_bootstrap.js" async></script> <script src="flutter_bootstrap.js" async></script>

View file

@ -52,12 +52,28 @@ class Adsgram {
/// Show a rewarded ad using the Adsgram JavaScript SDK /// Show a rewarded ad using the Adsgram JavaScript SDK
/// ///
/// This method: /// This method:
/// 1. Sets up JavaScript callbacks for reward/error events /// 1. Validates the ad configuration
/// 2. Calls the JavaScript showAdWithBlockId() function /// 2. Checks SDK availability
/// 3. Handles ad lifecycle events via callbacks /// 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 [Exception] if SDK is not available or ad fails to show
/// Throws [ArgumentError] if ad configuration is invalid
Future<void> showRewardedAd(AdsgramAd adConfig) async { Future<void> 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) { if (!isAvailable) {
final error = 'Adsgram SDK not available. Make sure sad.min.js is loaded.'; final error = 'Adsgram SDK not available. Make sure sad.min.js is loaded.';
developer.log(error, name: 'Adsgram'); developer.log(error, name: 'Adsgram');
@ -65,11 +81,23 @@ class Adsgram {
throw Exception(error); throw Exception(error);
} }
if (adConfig.blockId.isEmpty) { // Validate that JavaScript bridge functions exist
final error = 'Block ID is required'; try {
final showAdFunction = js_util.getProperty<dynamic>(
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'); developer.log(error, name: 'Adsgram');
adConfig.onError?.call(error); adConfig.onError?.call(error);
throw ArgumentError(error); throw Exception(error);
} }
try { try {
@ -131,6 +159,22 @@ class Adsgram {
}); });
// Register callbacks with JavaScript bridge // Register callbacks with JavaScript bridge
// Check if callback functions exist before calling
final setRewardCallback = js_util.getProperty<dynamic>(
js_util.globalThis,
'setRewardCallback',
);
final setErrorCallback = js_util.getProperty<dynamic>(
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<void>( js_util.callMethod<void>(
js_util.globalThis, js_util.globalThis,
'setRewardCallback', 'setRewardCallback',

View file

@ -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:flutter_test/flutter_test.dart';
import 'package:mnemo_cards_web_v2/utils/adsgram_stub.dart'; import 'package:mnemo_cards_web_v2/utils/adsgram_stub.dart';
@ -40,9 +41,45 @@ void main() {
adConfig.onError?.call('test error'); adConfig.onError?.call('test error');
expect(errorMessage, equals('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', () { test('isAvailable returns false when SDK not loaded', () {
final adsgram = Adsgram.instance; final adsgram = Adsgram.instance;
// In test environment, JavaScript interop won't have Adsgram SDK // In test environment, JavaScript interop won't have Adsgram SDK
@ -50,18 +87,12 @@ void main() {
expect(adsgram.isAvailable, isFalse); 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; final adsgram = Adsgram.instance;
bool errorCallbackCalled = false; bool errorCallbackCalled = false;
String? errorMessage; String? errorMessage;
const adConfig = AdsgramAd( final adConfig = AdsgramAd(
blockId: '12345',
rewardAmount: 1,
onError: null,
);
final adConfigWithCallback = AdsgramAd(
blockId: '12345', blockId: '12345',
rewardAmount: 1, rewardAmount: 1,
onError: (error) { onError: (error) {
@ -70,20 +101,11 @@ void main() {
}, },
); );
// Test without callback // Should throw Exception (not ArgumentError) when SDK unavailable
expect( expect(
() => adsgram.showRewardedAd(adConfig), () => adsgram.showRewardedAd(adConfig),
throwsException, throwsA(isA<Exception>()),
); );
// 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 { 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<ArgumentError>()),
);
});
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<ArgumentError>()),
);
});
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<Exception>());
// 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<ArgumentError>());
}
});
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<ArgumentError>()),
);
});
test('showAdWithBlockId throws Exception when SDK not available', () async {
final adsgram = Adsgram.instance; final adsgram = Adsgram.instance;
expect( expect(
() => adsgram.showAdWithBlockId('12345'), () => adsgram.showAdWithBlockId('12345'),
throwsException, throwsA(isA<Exception>()),
); );
}); });
test('showAdWithBlockId throws when blockId is empty', () async { test('showAdWithBlockId throws Exception when blockId is empty', () async {
final adsgram = Adsgram.instance; final adsgram = Adsgram.instance;
expect( expect(
() => adsgram.showAdWithBlockId(''), () => adsgram.showAdWithBlockId(''),
throwsException, throwsA(isA<Exception>()),
);
});
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<Exception>()),
); );
}); });
}); });

View file

@ -1,10 +1,28 @@
// Adsgram SDK integration for rewarded ads // Adsgram SDK integration for rewarded ads
// This file provides a bridge between Dart code and the Adsgram JavaScript SDK // This file provides a bridge between Dart code and the Adsgram JavaScript SDK
// Check if Adsgram SDK is loaded // Wait for SDK to load if it's not immediately available
if (typeof window.Adsgram === 'undefined') { (function() {
console.error('Adsgram SDK not loaded. Make sure sad.min.js is included in index.html'); 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 // Store callbacks for ad lifecycle events
let rewardCallback = null; let rewardCallback = null;
@ -18,17 +36,31 @@ const DEFAULT_BLOCK_ID = "16505";
* Initialize Adsgram ad controller with a specific block ID * Initialize Adsgram ad controller with a specific block ID
* @param {string} blockId - The Adsgram block ID * @param {string} blockId - The Adsgram block ID
* @returns {object} Adsgram ad controller instance * @returns {object} Adsgram ad controller instance
* @throws {Error} If SDK is not available or initialization fails
*/ */
function initAdController(blockId) { function initAdController(blockId) {
if (typeof window.Adsgram === 'undefined') { 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 { 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) { } catch (error) {
console.error('Failed to initialize Adsgram controller:', error); const errorMessage = error?.message || error?.toString() || 'Unknown initialization error';
throw 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 * @returns {Promise} Promise that resolves when ad completes or rejects on error
*/ */
function showAdWithBlockId(blockId) { function showAdWithBlockId(blockId) {
if (!blockId || typeof blockId !== 'string') { // Validate block ID
if (!blockId || typeof blockId !== 'string' || blockId.trim() === '') {
const error = 'Invalid block ID provided'; const error = 'Invalid block ID provided';
console.error(error); console.error(error);
if (errorCallback && typeof errorCallback === 'function') { 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)); return Promise.reject(new Error(error));
} }
@ -61,6 +112,19 @@ function showAdWithBlockId(blockId) {
// Initialize controller for this specific block // Initialize controller for this specific block
currentAdController = initAdController(blockId); 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 // Show the ad
return currentAdController.show().then((result) => { return currentAdController.show().then((result) => {
// Ad completed successfully - user watched till the end // Ad completed successfully - user watched till the end
@ -72,6 +136,7 @@ function showAdWithBlockId(blockId) {
rewardCallback(); rewardCallback();
} catch (callbackError) { } catch (callbackError) {
console.error('Error in reward callback:', 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 // Format error message
let errorMessage = 'Ad failed'; let errorMessage = 'Ad failed';
if (error && typeof error === 'object') { if (error) {
try { if (typeof error === 'object') {
errorMessage = JSON.stringify(error); if (error.message) {
} catch (e) { errorMessage = error.message;
} else {
try {
errorMessage = JSON.stringify(error);
} catch (e) {
errorMessage = error.toString();
}
}
} else {
errorMessage = error.toString(); errorMessage = error.toString();
} }
} else if (error) {
errorMessage = error.toString();
} }
// Call error callback if set // Call error callback if set
@ -105,7 +176,14 @@ function showAdWithBlockId(blockId) {
}); });
} catch (error) { } catch (error) {
// Initialization or show() call failed // 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); console.error('Failed to show ad:', errorMessage);
if (errorCallback && typeof errorCallback === 'function') { if (errorCallback && typeof errorCallback === 'function') {

View file

@ -1,10 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head>
<script src="https://telegram.org/js/telegram-web-app.js" defer=""></script>
<script src="https://sad.adsgram.ai/js/sad.min.js"></script>
<script src="foos.js"></script>
</head>
<head> <head>
<!-- <!--
If you are serving your web app in a path other than the root, change the If you are serving your web app in a path other than the root, change the
@ -37,6 +32,16 @@
<title>mnemo_cards_web_v2</title> <title>mnemo_cards_web_v2</title>
<link rel="manifest" href="manifest.json"> <link rel="manifest" href="manifest.json">
<!-- External SDKs -->
<!-- Telegram Web App SDK -->
<script src="https://telegram.org/js/telegram-web-app.js" defer></script>
<!-- Adsgram SDK for rewarded ads - must load before foos.js -->
<script src="https://sad.adsgram.ai/js/sad.min.js"></script>
<!-- JavaScript bridge for Adsgram SDK integration -->
<script src="foos.js"></script>
</head> </head>
<body> <body>
<script src="flutter_bootstrap.js" async></script> <script src="flutter_bootstrap.js" async></script>