개발/flutter

Flutter Webview Intent 처리 ERR_UNKNOWN_URL_SCHEME

덤벨로퍼 2020. 8. 13. 13:34

Modern Collection View 와 MVVM 패턴 가이드

 

[iOS] Modern Collection View & MVVM 패턴 가이드 - 인프런 | 강의

MVVM 패턴과 Modern Collection View를 사용해 네트워킹을 구현하고, 다양하고 동적인 Collection View를 자유자재로 다룰 수 있게 됩니다., - 강의 소개 | 인프런

www.inflearn.com

 

Webview로 결제를 구현해야 하는 순간이 찾아왔다.

 url 만 띄우면 될 줄 알았지만 세상은 그렇게 단순하지 않았다.

 

웹뷰로 url을 호출하면 

클라이언트는 결제를 어떤 방식으로 할 건지 선택하면 새로운 url 이 호출되고

만약 은행사의 앱 결제를 클릭할 시 앱을 켜줘야 하고 

그 앱이 깔려있지 않을 시 마켓에서 앱을 다운로드할 수 있도록  플레이스토어/앱스토어로 보내줘야 한다.

 

문제는 앱 결제였다. 

카카 오페이던 신한 앱카드 페이건 카카오 앱, 신한 앱을 켜줘야 하고 그것은 intent:// 스키마로 호출한다.

하지만 안드로이드의 웹뷰의 경우 https://가아닌 intent:// 스키마에 대한 처리가 되지 않았다.

이경우 안드로이드 네이티브 코드를 작성해 intent url에서는 특정 작업을 따로 실행해줘야 한다.

 

기본 웹뷰 구현은 이렇다. (flutter webview plugin 사용)

WebviewScaffold(
url: widget.url,
ignoreSSLErrors: true,
invalidUrlRegex: Platform.isAndroid
  ? '^(?!https://|http://|about:blank|data:).+'
	: null,
);

Web view plugin 에서는 두 가지 작업을 실행할 수 있다.

State 변경 시 (onStateChanged)

Url 변경시 (onUrlChanged)

 

State는 페이지 요청 시 시작/끝 이런 상태가 있다. 

여기서 해야 할 것은 만약 https 나아닌 다른 스키마를 url로 받아올 때

다른 작업을 실행시켜줘야 하는 것이다.

 

 

_onStateChanged = webView.onStateChanged.listen((state)  {
      final type = state.type;
      final url = state.url;
      print(url);
      if (mounted) {
        print('url $url $type');

        if (isAppLink(url)) {
          handleAppLink(url);
        }
      }
    });

 

 

webview가 호출되면서 위 함수가 호출되고 

첫 번째로 appLink 인지 확인하고  , 두 번째로 app Link 라면 이를 처리해주는 로직을 실행하면 된다.

App link 인지 아닌지는 단순히 scheme 만 본다.

bool isAppLink(String url) {
    final appScheme = Uri.parse(url).scheme;

    return appScheme != 'http' &&
        appScheme != 'https' &&
        appScheme != 'about:blank' &&
        appScheme != 'data';
  }

http/https 아니면 앱링크로 판단했다.

 

 

그럼 이제 해당 intent url을 처리하는 로직을 실행한다.

여기서 이제 네이티브 코드가 필요한 상황이고 그 네이티브 코드를 실행시키려면 Method channel을 사용하면 된다.

  static const platform = MethodChannel('이름아무거나');

Future<String> getAppUrl(String url) async {
    if (Platform.isAndroid) {
      return await platform
          .invokeMethod('getAppUrl', <String, Object>{'url': url});
    } else {
      return url;
    }
  }

'getAppUrl' 은 이후 channel의 메서드명이고 파라미터로 url 이 보내졌다.

이러면 네이티브 코드에서 channel '이름 아무거나'가 호출돼 코드를 실행한다.

 

  private static final String CHANNEL = "이름아무거나";

 

코드를 짜기 위해 해당 플러그인, 패키지들을 임포트 해야 한다.

import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.util.Log;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugin.common.MethodChannel.Result;
import io.flutter.plugin.common.PluginRegistry.Registrar;
new MethodChannel(getFlutterView(), CHANNEL).setMethodCallHandler(
        new MethodChannel.MethodCallHandler() {
          @Override
          public void onMethodCall(MethodCall call, MethodChannel.Result result) {
          
          
          }
        }
 });

자바 코드는 잘 모르므로 설명은 패스하고 저 안에 로직이 들어갈 것이다.

 

 switch (call.method) {
           
	case "getAppUrl":
    try {
        String url = call.argument("url");
        Log.i("url", url);
        Intent intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);
   
    	result.success(intent.getDataString());
    } catch (URISyntaxException e) {
   	 	result.notImplemented();
    } catch (ActivityNotFoundException e) {
    	result.notImplemented();
    }
break;

 

호출된 url 은 

intent://kakaopay/pg? url=https://pg-reseller-web.kakao.com/v1/어쩌고 저쩌고

였고 이를 intent parsing을 통해 얻은 결괏값은

kakaotalk://kakaopay/pg?url=https://pg-reseller-web.kakao.com/v1/어쩌고저쩌고

이다 이 url을 통해 카카오톡 앱을 실행시킬 것이다.

result.success를 통해 이 url을 리턴하면 이제 카톡 앱을 실행시키는 로직에서 처리한다,

 

카톡이든 뭐든 앱을 키는 패키지로 url_launcher를 사용했다.

https://pub.dev/packages/url_launcher

 

getAppUrl(url).then((value) async {
      if (await canLaunch(value)) {
        await launch(value);
      } else {
        final marketUrl = await getMarketUrl(url);
        await launch(marketUrl);
      }
    });

getAppUrl로 리턴 받은 url을 canLaunch()로 실행 가능 여부를 리턴 받는다

해당 앱이 있으면 true 가 리턴되고 앱을 그 url 로실행한다

만약 이 앱이 없다면 market url로 유도해야 한다.

 

이역시  메서드 채널을 이용해 플러터에서 부른 뒤 자바에서 처리한다.

await platform
        .invokeMethod('getMarketUrl', <String, Object>{'url': url});
case "getMarketUrl": {
    try {
      String url = call.argument("url");
      Log.i("url", url);
      Intent intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);
      String scheme = intent.getScheme();

      String packageName = intent.getPackage();
      if (packageName != null) {
      result.success("market://details?id=" + packageName);
    }

    	result.notImplemented();
    } catch (URISyntaxException e) {
    	result.notImplemented();
    } catch (ActivityNotFoundException e) {
    	result.notImplemented();
    }
   	 break;
    }

자바 코드에서 이 메서드 채널을 통해 플레이스토어 url을 리턴할 수 있도록 한다.

하나카드 앱 intent 의경우 인텐트가 이렇게 나온다

Intent { act=android.intent.action.VIEW dat=cloudpay://? tid=2437248682026208cloudpay://?tid=2437248682026208 pkg=com.hanaskcard.paycla }

Scheme 은 cloudpay

Package name 은  com.hanaskcard.paycla이다

이 패키지 네임을 가지고 ‘market://details? id='를 앞에 붙여 market url을 만들어주어 리턴하여

url_launcher로 플레이스토어를 실행시키면 된다.

 

iOS는 scheme 에따라 분리해 해당 앱 마켓 url을 직접 하드코딩으로 넣어줬다.

 

 

개발자 이직 비법 보러가기