visual_reward 1.3.3
visual_reward: ^1.3.3 copied to clipboard
Tenqube Visual Reward SDK for Flutter. A content webview SDK built on flutter_inappwebview for Android and iOS.
Flutter #
Flutter InAppWebView 기반 콘텐츠 SDK입니다.
요구사항 #
- Flutter 3.24+
- Dart 3.5+
- iOS 13.0+
- Android minSdk 21 (Flutter 3.35+ 앱은 기본값이 24)
설치 #
pub.dev #
- https://pub.dev/packages/visual_reward
- visual_reward SDK 버전 정보는 위 페이지를 확인해주세요.
pubspec.yaml에 아래 의존성을 추가합니다.
dependencies:
# visual_reward SDK
visual_reward: ^1.3.0
# GMA SDK: 호스트가 지정한 버전을 그대로 사용합니다. SDK 는 버전을 강제하지 않습니다.
# 이 플러그인이 양 플랫폼에 GMA 를 링크하며, 별도 네이티브 설정(gradle·Podfile)은 필요 없습니다.
google_mobile_ads: ^9.0.0
광고 엔진(Rewarded/Interstitial)은 visual_reward 가 자체 보유하고 초기화까지 책임집니다. 호스트가 제공해야 하는 것은 위의 GMA SDK 와 AdMob App ID 뿐입니다. 아래 「GMA SDK」 절을 참고하세요.
GMA 버전별 동작 #
| 호스트 GMA | 광고 기능 |
|---|---|
google_mobile_ads 5.3.x 이상 |
정상 동작 |
google_mobile_ads 5.3.x 미만 |
비활성 (로그로 안내, 크래시 없음) |
| 미링크 | 비활성 (로그로 안내, 크래시 없음) |
Android 는 GMA 두 세대를 모두 지원합니다. legacy(
play-services-ads)와 Next-Gen(ads-mobile-sdk) 중 호스트가 링크한 쪽에 맞춰 SDK 가 자동으로 맞춥니다. 앱이 지정할 설정은 없습니다. 단, Next-Gen 을 쓰는 경우 앱이minSdk 24·compileSdk 35이상이어야 합니다 (GMA Next-Gen 자체의 요구사항).
App ID #
- 호스트 앱에 AdMob/Ad Manager App ID가 필요합니다. ID가 없는 경우 요청주시기 바랍니다.
- App ID가 누락되면 앱 시작 시 크래시가 발생합니다.
-
AndroidManifest.xml
<application> <meta-data android:name="com.google.android.gms.ads.APPLICATION_ID" android:value="ca-app-pub-xxxxxxxxxxxxxxxx~yyyyyyyyyy" /> </application> -
Info.plist
<!-- App ID 누락 시 시작 크래시 (AdMob 또는 Ad Manager App ID) --> <key>GADApplicationIdentifier</key> <string>ca-app-pub-xxxxxxxxxxxxxxxx~yyyyyyyyyy</string> <!-- IDFA/ATT 사용 목적 문자열 --> <key>NSUserTrackingUsageDescription</key> <string>맞춤형 광고 제공을 위해 기기 식별자를 사용합니다.</string> <!-- SKAdNetwork (Google 제공 식별자) --> <key>SKAdNetworkItems</key> <array><!-- ... --></array>ATT 권한 요청은 SDK가 웹뷰 로드 전 네이티브
ATTrackingManager로 자동 처리합니다. 이미 권한이 결정된 경우 팝업 없이 즉시 진행하며,NSUserTrackingUsageDescription이 없으면 요청을 스킵하고 바로 로드합니다.
리워드·전면 광고를 쓴다면 App ID 를 선언하세요. 이 SDK 는 두 광고를 인앱으로 서빙하므로 App ID 가 필요합니다. 웹뷰 전용 우회 키(Android
com.google.android.gms.ads.INTEGRATION_MANAGER· iOSGADIntegrationManager=webview)는 GMA 의 App ID 검사를 건너뛰는 설정이라 이 경우에는 넣을 이유가 없습니다.App ID 가 없는 구성(리워드 웹뷰 안의 광고만 쓰는 경우)이라면 이 우회 키가 필요합니다.
- Android legacy: 넣지 않으면 앱 시작 즉시 크래시하고(
MobileAdsInitProvider→Missing application ID), 넣으면 크래시 없이registerWebView까지 정상 동작하는 것이 실측됐습니다. Next-Gen 은 이 키를 읽지 않으며 추가 설정이 필요 없습니다.- iOS: 넣지 않으면 앱 시작 시
GADInvalidInitializationException으로 크래시합니다.다만 이 구성에서 리워드·전면 광고가 실제로 서빙되는지는 미검증입니다.
app-ads.txt #
- 앱 스토어 등록정보의 개발자 웹사이트 도메인 루트
app-ads.txt에 아래 목록을 그대로 게시해주세요. 게시할 전체 목록은 https://visual.reward.tenqube.com/docs/app-ads.txt 에서 받을 수 있습니다. - 이미
app-ads.txt를 운영 중이라면 기존 줄을 지우지 말고 덧붙이세요. 목록이 갱신되면 다시 전달드립니다.
시퀀스 다이어그램 #
sequenceDiagram
title interaction VisualReward SDK
participant A as Host App
participant B as VisualReward SDK
participant C as VisualReward 서버
participant D as WebView 화면
A->>B: initialize(appKey, ...)
B->>C: config 요청
C-->>B: config 응답
B-->>A: onSuccess()
A->>B: open(serviceName, userId, ...)
B->>D: 웹뷰 열기
D->>B: 웹뷰 닫힘
B-->>A: onClose
사용법 #
디버그 설정 #
-
initialize()전에 호출합니다.import 'package:flutter/foundation.dart'; import 'package:visual_reward/visual_reward.dart'; VisualReward.setDebugEnabled(kDebugMode);
초기화 #
-
main()또는 앱 시작 시 한 번만 호출합니다.import 'package:flutter/widgets.dart'; import 'package:visual_reward/visual_reward.dart'; var rewardReady = false; Future<void> main() async { WidgetsFlutterBinding.ensureInitialized(); // 광고 엔진(GMA 초기화 포함)까지 이 호출이 함께 수행합니다. // 호스트는 MobileAds 를 직접 초기화하지 않습니다. SDK 가 대신 수행하고 완료를 기다립니다. // 필요한 것은 App ID 뿐입니다. VisualReward.initialize( appKey: 'tq_live_xxx', // 발급된 app_key adTestMode: false, // true 면 테스트 광고만 노출 timeout: const Duration(seconds: 5), // 기본값 onSuccess: () => rewardReady = true, onFailure: (e) { /* 초기화 실패 */ }, ); runApp(const MyApp()); } -
VisualReward.initialize파라미터파라미터 타입 필수 설명 기본값 appKeyString✅ stage별 app_key 발급 요청이 필요합니다. adTestModebool테스트 광고 노출 여부. 광고 엔진 첫 초기화 시점의 값만 적용됩니다 falsetimeoutDuration초기화 완료 대기 시간. 초과 시 VisualRewardInitializeTimeoutError발생Duration(seconds: 5)onSuccessVoidCallback?초기화 성공 콜백 nullonFailurevoid Function(VisualRewardError)?초기화 실패 콜백 nullonSuccess/onFailure는 리워드 설정 로드 결과에 대한 콜백입니다. 광고 엔진 초기화는 백그라운드에서 진행되며 실패해도 리워드 기능은 정상 동작합니다(광고만 비활성). 광고 엔진 상태는VisualReward로그 태그로 확인할 수 있습니다.광고용 도메인은 앱이 지정하지 않습니다.
appKey로 조회하는 서버 설정의ezoic.adDomain값을 사용하며, 설정에 값이 없으면 광고 기능만 비활성됩니다.initialize()재호출 시 리워드 설정은 새appKey로 교체됩니다(광고 도메인·adTestMode는 첫 초기화 값이 유지됩니다). 교체 중이던open()호출은 실패하지 않고 새 설정을 이어서 기다립니다.동의 관리(TCF v2 / GPP / US Privacy)는 SDK 가
SharedPreferences(Android) ·UserDefaults(iOS) 에서 자동으로 읽습니다. 디버그 로그는setDebugEnabled()값이 광고 엔진에도 함께 적용됩니다.Rewarded / Interstitial 광고는 웹 페이지가 요청합니다. 호스트가 광고를 직접 로드·표시하는 코드는 필요하지 않습니다.
웹뷰 열기 #
-
initialize()후에 호출합니다.import 'package:visual_reward/visual_reward.dart'; VisualReward.open( context: context, serviceName: 'reward_home', userId: user.id, // 리워드 귀속 사용자 locale: 'ko', extra: null, onClose: () { /* 웹뷰 닫힘 */ }, onError: (e) { /* 열기 실패 */ }, ); -
open 파라미터
파라미터 타입 필수 설명 기본값 contextBuildContext✅ 화면을 열 BuildContext serviceNameString✅ 사용 서비스별 serviceName 전달 예정입니다. userIdString✅ 리워드 귀속 사용자 식별자. 비어 있으면 VisualRewardUserIdNotSetErrorlocaleString?BCP 47 (RFC 5646) 언어 서브태그(ISO 639-1, 2자리 소문자)를 사용 "ko"extraString?포스트백 추가 데이터 (JSON serialize된 문자열) nullonCloseVoidCallback?웹뷰가 닫힐 때 호출되는 콜백 nullonErrorvoid Function(VisualRewardError)?에러 발생 시 호출되는 콜백 null한 번의
open()은onClose또는onError중 정확히 하나를 받습니다. 화면 트리에서 사라진BuildContext로 호출하면VisualRewardOpenFailedError로 알립니다(조용히 아무 콜백도 오지 않는 경우를 없앱니다).
AdPopcorn 브릿지 등록 #
-
AdPopcorn SDK(
adpopcornssp_flutter)를 사용하는 경우open호출 전에 등록합니다.import 'package:adpopcornssp_flutter/adpopcornssp_flutter.dart'; VisualReward.setAdPopcornBridgeHandler(AdPopcornInAppWebViewBridgeHandler());
호스트 WebView 브릿지 #
호스트 앱이 직접 소유한 WebView 의 웹 페이지에서 리워드 웹뷰를 열 수 있습니다. 리워드 웹뷰(SDK 소유)에는 필요 없습니다. 이미 자체 브릿지를 갖고 있습니다.
1. 네이티브: 브릿지 부착 #
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:visual_reward/visual_reward.dart';
class HostWebViewPage extends StatefulWidget {
const HostWebViewPage({super.key});
@override
State<HostWebViewPage> createState() => _HostWebViewPageState();
}
class _HostWebViewPageState extends State<HostWebViewPage> {
InAppWebViewController? _controller;
@override
void dispose() {
final controller = _controller;
if (controller != null) VisualReward.detachWebBridge(controller);
super.dispose();
}
@override
Widget build(BuildContext context) {
return InAppWebView(
initialUrlRequest: URLRequest(url: WebUri('https://your-page.example.com')),
onWebViewCreated: (controller) {
_controller = controller;
// 페이지 로드 이전에 부착합니다.
VisualReward.attachWebBridge(controller: controller, context: context);
},
);
}
}
| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
controller |
InAppWebViewController |
✅ | 브릿지를 붙일 호스트 WebView 컨트롤러 |
context |
BuildContext |
✅ | 리워드 웹뷰를 열 BuildContext |
attachWebBridge는 페이지 로드 이전에 호출해야 합니다.flutter_inappwebview의 핸들러는 등록 이후에 만들어지는 문서에만 주입되므로, 이미 로드된 페이지에는 다시 로드하기 전까지 나타나지 않습니다.
detachWebBridge를 잊지 마세요. 핸들러가 컨트롤러에 남아 있으면 사라진 화면의BuildContext로 리워드 웹뷰를 열려고 시도합니다.
VisualReward.initialize()가 선행돼야 합니다. 브릿지는 초기화를 대신하지 않습니다. 앱이 초기화한appKey설정(허용 오리진·광고 엔진)을 그대로 사용합니다. 초기화 전에 페이지가 호출하면NOT_INITIALIZED로 응답합니다.
2. 웹: 핸들러 호출 #
window.flutter_inappwebview.callHandler('visualRewardBridge', JSON.stringify({
userId: 'u-123', // 필수: 리워드 귀속 사용자
serviceName: 'anyService', // 필수: 실행 serviceName(전달 예정)
locale: 'ko', // 선택: 기본 "ko"
extra: '', // 선택: 포스트백 추가 데이터(JSON serialize된 문자열)
}));
| 필드 | 타입 | 필수 | 설명 | 기본값 |
|---|---|---|---|---|
userId |
string |
✅ | 리워드 귀속 사용자 | |
serviceName |
string |
✅ | 실행 serviceName(전달 예정) | |
locale |
string |
BCP 47 언어 서브태그 | "ko" |
|
extra |
string |
포스트백 추가 데이터 (JSON serialize된 문자열) | "" |
appKey·adTestMode· 대기 한도는 웹이 지정하지 않습니다. 모두 네이티브initialize()가 소유합니다(timeout이 설정 대기 한도로 쓰입니다).
응답 콜백은 페이지에서 전역 객체로 등록합니다.
window.visualRewardCallback = {
onClose: function () {}, // 리워드 웹뷰 닫힘
onError: function (code, message) {},
};
onError 의 code 는 네이티브 에러 타입에 대응합니다.
| code | 의미 |
|---|---|
NETWORK_ERROR |
설정 조회 네트워크 오류 |
INITIALIZE_TIMEOUT |
초기화 완료 대기 시간 초과 |
INVALID_APP_KEY |
존재하지 않는 app_key (404) |
SERVER_ERROR |
설정 서버 오류 (200/404 외 응답) |
PARSE_ERROR |
설정 응답 파싱 실패 |
NOT_INITIALIZED |
초기화 전 열기 시도 |
USER_ID_NOT_SET |
userId 없이 열기 시도 |
OPEN_FAILED |
리워드 화면 진입 실패 |
INVALID_PAYLOAD |
브릿지 자체: 필수 키 누락·JSON 파싱 실패 |
브릿지 호출 하나는
onClose또는onError중 정확히 하나를 받습니다(오리진 검증 실패는 예외로, 의도적으로 아무 것도 알리지 않습니다).설정 관련 코드(
NETWORK_ERROR·INVALID_APP_KEY·SERVER_ERROR·PARSE_ERROR·INITIALIZE_TIMEOUT)는 브릿지가 따로 조회해서 내는 것이 아니라 네이티브initialize()의 실패·대기 결과가 그대로 전달되는 것입니다.
브릿지가 주입됐는지 확인하고 호출하세요.
if (window.flutter_inappwebview && window.flutter_inappwebview.callHandler) {
window.flutter_inappwebview.callHandler('visualRewardBridge', JSON.stringify({ /* ... */ }));
}
3. 호출 가능한 페이지 (허용 오리진) #
앱이 initialize() 로 채택한 설정의 allowedDomains 에 호출한 페이지의 오리진이 포함돼야 실행됩니다. 검증에 실패하면 아무 것도 하지 않고 로그만 남깁니다(어떤 오리진이 허용되는지 탐색할 수 있는 신호를 웹에 주지 않기 위해 콜백도 호출하지 않습니다).
- 오리진은 최상위 페이지 URL 로 판정합니다. 같은 오리진 iframe 은 통과합니다
- 브릿지 호출은 SDK 상태(설정·광고 엔진)를 바꾸지 않습니다. 웹은
appKey를 지정할 수 없으므로 검증 기준을 고를 수 없습니다 file://로 로드한 페이지는 오리진 호스트가 비어 있어 항상 거부됩니다
에러 처리 #
에러 타입 #
| 타입 | 설명 |
|---|---|
VisualRewardNetworkError |
네트워크 연결 실패 |
VisualRewardInitializeTimeoutError |
initialize() 호출 후 완료까지 타임아웃 |
VisualRewardInvalidAppKeyError |
유효하지 않은 앱 키 (404) |
VisualRewardServerError(statusCode) |
서버 오류 (5xx) |
VisualRewardParseError |
응답 파싱 실패 |
VisualRewardNotInitializedError |
initialize() 호출 없이 open() 호출 |
VisualRewardUserIdNotSetError |
open() 의 userId 가 빈 문자열 |
VisualRewardOpenFailedError |
리워드 화면 진입 실패: 화면 트리에서 사라진 BuildContext 로 open() 호출 등 |
initialize 에러 처리 #
-
onFailure콜백에서 에러 타입을 분기합니다.import 'package:visual_reward/visual_reward.dart'; 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콜백으로 에러를 전달합니다. 콜백에서 에러 타입을 분기합니다.import 'package:visual_reward/visual_reward.dart'; VisualReward.open( context: context, serviceName: 'reward_home', userId: user.id, locale: 'ko', extra: null, onError: (e) { if (e is VisualRewardNotInitializedError) { // initialize() 미호출 } else if (e is VisualRewardUserIdNotSetError) { // userId 가 빈 문자열 } else if (e is VisualRewardOpenFailedError) { // 화면 진입 실패 } else if (e is VisualRewardNetworkError) { // 네트워크 연결 실패 } else if (e is VisualRewardServerError) { // 서버 오류: e.statusCode } }, );