Skip to content

Commit 6b48a6f

Browse files
merging main changes
2 parents 688c90b + 00256b4 commit 6b48a6f

11 files changed

Lines changed: 253 additions & 25 deletions

.github/PULL_REQUEST_TEMPLATE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,9 @@
2929
Internal to PayPal contributors should fill out this section. All others can delete.
3030

3131
PR should follow these steps before codeowners review will begin:
32-
1. PR should be opened in a draft state with the `tech lead review required`, and `inner source` label
32+
1. Comment `/inner source` on this PR — this will automatically add the `inner source` and `tech lead review required` labels. Open the PR in a draft state.
3333
2. PR should be reviewed by and approved by your team's technical lead, we do not allow LGTM reviews, there should be comments and feedback provided on all PR reviews
34-
3. Once the above steps are completed, the PR can be moved to ready to review with the `tech lead review required` label removed
34+
3. Once the above steps are completed, comment `/ready` on this PR — this will automatically remove the `tech lead review required` label. Move the PR to ready to review.
3535
4. PR comments must be addressed within 24 hours, if you are unable to address within this timeframe, move the PR back to a draft state so our team knows not to review
3636

3737
### Inner Source Checklist
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
name: Inner Source Labels
2+
3+
on:
4+
issue_comment:
5+
types: [created]
6+
7+
permissions:
8+
issues: write
9+
pull-requests: write
10+
11+
jobs:
12+
add-inner-source-labels:
13+
# Only fires on PR comments (not plain issues) containing the slash command
14+
if: >
15+
github.event.issue.pull_request != null &&
16+
contains(github.event.comment.body, '/inner source')
17+
runs-on: ubuntu-latest
18+
steps:
19+
- name: Acknowledge command
20+
continue-on-error: true
21+
uses: actions/github-script@v7
22+
with:
23+
script: |
24+
await github.rest.reactions.createForIssueComment({
25+
owner: context.repo.owner,
26+
repo: context.repo.repo,
27+
comment_id: context.payload.comment.id,
28+
content: 'eyes'
29+
});
30+
31+
- name: Add inner source labels
32+
uses: actions/github-script@v7
33+
with:
34+
script: |
35+
await github.rest.issues.addLabels({
36+
owner: context.repo.owner,
37+
repo: context.repo.repo,
38+
issue_number: context.issue.number,
39+
labels: ['inner source', 'tech lead review required']
40+
});
41+
42+
remove-tech-lead-label:
43+
# Only fires on PR comments containing the slash command
44+
if: >
45+
github.event.issue.pull_request != null &&
46+
contains(github.event.comment.body, '/ready')
47+
runs-on: ubuntu-latest
48+
steps:
49+
- name: Acknowledge command
50+
continue-on-error: true
51+
uses: actions/github-script@v7
52+
with:
53+
script: |
54+
await github.rest.reactions.createForIssueComment({
55+
owner: context.repo.owner,
56+
repo: context.repo.repo,
57+
comment_id: context.payload.comment.id,
58+
content: 'eyes'
59+
});
60+
61+
- name: Remove tech lead review required label
62+
uses: actions/github-script@v7
63+
with:
64+
script: |
65+
try {
66+
await github.rest.issues.removeLabel({
67+
owner: context.repo.owner,
68+
repo: context.repo.repo,
69+
issue_number: context.issue.number,
70+
name: 'tech lead review required'
71+
});
72+
} catch (e) {
73+
// 404 means the label wasn't on the PR — that's fine
74+
if (e.status !== 404) throw e;
75+
}

.github/workflows/release.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ jobs:
1414
uses: actions/checkout@v2
1515
with:
1616
fetch-depth: 0
17+
token: ${{ secrets.RELEASE_PAT }}
1718

1819
- name: Use Xcode 16.4.0
1920
run: sudo xcode-select -switch /Applications/Xcode_16.4.0.app

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
# PopupBridge iOS Release Notes
22

33
## unreleased
4-
54
* Add support for the PayPal app switch flow. When enabled, checkout launches the native PayPal app and falls back to `ASWebAuthenticationSession` if the launch fails. Requires additional `SceneDelegate` integration — see the README.
65
* Add a `POPPopupBridge` initializer that takes a `returnURLScheme` to enable the PayPal app switch flow.
76

7+
## 3.1.0 (2026-06-24)
8+
* Add `init(webView:returnURLScheme:prefersEphemeralWebBrowserSession:)` for the Venmo app switch flow. When a `returnURLScheme` is provided and the Venmo app is installed, PopupBridge sends that scheme to the web SDK as the return URL prefix (`window.popupBridge.getReturnUrlPrefix()`) so the Venmo app can deep-link back into your app.
9+
810
## 3.0.0 (2025-04-01)
911
* Breaking Changes
1012
* Bump minimum supported deployment target to iOS 16+

Demo/Demo/Info.plist

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,22 @@
1515
<key>CFBundlePackageType</key>
1616
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
1717
<key>CFBundleShortVersionString</key>
18-
<string>3.0.0</string>
18+
<string>3.1.0</string>
19+
<key>CFBundleURLTypes</key>
20+
<array>
21+
<dict>
22+
<key>CFBundleTypeRole</key>
23+
<string>Editor</string>
24+
<key>CFBundleURLName</key>
25+
<string>com.braintreepayments.Demo</string>
26+
<key>CFBundleURLSchemes</key>
27+
<array>
28+
<string>com.braintreepayments.Demo</string>
29+
</array>
30+
</dict>
31+
</array>
1932
<key>CFBundleVersion</key>
20-
<string>3.0.0</string>
33+
<string>3.1.0</string>
2134
<key>CFBundleURLTypes</key>
2235
<array>
2336
<dict>

PopupBridge.podspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Pod::Spec.new do |s|
22
s.name = 'PopupBridge'
3-
s.version = "3.0.0"
3+
s.version = "3.1.0"
44
s.summary = 'Use PopupBridge to enable your web view to open pages in a Safari View Controller'
55
s.description = <<-DESC
66
PopupBridge is an iOS library that allows WKWebViews to open popup windows in an SFSafariViewController

README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,30 @@ To enable PayPal installation detection (`window.popupBridge.isPayPalInstalled`)
5757
</array>
5858
```
5959

60+
### Register your return URL scheme (Venmo app switch)
61+
62+
When using the Venmo app switch flow, initialize PopupBridge with the URL scheme your app is registered to handle:
63+
64+
```swift
65+
popupBridge = POPPopupBridge(webView: webView, returnURLScheme: "your-app-scheme")
66+
```
67+
68+
When the Venmo app is installed, PopupBridge sends this scheme to the web SDK as the return URL prefix (`window.popupBridge.getReturnUrlPrefix()`), so the Venmo app can deep-link back into your app. You must register the same scheme under `CFBundleURLTypes` in your app's `Info.plist`:
69+
70+
```xml
71+
<key>CFBundleURLTypes</key>
72+
<array>
73+
<dict>
74+
<key>CFBundleURLSchemes</key>
75+
<array>
76+
<string>your-app-scheme</string>
77+
</array>
78+
</dict>
79+
</array>
80+
```
81+
82+
Additionally, you must add this scheme to your applications URL types. All 3 cases must match exactly to return to your app as expected after completing the app switch flow.
83+
6084
Sample App
6185
-------
6286

Sources/PopupBridge/POPPopupBridge.swift

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ public class POPPopupBridge: NSObject, WKScriptMessageHandler {
1515
private let messageHandlerName = "POPPopupBridge"
1616
private let sessionID = UUID().uuidString.replacingOccurrences(of: "-", with: "")
1717
private let webView: WKWebView
18+
private let returnURLScheme: String?
1819
private var application: URLOpener = UIApplication.shared
1920

2021
private var webAuthenticationSession: WebAuthenticationSession = WebAuthenticationSession()
@@ -24,6 +25,19 @@ public class POPPopupBridge: NSObject, WKScriptMessageHandler {
2425
/// keeping `POPPopupBridge` a coordinator.
2526
private var appSwitchHandler: PayPalAppSwitchHandler?
2627

28+
/// The URL scheme used consistently across the return URL prefix advertised to the JS
29+
/// (`getReturnUrlPrefix()`), the ASWeb `callbackURLScheme`, and the return-URL validation. When a
30+
/// `returnURLScheme` is provided and the Venmo app is installed, the flow app switches into the
31+
/// Venmo app (which returns via the merchant scheme), so the merchant scheme is used everywhere.
32+
/// Otherwise the SDK's internal callback scheme is used (the standard ASWeb popup flow). The web
33+
/// SDK reads `getReturnUrlPrefix()` for both flows, so all three must agree on this one scheme.
34+
private var returnURLPrefixScheme: String {
35+
if let returnURLScheme, application.isVenmoAppInstalled() {
36+
return returnURLScheme
37+
}
38+
return PopupBridgeConstants.callbackURLScheme
39+
}
40+
2741
// MARK: - Initializers
2842

2943
/// Initialize a Popup Bridge for the standard browser-based checkout flow.
@@ -49,12 +63,19 @@ public class POPPopupBridge: NSObject, WKScriptMessageHandler {
4963
)
5064
}
5165

52-
/// Initialize a Popup Bridge with the native PayPal app switch flow enabled.
66+
/// Initialize a Popup Bridge with a `returnURLScheme` to enable the native app switch flows.
5367
///
54-
/// Use this initializer to let the SDK launch the native PayPal app for checkout instead of opening a
55-
/// browser. Because the deep-link return arrives at the host app's `SceneDelegate` (outside the
68+
/// Use this initializer to let the SDK launch a native app for checkout instead of opening a
69+
/// browser. Because a deep-link return arrives at the host app's `SceneDelegate` (outside the
5670
/// WebView), a `returnURLScheme` is **required** — there is no flag to toggle and no scheme guessing.
5771
///
72+
/// The same `returnURLScheme` drives both native app switch flows:
73+
/// - **PayPal:** the SDK launches the native PayPal app and the return URL arrives at the host
74+
/// app's `SceneDelegate` (see the required integration below).
75+
/// - **Venmo:** when the Venmo app is installed, the flow app switches into the Venmo app, which
76+
/// deep-links back via this scheme. The scheme is then advertised to the JavaScript layer as the
77+
/// return URL prefix (instead of the SDK's internal ASWeb callback scheme).
78+
///
5879
/// **Required SceneDelegate integration:** the host app must forward incoming URLs from its
5980
/// `SceneDelegate` to PopupBridge via `PopupBridgeAppContextSwitcher.shared.handleReturnURL(_:)`,
6081
/// otherwise the checkout flow will hang indefinitely after the PayPal app returns. The method
@@ -69,10 +90,10 @@ public class POPPopupBridge: NSObject, WKScriptMessageHandler {
6990
/// - Parameters:
7091
/// - webView: The web view to add a script message handler to. Do not change the web view's
7192
/// configuration or user content controller after initializing Popup Bridge.
72-
/// - returnURLScheme: The URL scheme registered in the app's `Info.plist` that PopupBridge uses as
73-
/// the return URL for the checkout flow. PopupBridge does not guess the scheme from
74-
/// `CFBundleURLTypes`, since apps that register multiple URL schemes (e.g. Facebook, Google
75-
/// Sign-In) would resolve the wrong one.
93+
/// - returnURLScheme: The URL scheme registered under `CFBundleURLTypes` in the app's `Info.plist`
94+
/// that PopupBridge uses as the return URL for the checkout flow. PopupBridge does not guess the
95+
/// scheme from `CFBundleURLTypes`, since apps that register multiple URL schemes (e.g. Facebook,
96+
/// Google Sign-In) would resolve the wrong one.
7697
/// - prefersEphemeralWebBrowserSession: A Boolean that, when true, requests that the browser does
7798
/// not share cookies or other browsing data between the authentication session and the user's
7899
/// normal browser session. Defaults to `true`.
@@ -92,15 +113,16 @@ public class POPPopupBridge: NSObject, WKScriptMessageHandler {
92113

93114
/// Exposed for testing.
94115
///
95-
/// Designated initializer that injects a custom `WebAuthenticationSession` so tests can stub the
96-
/// browser authentication flow. Pass `returnURLScheme` and a mocked `URLOpener` to exercise the
97-
/// PayPal app switch flow end to end; omit them to test the standard browser flow with app switch
98-
/// disabled.
116+
/// Designated initializer that injects a custom `WebAuthenticationSession` and `URLOpener` so tests
117+
/// can stub the browser authentication flow and simulate whether the PayPal/Venmo apps are
118+
/// installed. Pass `returnURLScheme` and a mocked `URLOpener` to exercise the native app switch
119+
/// flows end to end; omit them to test the standard browser flow with app switch disabled.
99120
///
100-
/// A non-`nil` `returnURLScheme` selects the PayPal app switch flow; when it is `nil`, the standard
121+
/// A non-`nil` `returnURLScheme` enables the native app switch flows; when it is `nil`, the standard
101122
/// browser-based flow is used and the native app switch path stays disabled. Kept `internal` so the
102-
/// only public entry points are the two flow-specific initializers above, keeping the internal
103-
/// `URLOpener` out of the public API.
123+
/// only public entry points are the two initializers above, keeping the internal `URLOpener` out of
124+
/// the public API. `returnURLScheme` must be set before `configureWebView()` runs so the generated
125+
/// user script advertises the correct return URL prefix.
104126
init(
105127
webView: WKWebView,
106128
webAuthenticationSession: WebAuthenticationSession,
@@ -111,6 +133,7 @@ public class POPPopupBridge: NSObject, WKScriptMessageHandler {
111133
self.webView = webView
112134
self.application = application
113135
self.webAuthenticationSession = webAuthenticationSession
136+
self.returnURLScheme = returnURLScheme
114137

115138
super.init()
116139

@@ -119,6 +142,7 @@ public class POPPopupBridge: NSObject, WKScriptMessageHandler {
119142
}
120143
configureWebView()
121144
self.webAuthenticationSession.prefersEphemeralWebBrowserSession = prefersEphemeralWebBrowserSession
145+
self.webAuthenticationSession.callbackURLScheme = returnURLPrefixScheme
122146

123147
returnBlock = { [weak self] url in
124148
guard let script = self?.constructJavaScriptCompletionResult(returnURL: url) else {
@@ -161,7 +185,7 @@ public class POPPopupBridge: NSObject, WKScriptMessageHandler {
161185
/// - Returns: JavaScript formatted completion.
162186
func constructJavaScriptCompletionResult(returnURL: URL) -> String? {
163187
guard let urlComponents = URLComponents(url: returnURL, resolvingAgainstBaseURL: false),
164-
urlComponents.scheme?.caseInsensitiveCompare(PopupBridgeConstants.callbackURLScheme) == .orderedSame,
188+
urlComponents.scheme?.caseInsensitiveCompare(returnURLPrefixScheme) == .orderedSame,
165189
urlComponents.host?.caseInsensitiveCompare(PopupBridgeConstants.host) == .orderedSame
166190
else {
167191
return nil
@@ -197,7 +221,7 @@ public class POPPopupBridge: NSObject, WKScriptMessageHandler {
197221
let isPayPalAppSwitchAvailable = appSwitchHandler?.isPayPalAppInstalled() ?? false
198222

199223
let javascript = PopupBridgeUserScript(
200-
scheme: PopupBridgeConstants.callbackURLScheme,
224+
scheme: returnURLPrefixScheme,
201225
scriptMessageHandlerName: messageHandlerName,
202226
host: PopupBridgeConstants.host,
203227
isVenmoInstalled: application.isVenmoAppInstalled(),

Sources/PopupBridge/PopupBridge-Framework-Info.plist

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@
1515
<key>CFBundlePackageType</key>
1616
<string>FMWK</string>
1717
<key>CFBundleShortVersionString</key>
18-
<string>3.0.0</string>
18+
<string>3.1.0</string>
1919
<key>CFBundleVersion</key>
20-
<string>3.0.0</string>
20+
<string>3.1.0</string>
2121
<key>NSPrincipalClass</key>
2222
<string></string>
2323
</dict>

Sources/PopupBridge/WebAuthenticationSession.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ class WebAuthenticationSession: NSObject {
55

66
var authenticationSession: ASWebAuthenticationSession?
77
var prefersEphemeralWebBrowserSession: Bool = true
8+
var callbackURLScheme: String = PopupBridgeConstants.callbackURLScheme
89

910
func start(
1011
url: URL,
@@ -14,7 +15,7 @@ class WebAuthenticationSession: NSObject {
1415
) {
1516
self.authenticationSession = ASWebAuthenticationSession(
1617
url: url,
17-
callbackURLScheme: PopupBridgeConstants.callbackURLScheme
18+
callbackURLScheme: callbackURLScheme
1819
) { url, error in
1920
if let error = error as? NSError, error.code == ASWebAuthenticationSessionError.canceledLogin.rawValue {
2021
sessionDidCancel()

0 commit comments

Comments
 (0)