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
질문 남기면, 답변해드립니다!
728x90
'flutter' 카테고리의 다른 글
infinity_scroll_shell 오픈소스 배포 과정 기록 (6) | 2024.09.18 |
---|---|
flutter Understanding constraints (0) | 2024.05.08 |
구글플레이, App Store 배포 심사 탈락 기록 (1) | 2023.12.07 |
Flutter Android 12이상에서의 defalut 스플래시 화면 처리 솔루션 (2) | 2023.12.05 |
Flutter Autocomplete 커스텀 (0) | 2023.11.21 |
댓글