94 lines
3.4 KiB
Dart
94 lines
3.4 KiB
Dart
import 'dart:async';
|
|
import 'dart:io' show Platform;
|
|
|
|
import 'package:google_mobile_ads/google_mobile_ads.dart';
|
|
|
|
import '../data/backend.dart';
|
|
import '../i18n/app_locale.dart';
|
|
|
|
/// 광고 단위 ID — **실 값으로 바꾸는 곳은 여기 한 곳이다**(앱 ID 는 AndroidManifest ·
|
|
/// Info.plist 의 `GADApplicationIdentifier`).
|
|
///
|
|
/// 지금 값은 Google 이 공개한 **테스트 광고 단위**다(별첨 1 ⑥ 9주차 샌드박스). 테스트
|
|
/// 단위는 실 계정 없이 항상 테스트 광고를 채우고 수익·무효 트래픽 판정이 없다.
|
|
abstract final class AdUnits {
|
|
static const androidRewarded = 'ca-app-pub-3940256099942544/5224354917';
|
|
static const iosRewarded = 'ca-app-pub-3940256099942544/1712485313';
|
|
|
|
static String get rewarded =>
|
|
Platform.isIOS ? iosRewarded : androidRewarded;
|
|
}
|
|
|
|
/// 불러 둔 보상형 광고 한 편.
|
|
///
|
|
/// 광고 네트워크 경계다. 미디에이션(AppLovin MAX 등)을 붙일 때 이 뒤만 바꾼다.
|
|
abstract interface class RewardedAdHandle {
|
|
/// 전면으로 띄우고 닫힐 때까지 기다린다. 끝까지 봐서 보상 콜백이 왔으면 노출 ID,
|
|
/// 중간에 닫았거나 띄우지 못했으면 null. 끝나면 스스로 정리한다.
|
|
Future<String?> show();
|
|
|
|
/// 띄우지 않고 버린다(사용자가 로딩 중 건너뛴 경우).
|
|
void dispose();
|
|
}
|
|
|
|
/// Google Mobile Ads 보상형 광고를 불러온다.
|
|
///
|
|
/// 불러오지 못하면(채울 광고 없음 · 네트워크 · 시간 초과) `E-AD-01` 로 끊는다 —
|
|
/// 원장 B-03 「광고 로드 실패 시 기본 보상은 지급하고 추가 보상·횟수는 차감하지 않음」.
|
|
Future<RewardedAdHandle> loadGoogleRewardedAd({
|
|
Duration timeout = const Duration(seconds: 15),
|
|
}) async {
|
|
await (_initialized ??= MobileAds.instance.initialize());
|
|
final loaded = Completer<RewardedAd>();
|
|
await RewardedAd.load(
|
|
adUnitId: AdUnits.rewarded,
|
|
request: const AdRequest(),
|
|
rewardedAdLoadCallback: RewardedAdLoadCallback(
|
|
onAdLoaded: loaded.complete,
|
|
onAdFailedToLoad: (error) => loaded.completeError(error),
|
|
),
|
|
);
|
|
try {
|
|
return _GoogleRewardedAd(await loaded.future.timeout(timeout));
|
|
} catch (_) {
|
|
// 시간 초과 뒤에 늦게 도착한 광고는 띄우지 않고 버린다.
|
|
unawaited(loaded.future.then((ad) => ad.dispose(), onError: (_) {}));
|
|
throw BackendException('E-AD-01', appL10n.adLoadFailed);
|
|
}
|
|
}
|
|
|
|
Future<InitializationStatus>? _initialized;
|
|
|
|
class _GoogleRewardedAd implements RewardedAdHandle {
|
|
_GoogleRewardedAd(this._ad);
|
|
|
|
final RewardedAd _ad;
|
|
|
|
@override
|
|
Future<String?> show() async {
|
|
final closed = Completer<void>();
|
|
String? impression;
|
|
_ad.fullScreenContentCallback = FullScreenContentCallback(
|
|
onAdDismissedFullScreenContent: (_) => closed.complete(),
|
|
onAdFailedToShowFullScreenContent: (_, _) => closed.complete(),
|
|
);
|
|
try {
|
|
await _ad.show(
|
|
onUserEarnedReward: (_, _) {
|
|
// 서버가 이 값으로 같은 노출의 이중 지급을 막는다(`adRewardLogs/{id}`).
|
|
impression = _ad.responseInfo?.responseId ??
|
|
'gma-${DateTime.now().microsecondsSinceEpoch}';
|
|
},
|
|
);
|
|
await closed.future;
|
|
} finally {
|
|
// 띄우기가 던져도 네이티브 광고 객체를 남기지 않는다.
|
|
await _ad.dispose();
|
|
}
|
|
return impression;
|
|
}
|
|
|
|
@override
|
|
void dispose() => unawaited(_ad.dispose());
|
|
}
|