Skip to content

Commit 14062ae

Browse files
Merge pull request #134 from PaystackOSS/feat/google-pay
feat: add card channel to Google Pay payment request
2 parents a6bdd4b + 4bd6ad9 commit 14062ae

7 files changed

Lines changed: 115 additions & 29 deletions

File tree

‎dist/doc/developer-tools/inlinejs/payment-request.js‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ try {
1212
email: 'testuser@paystack.com',
1313
amount: 10000,
1414
currency: "NGN",
15+
channels: ['card', 'google_pay'],
1516
container: 'payment-request-buttons',
1617
loadPaystackCheckoutButton: 'pay-button',
1718
styles: {

‎src/doc/developer-tools/inlinejs/payment-request/index.js‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ try {
1212
email: 'testuser@paystack.com',
1313
amount: 10000,
1414
currency: "NGN",
15+
channels: ['card', 'google_pay'],
1516
container: 'payment-request-buttons',
1617
loadPaystackCheckoutButton: 'pay-button',
1718
styles: {

‎src/doc/guides/checkout-webview/handling-redirect/index.dart‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
@override
22
Widget build(BuildContext context) {
3+
// Deep-link/universal-link handoff for partner channels (e.g. Zap) isn't
4+
// documented for Flutter yet. See the React Native, iOS, and Android tabs
5+
// for the pattern this needs.
36
return Scaffold(
47
body: WebView(
58
initialUrl: 'https://checkout.paystack.com/7zu1ot06d0qn9h6',
Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,46 @@
11
import React from 'react';
2+
import { Linking } from 'react-native';
23
import { WebView } from 'react-native-webview';
34

5+
// String matchers match by prefix; RegExp matchers use `.test(url)`.
6+
// Prefix, not `includes` - otherwise `https://evil.com/?u=https://joinzap.com/app/`
7+
// would match.
8+
const DEEP_LINK_HOSTS = [
9+
'https://joinzap.com/app/',
10+
// add your own partner hosts here
11+
];
12+
13+
const shouldHandleExternally = (url, hosts = DEEP_LINK_HOSTS) =>
14+
!!url &&
15+
hosts.some((matcher) =>
16+
typeof matcher === 'string' ? url.indexOf(matcher) === 0 : matcher.test(url),
17+
);
18+
19+
const openExternalUrl = async (url) => {
20+
try {
21+
if (!(await Linking.canOpenURL(url))) return;
22+
await Linking.openURL(url);
23+
} catch {
24+
// Nothing actionable - leaving the WebView is best-effort.
25+
}
26+
};
427

528
export default function App() {
629

730
const authorization_url = 'https://checkout.paystack.com/luKuasMan';
831
const callback_url = 'https://yourcallback.com';
932
const cancel_url = "https://your-cancel-url.com";
1033

11-
onNavigationStateChange = state => {
12-
13-
const { url } = state;
34+
onShouldStartLoadWithRequest = request => {
35+
36+
const url = request.url;
1437

15-
if (!url) return;
38+
if (!url) return true;
39+
40+
if (shouldHandleExternally(url)) {
41+
void openExternalUrl(url);
42+
return false; // cancel the in-WebView load
43+
}
1644

1745
if (url === callback_url) {
1846
// get transaction reference from url and verify transaction, then redirect
@@ -25,13 +53,15 @@ export default function App() {
2553
// Use a navigator to pop off the view
2654
// Run the cancel payment function if you have one
2755
}
56+
57+
return true;
2858
};
2959

3060
return (
31-
<WebView
61+
<WebView
3262
source={{ uri: authorization_url }}
3363
style={{ marginTop: 40 }}
34-
onNavigationStateChange={ this.onNavigationStateChange }
64+
onShouldStartLoadWithRequest={ this.onShouldStartLoadWithRequest }
3565
/>
3666
);
3767
}

‎src/doc/guides/checkout-webview/handling-redirect/index.kt‎

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ class MainActivity : AppCompatActivity() {
77
private val cancelUrl: String
88
get() = "https://your-cancel-url.com"
99

10+
// Prefix, not substring - otherwise a page could smuggle the host into a
11+
// query string and force an external open.
12+
private val deepLinkPrefixes = listOf("https://joinzap.com/app/")
13+
1014
override fun onCreate(savedInstanceState: Bundle?) {
1115
// ...
1216
}
@@ -24,6 +28,23 @@ class MainActivity : AppCompatActivity() {
2428
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
2529
val url: Uri? = request?.url
2630

31+
if (url != null && deepLinkPrefixes.any { url.toString().startsWith(it) }) {
32+
// Partner-app handoff: resolve to an installed app only; do not open a browser.
33+
val intent = Intent(Intent.ACTION_VIEW, url).apply {
34+
addCategory(Intent.CATEGORY_BROWSABLE)
35+
// Requires API 30+. On older versions, query
36+
// packageManager.queryIntentActivities and only start the intent
37+
// if a non-browser activity handles it.
38+
flags = Intent.FLAG_ACTIVITY_REQUIRE_NON_BROWSER
39+
}
40+
return try {
41+
startActivity(intent)
42+
true // navigation consumed
43+
} catch (e: ActivityNotFoundException) {
44+
false // no app installed - let the WebView load the web page
45+
}
46+
}
47+
2748
if (url?.host == callbackUrl) {
2849
return true
2950
}
@@ -32,7 +53,7 @@ class MainActivity : AppCompatActivity() {
3253
// Run the cancel payment function if you have one
3354
return true
3455
}
35-
56+
3657
return super.shouldOverrideUrlLoading(view, request)
3758
}
3859
}
Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,70 @@
11
import WebKit
22

3-
class CheckoutViewController: UIViewController, WKNavigationDelegate {
4-
3+
class CheckoutViewController: UIViewController, WKNavigationDelegate, WKUIDelegate {
4+
5+
// Prefix, not substring - otherwise a page could smuggle the host into a
6+
// query string (e.g. https://evil.com/?u=https://joinzap.com/app/) and
7+
// force an external open.
8+
private let deepLinkPrefixes = ["https://joinzap.com/app/"]
9+
510
// ..........
6-
7-
//This is helper to get url params
11+
12+
//This is helper to get url params
813
func getQueryStringParameter(url: String, param: String) -> String? {
914
guard let url = URLComponents(string: url) else { return nil }
1015
return url.queryItems?.first(where: { $0.name == param })?.value
1116
}
12-
17+
1318
// This is a WKNavigationDelegate func we can use to handle redirection
14-
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction,
15-
decisionHandler: @escaping ((WKNavigationActionPolicy) -> Void) {
19+
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction,
20+
decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
1621

1722
if let url = navigationAction.request.url {
18-
23+
24+
// Partner-app handoff: cancel the load and ask the OS to open it.
25+
// `universalLinksOnly: true` means open the app, or fail - never fall
26+
// back to loading the page in Safari.
27+
if deepLinkPrefixes.contains(where: { url.absoluteString.hasPrefix($0) }) {
28+
UIApplication.shared.open(url, options: [.universalLinksOnly: true]) { opened in
29+
if !opened {
30+
UIApplication.shared.open(url) // app not installed; web fallback
31+
}
32+
}
33+
decisionHandler(.cancel)
34+
return
35+
}
36+
1937
/*
2038
We control here when the user wants to cancel a payment.
21-
By default a cancel action redirects to http://cancelurl.com/.
22-
Based on our workflow we can for example remove the webview or push
23-
another view to the user.
39+
By default a cancel action redirects to http://cancelurl.com/.
40+
Based on our workflow we can for example remove the webview or push
41+
another view to the user.
2442
*/
2543
if url.absoluteString == "http://cancelurl.com/"{
2644
decisionHandler(.cancel)
2745
}
2846
else{
2947
decisionHandler(.allow)
3048
}
31-
32-
//After a successful transaction we can check if the current url is the callback url
33-
//and do what makes sense for our workflow. We can get the transaction reference for example.
49+
50+
//After a successful transaction we can check if the current url is the callback url
51+
//and do what makes sense for our workflow. We can get the transaction reference for example.
3452

3553
if url.absoluteString.hasPrefix(callbackUrl){
3654
let reference = getQueryStringParameter(url: url.absoluteString, param: "reference")
3755
print("reference \(reference)")
3856
}
3957
}
4058
}
41-
}
59+
60+
// A link with target="_blank" produces no navigation action in the main
61+
// frame, so without this WKUIDelegate method the tap is silently dropped.
62+
// Requires `webView.uiDelegate = self` to be set where the WebView is created.
63+
func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration,
64+
for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
65+
if navigationAction.targetFrame == nil {
66+
webView.load(navigationAction.request)
67+
}
68+
return nil
69+
}
70+
}
Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
import WebKit
22

3-
class CheckoutViewController: UIViewController, WKNavigationDelegate {
4-
5-
let callbackUrl = "CALLBACK_URL_GOES_HERE"
6-
let pstkUrl = "AUTHORIZATION_URL_GOES_HERE"
3+
class CheckoutViewController: UIViewController, WKNavigationDelegate, WKUIDelegate {
4+
5+
let callbackUrl = "CALLBACK_URL_GOES_HERE"
6+
let pstkUrl = "AUTHORIZATION_URL_GOES_HERE"
77
let 3dsUrl = "https://standard.paystack.co/close"
88

99
@IBOutlet weak var webView: WKWebView!
10-
10+
1111
override func viewDidLoad() {
1212
super.viewDidLoad()
13-
13+
1414
let urlRequest = URLRequest.init(url: URL.init(string: pstkUrl)!)
1515
webView.load(urlRequest)
1616
self.webView.navigationDelegate = self
17-
}
17+
self.webView.uiDelegate = self
18+
}
1819
}

0 commit comments

Comments
 (0)