브라우저가 아닌 모바일 웹뷰로 카드 결제창을 띄울 때 카드사별 결제수단을 인증하려면 외부 앱(3rd-party 앱)을 연동해야 합니다. 연동에 필요한 외부 앱 스킴(App URL Scheme)목록과 추가 로직을 살펴보세요.
웹뷰는 아래와 같은 과정에서 사용됩니다.
- 주문 정보를 작성하고 결제창을 호출합니다.
- 카드사를 선택하고 다음 단계로 이동합니다.
- 선택한 카드사·은행의 결제 앱이 열립니다. 외부 앱이 실행되는 시점입니다.
- 결제 정보를 입력하고 결제를 완료합니다.
- 상점의 결제 페이지로 돌아옵니다.
- 앱 스킴(
appScheme
) 파라미터를 추가하면 별도의 처리 없이도 외부 앱에서 상점 앱으로 돌아올 수 있습니다. 카드 결제 정보 파라미터를 참고하세요.
- 앱 스킴(
이 페이지에서는 3번 단계에서 외부 앱을 여는 방법을 다룹니다. 웹뷰에서 각 은행의 결제 앱을 실행시키면서 앱 간 일어나는 이동(App to App)입니다.
내 상점 앱에서 인증을 위해 이동하게 되는 3rd-party 앱에는 ISP 앱, 카드사별 앱카드 등이 있습니다. 이동할 앱 스킴 리스트를 보고 추가해보세요.
카드사·본인확인기관 | 앱 스킴 |
---|---|
토스페이 | supertoss:// |
국민카드 | kb-acp:// , liivbank:/ , newliiv:// , kbbank:// |
농협카드 | nhappcardansimclick:// , nhallonepayansimclick:// , nonghyupcardansimclick:// |
롯데카드 | lottesmartpay:// , lotteappcard:// |
삼성카드 | mpocket.online.ansimclick:// , ansimclickscard:// , tswansimclick:// , ansimclickipcollect:// , vguardstart:// , samsungpay:// , scardcertiapp:// |
신한카드 | shinhan-sr-ansimclick:// , smshinhanansimclick:// |
우리카드 | com.wooricard.wcard:// , newsmartpib:// |
씨티카드 | citispay:// , citicardappkr:// , citimobileapp:// |
하나카드 | cloudpay:// , hanawalletmembers:// |
현대카드 | hdcardappcardansimclick:// , smhyundaiansimclick:// |
간편결제 | shinsegaeeasypayment:// , payco:// , lpayapp:// |
ISP(BC/국민) | ispmobile:// |
모바일 PASS(본인 인증) | tauthlink:// , ktauthexternalcall:// , upluscorporation:// |
먼저 AndroidManifest.xml
파일에 카드앱·은행앱 패키지를 등록합니다. 패키지를 등록하지 않으면 앱이 설치되어 있어도 스토어로 이동하고, Google 정책을 위반하게 됩니다.
// AndroidManifest.xml
<queries>
<package android:name="com.kakao.talk" /> <!-- 카카오톡 -->
<package android:name="com.shcard.smartpay" /> <!-- 신한페이판 -->
<package android:name="com.shinhancard.smartshinhan" /> <!-- 신한페이판-공동인증서 -->
<package android:name="com.hyundaicard.appcard" /> <!-- 현대카드 -->
<package android:name="com.lumensoft.touchenappfree" /> <!-- 현대카드-공동인증서 -->
<package android:name="kr.co.samsungcard.mpocket" /> <!-- 삼성카드 -->
<package android:name="nh.smart.nhallonepay" /> <!-- 올원페이 -->
<package android:name="com.kbcard.cxh.appcard" /> <!-- KB Pay -->
<package android:name="com.kbstar.liivbank" /> <!-- Liiv(KB국민은행) -->
<package android:name="com.kbstar.reboot" /> <!-- Liiv Reboot(KB국민은행) -->
<package android:name="kvp.jjy.MispAndroid320" /> <!-- ISP/페이북 -->
<package android:name="com.lcacApp" /> <!-- 롯데카드 -->
<package android:name="com.hanaskcard.paycla" /> <!-- 하나카드 -->
<package android:name="kr.co.hanamembers.hmscustomer" /> <!-- 하나멤버스 -->
<package android:name="kr.co.citibank.citimobile" /> <!-- 씨티모바일 -->
<package android:name="com.wooricard.wpay" /> <!-- 우리페이 -->
<package android:name="com.wooricard.smartapp" /> <!-- 우리카드 -->
<package android:name="com.wooribank.smart.npib" /> <!-- 우리WON뱅킹 -->
<package android:name="viva.republica.toss" /> <!-- 토스뱅크 -->
<package android:name="com.nhnent.payapp" /> <!-- PAYCO -->
<package android:name="com.ssg.serviceapp.android.egiftcertificate" /> <!-- SSGPAY -->
<package android:name="com.kakao.talk" /> <!-- 카카오페이 -->
<package android:name="com.nhn.android.search" /> <!-- 네이버페이 -->
<package android:name="com.lottemembers.android" /> <!-- L.POINT -->
<package android:name="com.samsung.android.spay" /> <!-- 삼성페이 -->
<package android:name="com.samsung.android.spaylite" /> <!-- 삼성페이 -->
<package android:name="com.lge.lgpay" /> <!-- 엘지페이 -->
<package android:name="com.lguplus.paynow" /> <!-- 페이나우 -->
<package android:name="com.kftc.bankpay.android" /> <!-- 뱅크페이 -->
<package android:name="com.TouchEn.mVaccine.webs" /> <!-- TouchEn mVaccine (신한) -->
<package android:name="kr.co.shiftworks.vguardweb" /> <!-- V-Guard (삼성) -->
<package android:name="com.ahnlab.v3mobileplus" /> <!-- V3 (NH, 현대) -->
</queries>
AndroidManifest.xml
파일에 필요한 앱 스킴을 추가했다면, 앱 간(App to App) 이동에 필요한 아래 코드를 살펴보세요.
WebViewClient의 shouldOverrideUrlLoading
함수에 오버라이딩 로직을 추가하세요.
추가하지 않으면 웹뷰에서 외부 앱을 호출하거나 마켓(market://
)으로 연결할 때 net::ERR_UNKNOWN_URL_SCHEME
에러가 발생합니다.
override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean
url?.let {
if (!URLUtil.isNetworkUrl(url) && !URLUtil.isJavaScriptUrl(url)) {
val uri = try {
Uri.parse(url)
} catch (e: Exception) {
return false
}
return when (uri.scheme) {
"intent" -> {
startSchemeIntent(it)
}
else -> {
return try {
startActivity(Intent(Intent.ACTION_VIEW, uri))
true
} catch (e: java.lang.Exception) {
false
}
}
}
} else {
return false
}
} ?: return false
private fun startSchemeIntent(url: String): Boolean {
val schemeIntent: Intent = try {
Intent.parseUri(url, Intent.URI_INTENT_SCHEME)
} catch (e: URISyntaxException) {
return false
}
try {
startActivity(schemeIntent)
return true
} catch (e: ActivityNotFoundException) {
val packageName = schemeIntent.getPackage()
if (!packageName.isNullOrBlank()) {
startActivity(
Intent(
Intent.ACTION_VIEW,
Uri.parse("market://details?id=$packageName")
)
)
return true
}
}
return false
}
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (!URLUtil.isNetworkUrl(url) && !URLUtil.isJavaScriptUrl(url)) {
final Uri uri;
try {
uri = Uri.parse(url);
} catch (Exception e) {
return false;
}
if ("intent".equals(uri.getScheme())) {
return startSchemeIntent(url);
} else {
try {
startActivity(new Intent(Intent.ACTION_VIEW, uri));
return true;
} catch (Exception e) {
return false;
}
}
}
return false;
}
private boolean startSchemeIntent(String url) {
final Intent schemeIntent;
try {
schemeIntent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);
} catch (URISyntaxException e) {
return false;
}
try {
startActivity(schemeIntent);
return true;
} catch (ActivityNotFoundException e) {
final String packageName = schemeIntent.getPackage();
if (!TextUtils.isEmpty(packageName)) {
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=" + packageName)));
return true;
}
}
return false;
}
위와 같이 구현하기 어려우면 패키지 공개 상태 관리 대응을 확인해보세요.
먼저 Info.plist
에 LSApplicationQueriesSchemes 를 추가하고 카드사, 은행의 앱 스킴을 배열에 넣어 주세요. 설정하지 않으면, 앱이 열리지 않고 콘솔 쪽에 canOpenURL : failed for URL
에러가 발생합니다.
//Info.plist
<key>LSApplicationQueriesSchemes</key>
<array>
<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>
</array>
XCode에 필요한 앱 스킴을 추가했다면, 앱 간(App to App) 이동에 필요한 아래 코드를 살펴보세요.
웹뷰에서 URL이 변경될 때 페이지 전환 대신 내 상점의 앱 스킴을 실행시키려면 WKNavigationDelegate protocol 중 아래 코드에 해당하는 메서드를 구현해야 합니다.
func webView(
_ webView: WKWebView,
decidePolicyFor navigationAction: WKNavigationAction,
decisionHandler: @escaping (WKNavigationActionPolicy) -> Void
) {
if let url = navigationAction.request.url,
url.scheme != "http" && url.scheme != "https" {
UIApplication.shared.open(url, options: [:], completionHandler:{ (success) in
if !(success){
/*앱이 설치되어 있지 않을 때*/
}
})
decisionHandler(.cancel)
} else {
decisionHandler(.allow)
}
}
- (void)webView:(WKWebView *)webView
decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction
decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
NSURL* url = navigationAction.request.URL;
if (url != nil && ![url.scheme isEqual:@"http"] &&
![url.scheme isEqual:@"https"]) {
[[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil];
decisionHandler(WKNavigationActionPolicyCancel);
return;
}
decisionHandler(WKNavigationActionPolicyAllow);
}
- Flutter 3.0 이상
- Android compileSdkVersion 33 이상
- Android minSdkVersion 21 이상
- iOS 11 이상
pubspec.yaml
파일에 url_launcher
, webview_flutter
플러그인을 추가하세요.
dependencies:
flutter:
sdk: flutter
url_launcher: ^6.1.5
webview_flutter: ^3.0.4
각 OS에 필요한 앱 스킴을 추가했다면, 앱 간(App to App) 이동에 필요한 아래 코드를 살펴보세요.
Android에서 카드사·은행 앱으로 이동할 때 Intent 스킴 URL을 사용합니다. Native에서 Intent 스킴 URL을 처리하세요.
static const channel = const MethodChannel('com.flutter.tosspayments/convertUrl');
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Tosspayments Flutter Sample")),
body: WebView(
initialUrl: "#### 웹뷰 URL을 설정하세요 #####",
onPageStarted: (url) {},
onPageFinished: (url) {},
navigationDelegate: (request) async {
Uri uri = Uri.parse(request.url);
String finalUrl = request.url;
// 웹뷰 브라우저에서 접근 가능한 주소(https 등)라면 URL로 이동
if (uri.scheme == 'http' ||
uri.scheme == 'https' ||
uri.scheme == 'about') {
return NavigationDecision.navigate;
}
// OS별로 구분하여 기타 주소 실행
if (Platform.isAndroid) {
// Android는 Native(Kotlin)로 URL을 전달해 Intent 처리 후 리턴
await _convertIntentToAppUrl(request.url).then((value) async {
finalUrl = value; // 앱이 설치되었을 경우
});
try {
await launchUrlString(finalUrl);
} catch(e){ // URL 실행 불가 시, 앱 미설치로 판단하여 마켓 URL 실행
finalUrl= await _convertIntentToMarketUrl(request.url);
launchUrlString(finalUrl);
}
} else if(Platform.isIOS){
launchUrlString(finalUrl);
}
return NavigationDecision.prevent;
},
javascriptMode: JavascriptMode.unrestricted,
),
);
}
// Intent 스킴 URL을 안드로이드 웹뷰에서 접근 가능하도록 변환
Future<String> _convertIntentToAppUrl(String text) async {
return await channel.invokeMethod('getAppUrl', <String, Object>{'url': text});
}
// Intent 스킴 URL을 Market URL로 변환
Future<String> _convertIntentToMarketUrl(String text) async {
return await channel.invokeMethod('getMarketUrl', <String, Object>{'url': text});
}
MethodChannel
을 사용해서 Intent 스킴 URL을 Flutter에서 Android Native로 보냅니다.- Android의 Intent 라이브러리로 Intent 스킴 URL을 앱 스킴 URL로 변환하고 다시 Flutter로 전송합니다.
private val CHANNEL = "com.flutter.tosspayments/convertUrl"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
GeneratedPluginRegistrant.registerWith(flutterEngine!!)
MethodChannel(flutterEngine!!.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
when {
// Intent 스킴 URL을 안드로이드 웹뷰에서 접근가능하도록 변환
call.method.equals("getAppUrl") -> {
val url: String = call.argument("url")!!
val intent = Intent.parseUri(url, URI_INTENT_SCHEME)
result.success(intent.dataString)
}
// Intent 스킴 URL을 playStore Market URL로 변환
call.method.equals("getMarketUrl") -> {
val url: String = call.argument("url")!!
val packageName = Intent.parseUri(url, URI_INTENT_SCHEME).getPackage()
val marketUrl = Intent(
Intent.ACTION_VIEW,
Uri.parse("market://details?id=$packageName")
)
result.success(marketUrl.dataString)
}
}
}
}