visual_reward 1.4.7
visual_reward: ^1.4.7 copied to clipboard
Tenqube Visual Reward SDK for Flutter. A content webview SDK built on flutter_inappwebview for Android and iOS.
Flutter SDK #
Flutter InAppWebView 기반 콘텐츠 SDK를 앱에 연동합니다.
준비 #
요구사항 #
- Flutter 3.24+ / Dart 3.5+
- iOS 14.0+
- Android minSdk 24
- Android Gradle Plugin 8.x / Kotlin Gradle Plugin 2.2 이상
pub.dev #
pubspec.yaml에 아래 의존성을 추가합니다. 최신 버전은
pub.dev에서 확인할 수 있습니다.
dependencies:
# visual_reward SDK
visual_reward: <version>
# GMA SDK: 호스트가 지정한 버전을 그대로 사용합니다. SDK 는 버전을 강제하지 않습니다.
# google_mobile_ads 가 양 플랫폼에 GMA 를 링크합니다. visual_reward 는 GMA 를 링크하지 않고
# 런타임에 발견해 사용하므로, 미링크 시 광고만 비활성됩니다.
google_mobile_ads: ^9.0.0
GMA 버전별 동작 #
google_mobile_ads |
광고 기능 |
|---|---|
| 5.2.0 이상 | 정상 동작 |
| 5.2.0 미만 | Android 광고 비활성. iOS 는 정상 |
| 미선언 | 광고 비활성 (크래시 없음) |
AndroidManifest 설정 #
<application> 안에 선언할 meta-data 가 네이티브 광고 사용 여부에 따라 달라집니다.
두 키를 동시에 넣지 마세요.
네이티브 광고를 사용하는 경우
App ID 만 선언하면 됩니다. 등록 방법은 App ID를 참고하세요.
네이티브 광고를 사용하지 않는 경우
App ID 검사를 건너뛰는 키를 넣습니다.
<!-- android/app/src/main/AndroidManifest.xml -->
<!-- 이 키가 없으면 앱 시작 즉시 IllegalStateException 으로 크래시합니다 -->
<meta-data
android:name="com.google.android.gms.ads.INTEGRATION_MANAGER"
android:value="webview" />
Info.plist 설정 #
IDFA 수익화에 필요한 키는 광고 사용 여부와 무관하게 넣습니다.
<!-- ios/Runner/Info.plist -->
<key>NSUserTrackingUsageDescription</key>
<string>맞춤형 광고 제공을 위해 기기 식별자를 사용합니다.</string>
네이티브 광고를 사용하는 경우
App ID 만 선언하면 됩니다. 등록 방법은 App ID를 참고하세요.
네이티브 광고를 사용하지 않는 경우
App ID 검사를 건너뛰는 키를 넣습니다. 없으면 앱 시작 시 GADInvalidInitializationException 으로
크래시합니다.
<!-- ios/Runner/Info.plist -->
<key>GADIntegrationManager</key>
<string>webview</string>
사용법 #
동작 흐름 #
initialize()가 서버에서 설정을 로드하고, 이후 open()이 리워드 웹뷰를 엽니다.
sequenceDiagram
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(context: ..., 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();
VisualReward.initialize(
appKey: 'app_key', // 발급된 app_key
adTestMode: false, // 테스트 광고 활성화 여부
timeout: const Duration(seconds: 5), // 기본값
onSuccess: () => rewardReady = true,
onFailure: (e) { /* 초기화 실패 */ },
);
runApp(const MyApp());
}
::: info
onSuccess/onFailure는 리워드 설정 로드 결과에 대한 콜백입니다. 광고 엔진 초기화는 백그라운드에서
진행되며 실패해도 리워드 기능은 정상 동작합니다(광고만 비활성).
:::
VisualReward.initialize 파라미터
| 파라미터 | 타입 | 필수 | 설명 | 기본값 |
|---|---|---|---|---|
appKey |
String |
✔ | 발급받은 app_key. stage별로 발급 요청이 필요합니다. | |
adTestMode |
bool |
테스트 광고 활성화 여부. 광고 엔진 첫 초기화 시점의 값만 적용됩니다. | false |
|
timeout |
Duration |
open() 이 초기화 완료를 기다리는 상한. 초과 시 open() 이 VisualRewardInitializeTimeoutError 로 실패합니다. |
5초 |
|
showDefaultErrorUi |
bool |
open() 이 실패했을 때 SDK 가 기본 안내 화면을 띄운 뒤 onError 를 호출합니다. 원격 안내 페이지를 먼저 띄우고, 실패하면 앱에 동봉된 HTML 로 떨어집니다. 화면에 표시되는 진단 코드는 기본 안내 화면 code 를 참고하세요. |
true |
|
onSuccess |
VoidCallback? |
초기화 성공 콜백 | ||
onFailure |
void Function(VisualRewardError)? |
초기화 실패 콜백 |
::: warning 이미 onError 로 안내하고 있다면 showDefaultErrorUi: false 로 끄세요
기본값이 켜져 있어 그대로 두면 안내가 두 번 뜹니다.
:::
웹뷰 열기 #
SDK 초기화 성공 이후에 호출합니다.
import 'package:visual_reward/visual_reward.dart';
if (rewardReady) {
VisualReward.open(
context: context,
serviceName: 'anyService', // 서비스 식별자
userId: user.id, // 사용자 식별자
locale: 'ko',
extra: null,
onClose: () { /* 웹뷰 닫힘 */ },
onError: (e) { /* 에러 처리 */ },
);
}
VisualReward.open 파라미터
| 파라미터 | 타입 | 필수 | 설명 | 기본값 |
|---|---|---|---|---|
context |
BuildContext |
✔ | 리워드 웹뷰를 열 BuildContext | |
serviceName |
String |
✔ | 서비스 식별자. 사용할 값은 별도로 전달드립니다. | |
userId |
String |
✔ | 사용자 식별자. 비어 있으면 VisualRewardUserIdNotSetError 가 발생합니다. |
|
locale |
String? |
BCP 47 언어 서브태그(ISO 639-1, 2자리 소문자) | "ko" |
|
extra |
String? |
포스트백 추가 데이터(JSON serialize된 문자열) | ||
onClose |
VoidCallback? |
웹뷰가 닫힐 때 호출되는 콜백 | ||
onError |
void Function(VisualRewardError)? |
에러 발생 시 호출되는 콜백 |
::: info
한 번의 open()은 onClose 또는 onError 중 정확히 하나를 받습니다. 화면 트리에서 사라진
BuildContext 로 호출하면 VisualRewardOpenFailedError 로 알립니다. 닫힘 트리거가 여러 번
발생해도 콜백은 한 번만 통보됩니다.
:::
호스트 WebView 브릿지 #
호스트 앱이 직접 소유한 WebView의 웹 페이지에서 리워드 웹뷰를 열 수 있습니다. 네이티브에서 브릿지를 붙인 뒤, 웹 페이지에서 호출합니다.
네이티브: 브릿지 부착
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 |
::: warning
attachWebBridge 는 웹이 브릿지를 호출하기 전에 붙어 있어야 합니다. onWebViewCreated 에서 붙이면
항상 안전합니다.
화면을 떠날 때 detachWebBridge 를 잊지 마세요. 핸들러가 컨트롤러에 남아 있으면 사라진 화면의
BuildContext 로 리워드 웹뷰를 열려고 시도합니다.
:::
웹: 핸들러 호출
window.flutter_inappwebview.callHandler('visualRewardBridge', JSON.stringify({
userId: 'u-123', // 필수: 사용자 식별자
serviceName: 'anyService', // 필수: 서비스 식별자
locale: 'ko', // 선택: 기본 "ko"
extra: '', // 선택: 포스트백 추가 데이터(JSON serialize된 문자열)
}));
| 파라미터 | 타입 | 필수 | 설명 | 기본값 |
|---|---|---|---|---|
userId |
string |
✔ | 사용자 식별자 | |
serviceName |
string |
✔ | 서비스 식별자. 사용할 값은 별도로 전달드립니다. | |
locale |
string |
BCP 47 언어 서브태그 | "ko" |
|
extra |
string |
포스트백 추가 데이터(JSON serialize된 문자열) |
응답 콜백은 페이지에서 전역 객체로 등록합니다.
window.visualRewardCallback = {
onClose: function () {}, // 리워드 웹뷰 닫힘
onError: function (code, message) {},
};
onError의 code는 네이티브 에러 타입에 대응합니다.
| code | 의미 |
|---|---|
NETWORK_ERROR |
설정 조회 네트워크 오류 |
INITIALIZE_TIMEOUT |
open() 이 설정 완료를 기다렸으나 시간 초과 |
INVALID_APP_KEY |
존재하지 않는 app_key |
SERVER_ERROR |
설정 서버 오류 |
PARSE_ERROR |
설정 응답 파싱 실패 |
NOT_INITIALIZED |
앱이 initialize()를 호출하기 전에 열기 시도 |
USER_ID_NOT_SET |
userId 누락 또는 빈 문자열 |
OPEN_FAILED |
리워드 화면 진입 실패 |
INVALID_PAYLOAD |
serviceName 누락 또는 JSON 파싱 실패 (브릿지 자체 검증). userId 누락은 USER_ID_NOT_SET 으로 옵니다 |
브릿지 주입 여부를 확인하고 호출하세요.
if (window.flutter_inappwebview && window.flutter_inappwebview.callHandler) {
window.flutter_inappwebview.callHandler('visualRewardBridge', JSON.stringify({ /* ... */ }));
}
네이티브 광고 설정 #
네이티브 광고를 서빙하는 데 필요한 설정입니다. App ID는 광고 요청에,
app-ads.txt는 이 앱의 광고 인벤토리를 누가 판매할 수 있는지 선언하는 데 쓰입니다.
둘 다 없으면 광고가 채워지지 않습니다.
App ID #
AdMob/Ad Manager App ID가 필요합니다. 없다면 요청해주세요. 양 플랫폼 모두에 선언합니다.
<!-- android/app/src/main/AndroidManifest.xml -->
<application>
<meta-data
android:name="com.google.android.gms.ads.APPLICATION_ID"
android:value="ca-app-pub-xxxxxxxxxxxxxxxx~yyyyyyyyyy" />
</application>
<!-- ios/Runner/Info.plist -->
<key>GADApplicationIdentifier</key>
<string>ca-app-pub-xxxxxxxxxxxxxxxx~yyyyyyyyyy</string>
<!-- SKAdNetwork (Google 제공 식별자) -->
<key>SKAdNetworkItems</key>
<array><!-- ... --></array>
app-ads.txt #
앱 스토어 등록정보에 있는 개발자 웹사이트의 도메인 루트에 app-ads.txt 를 아래 내용 그대로
게시해주세요(https://<개발자-웹사이트>/app-ads.txt).
::: warning
이미 app-ads.txt 를 운영 중이라면 기존 줄을 지우지 말고 아래 목록을 덧붙이세요. 중복 줄은 제거해도
됩니다. 목록이 갱신되면 다시 전달드립니다.
:::
::: details app-ads.txt 전체 내용 (127줄) <<< @/public/app-ads.txt :::
AdPopcorn SSP 브릿지 #
AdPopcorn SDK를 쓰지 않는다면 이 절을 건너뛰세요.
flutter_inappwebview 컨트롤러에 핸들러를 얹어야 하므로 호스트가 인스턴스를 넘겨줘야
합니다. 넘기지 않으면 리워드 웹뷰 안의 AdPopcorn 배너가 서빙되지 않습니다.
import 'package:visual_reward/visual_reward.dart';
// initialize() 전후 어디서든, open() 보다 먼저 한 번만 호출합니다.
VisualReward.setAdPopcornBridgeHandler(AdPopcornInAppWebViewBridgeHandler());
| 파라미터 | 타입 | 필수 | 설명 | 기본값 |
|---|---|---|---|---|
handler |
dynamic |
✔ | AdPopcorn SDK의 AdPopcornInAppWebViewBridgeHandler 인스턴스 |
문제 해결 #
에러 타입 #
onFailure/onError 콜백에서 is 로 분기합니다.
| 타입 | 설명 |
|---|---|
VisualRewardNetworkError |
네트워크 연결 실패 |
VisualRewardInitializeTimeoutError |
open() 이 설정 조회를 timeout 까지 기다렸으나 오지 않음. initialize() 의 실패 콜백으로는 오지 않습니다 |
VisualRewardInvalidAppKeyError |
존재하지 않는 app_key |
VisualRewardServerError(statusCode) |
설정 서버 오류 |
VisualRewardParseError |
설정 응답 파싱 실패 |
VisualRewardNotInitializedError |
initialize() 호출 없이 open() 호출 |
VisualRewardUserIdNotSetError |
open()의 userId가 빈 문자열 |
VisualRewardOpenFailedError |
리워드 화면 진입 실패 (예: 화면 트리에서 사라진 BuildContext) |
기본 안내 화면 code #
showDefaultErrorUi 가 켜져 있을 때 뜨는 화면의 코드입니다. 문구는 사용자가 취할 수 있는 행동 기준으로
3종이고, 화면 하단의 code 는 원인별로 다릅니다. 사용자가 알려준 코드로 원인을 좁힐 수 있습니다.
| code | 원인 | 안내 문구 |
|---|---|---|
N01 |
open() 의 userId 가 비어 있음 |
필수 정보가 존재하지 않습니다. |
N02 |
open JS 인터페이스 요청 페이로드 오류 |
〃 |
N03 |
네트워크 오류(DNS·연결 실패), 잘못된 serviceName 혹은 누락, 페이지 로드 실패 |
네트워크에 연결할 수 없습니다. |
N04 |
open() 이 설정 조회를 timeout 안에 받지 못함 |
〃 |
N05 |
initialize() 전 open() 호출 |
일시적인 오류가 발생했습니다. (앱 재시작 안내) |
N06 |
설정 서버 오류 | 일시적인 오류가 발생했습니다. |
N07 |
설정 응답 파싱 실패 | 〃 |
N08 |
존재하지 않는 앱 키 | 〃 |
N09 |
open() 에 넘긴 BuildContext 가 화면 트리에서 사라짐 |
〃 |
증상별 점검 #
::: details iOS 빌드가 deployment target 오류로 실패해요
Podfile 의 platform :ios, '14.0' 과 Xcode 에서 Runner 타겟의 Minimum Deployments 를 양쪽 다
14.0 으로 올려야 합니다. Flutter 템플릿 기본값이 13.0 이라 Podfile 만 고치면 실패합니다.
:::
::: details AGP 9 · Kotlin 2.1.x 에서 Android 빌드가 실패해요
AGP 9 는 필수 의존성 flutter_inappwebview 가 지원하지 않고, Kotlin Gradle Plugin 2.1.x 로는 플러그인
의존성 컴파일이 실패합니다. android/settings.gradle.kts 에서 AGP 8.x / KGP 2.2 이상을 확인하세요.
:::
::: details 광고가 나오지 않아요
google_mobile_ads 선언 여부와 링크되는 GMA 아티팩트 버전(GMA 버전별 동작), 양
플랫폼의 App ID를 확인하세요. 비활성 사유는 VisualReward 로그 태그에 남습니다.
:::
::: details 네이티브 광고만 서빙되지 않아요
호스트가 MobileAds 를 직접 초기화하고 있는지 확인하세요. 광고 엔진 초기화는 SDK 가 담당합니다.
호스트가 먼저 초기화하면 그때의 App ID 가 유지되어 SDK 의 설정이 무시될 수 있습니다.
:::
::: details VisualRewardNotInitializedError 가 발생해요
initialize() 성공 이후에 open()·브릿지를 호출하세요.
:::
::: details VisualRewardInitializeTimeoutError 가 발생해요
네트워크 상태를 확인하고, 필요하면 timeout 을 늘리세요.
타임아웃은 대기를 끊은 것일 뿐 조회 자체는 계속됩니다. 조회가 늦게라도 성공하면 그 설정이 채택되므로
다음 open() 은 정상적으로 열립니다.
:::
::: details VisualRewardOpenFailedError 가 발생해요
화면 트리에 살아 있는 BuildContext 로 호출했는지 확인하세요.
:::
::: details 페이지에 브릿지가 없어요
페이지가 브릿지를 호출하는 시점보다 attachWebBridge 가 먼저인지 확인하세요.
onWebViewCreated 에서 붙이면 항상 먼저입니다.
:::
::: details 브릿지를 호출해도 아무 반응이 없어요
호출한 페이지의 오리진이 allowedDomains에 포함돼 있는지 확인하세요.
이 값은 app_key 발급 시 지정되므로 발급 담당자에게 확인하세요.
검증에 실패하면 콜백 없이 로그만 남습니다.
:::
::: details VisualRewardNetworkError 가 발생해요
설정 조회가 네트워크 단계에서 실패한 경우입니다. VisualReward 로그의 원인을 먼저 확인하세요.
호스트를 찾지 못했다는 오류(Unable to resolve host / cannot find host)면 DNS 조회 실패입니다.
서버 장애가 아니라 기기 쪽 문제이므로 다음을 확인하세요.
- 기기의 비공개 DNS(Private DNS) 설정
- 광고 차단 앱 · VPN 사용 여부 (도메인에
reward가 포함돼 차단 목록에 걸리는 사례가 있습니다) - 사내망 · 학교망 프록시 정책
- 브라우저에서
https://visual.reward.tenqube.com접속 가능 여부
일시적인 실패라면 다음 open() 이 설정을 자동으로 다시 조회하므로 앱 재시작 없이 회복됩니다.
:::
해결되지 않으면 setDebugEnabled(true) 로 로그를 켠 뒤 VisualReward 태그 로그와 함께 문의해주세요.