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(…ads.APPLICATION_ID), 쓰지 않으면 App ID 검사를 건너뛰는 키 (…ads.INTEGRATION_MANAGER)를 넣습니다. 두 키를 동시에 넣지 마세요.

Android — 네이티브 광고를 사용하는 경우

App ID 만 선언하면 됩니다. 등록 방법은 App ID를 참고하세요.

Android — 네이티브 광고를 사용하지 않는 경우

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 수익화에 필요한 키는 광고 사용 여부와 무관하게 넣습니다.

ATT 프롬프트는 SDK 가 띄웁니다. 리워드 웹뷰를 열 때 권한 상태가 미결정이면 SDK 가 프롬프트를 띄우고 사용자 응답을 기다린 뒤 웹뷰를 로드하므로, 첫 광고 요청부터 IDFA 가 실립니다. 호스트는 아래 문자열만 넣으면 되고 별도 호출이 필요 없습니다. 이 키가 없으면 SDK 는 요청 자체를 건너뜁니다 (Apple 이 문자열 없는 요청을 거부하기 때문입니다). 그 경우 IDFA 는 전부 0 이 되어 입찰이 크게 떨어집니다.

<!-- ios/Runner/Info.plist -->
<key>NSUserTrackingUsageDescription</key>
<string>맞춤형 광고 제공을 위해 기기 식별자를 사용합니다.</string>

iOS — 네이티브 광고를 사용하는 경우

App ID 만 선언하면 됩니다. 등록 방법은 App ID를 참고하세요.

iOS — 네이티브 광고를 사용하지 않는 경우

App ID 검사를 건너뛰는 키를 넣습니다. 없으면 앱 시작 시 GADInvalidInitializationException 으로 크래시합니다.

<!-- ios/Runner/Info.plist -->
<key>GADIntegrationManager</key>
<string>webview</string>

사용법

동작 흐름

initialize()가 서버에서 설정을 로드하고, 이후 open()이 리워드 웹뷰를 엽니다.

sequenceDiagram
    participant A as Host App
    participant B as Visual Reward SDK
    participant C as Visual Reward 서버
    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';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  VisualReward.initialize(
    appKey: 'app_key',                    // 발급된 app_key
    adTestMode: false,                    // 테스트 광고 활성화 여부
    timeout: const Duration(seconds: 5),  // 기본값
    onSuccess: () { /* SDK 준비 완료 */ },
    onFailure: (e) { /* 초기화 실패 — 로깅 용도로만 쓰세요 */ },
  );

  runApp(const MyApp());
}

참고

onSuccess/onFailure는 리워드 설정 로드 결과에 대한 콜백입니다. 광고 엔진 초기화는 백그라운드에서 진행되며 실패해도 리워드 기능은 정상 동작합니다(광고만 비활성). 초기화가 실패해도 이후 open()이 설정을 다시 조회하므로 onFailure로 리워드 진입점을 막지 마세요. 최종 실패는 open()의 onError로 처리합니다.

VisualReward.initialize 파라미터

파라미터 타입 필수 설명 기본값
appKey String ✔ 발급받은 app_key. stage별로 발급 요청이 필요합니다.
adTestMode bool 테스트 광고 활성화 여부. 광고 엔진 첫 초기화 시점의 값만 적용됩니다. false
timeout Duration open() 이 설정 조회를 기다리는 상한. 초과 시 open() 이 VisualRewardInitializeTimeoutError 로 실패합니다. initialize() 자체의 완료 대기 시간이 아닙니다. 5초
showDefaultErrorUi bool open() 이 실패했을 때 SDK 가 디폴트 에러 페이지를 띄운 뒤 onError 를 호출합니다. 원격 안내 페이지를 먼저 띄우고, 실패하면 앱에 동봉된 HTML 로 떨어집니다. 화면에 표시되는 진단 코드는 디폴트 에러 페이지 code 를 참고하세요. 리워드 화면이 뜬 뒤 페이지 로드가 실패한 경우는 이 설정과 무관합니다 — 바로 아래 주의사항을 참고하세요. true
onSuccess VoidCallback? 초기화 성공 콜백
onFailure void Function(VisualRewardError)? 초기화 실패 콜백

showDefaultErrorUi 를 끌 때는 자체 안내가 반드시 있어야 합니다

  • 이미 onError 로 안내하고 있다면 false 로 끄세요 — 그대로 두면 안내가 두 번 뜹니다.
  • 끄면 open() 이 실패해도 SDK 는 아무 화면도 띄우지 않습니다. onError 에서 반드시 자체 안내를 띄우세요. 비워 두면 사용자에게는 눌러도 아무 반응이 없는 것으로 보입니다.
  • 단, 리워드 화면이 이미 뜬 뒤 페이지 로드가 실패하면 이 설정과 무관하게 SDK 가 동봉 안내 화면 (N03)으로 채웁니다. 그 시점에는 화면 소유권이 SDK 에 있어 호스트가 대신 그릴 수단이 없고, 두지 않으면 빈 화면이 남기 때문입니다. 이 경우 onError 는 호출되지 않고, 사용자가 확인 을 누르면 onClose 가 나갑니다.

웹뷰 열기

initialize() 호출 이후에 사용합니다.

import 'package:visual_reward/visual_reward.dart';

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)? 에러 발생 시 호출되는 콜백. 리워드 페이지 로드 실패는 이 콜백으로 오지 않습니다(SDK 가 안내 화면으로 처리).

참고

화면이 열린 open()은 onClose 또는 onError 중 정확히 하나를 받습니다. 화면 트리에서 사라진 BuildContext 로 호출하면 VisualRewardOpenFailedError 로 알립니다. 닫힘 트리거가 여러 번 발생해도 콜백은 한 번만 통보됩니다.

이미 열려 있으면 호출이 무시됩니다

리워드 화면은 한 번에 하나만 뜹니다. 이미 열려 있는 상태에서 부른 open()은 경고 로그만 남기고 조용히 무시되며, onClose도 onError도 호출되지 않습니다. 버튼 연타로 화면이 겹쳐 뜨거나 먼저 호출한 쪽의 콜백이 유실되는 것을 막기 위한 동작입니다. 설정을 조회하는 중(아래 참고)도 열려 있는 것으로 칩니다.

open() 호출과 화면 표시 사이에는 지연이 있을 수 있습니다

설정이 아직 없으면 open()이 서버 조회를 기다립니다(최대 timeout, 기본 5초). 그동안 화면은 뜨지 않고 콜백도 오지 않으므로, 호스트에서 로딩 표시를 띄우고 onClose/onError에서 내리는 것을 권장합니다. 무시된 호출은 콜백이 오지 않으니 로딩 표시는 호출 건수로 세지 말고 화면 상태 하나로 관리하세요.

리워드 화면은 화면 하단 끝까지 그립니다

웹뷰가 상단은 SafeArea 를 지키고 하단은 홈 인디케이터/제스처바 영역까지 채웁니다(1.4.9~). 키보드가 올라오면 화면이 밀려 올라가는 동작은 그대로입니다.

Android 는 앱이 edge-to-edge 여야 효과가 납니다. targetSdk 35 이상이면 기본으로 적용되고, 그 아래를 쓰신다면 호스트에서 켜 주세요 — 앱 전역(프로세스 단위) 설정이라 SDK 가 대신 켜지 않습니다.

SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);

하단 여백은 리워드 페이지가 viewport-fit=cover 와 env(safe-area-inset-bottom) 으로 잡습니다. Android 는 WebView 가 이 값을 제스처바에 대해 0 으로 보고하므로 CSS 만으로는 여백이 생기지 않고, 페이지가 따로 대응하지 않으면 하단에 고정된 요소가 제스처바와 겹칠 수 있습니다.

호스트 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

주의

attachWebBridge 는 웹이 브릿지를 호출하기 전에 붙어 있어야 합니다. onWebViewCreated 에서 붙이면 항상 안전합니다.

화면을 떠날 때 detachWebBridge 를 잊지 마세요. 핸들러가 컨트롤러에 남아 있으면 사라진 화면의 BuildContext 로 리워드 웹뷰를 열려고 시도합니다.

최상위 문서에서만 호출할 수 있습니다. iframe 안에서 부른 호출은 응답 없이 무시됩니다 — 허용 도메인 페이지에 삽입된 광고 프레임이 리워드 웹뷰를 열지 못하게 하는 장치입니다(Android · iOS 모두 적용).

웹: 핸들러 호출

window.flutter_inappwebview.callHandler('visualRewardBridge', JSON.stringify({
  userId: 'u-123',            // 필수: 사용자 식별자
  serviceName: 'anyService',  // 필수: 서비스 식별자
  locale: 'ko',               // 선택: 기본 "ko"
  extra: '',                  // 선택: 포스트백 추가 데이터(JSON serialize된 문자열)
}));
파라미터 타입 필수 설명 기본값
userId string ✔ 사용자 식별자
serviceName string ✔ 서비스 식별자. 사용할 값은 별도로 전달드립니다. 키를 생략하거나 문자열이 아니면 INVALID_PAYLOAD입니다.
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 으로 옵니다
SERVICE_NOT_FOUND 전달받지 않은 serviceName을 지정함

브릿지 주입 여부를 확인하고 호출하세요.

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>

<!-- GMA 를 Ad Manager 모드로 돌립니다. 우리 광고는 Ad Manager 지면으로 서빙됩니다 -->
<key>GADIsAdManagerApp</key>
<true/>

<!-- SKAdNetwork (Google 제공 식별자) -->
<key>SKAdNetworkItems</key>
<array>
    <dict>
        <key>SKAdNetworkIdentifier</key>
        <string>cstr6suwn9.skadnetwork</string>
    </dict>
    <!-- 나머지 서드파티 buyer 식별자도 같은 형태로 이어서 넣습니다 -->
</array>

팁

cstr6suwn9.skadnetwork 는 Google 자신의 식별자입니다. 실제로는 여기에 서드파티 buyer 식별자까지 모두 넣어야 설치 기여(attribution)가 잡히고, 목록은 수시로 갱신됩니다. 최신 전체 목록은 AdMob iOS 퀵스타트와 서드파티 SKAdNetwork 목록에서 확인하세요. 식별자는 소문자로 넣습니다.

Apple 은 이 키를 앱 본체의 Info.plist 에서만 읽습니다. 프레임워크나 SDK 의 Info.plist 는 합쳐지지 않으므로, SDK 를 붙였더라도 호스트 앱에 직접 넣어야 합니다.

app-ads.txt

앱 스토어 등록정보에 있는 개발자 웹사이트의 도메인 루트에 app-ads.txt 를 아래 내용 그대로 게시해주세요(https://<개발자-웹사이트>/app-ads.txt).

주의

이미 app-ads.txt 를 운영 중이라면 기존 줄을 지우지 말고 아래 목록을 덧붙이세요. 중복 줄은 제거해도 됩니다. 목록이 갱신되면 다시 전달드립니다.

게시할 전체 목록은 docs.tenqube.com/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 인스턴스

openWindow 로 띄운 새 화면에서도 사용하려면 (eventSink)

핸들러에 eventSink 를 달아 배달을 SDK에 맡기면, SDK가 살아 있는 화면 전부에 이벤트를 뿌리고 각 화면은 페이로드의 placementId 로 자기 것만 처리합니다.

AdPopcorn 문서에서 그대로 복사해 온 핸들러 클래스에서 필드 하나와 _handleNativeEvent 를 아래처럼 고칩니다.

class AdPopcornInAppWebViewBridgeHandler {
  InAppWebViewController? _webViewController;

  /// VisualReward 가 주입하는 배달 싱크. null 이면 종전처럼 단일 컨트롤러로 배달합니다.
  void Function(String js)? eventSink;

  void _handleNativeEvent(String data) {
    final js = publishEvent("NativeEvent", data);
    final sink = eventSink;
    if (sink != null) {
      sink(js);
    } else {
      _webViewController?.evaluateJavascript(source: js);
    }
  }
  // … 나머지는 변경 없습니다.
}

문제 해결

에러 타입

onFailure/onError 콜백에서 is 로 분기합니다.

1.6.0에서 오류 타입이 추가되었습니다. 1.6.0에서 오류 타입 VisualRewardServiceNotFoundError가 추가되었습니다. 오류를 switch로 분기하면서 default가 없다면 이 타입의 분기를 추가해야 빌드됩니다.

타입 설명
VisualRewardNetworkError 네트워크 연결 실패
VisualRewardInitializeTimeoutError open() 이 설정 조회를 timeout 까지 기다렸으나 오지 않음. initialize() 의 실패 콜백으로는 오지 않습니다
VisualRewardInvalidAppKeyError 존재하지 않는 app_key
VisualRewardServerError(statusCode) 설정 서버 오류
VisualRewardParseError 설정 응답 파싱 실패
VisualRewardNotInitializedError initialize() 호출 없이 open() 호출
VisualRewardUserIdNotSetError open()의 userId가 빈 문자열
VisualRewardServiceNotFoundError 전달받지 않은 serviceName을 지정함
VisualRewardOpenFailedError 리워드 화면 진입 실패 (예: 화면 트리에서 사라진 BuildContext)

디폴트 에러 페이지 code

showDefaultErrorUi 가 켜져 있을 때 뜨는 화면의 코드입니다(리워드 페이지 로드 실패는 이 설정과 무관하게 N03 으로 뜹니다). 문구는 사용자가 취할 수 있는 행동 기준으로 3종이고, 화면 하단의 code 는 원인별로 다릅니다. 사용자가 알려준 코드로 원인을 좁힐 수 있습니다.

code 원인 안내 문구
N01 open() 의 userId 가 비어 있음 필수 정보가 존재하지 않습니다.
N02 open JS 인터페이스 요청 페이로드 오류 〃
N03 네트워크 오류(DNS·연결 실패), 잘못된 serviceName 혹은 누락, 페이지 로드 실패 네트워크에 연결할 수 없습니다.
N04 open() 이 설정 조회를 timeout 안에 받지 못함 〃
N05 initialize() 전 open() 호출 일시적인 오류가 발생했습니다. (앱 재시작 안내)
N06 설정 서버 오류 일시적인 오류가 발생했습니다.
N07 설정 응답 파싱 실패 〃
N08 존재하지 않는 앱 키 〃
N09 open() 에 넘긴 BuildContext 가 화면 트리에서 사라짐 〃
N10 전달받지 않은 serviceName을 지정함 필수 정보가 존재하지 않습니다.

증상별 점검

iOS 빌드가 deployment target 오류로 실패해요

Podfile 의 platform :ios, '14.0' 과 Xcode 에서 Runner 타겟의 Minimum Deployments 를 양쪽 다 14.0 으로 올려야 합니다. Flutter 템플릿 기본값이 13.0 이라 Podfile 만 고치면 실패합니다.

AGP 9 · Kotlin 2.1.x 에서 Android 빌드가 실패해요

필수 의존성인 flutter_inappwebview 가 아직 AGP 9 를 지원하지 않고, Kotlin Gradle Plugin 2.1.x 로는 플러그인 의존성 컴파일이 실패합니다. android/settings.gradle.kts 에서 AGP 8.x / KGP 2.2 이상을 확인하세요.

광고가 나오지 않아요

google_mobile_ads 선언 여부와 링크되는 GMA 아티팩트 버전(GMA 버전별 동작), 양 플랫폼의 App ID를 확인하세요. 비활성 사유는 VisualReward 태그 로그(Android)와 콘솔 로그(iOS)에 남습니다.

네이티브 광고만 서빙되지 않아요

호스트가 MobileAds 를 직접 초기화하고 있는지 확인하세요. 광고 엔진 초기화는 SDK 가 담당합니다. 호스트가 먼저 초기화하면 그때의 App ID 가 유지되어 SDK 의 설정이 무시될 수 있습니다.

VisualRewardNotInitializedError 가 발생해요

initialize() 성공 이후에 open()·브릿지를 호출하세요.

VisualRewardInitializeTimeoutError 가 발생해요

네트워크 상태를 확인하고, 필요하면 timeout 을 늘리세요. 타임아웃은 대기를 끊은 것일 뿐 조회 자체는 계속됩니다. 조회가 늦게라도 성공하면 그 설정이 채택되므로 다음 open() 은 정상적으로 열립니다.

VisualRewardOpenFailedError 가 발생해요

화면 트리에 살아 있는 BuildContext 로 호출했는지 확인하세요.

페이지에 브릿지가 없어요

페이지가 브릿지를 호출하는 시점보다 attachWebBridge 가 먼저인지 확인하세요. onWebViewCreated 에서 붙이면 항상 먼저입니다.

브릿지를 호출해도 아무 반응이 없어요

호출한 페이지의 오리진이 allowedDomains에 포함돼 있는지 확인하세요. 이 값은 app_key 발급 시 지정되므로 발급 담당자에게 확인하세요. 검증에 실패하면 콜백 없이 로그만 남습니다.

VisualRewardNetworkError 가 발생해요

설정 조회가 네트워크 단계에서 실패한 경우입니다. VisualReward 로그의 원인을 먼저 확인하세요.

호스트를 찾지 못했다는 오류(Unable to resolve host / cannot find host)면 DNS 조회 실패입니다. 서버 장애가 아니라 기기 쪽 문제이므로 다음을 확인하세요.

  • 기기의 비공개 DNS(Private DNS) 설정
  • 광고 차단 앱 · VPN 사용 여부 (도메인에 reward 가 포함돼 차단 목록에 걸리는 사례가 있습니다)
  • 사내망 · 학교망 프록시 정책
  • 브라우저에서 https://visual.reward.tenqube.com 접속 가능 여부

일시적인 실패라면 다음 open() 이 설정을 자동으로 다시 조회하므로 앱 재시작 없이 회복됩니다.

해결되지 않으면 setDebugEnabled(true) 로 로그를 켠 뒤 VisualReward 로그와 함께 문의해주세요.

Libraries

visual_reward
Tenqube Visual Reward SDK for Flutter.