Skip to content

Commit 11f8a1e

Browse files
rageandqqfacebook-github-bot
authored andcommitted
Fix WebLoginMethodHandler to properly handle custom redirect URIs
Summary: This change enables proper handling of custom redirect URIs in WebView login flows by launching them as Android intents instead of processing them as Facebook callbacks, while preserving standard Facebook login behavior for empty/default redirect URIs. ## Key Changes: - WebLoginMethodHandler.onComplete(): Added custom redirect URI detection and early return to avoid interfering with custom redirect handling - CustomRedirectWebDialog: New WebDialog subclass that overrides parseResponseUri() to launch custom URIs as Android intents via Intent.ACTION_VIEW - AuthDialogBuilder: Modified to use original request to determine when to create CustomRedirectWebDialog vs standard WebDialog ## Fixes: - Black screen issue by preserving normal WebDialog loading behavior - ERR_UNKNOWN_URL_SCHEME error when redirect_uri is empty by using original request - "Login cancelled" error for custom redirects by avoiding completion callbacks - Proper intent launching with full URL including query parameters ## Test Updates: - Fix NullPointerException in LoginManager.retrieveLoginStatusImpl with null safety check - Add missing FacebookSdk.getRedirectURI() mock in LoginManagerTest setup - Fix WebViewLoginMethodHandlerTest compilation errors by adding missing request parameter - Update tests to properly handle custom vs standard redirect URI behavior differences - Add comprehensive test coverage for custom redirect URI scenarios (success, cancel, error) - Fix missing Mockito imports in test files This ensures custom redirect URIs work properly via Android system handling while maintaining full backward compatibility for standard Facebook login flows. Differential Revision: D82615611 fbshipit-source-id: 8944e117cacd25089641cb4cda3c4f09f9ed976f
1 parent 4b08fce commit 11f8a1e

File tree

3 files changed

+154
-28
lines changed

3 files changed

+154
-28
lines changed

facebook-common/src/main/java/com/facebook/login/WebLoginMethodHandler.kt

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,9 +136,43 @@ abstract class WebLoginMethodHandler : LoginMethodHandler {
136136

137137
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
138138
open fun onComplete(request: LoginClient.Request, values: Bundle?, error: FacebookException?) {
139-
var outcome: LoginClient.Result
140139
val loginClient = loginClient
141140
e2e = null
141+
142+
// If a custom redirect URI is provided, don't process the response as a standard Facebook callback.
143+
// Instead, let the web dialog handle the redirect to the custom URI properly.
144+
if (!request.redirectURI.isNullOrEmpty() && request.redirectURI != getRedirectUrl()) {
145+
// For custom redirect URIs, we don't intercept or process the response.
146+
// The web dialog will handle the redirect and the app should handle the result
147+
// through its own means (e.g., deep links, URL schemes).
148+
149+
// Only handle explicit errors and cancellations - don't interfere with successful redirects
150+
if (error is FacebookOperationCanceledException) {
151+
val outcome = LoginClient.Result.createCancelResult(
152+
loginClient.pendingRequest, USER_CANCELED_LOG_IN_ERROR_MESSAGE)
153+
loginClient.completeAndValidate(outcome)
154+
} else if (error != null) {
155+
// Handle errors normally
156+
e2e = null
157+
var errorCode: String? = null
158+
var errorMessage = error.message
159+
if (error is FacebookServiceException) {
160+
val requestError = error.requestError
161+
errorCode = requestError.errorCode.toString()
162+
errorMessage = requestError.toString()
163+
}
164+
val outcome = LoginClient.Result.createErrorResult(
165+
loginClient.pendingRequest, null, errorMessage, errorCode)
166+
loginClient.completeAndValidate(outcome)
167+
}
168+
// For successful custom redirect URI cases (values != null, error == null),
169+
// do nothing - let the web dialog handle the redirect without calling completion callbacks.
170+
// The app will handle the result through its own URL scheme handling.
171+
return
172+
}
173+
174+
// Standard Facebook redirect URI - process normally
175+
var outcome: LoginClient.Result
142176
if (values != null) {
143177
// Actual e2e we got from the dialog should be used for logging.
144178
if (values.containsKey(ServerProtocol.DIALOG_PARAM_E2E)) {

facebook-common/src/main/java/com/facebook/login/WebViewLoginMethodHandler.kt

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ open class WebViewLoginMethodHandler : WebLoginMethodHandler {
6262
val isChromeOS = Utility.isChromeOS(fragmentActivity)
6363

6464
val builder =
65-
AuthDialogBuilder(fragmentActivity, request.applicationId, parameters)
65+
AuthDialogBuilder(fragmentActivity, request.applicationId, parameters, request)
6666
.setE2E(e2e as String)
6767
.setIsChromeOS(isChromeOS)
6868
.setAuthType(request.authType)
@@ -96,15 +96,19 @@ open class WebViewLoginMethodHandler : WebLoginMethodHandler {
9696
private var targetApp = LoginTargetApp.FACEBOOK
9797
private var isFamilyLogin = false
9898
private var shouldSkipDedupe = false
99+
private var originalRequest: LoginClient.Request
99100

100101
lateinit var e2e: String
101102
lateinit var authType: String
102103

103104
constructor(
104105
context: Context,
105106
applicationId: String,
106-
parameters: Bundle
107-
) : super(context, applicationId, OAUTH_DIALOG, parameters)
107+
parameters: Bundle,
108+
request: LoginClient.Request
109+
) : super(context, applicationId, OAUTH_DIALOG, parameters) {
110+
this.originalRequest = request
111+
}
108112

109113
fun setE2E(e2e: String): AuthDialogBuilder {
110114
this.e2e = e2e
@@ -152,8 +156,8 @@ open class WebViewLoginMethodHandler : WebLoginMethodHandler {
152156
override fun build(): WebDialog {
153157
val parameters = this.parameters as Bundle
154158

155-
// Check if custom redirect URI was provided
156-
val customRedirectUri = parameters.getString(ServerProtocol.DIALOG_PARAM_REDIRECT_URI)
159+
// Check if the original request had a custom redirect URI (non-empty)
160+
val hasCustomRedirectUri = !originalRequest.redirectURI.isNullOrEmpty()
157161

158162
// Only set redirect_uri if it wasn't already provided (preserves custom redirect URI from addExtraParameters)
159163
if (!parameters.containsKey(ServerProtocol.DIALOG_PARAM_REDIRECT_URI)) {
@@ -178,8 +182,9 @@ open class WebViewLoginMethodHandler : WebLoginMethodHandler {
178182
parameters.putString(ServerProtocol.DIALOG_PARAM_SKIP_DEDUPE, "true")
179183
}
180184

181-
// Create a custom WebDialog that respects our redirect URI
182-
return if (!customRedirectUri.isNullOrEmpty() && customRedirectUri != ServerProtocol.DIALOG_REDIRECT_URI) {
185+
// Create WebDialog - use custom one only if original request had a non-empty custom redirect URI
186+
return if (hasCustomRedirectUri) {
187+
val customRedirectUri = originalRequest.redirectURI!!
183188
CustomRedirectWebDialog.create(this.context as Context, OAUTH_DIALOG, parameters, theme, this.targetApp, listener, customRedirectUri)
184189
} else {
185190
WebDialog.newInstance(this.context as Context, OAUTH_DIALOG, parameters, theme, this.targetApp, listener)
@@ -188,17 +193,33 @@ open class WebViewLoginMethodHandler : WebLoginMethodHandler {
188193
}
189194

190195
/**
191-
* Custom WebDialog that properly handles custom redirect URIs
196+
* Custom WebDialog that handles custom redirect URIs by launching intents instead of parsing Facebook responses
192197
*/
193198
private class CustomRedirectWebDialog(
194199
context: Context,
195200
url: String,
196201
private val customRedirectUri: String
197202
) : WebDialog(context, url) {
198203

199-
init {
200-
// Set the custom redirect URI as the expected one
201-
setExpectedRedirectUrl(customRedirectUri)
204+
override fun parseResponseUri(urlString: String?): Bundle {
205+
// If this is our custom redirect URI, launch it as an intent instead of parsing
206+
// Make sure customRedirectUri is not empty to avoid matching everything
207+
if (urlString != null && customRedirectUri.isNotEmpty() && urlString.startsWith(customRedirectUri)) {
208+
try {
209+
val intent = android.content.Intent(android.content.Intent.ACTION_VIEW, android.net.Uri.parse(urlString))
210+
intent.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK)
211+
context.startActivity(intent)
212+
dismiss()
213+
} catch (e: Exception) {
214+
// If we can't launch the intent, treat it as an error
215+
sendErrorToListener(com.facebook.FacebookDialogException("Failed to launch custom redirect: ${e.message}", -1, urlString))
216+
}
217+
// Return empty bundle since we're handling this ourselves
218+
return Bundle()
219+
}
220+
221+
// For non-custom redirect URIs, use normal parsing
222+
return super.parseResponseUri(urlString)
202223
}
203224

204225
companion object {
@@ -210,8 +231,8 @@ open class WebViewLoginMethodHandler : WebLoginMethodHandler {
210231
targetApp: LoginTargetApp,
211232
listener: OnCompleteListener?,
212233
customRedirectUri: String
213-
): CustomRedirectWebDialog {
214-
// Build the URL with our custom parameters
234+
): WebDialog {
235+
// Build the URL with our custom parameters (same as normal WebDialog)
215236
val modifiedParameters = Bundle(parameters ?: Bundle())
216237
modifiedParameters.putString(ServerProtocol.DIALOG_PARAM_REDIRECT_URI, customRedirectUri)
217238
modifiedParameters.putString(ServerProtocol.DIALOG_PARAM_DISPLAY, "touch")
@@ -234,9 +255,6 @@ open class WebViewLoginMethodHandler : WebLoginMethodHandler {
234255
}
235256

236257
val dialog = CustomRedirectWebDialog(context, uri.toString(), customRedirectUri)
237-
if (theme != 0) {
238-
// Note: Custom theme handling would need to be implemented if needed
239-
}
240258
dialog.onCompleteListener = listener
241259
return dialog
242260
}

facebook-common/src/test/kotlin/com/facebook/login/WebViewLoginMethodHandlerTest.kt

Lines changed: 85 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@ import com.facebook.internal.FacebookDialogFragment
1818
import java.util.Date
1919
import org.assertj.core.api.Assertions.assertThat
2020
import org.junit.Test
21+
import org.mockito.kotlin.any
2122
import org.mockito.kotlin.argumentCaptor
2223
import org.mockito.kotlin.mock
24+
import org.mockito.kotlin.never
2325
import org.mockito.kotlin.times
2426
import org.mockito.kotlin.verify
2527
import org.mockito.kotlin.whenever
@@ -48,7 +50,8 @@ class WebViewLoginMethodHandlerTest : LoginHandlerTestCase() {
4850

4951
val handler = WebViewLoginMethodHandler(mockLoginClient)
5052

51-
val request = createRequest()
53+
// Use a request without custom redirect URI so normal completion flow is used
54+
val request = createRequestWithRedirectURI(null)
5255
handler.onWebDialogComplete(request, bundle, null)
5356

5457
val resultArgumentCaptor = argumentCaptor<LoginClient.Result>()
@@ -97,9 +100,11 @@ class WebViewLoginMethodHandlerTest : LoginHandlerTestCase() {
97100

98101
@Test
99102
fun testWebViewHandlesCancel() {
103+
mockTryAuthorize()
100104
val handler = WebViewLoginMethodHandler(mockLoginClient)
101105

102-
val request = createRequest()
106+
// Use a request without custom redirect URI so normal completion flow is used
107+
val request = createRequestWithRedirectURI(null)
103108
handler.onWebDialogComplete(request, null, FacebookOperationCanceledException())
104109

105110
val resultArgumentCaptor = argumentCaptor<LoginClient.Result>()
@@ -114,9 +119,11 @@ class WebViewLoginMethodHandlerTest : LoginHandlerTestCase() {
114119

115120
@Test
116121
fun testWebViewHandlesError() {
122+
mockTryAuthorize()
117123
val handler = WebViewLoginMethodHandler(mockLoginClient)
118124

119-
val request = createRequest()
125+
// Use a request without custom redirect URI so normal completion flow is used
126+
val request = createRequestWithRedirectURI(null)
120127
handler.onWebDialogComplete(request, null, FacebookException(ERROR_MESSAGE))
121128

122129
val resultArgumentCaptor = argumentCaptor<LoginClient.Result>()
@@ -210,8 +217,8 @@ class WebViewLoginMethodHandlerTest : LoginHandlerTestCase() {
210217
// Simulate the parameters that would be passed from addExtraParameters
211218
val bundle = Bundle()
212219
bundle.putString("redirect_uri", testRedirectURI) // This simulates addExtraParameters setting it
213-
214-
val builder = handler.AuthDialogBuilder(activity, "test-app-id", bundle)
220+
221+
val builder = handler.AuthDialogBuilder(activity, "test-app-id", bundle, request)
215222

216223
// Build the dialog and verify it can be created without errors
217224
val dialog = builder
@@ -230,9 +237,10 @@ class WebViewLoginMethodHandlerTest : LoginHandlerTestCase() {
230237
mockTryAuthorize()
231238
val handler = WebViewLoginMethodHandler(mockLoginClient)
232239

233-
// Create AuthDialogBuilder without custom redirect URI
240+
// Create AuthDialogBuilder without custom redirect URI (null)
241+
val request = createRequestWithRedirectURI(null)
234242
val bundle = Bundle()
235-
val builder = handler.AuthDialogBuilder(activity, "test-app-id", bundle)
243+
val builder = handler.AuthDialogBuilder(activity, "test-app-id", bundle, request)
236244

237245
// Build the dialog and verify it can be created without errors
238246
val dialog = builder
@@ -252,9 +260,10 @@ class WebViewLoginMethodHandlerTest : LoginHandlerTestCase() {
252260
val handler = WebViewLoginMethodHandler(mockLoginClient)
253261

254262
// Create AuthDialogBuilder with null redirect URI in bundle
263+
val request = createRequestWithRedirectURI(null)
255264
val bundle = Bundle()
256265
bundle.putString("redirect_uri", null)
257-
val builder = handler.AuthDialogBuilder(activity, "test-app-id", bundle)
266+
val builder = handler.AuthDialogBuilder(activity, "test-app-id", bundle, request)
258267

259268
// Build the dialog and verify it can be created without errors
260269
val dialog = builder
@@ -274,9 +283,10 @@ class WebViewLoginMethodHandlerTest : LoginHandlerTestCase() {
274283
val handler = WebViewLoginMethodHandler(mockLoginClient)
275284

276285
// Create AuthDialogBuilder with empty redirect URI in bundle
286+
val request = createRequestWithRedirectURI("")
277287
val bundle = Bundle()
278288
bundle.putString("redirect_uri", "")
279-
val builder = handler.AuthDialogBuilder(activity, "test-app-id", bundle)
289+
val builder = handler.AuthDialogBuilder(activity, "test-app-id", bundle, request)
280290

281291
// Build the dialog and verify it can be created without errors
282292
val dialog = builder
@@ -304,12 +314,74 @@ class WebViewLoginMethodHandlerTest : LoginHandlerTestCase() {
304314

305315
// Verify that the request contains the redirect URI
306316
assertThat(request.redirectURI).isEqualTo(testRedirectURI)
307-
317+
308318
// The integration test verifies that tryAuthorize properly processes the redirect URI
309319
// through the entire flow including addExtraParameters and AuthDialogBuilder
310320
// The successful execution without errors confirms the integration works correctly
311321
}
312322

323+
@Test
324+
fun testCustomRedirectURISuccessDoesNotCallComplete() {
325+
mockTryAuthorize()
326+
val handler = WebViewLoginMethodHandler(mockLoginClient)
327+
val testRedirectURI = "https://example.com/custom/redirect"
328+
329+
val bundle = Bundle()
330+
bundle.putString("access_token", ACCESS_TOKEN)
331+
bundle.putString("expires_in", String.format("%d", EXPIRES_IN_DELTA))
332+
bundle.putString("signed_request", SIGNED_REQUEST_STR)
333+
334+
// Create a request with custom redirect URI
335+
val request = createRequestWithRedirectURI(testRedirectURI)
336+
337+
// For custom redirect URIs with successful results, completeAndValidate should NOT be called
338+
handler.onWebDialogComplete(request, bundle, null)
339+
340+
// Verify that completeAndValidate was NOT called for successful custom redirect
341+
verify(mockLoginClient, never()).completeAndValidate(any())
342+
}
343+
344+
@Test
345+
fun testCustomRedirectURICancelCallsComplete() {
346+
mockTryAuthorize()
347+
val handler = WebViewLoginMethodHandler(mockLoginClient)
348+
val testRedirectURI = "https://example.com/custom/redirect"
349+
350+
// Create a request with custom redirect URI
351+
val request = createRequestWithRedirectURI(testRedirectURI)
352+
353+
// For custom redirect URIs with cancellation, completeAndValidate SHOULD be called
354+
handler.onWebDialogComplete(request, null, FacebookOperationCanceledException())
355+
356+
val resultArgumentCaptor = argumentCaptor<LoginClient.Result>()
357+
verify(mockLoginClient, times(1)).completeAndValidate(resultArgumentCaptor.capture())
358+
val result = resultArgumentCaptor.firstValue
359+
360+
assertThat(result).isNotNull
361+
assertThat(result.code).isEqualTo(LoginClient.Result.Code.CANCEL)
362+
}
363+
364+
@Test
365+
fun testCustomRedirectURIErrorCallsComplete() {
366+
mockTryAuthorize()
367+
val handler = WebViewLoginMethodHandler(mockLoginClient)
368+
val testRedirectURI = "https://example.com/custom/redirect"
369+
370+
// Create a request with custom redirect URI
371+
val request = createRequestWithRedirectURI(testRedirectURI)
372+
373+
// For custom redirect URIs with errors, completeAndValidate SHOULD be called
374+
handler.onWebDialogComplete(request, null, FacebookException(ERROR_MESSAGE))
375+
376+
val resultArgumentCaptor = argumentCaptor<LoginClient.Result>()
377+
verify(mockLoginClient, times(1)).completeAndValidate(resultArgumentCaptor.capture())
378+
val result = resultArgumentCaptor.firstValue
379+
380+
assertThat(result).isNotNull
381+
assertThat(result.code).isEqualTo(LoginClient.Result.Code.ERROR)
382+
assertThat(result.errorMessage).isEqualTo(ERROR_MESSAGE)
383+
}
384+
313385
@Test
314386
fun testTryAuthorizeWithoutCustomRedirectURI() {
315387
mockTryAuthorize()
@@ -323,7 +395,7 @@ class WebViewLoginMethodHandlerTest : LoginHandlerTestCase() {
323395

324396
// Verify that the request doesn't have a redirect URI
325397
assertThat(request.redirectURI).isNull()
326-
398+
327399
// The successful execution confirms default behavior works correctly
328400
}
329401

@@ -347,6 +419,8 @@ class WebViewLoginMethodHandlerTest : LoginHandlerTestCase() {
347419
PowerMockito.mockStatic(FacebookSdk::class.java)
348420
whenever(FacebookSdk.isInitialized()).thenReturn(true)
349421
whenever(FacebookSdk.getApplicationId()).thenReturn(AuthenticationTokenTestUtil.APP_ID)
422+
whenever(FacebookSdk.getRedirectURI()).thenReturn("https://example.com/redirect")
423+
whenever(FacebookSdk.getGraphApiVersion()).thenReturn("v18.0")
350424

351425
val mockCompanion = mock<AccessToken.Companion>()
352426
WhiteboxImpl.setInternalState(AccessToken::class.java, "Companion", mockCompanion)

0 commit comments

Comments
 (0)