본문 바로가기
flutter

Flutter - toss payment결제 페이지 webView 연동

by Rogan_Kim 2024. 5. 9.
728x90

 

flutter에서 WebView로 tossPayment 결제피이지를 WebView로 연동하면서 겪었던 시행착오를 다시 겪지 않도록,

핵심만 기록해 보았습니다. (작성 완료까지 15분 걸림.)

 

환경 

Flutter version 3.19.5

dart version 3.3.3

 

pubspec.yml (사용한 패키지)

webview_flutter: 4.7.0
url_launcher: 6.1.14
tosspayments_widget_sdk_flutter: 2.0.2

 

 

flow

1. ios, android 각각 scheme추가해주기

2. 안드로이드의 경우 MethodChanner을 활용하여 앱 열기 혹은 playStore로 이동

3. WebView를 실행해서 scheme이 "intent://"일경우 웹뷰로 접속하지 말고, "setNavigationDelegte" 세팅으로 launchUrl로 실행하기

 

 

flow별  핵심 코드

"androidManifest.xml"에 scheme 추가 (아래 참고링크의 공식문서에 나와있습니다)

<intent-filter>
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />
                    <data android:scheme="supertoss" />
                    <data android:scheme="kb-acp" />
                    <data android:scheme="liivbank" />
                    <data android:scheme="newliiv" />
                    <data android:scheme="kbbank" />
                    <data android:scheme="nhappcardansimclick" />
                    <data android:scheme="nhallonepayansimclick" />
                    <data android:scheme="nonghyupcardansimclick" />
                    <data android:scheme="lottesmartpay" />
                    <data android:scheme="lotteappcard" />
                    <data android:scheme="mpocket.online.ansimclick" />
                    <data android:scheme="vguardstart" />
                    <data android:scheme="samsungpay" />
                    <data android:scheme="monimopay" />
                    <data android:scheme="monimopayauth" />
                    <data android:scheme="shinhan-sr-ansimclick" />
                    <data android:scheme="smshinhanansimclick" />
                    <data android:scheme="com.wooricard.wcard" />
                    <data android:scheme="newsmartpib" />
                    <data android:scheme="citispay" />
                    <data android:scheme="citicardappkr" />
                    <data android:scheme="citicardappkr" />
                    <data android:scheme="citimobileapp" />
                    <data android:scheme="cloudpay" />
                    <data android:scheme="hanawalletmembers" />
                    <data android:scheme="hdcardappcardansimclick" />
                    <data android:scheme="smhyundaiansimclick" />
                    <data android:scheme="shinsegaeeasypayment" />
                    <data android:scheme="payco" />
                    <data android:scheme="lpayapp" />
                    <data android:scheme="ispmobile" />
                    <data android:scheme="tauthlink" />
                    <data android:scheme="ktauthexternalcall" />
                    <data android:scheme="upluscorporation" />
 </intent-filter>

 

 

 

ios "info.list"의 LSApplicationQueriesSchemes에 추가

 

<key>LSApplicationQueriesSchemes</key>
<array>
		<string>naversearchapp</string>
		<string>naversearchthirdlogin</string>
		<string>supertoss</string>
		<string>kb-acp</string>
		<string>liivbank</string>
		<string>newliiv</string>
		<string>kbbank</string>
		<string>nhappcardansimclick</string>
		<string>nhallonepayansimclick</string>
		<string>nonghyupcardansimclick</string>
		<string>lottesmartpay</string>
		<string>lotteappcard</string>
		<string>mpocket.online.ansimclick</string>
		<string>ansimclickscard</string>
		<string>tswansimclick</string>
		<string>ansimclickipcollect</string>
		<string>vguardstart</string>
		<string>samsungpay</string>
		<string>scardcertiapp</string>
		<string>shinhan-sr-ansimclick</string>
		<string>smshinhanansimclick</string>
		<string>com.wooricard.wcard</string>
		<string>newsmartpib</string>
		<string>citispay</string>
		<string>citicardappkr</string>
		<string>citimobileapp</string>
		<string>cloudpay</string>
		<string>hanawalletmembers</string>
		<string>hdcardappcardansimclick</string>
		<string>smhyundaiansimclick</string>
		<string>shinsegaeeasypayment</string>
		<string>payco</string>
		<string>lpayapp</string>
		<string>ispmobile</string>
		<string>tauthlink</string>
		<string>ktauthexternalcall</string>
		<string>upluscorporation</string>
		<string>kftc-bankpay</string>
		<string>kakaotalk</string>
		<string>wooripay</string>
		<string>lmslpay</string>
		<string>naversearchthirdlogin</string>
		<string>hanaskcardmobileportal</string>
		<string>kb-bankpay</string>
</array>

 

MainActivity.kt의 MainActivity class에서 MethodChannel 코드 추가

        MethodChannel(flutterEngine!!.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { 
            call, result ->
            when {
                call.method.equals("getAppUrl") -> {
                    try {
                        val url: String = call.argument("url")!!
                        val intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME)
                        result.success(intent.dataString)
                    } catch (e: URISyntaxException) {
                        result.notImplemented()
                    } catch (e: ActivityNotFoundException) {
                        result.notImplemented()
                    }
                }
                call.method.equals("getMarketUrl") -> {
                    try {
                        val url: String = call.argument("url")!!
                        val packageName = Intent.parseUri(url, Intent.URI_INTENT_SCHEME).getPackage()
                        val marketUrl = Intent(
                            Intent.ACTION_VIEW,
                            Uri.parse("market://details?id=$packageName")
                        )
                        result.success(marketUrl.dataString)
                    } catch (e: URISyntaxException) {
                        result.notImplemented()
                    } catch (e: ActivityNotFoundException) {
                        result.notImplemented()
                    }
                }
            }
        }
    }

 

 

 

Webview 구현

(import및 도메인 전부 제거 후 입니다)

class FishingPaymentStep2Screen extends StatefulWidget {
  const FishingPaymentStep2Screen({
    super.key,
  });

  @override
  State<FishingPaymentStep2Screen> createState() =>
      _FishingPaymentStep2ScreenState();
}

class _FishingPaymentStep2ScreenState extends State<FishingPaymentStep2Screen> {
  final _getToken = sl<HiveBoxUtil>().getData(HiveBoxKeys.token);
  static const channel = MethodChannel("com.flutter.tosspayment");
  late final WebViewController _webViewController = WebViewController()
    ..setJavaScriptMode(JavaScriptMode.unrestricted)
    ..setNavigationDelegate(
      NavigationDelegate(
        onProgress: (int progress) {},
        onPageStarted: (String url) {},
        onPageFinished: (String url) {},
        onWebResourceError: (WebResourceError error) {},
        onNavigationRequest: (NavigationRequest request) async {
          Uri uri = Uri.parse(request.url);
          String finalUrl = request.url;
          if (uri.scheme == "http" || uri.scheme == 'https') {
            return NavigationDecision.navigate;
          }

          // Intent URL일 경우, OS별로 구분하여 실행
          if (Platform.isAndroid) {
            // [NOTE] Android의 경우, Native(kotlin)으로 url을 전달해 INTENT처리 후 리턴받는다.
            final value = await _convertIntentToAppUrl(request.url);

            if (value != null) {
              final appScheme = ConvertUrl(
                finalUrl,
              ); // Intent URL을 앱 스킴 URL로 변환
              if (appScheme.isAppLink()) {
                // 앱 스킴 URL인지 확인
                appScheme.launchApp(
                  mode: LaunchMode.externalApplication,
                ); // 앱 설치 상태에 따라 앱 실행 또는 마켓으로 이동
              }
            }
            try {
              /// dart uri.parse가 대문자 > 소문자로 변환시켜, launchUrl 대신 launchUrlString 사용
              await launchUrlString(finalUrl);
            } catch (e) {
              // 앱이 설치되어 있지 않는 경우, playStore로 이동
              final value = await _convertIntentToMarketURl(request.url);
              if (value != null) {
                finalUrl = value;
              }
            }
          } else if (Platform.isIOS) {
            launchUrlString(finalUrl);
          }
          return NavigationDecision.prevent;
        },
      ),
    )
    ..loadRequest(
      _generatedUri(),
      headers: {
        'authorization': _getToken['accessToken'], // 'Bearer'는 필요에 따라 변경
      },
    )
    ..addJavaScriptChannel(
      "hitupPayment",
      onMessageReceived: (p0) {
        final responseJson = jsonDecode(p0.message);
        final response = JavaScriptChannelDTO.fromJson(responseJson);
        context.pop<JavaScriptChannelDTO>(response);
      },
    );

  Uri _generatedUri() {
    String queryParameter = "{쿼리 만들기}";

    final uri = Uri.parse(
        "https://{{도메인}}/payment/request?$queryParameter");
    return uri;
  }

  Future<String?> _convertIntentToAppUrl(String text) async {
    final message = await channel.invokeMethod<String>(
      "getAppUrl",
      {'url': text},
    );
    return message;
  }

  Future<String?> _convertIntentToMarketURl(String text) async {
    final message =
        await channel.invokeMethod<String>("getMarketUrl", {'url': text});
    return message as String;
  }

  @override
  void initState() {
    super.initState();
  }

  @override
  void dispose() {
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: WebViewWidget(
          controller: _webViewController,
        ),
      ),
    );
  }
}

 

 

참고자료

- https://docs.tosspayments.com/guides/webview

- http://chrome-extension://mhnlakgilnojmhinhkckjpncpbhabphi/pages/pdf/web/viewer.html?file=https%3A%2F%2Fanswer-overflow-discord-attachments.s3.amazonaws.com%2F1072747046492184636%2FFlutter_-_Tosspayments___.pdf

 

 

질문 남기면, 답변해드립니다!

728x90

댓글