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:
parent
19f0b952c0
commit
3de4932ae2
9 changed files with 335 additions and 62 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
cbc289e61044aa6e6f63e7abe894d47e
|
||||
1c4dc119e18bcf1ad36e5635fba58ce0
|
||||
|
|
@ -38,6 +38,6 @@ _flutter.buildConfig = {"engineRevision":"d3d45dcf251823c1769909cd43698d126db38d
|
|||
|
||||
_flutter.loader.load({
|
||||
serviceWorkerSettings: {
|
||||
serviceWorkerVersion: "4289911251"
|
||||
serviceWorkerVersion: "2237867940"
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,10 +1,5 @@
|
|||
<!DOCTYPE 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>
|
||||
<!--
|
||||
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>
|
||||
<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>
|
||||
<body>
|
||||
<script src="flutter_bootstrap.js" async></script>
|
||||
|
|
|
|||
|
|
@ -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<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) {
|
||||
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<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 ArgumentError(error);
|
||||
throw Exception(error);
|
||||
}
|
||||
} catch (e) {
|
||||
final error = 'Failed to access JavaScript bridge: $e';
|
||||
developer.log(error, name: 'Adsgram');
|
||||
adConfig.onError?.call(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<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.globalThis,
|
||||
'setRewardCallback',
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
||||
group('Adsgram', () {
|
||||
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 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<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 {
|
||||
|
|
@ -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;
|
||||
|
||||
expect(
|
||||
() => 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;
|
||||
|
||||
expect(
|
||||
() => 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>()),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,11 +1,29 @@
|
|||
// 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;
|
||||
let errorCallback = 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') {
|
||||
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,15 +147,21 @@ function showAdWithBlockId(blockId) {
|
|||
|
||||
// Format error message
|
||||
let errorMessage = 'Ad failed';
|
||||
if (error && typeof error === 'object') {
|
||||
if (error) {
|
||||
if (typeof error === 'object') {
|
||||
if (error.message) {
|
||||
errorMessage = error.message;
|
||||
} else {
|
||||
try {
|
||||
errorMessage = JSON.stringify(error);
|
||||
} catch (e) {
|
||||
errorMessage = error.toString();
|
||||
}
|
||||
} else if (error) {
|
||||
}
|
||||
} else {
|
||||
errorMessage = error.toString();
|
||||
}
|
||||
}
|
||||
|
||||
// Call error callback if set
|
||||
if (errorCallback && typeof errorCallback === 'function') {
|
||||
|
|
@ -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') {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,5 @@
|
|||
<!DOCTYPE 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>
|
||||
<!--
|
||||
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>
|
||||
<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>
|
||||
<body>
|
||||
<script src="flutter_bootstrap.js" async></script>
|
||||
|
|
|
|||
Loading…
Reference in a new issue