diff --git a/dist/doc/developer-tools/inlinejs/payment-request.js b/dist/doc/developer-tools/inlinejs/payment-request.js index daacdd9..8df59ef 100644 --- a/dist/doc/developer-tools/inlinejs/payment-request.js +++ b/dist/doc/developer-tools/inlinejs/payment-request.js @@ -12,6 +12,7 @@ try { email: 'testuser@paystack.com', amount: 10000, currency: "NGN", + channels: ['card', 'google_pay'], container: 'payment-request-buttons', loadPaystackCheckoutButton: 'pay-button', styles: { diff --git a/src/doc/developer-tools/inlinejs/payment-request/index.js b/src/doc/developer-tools/inlinejs/payment-request/index.js index 04f3efa..04ddeff 100644 --- a/src/doc/developer-tools/inlinejs/payment-request/index.js +++ b/src/doc/developer-tools/inlinejs/payment-request/index.js @@ -12,6 +12,7 @@ try { email: 'testuser@paystack.com', amount: 10000, currency: "NGN", + channels: ['card', 'google_pay'], container: 'payment-request-buttons', loadPaystackCheckoutButton: 'pay-button', styles: { diff --git a/src/doc/guides/checkout-webview/handling-redirect/index.dart b/src/doc/guides/checkout-webview/handling-redirect/index.dart index 55e3cdf..46ff410 100644 --- a/src/doc/guides/checkout-webview/handling-redirect/index.dart +++ b/src/doc/guides/checkout-webview/handling-redirect/index.dart @@ -1,5 +1,8 @@ @override Widget build(BuildContext context) { + // Deep-link/universal-link handoff for partner channels (e.g. Zap) isn't + // documented for Flutter yet. See the React Native, iOS, and Android tabs + // for the pattern this needs. return Scaffold( body: WebView( initialUrl: 'https://checkout.paystack.com/7zu1ot06d0qn9h6', diff --git a/src/doc/guides/checkout-webview/handling-redirect/index.js b/src/doc/guides/checkout-webview/handling-redirect/index.js index 3ac3f2b..116fef3 100644 --- a/src/doc/guides/checkout-webview/handling-redirect/index.js +++ b/src/doc/guides/checkout-webview/handling-redirect/index.js @@ -1,6 +1,29 @@ import React from 'react'; +import { Linking } from 'react-native'; import { WebView } from 'react-native-webview'; +// String matchers match by prefix; RegExp matchers use `.test(url)`. +// Prefix, not `includes` - otherwise `https://evil.com/?u=https://joinzap.com/app/` +// would match. +const DEEP_LINK_HOSTS = [ + 'https://joinzap.com/app/', + // add your own partner hosts here +]; + +const shouldHandleExternally = (url, hosts = DEEP_LINK_HOSTS) => + !!url && + hosts.some((matcher) => + typeof matcher === 'string' ? url.indexOf(matcher) === 0 : matcher.test(url), + ); + +const openExternalUrl = async (url) => { + try { + if (!(await Linking.canOpenURL(url))) return; + await Linking.openURL(url); + } catch { + // Nothing actionable - leaving the WebView is best-effort. + } +}; export default function App() { @@ -8,11 +31,16 @@ export default function App() { const callback_url = 'https://yourcallback.com'; const cancel_url = "https://your-cancel-url.com"; - onNavigationStateChange = state => { - - const { url } = state; + onShouldStartLoadWithRequest = request => { + + const url = request.url; - if (!url) return; + if (!url) return true; + + if (shouldHandleExternally(url)) { + void openExternalUrl(url); + return false; // cancel the in-WebView load + } if (url === callback_url) { // get transaction reference from url and verify transaction, then redirect @@ -25,13 +53,15 @@ export default function App() { // Use a navigator to pop off the view // Run the cancel payment function if you have one } + + return true; }; return ( - ); } \ No newline at end of file diff --git a/src/doc/guides/checkout-webview/handling-redirect/index.kt b/src/doc/guides/checkout-webview/handling-redirect/index.kt index 249ee0f..654a355 100644 --- a/src/doc/guides/checkout-webview/handling-redirect/index.kt +++ b/src/doc/guides/checkout-webview/handling-redirect/index.kt @@ -7,6 +7,10 @@ class MainActivity : AppCompatActivity() { private val cancelUrl: String get() = "https://your-cancel-url.com" + // Prefix, not substring - otherwise a page could smuggle the host into a + // query string and force an external open. + private val deepLinkPrefixes = listOf("https://joinzap.com/app/") + override fun onCreate(savedInstanceState: Bundle?) { // ... } @@ -24,6 +28,23 @@ class MainActivity : AppCompatActivity() { override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean { val url: Uri? = request?.url + if (url != null && deepLinkPrefixes.any { url.toString().startsWith(it) }) { + // Partner-app handoff: resolve to an installed app only; do not open a browser. + val intent = Intent(Intent.ACTION_VIEW, url).apply { + addCategory(Intent.CATEGORY_BROWSABLE) + // Requires API 30+. On older versions, query + // packageManager.queryIntentActivities and only start the intent + // if a non-browser activity handles it. + flags = Intent.FLAG_ACTIVITY_REQUIRE_NON_BROWSER + } + return try { + startActivity(intent) + true // navigation consumed + } catch (e: ActivityNotFoundException) { + false // no app installed - let the WebView load the web page + } + } + if (url?.host == callbackUrl) { return true } @@ -32,7 +53,7 @@ class MainActivity : AppCompatActivity() { // Run the cancel payment function if you have one return true } - + return super.shouldOverrideUrlLoading(view, request) } } diff --git a/src/doc/guides/checkout-webview/handling-redirect/index.swift b/src/doc/guides/checkout-webview/handling-redirect/index.swift index 21416a3..2d91fd1 100644 --- a/src/doc/guides/checkout-webview/handling-redirect/index.swift +++ b/src/doc/guides/checkout-webview/handling-redirect/index.swift @@ -1,26 +1,44 @@ import WebKit -class CheckoutViewController: UIViewController, WKNavigationDelegate { - +class CheckoutViewController: UIViewController, WKNavigationDelegate, WKUIDelegate { + + // Prefix, not substring - otherwise a page could smuggle the host into a + // query string (e.g. https://evil.com/?u=https://joinzap.com/app/) and + // force an external open. + private let deepLinkPrefixes = ["https://joinzap.com/app/"] + // .......... - - //This is helper to get url params + + //This is helper to get url params func getQueryStringParameter(url: String, param: String) -> String? { guard let url = URLComponents(string: url) else { return nil } return url.queryItems?.first(where: { $0.name == param })?.value } - + // This is a WKNavigationDelegate func we can use to handle redirection - func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, - decisionHandler: @escaping ((WKNavigationActionPolicy) -> Void) { + func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, + decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { if let url = navigationAction.request.url { - + + // Partner-app handoff: cancel the load and ask the OS to open it. + // `universalLinksOnly: true` means open the app, or fail - never fall + // back to loading the page in Safari. + if deepLinkPrefixes.contains(where: { url.absoluteString.hasPrefix($0) }) { + UIApplication.shared.open(url, options: [.universalLinksOnly: true]) { opened in + if !opened { + UIApplication.shared.open(url) // app not installed; web fallback + } + } + decisionHandler(.cancel) + return + } + /* We control here when the user wants to cancel a payment. - By default a cancel action redirects to http://cancelurl.com/. - Based on our workflow we can for example remove the webview or push - another view to the user. + By default a cancel action redirects to http://cancelurl.com/. + Based on our workflow we can for example remove the webview or push + another view to the user. */ if url.absoluteString == "http://cancelurl.com/"{ decisionHandler(.cancel) @@ -28,9 +46,9 @@ class CheckoutViewController: UIViewController, WKNavigationDelegate { else{ decisionHandler(.allow) } - - //After a successful transaction we can check if the current url is the callback url - //and do what makes sense for our workflow. We can get the transaction reference for example. + + //After a successful transaction we can check if the current url is the callback url + //and do what makes sense for our workflow. We can get the transaction reference for example. if url.absoluteString.hasPrefix(callbackUrl){ let reference = getQueryStringParameter(url: url.absoluteString, param: "reference") @@ -38,4 +56,15 @@ class CheckoutViewController: UIViewController, WKNavigationDelegate { } } } -} \ No newline at end of file + + // A link with target="_blank" produces no navigation action in the main + // frame, so without this WKUIDelegate method the tap is silently dropped. + // Requires `webView.uiDelegate = self` to be set where the WebView is created. + func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, + for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? { + if navigationAction.targetFrame == nil { + webView.load(navigationAction.request) + } + return nil + } +} diff --git a/src/doc/guides/checkout-webview/initialize-webview/index.swift b/src/doc/guides/checkout-webview/initialize-webview/index.swift index 8f80b7a..706fd27 100644 --- a/src/doc/guides/checkout-webview/initialize-webview/index.swift +++ b/src/doc/guides/checkout-webview/initialize-webview/index.swift @@ -1,18 +1,19 @@ import WebKit -class CheckoutViewController: UIViewController, WKNavigationDelegate { - - let callbackUrl = "CALLBACK_URL_GOES_HERE" - let pstkUrl = "AUTHORIZATION_URL_GOES_HERE" +class CheckoutViewController: UIViewController, WKNavigationDelegate, WKUIDelegate { + + let callbackUrl = "CALLBACK_URL_GOES_HERE" + let pstkUrl = "AUTHORIZATION_URL_GOES_HERE" let 3dsUrl = "https://standard.paystack.co/close" @IBOutlet weak var webView: WKWebView! - + override func viewDidLoad() { super.viewDidLoad() - + let urlRequest = URLRequest.init(url: URL.init(string: pstkUrl)!) webView.load(urlRequest) self.webView.navigationDelegate = self - } + self.webView.uiDelegate = self + } } \ No newline at end of file