visual_reward 1.1.8
visual_reward: ^1.1.8 copied to clipboard
Tenqube Visual Reward SDK for Flutter. A reward-ad webview SDK built on flutter_inappwebview for Android and iOS.
Flutter #
Flutter InAppWebView 기반 리워드 광고 SDK입니다.
요구사항 #
- Flutter 3.38.1+
- Dart 3.10.0+
- iOS 12.0+
- Android minSdk 21
설치 #
pub.dev 에서 설치합니다.
flutter pub add visual_reward
또는 pubspec.yaml에 직접 추가합니다.
dependencies:
visual_reward: ^<version>
직접 의존성은 WebView 엔진인 flutter_inappwebview 하나뿐입니다. 기기·앱 정보, 광고 식별자(ADID/IDFA), ATT 요청, 외부 URL·설정 열기, 캐시 경로 등은 SDK가 네이티브(Android Kotlin / iOS Swift) 채널로 직접 처리하므로, 별도 플러그인 의존성이 없어 호스트 앱의 다른 플러그인 버전과 충돌하지 않습니다. flutter_inappwebview는 전이 의존성으로 자동 해석되므로 호스트 pubspec.yaml에 별도로 나열할 필요가 없습니다.
로컬 path 설치 (오프라인·사내 배포 등)
전달받은 visual_reward 폴더를 프로젝트 내 원하는 위치에 복사한 뒤 path 의존성으로 추가합니다.
dependencies:
visual_reward:
path: ./visual_reward # 복사한 폴더 경로
R8 / ProGuard #
SDK에 Consumer ProGuard 규칙(consumer-rules.pro)이 포함되어 있어 호스트 앱의 R8 에 자동 적용됩니다. flutter_inappwebview 도 자체 규칙을 포함하므로, 일반적으로 호스트 앱에 별도 규칙을 추가할 필요가 없습니다.
자동 적용되는 규칙은 다음과 같습니다. (직접 문제가 있을 때 android/app/proguard-rules.pro에 수동으로 넣어도 됩니다.)
# Flutter 플러그인
-keep class com.tenqube.visual_reward.** { *; }
# AdMob WebView API (Class.forName 문자열 리플렉션으로 참조 — R8가 못 봄)
# 호스트 앱에 Google Mobile Ads SDK가 없으면 이 룰은 무시됨
-keep class com.google.android.libraries.ads.mobile.sdk.MobileAds { *; }
-keep class com.google.android.gms.ads.MobileAds { *; }
권한 #
Android #
광고 ID 접근 권한(com.google.android.gms.permission.AD_ID)은 SDK가 의존하는 play-services-ads-identifier가 매니페스트 병합으로 자동 추가하므로, 호스트가 별도로 선언할 필요가 없습니다. (광고 ID를 수집하는 앱이 이 권한을 선언하는 것은 Play 정책상 올바른 동작입니다.)
아동 대상 앱 등 광고 ID 권한을 원치 않는 경우에만 호스트 AndroidManifest.xml에서 명시적으로 제거할 수 있습니다.
<uses-permission android:name="com.google.android.gms.permission.AD_ID"
tools:node="remove" />
iOS #
IDFA 조회(getIDFA)를 사용하려면 Info.plist에 ATT 사용 목적 문자열을 추가해야 합니다.
<key>NSUserTrackingUsageDescription</key>
<string>맞춤형 광고 제공을 위해 기기 식별자를 사용합니다.</string>
ATT 권한 요청은 SDK가 웹뷰 로드 전 네이티브
ATTrackingManager로 자동 처리합니다. 이미 권한이 결정된 경우 팝업 없이 즉시 진행됩니다.NSUserTrackingUsageDescription이 없으면 요청을 스킵하고 바로 로드합니다.
사용법 #
디버그 설정 #
initialize() 전에 호출합니다.
import 'package:visual_reward/visual_reward.dart';
VisualReward.setDebugEnabled(kDebugMode);
초기화 #
main() 또는 앱 시작 시 한 번만 호출합니다.
import 'package:visual_reward/visual_reward.dart';
VisualReward.initialize(
appKey: 'tq_live_xxx',
timeout: const Duration(seconds: 5), // 기본값
onSuccess: () { /* SDK 준비 완료 */ },
onFailure: (e) { /* 초기화 실패 */ },
);
initialize 파라미터 #
| 파라미터 | 타입 | 필수 | 기본값 | 설명 |
|---|---|---|---|---|
appKey |
String |
✅ | 앱 식별 키 | |
timeout |
Duration |
Duration(seconds: 5) |
초기화 완료 대기 시간. 초과 시 VisualRewardInitializeTimeoutError 발생 |
|
onSuccess |
VoidCallback? |
null |
초기화 성공 콜백 (선택) | |
onFailure |
void Function(VisualRewardError)? |
null |
초기화 실패 콜백 (선택) |
사용자 설정 #
로그인 후 또는 사용자가 변경될 때 호출합니다. open() 전에 반드시 호출해야 합니다.
VisualReward.setUserId(user.id);
웹뷰 열기 #
import 'package:visual_reward/visual_reward.dart';
VisualReward.open(
context: context,
serviceName: 'reward_home',
locale: 'ko',
extra: null,
onClose: () { /* 웹뷰 닫힘 */ },
onError: (e) { /* 열기 실패 */ },
);
open 파라미터 #
| 파라미터 | 타입 | 필수 | 기본값 | 설명 |
|---|---|---|---|---|
context |
BuildContext |
✅ | 화면을 열 BuildContext | |
serviceName |
String |
✅ | 서비스 이름 | |
locale |
String? |
'ko' |
언어 코드 | |
extra |
String? |
null |
추가 query parameter (선택) | |
onClose |
VoidCallback? |
null |
웹뷰가 닫힐 때 호출되는 콜백 (선택) | |
onError |
void Function(VisualRewardError)? |
null |
웹뷰를 열지 못했을 때 호출되는 콜백 (선택) |
AdPopcorn 브릿지 등록 (선택) #
AdPopcorn SDK(adpopcornssp_flutter: ^1.0.17)가 있는 경우, open 호출 전에 등록합니다.
import 'package:adpopcornssp_flutter/adpopcornssp_flutter.dart';
VisualReward.setAdPopcornBridgeHandler(AdPopcornInAppWebViewBridgeHandler());
VisualReward.open(
context: context,
serviceName: 'reward_home',
locale: 'ko',
extra: null,
);
AdPopcorn SDK가 없으면 호출하지 않아도 됩니다.
AdMob WebView API (선택, best-effort) #
호스트 앱에 Google Mobile Ads SDK가 링크돼 있으면, SDK가 리워드 웹뷰를 자동으로 registerWebView에 등록해 WebView 내부 광고(AdSense/GPT/IMA HTML5)의 수익화·측정 신호를 개선합니다. GMA SDK가 없으면 조용히 무시되며, 광고 표시·보상 로직에는 영향이 없습니다.
이 SDK가 하는 일은 리플렉션으로 registerWebView를 호출하는 것뿐입니다(Next-Gen 우선, 없으면 레거시 폴백). SDK 의존성·App ID·초기화는 모두 호스트 앱에서 설정해야 하며, 설정은 (1) 사용하는 SDK 세대와 (2) 호스트가 자체 광고도 서빙하는지에 따라 달라집니다.
1. 의존성 #
-
Flutter (권장, 레거시 세대):
google_mobile_ads플러그인을 추가하면 양 플랫폼에 GMA 심볼이 링크됩니다.flutter pub add google_mobile_ads -
Next-Gen (직접 링크): 별도 Flutter 플러그인이 없어 네이티브 의존성을 직접 추가해야 합니다(Android:
com.google.android.libraries.ads.mobile.sdk:ads-mobile-sdk). 대부분 위의google_mobile_ads(레거시)로 충분합니다.
2. App ID / 초기화 #
호스트 앱 시작 시 MobileAds를 1회 초기화합니다.
import 'package:flutter/widgets.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
MobileAds.instance.initialize();
runApp(const MyApp());
}
App ID는 아래 두 경우에 따라 설정합니다. 레거시는 WebView 전용 App ID 상수가 없어, WebView 전용이어도 실제 App ID가 필수입니다(없으면 시작 크래시). 이 부담을 없앤 것이 Next-Gen의 WebView 전용 상수입니다.
| 경우 | Android (AndroidManifest.xml) |
iOS (Info.plist) |
|---|---|---|
| WebView 신호 전용 (자체 광고 미서빙) | 실제 App ID + INTEGRATION_MANAGER=webview |
실제 App ID (GADApplicationIdentifier) |
| 자체 광고 병행 | 실제 App ID만 (INTEGRATION_MANAGER 생략) |
실제 App ID (GADApplicationIdentifier) |
Android — android/app/src/main/AndroidManifest.xml
<application>
<!-- App ID 누락 시 시작 크래시 (AdMob 또는 Ad Manager App ID) -->
<meta-data
android:name="com.google.android.gms.ads.APPLICATION_ID"
android:value="ca-app-pub-xxxxxxxxxxxxxxxx~yyyyyyyyyy" />
<!-- WebView 전용일 때만 추가. 자체 광고도 서빙하면 이 메타데이터는 삭제 -->
<meta-data
android:name="com.google.android.gms.ads.INTEGRATION_MANAGER"
android:value="webview" />
</application>
iOS — ios/Runner/Info.plist
<key>GADApplicationIdentifier</key>
<string>ca-app-pub-xxxxxxxxxxxxxxxx~yyyyyyyyyy</string>
registerWebView용 Consumer ProGuard(R8) 규칙은 SDK에 포함돼 자동 적용됩니다.
3. 연동 검증 #
registerWebView가 호출되면 네이티브 로그(태그/프리픽스 VisualReward)가 남습니다. (Android: Logcat / iOS: Xcode 콘솔)
등록은 리워드 페이지 로드가 끝난 뒤(iOS: onLoadStop) 네이티브 뷰 계층에서 WebView를 탐색해 수행합니다.
- 성공:
AdMob registerWebView registered- iOS:
... registered (WKWebView N개 중 1개 등록) - Android:
... registered (<사용된 클래스명>)
- iOS:
- 미링크:
Google Mobile Ads SDK not linked — AdMob WebView API not registered - WebView 미검출:
AdMob registerWebView skipped (...)- iOS: WebView가 여럿이면 화면에 보이는 가장 큰 WebView를 리워드 웹뷰로 간주해 등록하며, 0개일 때만 스킵(
WKWebView 미검출) - Android: WebView가 정확히 1개일 때만 등록하며, 그 외(0개·복수)에는 스킵(
WebView N개 발견)
- iOS: WebView가 여럿이면 화면에 보이는 가장 큰 WebView를 리워드 웹뷰로 간주해 등록하며, 0개일 때만 스킵(
- 링크됐으나 실패: (Android)
... registerWebView failed/ (iOS)GADMobileAds present but registerWebView unavailable
Google 테스트 페이지로 초록 상태 바까지 확인하려면, 해당 도메인(google.github.io)을 원격 설정 allowedDomains에 추가한 상태에서 그 페이지를 SDK 웹뷰로 로드해야 합니다. (설정 캐시 TTL 1시간 주의)
⚠️ Flutter는
flutter_inappwebview가 내부 WebView 인스턴스를 노출하지 않아, 네이티브 뷰 계층에서 WebView를 탐색해 등록하는 best-effort 방식입니다. 플러그인 버전에 따라 탐색이 실패할 수 있으므로 실제 기기에서 위 로그로 검증하세요.
에러 처리 #
에러 타입 #
| 에러 타입 | 설명 |
|---|---|
VisualRewardNetworkError |
네트워크 연결 실패 |
VisualRewardInitializeTimeoutError |
initialize() 호출 후 완료까지 타임아웃 |
VisualRewardInvalidAppKeyError |
존재하지 않는 앱 키 (404) |
VisualRewardServerError(statusCode) |
서버 오류 (5xx) |
VisualRewardParseError |
응답 파싱 실패 |
VisualRewardNotInitializedError |
initialize() 호출 없이 open() 호출 |
VisualRewardUserIdNotSetError |
setUserId() 호출 없이 open() 호출 |
initialize 에러 처리 #
onFailure 콜백에서 에러 타입을 분기합니다.
VisualReward.initialize(
appKey: 'tq_live_xxx',
timeout: const Duration(seconds: 5),
onSuccess: () { /* SDK 준비 완료 */ },
onFailure: (e) {
if (e is VisualRewardNetworkError) {
// 네트워크 연결 실패
} else if (e is VisualRewardInitializeTimeoutError) {
// 초기화 타임아웃
} else if (e is VisualRewardInvalidAppKeyError) {
// 존재하지 않는 앱 키
} else if (e is VisualRewardServerError) {
// 서버 오류: e.statusCode
} else if (e is VisualRewardParseError) {
// 응답 파싱 실패
}
},
);
open 에러 처리 #
open()은 예외를 throw하지 않고 onError 콜백으로 에러를 전달합니다. 콜백에서 에러 타입을 분기합니다.
VisualReward.open(
context: context,
serviceName: 'reward_home',
locale: 'ko',
extra: null,
onError: (e) {
if (e is VisualRewardNotInitializedError) {
// initialize() 미호출
} else if (e is VisualRewardUserIdNotSetError) {
// setUserId() 미호출
} else if (e is VisualRewardNetworkError) {
// 네트워크 연결 실패
} else if (e is VisualRewardServerError) {
// 서버 오류: e.statusCode
}
},
);
SDK 동작 #
아래 동작은 모두 VisualReward.open으로 열린 웹뷰 화면에만 적용됩니다. 호스트 앱의 다른 화면이나 웹뷰에는 영향을 주지 않습니다.
가로 모드 차단 #
웹뷰 화면은 항상 세로 모드로 고정됩니다. 화면을 닫으면 기존 방향 설정이 자동으로 복원됩니다.
영상 광고 자동재생 #
사용자 제스처 없이 미디어(영상 광고)가 자동으로 인라인 재생됩니다.
javaScriptEnabled: true— JavaScript 활성화domStorageEnabled: true— DOM Storage 활성화mediaPlaybackRequiresUserGesture: false— 제스처 없이 자동재생 허용allowsInlineMediaPlayback: true(iOS) — 전체화면 강제 전환 없이 인라인 재생allowsPictureInPictureMediaPlayback: false(iOS) — PIP 비활성화
ATT 권한 요청 (iOS) #
웹뷰 로드 전 네이티브 ATTrackingManager로 추적 권한을 자동으로 요청합니다. 이미 권한이 결정된 경우 팝업 없이 즉시 진행됩니다. NSUserTrackingUsageDescription이 없으면 요청을 스킵하고 바로 로드합니다.
스와이프 백 (iOS) #
웹뷰 내 이전 페이지로 돌아가는 스와이프 백 제스처가 기본으로 활성화되어 있습니다. swipeBackEnabled JS 핸들러로 웹에서 활성화 여부를 제어할 수 있습니다.
Mixed Content 허용 (Android) #
HTTPS 페이지 내 HTTP 리소스 로드를 허용합니다. Android 5.0 이상에서 기본 차단되는 mixed content를 광고 크리에이티브가 정상적으로 로드될 수 있도록 허용합니다.
스크롤 바운스 비활성화 (iOS) #
iOS 웹뷰 스크롤이 콘텐츠 끝에 도달해도 바운스 효과가 발생하지 않습니다(disallowOverScroll: true). Android는 해당 없습니다.
백그라운드 미디어 중단 #
앱이 백그라운드로 진입하면 미디어 재생이 중단됩니다.
- Android: 이 웹뷰만
pause()(per-instance)로 렌더링·JS를 일시정지, 포그라운드 복귀 시resume()으로 재개. 전역pauseTimers()는 호출하지 않아 같은 앱의 전면 광고 화면이 덮을 때 광고 웹뷰가 멈추지 않습니다. - iOS:
setAllMediaPlaybackSuspended(true)로 미디어 재생 명시적 중단
URL 처리 #
| 케이스 | 동작 |
|---|---|
| 허용 도메인 링크 | 내부 웹뷰에서 로드 |
| 비허용 도메인 링크 | 외부 브라우저로 오픈 |
window.open / target="_blank" |
도메인 정책에 따라 내부/외부 분기 |
intent:// 스킴 (Android) |
앱 실행, 미설치 시 browser_fallback_url 또는 Play Store로 이동 |
mailto:, tel:, itms-apps: 등 비 http(s) 스킴 |
외부로 전달 |
허용 도메인은
initialize()시 서버에서 수신한allowedDomains설정이 적용됩니다.
JS Interface #
웹에서 네이티브 기능을 호출하는 인터페이스입니다.
호출 형식 #
// 파라미터 없음
window.flutter_inappwebview.callHandler("fncName");
// 파라미터 있음
window.flutter_inappwebview.callHandler("fncName", JSON.stringify({ ... }));
콜백은 window.nativeCallback 객체로 수신합니다.
window.nativeCallback.fncName = (result) => { ... };
getAppVersion() #
앱 버전을 가져옵니다.
window.flutter_inappwebview.callHandler("getAppVersion");
window.nativeCallback.getAppVersion = (version) => {
console.log(version); // "1.0.0"
};
getADID() — Android Only #
디바이스 광고 식별자(Advertising ID)를 가져옵니다. iOS에서는 동작하지 않습니다(콜백 없음).
window.flutter_inappwebview.callHandler("getADID");
window.nativeCallback.getADID = (adId) => {
console.log(adId); // "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
};
getIDFA() — iOS Only #
디바이스 광고 식별자(IDFA)를 가져옵니다. ATT 권한 허용 시 IDFA 반환, 미허용 시 빈 문자열을 반환합니다. Android에서는 동작하지 않습니다(콜백 없음).
window.flutter_inappwebview.callHandler("getIDFA");
window.nativeCallback.getIDFA = (idfa) => {
console.log(idfa); // "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
};
getIDFV() — iOS Only #
디바이스 벤더 식별자(IDFV)를 가져옵니다. Android에서는 동작하지 않습니다(콜백 없음).
window.flutter_inappwebview.callHandler("getIDFV");
window.nativeCallback.getIDFV = (idfv) => {
console.log(idfv); // "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
};
getDeviceInfo() #
기기 정보를 JSON 문자열로 가져옵니다.
window.flutter_inappwebview.callHandler("getDeviceInfo");
window.nativeCallback.getDeviceInfo = (deviceInfo) => {
const info = JSON.parse(deviceInfo);
// Android: { network: "wifi", model: "SM-S911N", manufacturer: "Samsung", osVersion: "14" }
// iOS: { network: "wifi", model: "iPhone16,1", manufacturer: "Apple", osVersion: "17.2" }
};
| 필드 | 타입 | 값 |
|---|---|---|
network |
string |
"wifi" | "mobile". 연결 없으면 키 생략 |
model |
string |
기기 모델명 |
manufacturer |
string |
제조사 |
osVersion |
string |
OS 버전 |
registerBackKeyListener() — Android Only #
디바이스 Back Key 이벤트 권한을 얻습니다. 등록 후 Back Key가 눌리면 onBackKeyPress가 호출됩니다. iOS는 하드웨어 백키가 없어 항상 "0"(실패)을 즉시 반환합니다.
window.flutter_inappwebview.callHandler("registerBackKeyListener");
window.nativeCallback.registerBackKeyListener = (success) => {
// "1": 권한 획득 성공 (Android)
// "0": 권한 획득 실패 (iOS)
};
window.nativeCallback.onBackKeyPress = () => {
// Back Key 눌렸을 때 처리
};
unregisterBackKeyListener() — Android Only #
Back Key 이벤트 권한을 해제합니다. 호출 후 Back Key는 웹뷰 히스토리 뒤로가기 또는 화면 종료로 동작합니다. iOS는 항상 no-op입니다.
window.flutter_inappwebview.callHandler("unregisterBackKeyListener");
swipeBackEnabled() — iOS Only #
iOS 스와이프 백 기능 활성화 여부를 설정합니다.
window.flutter_inappwebview.callHandler(
"swipeBackEnabled",
JSON.stringify({ enabled: false }),
);
finish() #
현재 웹뷰를 종료합니다.
window.flutter_inappwebview.callHandler("finish");
openTrackingSettings() #
앱 설정 화면을 엽니다. 광고 추적 권한을 변경할 수 있도록 안내할 때 사용합니다.
window.flutter_inappwebview.callHandler("openTrackingSettings");
openExternalBrowser() #
외부 브라우저를 엽니다.
window.flutter_inappwebview.callHandler(
"openExternalBrowser",
JSON.stringify({ url: "https://example.com" }),
);
pushUrl() #
현재 웹뷰에서 URL을 변경합니다.
window.flutter_inappwebview.callHandler(
"pushUrl",
JSON.stringify({
url: "https://example.com",
replace: false, // true: history 교체, false: history 스택에 추가 (기본값)
}),
);