pyios-firebase 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. pyios_firebase/__init__.py +26 -0
  2. pyios_firebase/analytics/__init__.py +434 -0
  3. pyios_firebase/analytics/names.py +251 -0
  4. pyios_firebase/app_check/__init__.py +483 -0
  5. pyios_firebase/auth/__init__.py +131 -0
  6. pyios_firebase/auth/_apple_name_cache.py +97 -0
  7. pyios_firebase/auth/_common.py +121 -0
  8. pyios_firebase/auth/_ui.py +21 -0
  9. pyios_firebase/auth/apple.py +326 -0
  10. pyios_firebase/auth/auth.py +592 -0
  11. pyios_firebase/auth/credentials.py +551 -0
  12. pyios_firebase/auth/errors.py +321 -0
  13. pyios_firebase/auth/facebook.py +240 -0
  14. pyios_firebase/auth/google.py +167 -0
  15. pyios_firebase/auth/multi_factor.py +394 -0
  16. pyios_firebase/auth/results.py +361 -0
  17. pyios_firebase/auth/user.py +457 -0
  18. pyios_firebase/core/__init__.py +32 -0
  19. pyios_firebase/crashlytics/__init__.py +463 -0
  20. pyios_firebase/database/__init__.py +62 -0
  21. pyios_firebase/database/database.py +144 -0
  22. pyios_firebase/database/errors.py +80 -0
  23. pyios_firebase/database/query.py +569 -0
  24. pyios_firebase/database/reference.py +428 -0
  25. pyios_firebase/database/snapshot.py +218 -0
  26. pyios_firebase/database/values.py +271 -0
  27. pyios_firebase/firestore/__init__.py +59 -0
  28. pyios_firebase/firestore/_limits.py +48 -0
  29. pyios_firebase/firestore/_query_state.py +228 -0
  30. pyios_firebase/firestore/_write.py +18 -0
  31. pyios_firebase/firestore/aggregate.py +137 -0
  32. pyios_firebase/firestore/collection_reference.py +100 -0
  33. pyios_firebase/firestore/document_change.py +63 -0
  34. pyios_firebase/firestore/document_reference.py +229 -0
  35. pyios_firebase/firestore/document_snapshot.py +102 -0
  36. pyios_firebase/firestore/enums.py +66 -0
  37. pyios_firebase/firestore/field_value.py +94 -0
  38. pyios_firebase/firestore/filter.py +319 -0
  39. pyios_firebase/firestore/firestore.py +184 -0
  40. pyios_firebase/firestore/listener_registration.py +55 -0
  41. pyios_firebase/firestore/query.py +679 -0
  42. pyios_firebase/firestore/query_snapshot.py +103 -0
  43. pyios_firebase/firestore/snapshot_metadata.py +36 -0
  44. pyios_firebase/firestore/transaction.py +130 -0
  45. pyios_firebase/firestore/write_batch.py +125 -0
  46. pyios_firebase/functions/__init__.py +329 -0
  47. pyios_firebase/messaging/__init__.py +37 -0
  48. pyios_firebase/messaging/messaging.py +435 -0
  49. pyios_firebase/messaging/notifications.py +292 -0
  50. pyios_firebase/performance/__init__.py +486 -0
  51. pyios_firebase/py.typed +0 -0
  52. pyios_firebase/remote_config/__init__.py +660 -0
  53. pyios_firebase/storage/__init__.py +41 -0
  54. pyios_firebase/storage/errors.py +103 -0
  55. pyios_firebase/storage/metadata.py +202 -0
  56. pyios_firebase/storage/reference.py +402 -0
  57. pyios_firebase/storage/storage.py +120 -0
  58. pyios_firebase/storage/task.py +198 -0
  59. pyios_firebase/types.py +93 -0
  60. pyios_firebase/util.py +413 -0
  61. pyios_firebase-0.2.0.dist-info/METADATA +1969 -0
  62. pyios_firebase-0.2.0.dist-info/RECORD +65 -0
  63. pyios_firebase-0.2.0.dist-info/WHEEL +5 -0
  64. pyios_firebase-0.2.0.dist-info/licenses/LICENSE +21 -0
  65. pyios_firebase-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,321 @@
1
+ """Firebase Authentication errors.
2
+
3
+ Every auth callback's ``error`` is an ``AuthError``: a ``FirebaseError`` with
4
+ extra keys. Compare ``error.code`` with ``AuthErrorCode``::
5
+
6
+ def on_signed_in(result, error):
7
+ if error is not None:
8
+ if error.code == AuthErrorCode.WRONG_PASSWORD:
9
+ ...
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ __all__ = ("AUTH_ERROR_DOMAIN", "AuthError", "AuthErrorCode", "auth_error_from_pointer")
15
+
16
+ from typing import TYPE_CHECKING, Any, Final
17
+
18
+ from ..types import FirebaseError, ObjCObject
19
+ from ..util import nserror_to_dict, objc_instance
20
+
21
+ if TYPE_CHECKING:
22
+ from .credentials import AuthCredential
23
+ from .multi_factor import MultiFactorResolver
24
+
25
+ AUTH_ERROR_DOMAIN: Final = "FIRAuthErrorDomain"
26
+ """The ``error.domain`` of Firebase Authentication errors."""
27
+
28
+ _NAME_KEY: Final = "FIRAuthErrorUserInfoNameKey"
29
+ _EMAIL_KEY: Final = "FIRAuthErrorUserInfoEmailKey"
30
+ _UPDATED_CREDENTIAL_KEY: Final = "FIRAuthErrorUserInfoUpdatedCredentialKey"
31
+ _RESOLVER_KEY: Final = "FIRAuthErrorUserInfoMultiFactorResolverKey"
32
+
33
+
34
+ class AuthErrorCode:
35
+ """The ``error.code`` values of Firebase Authentication errors.
36
+
37
+ Generated from ``FIRAuthErrorCode`` in the Firebase iOS SDK (13.1.1). The
38
+ numbers are fixed in the SDK; check them when you update it.
39
+ """
40
+
41
+ INVALID_CUSTOM_TOKEN: Final = 17000
42
+ """Indicates a validation error with the custom token."""
43
+ CUSTOM_TOKEN_MISMATCH: Final = 17002
44
+ """Indicates the service account and the API key belong to different projects."""
45
+ INVALID_CREDENTIAL: Final = 17004
46
+ """Indicates the IDP token or requestUri is invalid."""
47
+ USER_DISABLED: Final = 17005
48
+ """Indicates the user's account is disabled on the server."""
49
+ OPERATION_NOT_ALLOWED: Final = 17006
50
+ """Indicates the administrator disabled sign in with the specified identity provider."""
51
+ EMAIL_ALREADY_IN_USE: Final = 17007
52
+ """Indicates the email used to attempt a sign up is already in use."""
53
+ INVALID_EMAIL: Final = 17008
54
+ """Indicates the email is invalid."""
55
+ WRONG_PASSWORD: Final = 17009
56
+ """Indicates the user attempted sign in with a wrong password."""
57
+ TOO_MANY_REQUESTS: Final = 17010
58
+ """Indicates that too many requests were made to a server method."""
59
+ USER_NOT_FOUND: Final = 17011
60
+ """Indicates the user account was not found."""
61
+ ACCOUNT_EXISTS_WITH_DIFFERENT_CREDENTIAL: Final = 17012
62
+ """Indicates account linking is required."""
63
+ REQUIRES_RECENT_LOGIN: Final = 17014
64
+ """Indicates the user has attempted to change email or password more than 5 minutes after signing in."""
65
+ PROVIDER_ALREADY_LINKED: Final = 17015
66
+ """Indicates an attempt to link a provider to which the account is already linked."""
67
+ NO_SUCH_PROVIDER: Final = 17016
68
+ """Indicates an attempt to unlink a provider that is not linked."""
69
+ INVALID_USER_TOKEN: Final = 17017
70
+ """Indicates user's saved auth credential is invalid the user needs to sign in again."""
71
+ NETWORK_ERROR: Final = 17020
72
+ """Indicates a network error occurred (such as a timeout interrupted connection or unreachable host)."""
73
+ USER_TOKEN_EXPIRED: Final = 17021
74
+ """Indicates the saved token has expired for example the user may have changed account password on another device."""
75
+ INVALID_API_KEY: Final = 17023
76
+ """Indicates an invalid API key was supplied in the request."""
77
+ USER_MISMATCH: Final = 17024
78
+ """Indicates that an attempt was made to reauthenticate with a user which is not the current user."""
79
+ CREDENTIAL_ALREADY_IN_USE: Final = 17025
80
+ """Indicates an attempt to link with a credential that has already been linked with a different Firebase account."""
81
+ WEAK_PASSWORD: Final = 17026
82
+ """Indicates an attempt to set a password that is considered too weak."""
83
+ APP_NOT_AUTHORIZED: Final = 17028
84
+ """Indicates the App is not authorized to use Firebase Authentication with the provided API Key."""
85
+ EXPIRED_ACTION_CODE: Final = 17029
86
+ """Indicates the OOB code is expired."""
87
+ INVALID_ACTION_CODE: Final = 17030
88
+ """Indicates the OOB code is invalid."""
89
+ INVALID_MESSAGE_PAYLOAD: Final = 17031
90
+ """Indicates that there are invalid parameters in the payload during a 'send password reset email' attempt."""
91
+ INVALID_SENDER: Final = 17032
92
+ """Indicates that the sender email is invalid during a 'send password reset email' attempt."""
93
+ INVALID_RECIPIENT_EMAIL: Final = 17033
94
+ """Indicates that the recipient email is invalid."""
95
+ MISSING_EMAIL: Final = 17034
96
+ """Indicates that an email address was expected but one was not provided."""
97
+ MISSING_IOS_BUNDLE_ID: Final = 17036
98
+ """Indicates that the iOS bundle ID is missing when a iOS App Store ID is provided."""
99
+ MISSING_ANDROID_PACKAGE_NAME: Final = 17037
100
+ """Indicates that the android package name is missing when the `androidInstallApp` flag is set to `true`."""
101
+ UNAUTHORIZED_DOMAIN: Final = 17038
102
+ """Indicates that the domain specified in the continue URL is not allowlisted in the Firebase console."""
103
+ INVALID_CONTINUE_URI: Final = 17039
104
+ """Indicates that the domain specified in the continue URI is not valid."""
105
+ MISSING_CONTINUE_URI: Final = 17040
106
+ """Indicates that a continue URI was not provided in a request to the backend which requires one."""
107
+ MISSING_PHONE_NUMBER: Final = 17041
108
+ """Indicates that a phone number was not provided in a call to `verifyPhoneNumber:completion:`."""
109
+ INVALID_PHONE_NUMBER: Final = 17042
110
+ """Indicates that an invalid phone number was provided in a call to `verifyPhoneNumber:completion:`."""
111
+ MISSING_VERIFICATION_CODE: Final = 17043
112
+ """Indicates that the phone auth credential was created with an empty verification code."""
113
+ INVALID_VERIFICATION_CODE: Final = 17044
114
+ """Indicates that an invalid verification code was used in the verifyPhoneNumber request."""
115
+ MISSING_VERIFICATION_ID: Final = 17045
116
+ """Indicates that the phone auth credential was created with an empty verification ID."""
117
+ INVALID_VERIFICATION_ID: Final = 17046
118
+ """Indicates that an invalid verification ID was used in the verifyPhoneNumber request."""
119
+ MISSING_APP_CREDENTIAL: Final = 17047
120
+ """Indicates that the APNS device token is missing in the verifyClient request."""
121
+ INVALID_APP_CREDENTIAL: Final = 17048
122
+ """Indicates that an invalid APNS device token was used in the verifyClient request."""
123
+ SESSION_EXPIRED: Final = 17051
124
+ """Indicates that the SMS code has expired."""
125
+ QUOTA_EXCEEDED: Final = 17052
126
+ """Indicates that the quota of SMS messages for a given project has been exceeded."""
127
+ MISSING_APP_TOKEN: Final = 17053
128
+ """Indicates that the APNs device token could not be obtained."""
129
+ NOTIFICATION_NOT_FORWARDED: Final = 17054
130
+ """Indicates that the app fails to forward remote notification to FIRAuth."""
131
+ APP_NOT_VERIFIED: Final = 17055
132
+ """Indicates that the app could not be verified by Firebase during phone number authentication."""
133
+ CAPTCHA_CHECK_FAILED: Final = 17056
134
+ """Indicates that the reCAPTCHA token is not valid."""
135
+ WEB_CONTEXT_ALREADY_PRESENTED: Final = 17057
136
+ """Indicates that an attempt was made to present a new web context while one was already being presented."""
137
+ WEB_CONTEXT_CANCELLED: Final = 17058
138
+ """Indicates that the URL presentation was cancelled prematurely by the user."""
139
+ APP_VERIFICATION_USER_INTERACTION_FAILURE: Final = 17059
140
+ """Indicates a general failure during the app verification flow."""
141
+ INVALID_CLIENT_ID: Final = 17060
142
+ """Indicates that the clientID used to invoke a web flow is invalid."""
143
+ WEB_NETWORK_REQUEST_FAILED: Final = 17061
144
+ """Indicates that a network request within a SFSafariViewController or WKWebView failed."""
145
+ WEB_INTERNAL_ERROR: Final = 17062
146
+ """Indicates that an internal error occurred within a SFSafariViewController or WKWebView."""
147
+ WEB_SIGN_IN_USER_INTERACTION_FAILURE: Final = 17063
148
+ """Indicates a general failure during a web sign-in flow."""
149
+ LOCAL_PLAYER_NOT_AUTHENTICATED: Final = 17066
150
+ """Indicates that the local player was not authenticated prior to attempting Game Center signin."""
151
+ NULL_USER: Final = 17067
152
+ """Indicates that a non-null user was expected as an argument to the operation but a null user was provided."""
153
+ INVALID_PROVIDER_ID: Final = 17071
154
+ """Represents the error code for when the given provider id for a web operation is invalid."""
155
+ TENANT_ID_MISMATCH: Final = 17072
156
+ """Represents the error code for when an attempt is made to update the current user with a tenantId that differs from the current FirebaseAuth instance's tenantId."""
157
+ UNSUPPORTED_TENANT_OPERATION: Final = 17073
158
+ """Represents the error code for when a request is made to the backend with an associated tenant ID for an operation that does not support multi-tenancy."""
159
+ INVALID_HOSTING_LINK_DOMAIN: Final = 17214
160
+ """Indicates that the provided Firebase Hosting Link domain is not owned by the current project."""
161
+ REJECTED_CREDENTIAL: Final = 17075
162
+ """Indicates that the credential is rejected because it's malformed or mismatching."""
163
+ GAME_KIT_NOT_LINKED: Final = 17076
164
+ """Indicates that the GameKit framework is not linked prior to attempting Game Center signin."""
165
+ SECOND_FACTOR_REQUIRED: Final = 17078
166
+ """Indicates that the second factor is required for signin."""
167
+ MISSING_MULTI_FACTOR_SESSION: Final = 17081
168
+ """Indicates that the multi factor session is missing."""
169
+ MISSING_MULTI_FACTOR_INFO: Final = 17082
170
+ """Indicates that the multi factor info is missing."""
171
+ INVALID_MULTI_FACTOR_SESSION: Final = 17083
172
+ """Indicates that the multi factor session is invalid."""
173
+ MULTI_FACTOR_INFO_NOT_FOUND: Final = 17084
174
+ """Indicates that the multi factor info is not found."""
175
+ ADMIN_RESTRICTED_OPERATION: Final = 17085
176
+ """Indicates that the operation is admin restricted."""
177
+ UNVERIFIED_EMAIL: Final = 17086
178
+ """Indicates that the email is required for verification."""
179
+ SECOND_FACTOR_ALREADY_ENROLLED: Final = 17087
180
+ """Indicates that the second factor is already enrolled."""
181
+ MAXIMUM_SECOND_FACTOR_COUNT_EXCEEDED: Final = 17088
182
+ """Indicates that the maximum second factor count is exceeded."""
183
+ UNSUPPORTED_FIRST_FACTOR: Final = 17089
184
+ """Indicates that the first factor is not supported."""
185
+ EMAIL_CHANGE_NEEDS_VERIFICATION: Final = 17090
186
+ """Indicates that the a verified email is required to changed to."""
187
+ MISSING_CLIENT_IDENTIFIER: Final = 17093
188
+ """Indicates that the request does not contain a client identifier."""
189
+ MISSING_OR_INVALID_NONCE: Final = 17094
190
+ """Indicates that the nonce is missing or invalid."""
191
+ BLOCKING_CLOUD_FUNCTION_ERROR: Final = 17105
192
+ """Raised when a Cloud Function returns a blocking error."""
193
+ RECAPTCHA_NOT_ENABLED: Final = 17200
194
+ """Indicates that reCAPTCHA Enterprise integration is not enabled for this project."""
195
+ MISSING_RECAPTCHA_TOKEN: Final = 17201
196
+ """Indicates that the reCAPTCHA token is missing from the backend request."""
197
+ INVALID_RECAPTCHA_TOKEN: Final = 17202
198
+ """Indicates that the reCAPTCHA token sent with the backend request is invalid."""
199
+ INVALID_RECAPTCHA_ACTION: Final = 17203
200
+ """Indicates that the requested reCAPTCHA action is invalid."""
201
+ MISSING_CLIENT_TYPE: Final = 17204
202
+ """Indicates that the client type is missing from the request."""
203
+ MISSING_RECAPTCHA_VERSION: Final = 17205
204
+ """Indicates that the reCAPTCHA version is missing from the request."""
205
+ INVALID_RECAPTCHA_VERSION: Final = 17206
206
+ """Indicates that the reCAPTCHA version sent to the backend is invalid."""
207
+ INVALID_REQ_TYPE: Final = 17207
208
+ """Indicates that the request type sent to the backend is invalid."""
209
+ RECAPTCHA_SDK_NOT_LINKED: Final = 17208
210
+ """Indicates that the reCAPTCHA SDK is not linked to the app."""
211
+ RECAPTCHA_SITE_KEY_MISSING: Final = 17209
212
+ """Indicates that the reCAPTCHA SDK site key wasn't found."""
213
+ RECAPTCHA_ACTION_CREATION_FAILED: Final = 17210
214
+ """Indicates that the reCAPTCHA SDK actions class failed to create."""
215
+ PASSWORD_DOES_NOT_MEET_REQUIREMENTS: Final = 17211
216
+ """Indicates that the password does not meet the project's password policy."""
217
+ KEYCHAIN_ERROR: Final = 17995
218
+ """Indicates an error occurred while attempting to access the keychain."""
219
+ INTERNAL_ERROR: Final = 17999
220
+ """Indicates an internal error occurred."""
221
+ MALFORMED_JWT: Final = 18000
222
+ """Raised when a JWT fails to parse correctly."""
223
+
224
+
225
+ _CODE_NAMES: Final[dict[int, str]] = {
226
+ value: name for name, value in vars(AuthErrorCode).items() if isinstance(value, int)
227
+ }
228
+
229
+
230
+ class AuthError(FirebaseError):
231
+ """An error from Firebase Authentication (or from a sign-in sheet)."""
232
+
233
+ name: str
234
+ """The ``AuthErrorCode`` name of ``code`` (for example ``"WRONG_PASSWORD"``),
235
+ or ``""`` for errors from other domains."""
236
+ email: str | None
237
+ """The email address the error is about, when Firebase gives one (for
238
+ example with ``ACCOUNT_EXISTS_WITH_DIFFERENT_CREDENTIAL``)."""
239
+ updated_credential: AuthCredential | None
240
+ """With ``ACCOUNT_EXISTS_WITH_DIFFERENT_CREDENTIAL`` and
241
+ ``CREDENTIAL_ALREADY_IN_USE``: a credential to sign in with instead."""
242
+ resolver: MultiFactorResolver | None
243
+ """With ``SECOND_FACTOR_REQUIRED``: finishes the sign-in with a second
244
+ factor (see ``pyios_firebase.auth.multi_factor``)."""
245
+ cancelled: bool
246
+ """True when the user closed a sign-in sheet (Google, Apple, web
247
+ providers): not a failure to report."""
248
+ message: str
249
+ """A sentence to show to the user, when the library has a better one than
250
+ ``description`` (Apple sign-in); otherwise ``""``."""
251
+
252
+
253
+ def auth_error_from_nserror(error: ObjCObject) -> AuthError:
254
+ """Turn an ``NSError`` from Firebase Auth into an ``AuthError``.
255
+
256
+ Args:
257
+ error: the ``NSError``.
258
+
259
+ Returns:
260
+ An ``AuthError`` with the ``FirebaseError`` keys, plus ``name``,
261
+ ``email``, ``updated_credential``, ``resolver``, ``cancelled``
262
+ (False) and ``message`` (``""``).
263
+ """
264
+ from .credentials import AuthCredential
265
+ from .multi_factor import MultiFactorResolver
266
+
267
+ result = AuthError(nserror_to_dict(error) or {})
268
+ is_auth = result.domain == AUTH_ERROR_DOMAIN
269
+ result.name = _CODE_NAMES.get(result.code, "") if is_auth else ""
270
+ user_info = error.userInfo
271
+ email = user_info.objectForKey_(_EMAIL_KEY)
272
+ result.email = None if email is None else str(email)
273
+ credential = user_info.objectForKey_(_UPDATED_CREDENTIAL_KEY)
274
+ result.updated_credential = (
275
+ None if credential is None else AuthCredential(credential)
276
+ )
277
+ resolver = user_info.objectForKey_(_RESOLVER_KEY)
278
+ result.resolver = None if resolver is None else MultiFactorResolver(resolver)
279
+ result.cancelled = is_auth and result.code == AuthErrorCode.WEB_CONTEXT_CANCELLED
280
+ result.message = ""
281
+ return result
282
+
283
+
284
+ def auth_error_from_pointer(error_ptr: Any) -> AuthError | None:
285
+ """``auth_error_from_nserror`` for a raw block argument.
286
+
287
+ Args:
288
+ error_ptr: the ``NSError *`` a completion block received, or a null
289
+ pointer / ``None`` when there was no error.
290
+
291
+ Returns:
292
+ An ``AuthError``, or ``None`` when there was no error.
293
+ """
294
+ return auth_error_from_nserror(objc_instance(error_ptr)) if error_ptr else None
295
+
296
+
297
+ def simple_auth_error(
298
+ description: str, code: int = 0, *, cancelled: bool = False
299
+ ) -> AuthError:
300
+ """An ``AuthError`` made in Python, for failures before Firebase is called.
301
+
302
+ Args:
303
+ description: what went wrong.
304
+ code: the error code; 0 when there is none.
305
+ cancelled: True when the user cancelled.
306
+
307
+ Returns:
308
+ An ``AuthError`` with an empty ``domain``.
309
+ """
310
+ return AuthError(
311
+ code=code,
312
+ description=description,
313
+ domain="",
314
+ underlying_error=None,
315
+ name="",
316
+ email=None,
317
+ updated_credential=None,
318
+ resolver=None,
319
+ cancelled=cancelled,
320
+ message="",
321
+ )
@@ -0,0 +1,240 @@
1
+ """Facebook Login, then Firebase Auth.
2
+
3
+ from pyios_firebase.auth import facebook
4
+
5
+ facebook.initialize_facebook() # once, at app start
6
+
7
+ def on_signed_in(result, error):
8
+ if error is not None:
9
+ if not error.cancelled:
10
+ print("Sign-in failed:", error.description)
11
+ return
12
+ print("Signed in:", result.user.uid)
13
+
14
+ facebook.start_facebook_sign_in(on_signed_in)
15
+
16
+ Both of Facebook's login modes work:
17
+
18
+ - classic login gives an access token (also usable with the Graph API);
19
+ - Limited Login (``limited=True``, and what iOS falls back to when the user
20
+ has not allowed tracking) gives an OpenID Connect token, checked with a
21
+ nonce.
22
+
23
+ Needs the ``FacebookLogin`` framework (Swift package
24
+ ``https://github.com/facebook/facebook-ios-sdk``, product ``FacebookLogin``),
25
+ the Facebook provider enabled in the Firebase console, and in ``Info.plist``:
26
+ ``FacebookAppID``, ``FacebookClientToken``, ``FacebookDisplayName``, the URL
27
+ scheme ``fb<APP_ID>``, and ``LSApplicationQueriesSchemes`` with ``fbapi`` and
28
+ ``fb-messenger-share-api``.
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ __all__ = (
34
+ "FACEBOOK_LOGIN_TRACKING_ENABLED",
35
+ "FACEBOOK_LOGIN_TRACKING_LIMITED",
36
+ "get_facebook_credential",
37
+ "initialize_facebook",
38
+ "on_facebook_handle_url",
39
+ "on_facebook_handle_url_kivy",
40
+ "sign_out_of_facebook",
41
+ "start_facebook_sign_in",
42
+ )
43
+
44
+ import hashlib
45
+ import secrets
46
+ from collections.abc import Callable, Sequence
47
+ from typing import Any, Final
48
+
49
+ from rubicon.objc import ns_from_py, objc_id
50
+
51
+ from ..types import ObjCObject
52
+ from ..util import objc_class, objc_instance, one_shot_block
53
+ from ._common import flag
54
+ from ._ui import root_view_controller
55
+ from .auth import Auth
56
+ from .credentials import AuthCredential, FacebookAuthProvider, OAuthProvider
57
+ from .errors import AuthError, auth_error_from_nserror, simple_auth_error
58
+ from .results import AuthDataResult
59
+
60
+ FACEBOOK_LOGIN_TRACKING_ENABLED: Final = 0
61
+ """``FBSDKLoginTrackingEnabled``: classic login (an access token)."""
62
+ FACEBOOK_LOGIN_TRACKING_LIMITED: Final = 1
63
+ """``FBSDKLoginTrackingLimited``: Limited Login (an OpenID Connect token)."""
64
+
65
+ DEFAULT_PERMISSIONS: Final = ("public_profile", "email")
66
+
67
+ CredentialCallback = Callable[[AuthCredential | None, AuthError | None], None]
68
+ DataResultCallback = Callable[[AuthDataResult | None, AuthError | None], None]
69
+
70
+ # each login keeps its LoginManager alive until Facebook answers
71
+ _pending: set[ObjCObject] = set()
72
+
73
+
74
+ def _application() -> ObjCObject:
75
+ return objc_class("UIApplication").sharedApplication
76
+
77
+
78
+ def initialize_facebook() -> bool:
79
+ """Start the Facebook SDK. Call it once, at app start (Facebook's
80
+ ``application(_:didFinishLaunchingWithOptions:)`` step).
81
+
82
+ Returns:
83
+ What the Facebook SDK returns (True when it started).
84
+ """
85
+ delegate = objc_class("FBSDKApplicationDelegate").sharedInstance
86
+ return bool(
87
+ delegate.application_didFinishLaunchingWithOptions_(_application(), None)
88
+ )
89
+
90
+
91
+ def on_facebook_handle_url(url_string: str) -> bool:
92
+ """Give a URL the app was opened with to the Facebook SDK (the login
93
+ redirect back from the Facebook app or Safari).
94
+
95
+ Args:
96
+ url_string: the URL.
97
+
98
+ Returns:
99
+ True if the Facebook SDK handled it.
100
+ """
101
+ url = objc_class("NSURL").URLWithString_(url_string)
102
+ delegate = objc_class("FBSDKApplicationDelegate").sharedInstance
103
+ return bool(
104
+ delegate.application_openURL_options_(_application(), url, ns_from_py({}))
105
+ )
106
+
107
+
108
+ def on_facebook_handle_url_kivy(
109
+ _window: Any, filename: str | bytes, *_args: Any
110
+ ) -> None:
111
+ """Kivy handler: ``Window.bind(on_drop_file=on_facebook_handle_url_kivy)``.
112
+
113
+ Kivy (SDL) passes URLs the app was opened with as "dropped files"; this
114
+ forwards them to ``on_facebook_handle_url``.
115
+
116
+ Args:
117
+ filename: the URL, as Kivy passes it (``bytes`` or ``str``). The
118
+ other arguments Kivy passes (the window, the position) are ignored.
119
+ """
120
+ url_string = filename.decode("utf-8") if isinstance(filename, bytes) else filename
121
+ on_facebook_handle_url(url_string)
122
+
123
+
124
+ def _credential_from_result(
125
+ result: ObjCObject, raw_nonce: str
126
+ ) -> AuthCredential | AuthError:
127
+ if flag(result, "isCancelled"):
128
+ return simple_auth_error("Facebook login was cancelled", cancelled=True)
129
+ if result.token is not None:
130
+ return FacebookAuthProvider.credential(str(result.token.tokenString))
131
+ if result.authenticationToken is not None:
132
+ return OAuthProvider.credential(
133
+ FacebookAuthProvider.PROVIDER_ID,
134
+ id_token=str(result.authenticationToken.tokenString),
135
+ raw_nonce=raw_nonce,
136
+ )
137
+ return simple_auth_error("Facebook login gave no token")
138
+
139
+
140
+ def get_facebook_credential(
141
+ callback: CredentialCallback,
142
+ permissions: Sequence[str] = DEFAULT_PERMISSIONS,
143
+ limited: bool = False,
144
+ ) -> None:
145
+ """Show Facebook Login and give back a Firebase credential, without signing
146
+ in to Firebase (for example to link Facebook to the current user).
147
+
148
+ Args:
149
+ callback: gets ``(credential, None)``, or ``(None, error)``.
150
+ ``error.cancelled`` is True when the user cancelled.
151
+ permissions: the Facebook permissions to ask for.
152
+ limited: True for Limited Login (no tracking). Facebook may also use
153
+ Limited Login on its own when the user has not allowed tracking;
154
+ both give a working credential.
155
+ """
156
+ raw_nonce = secrets.token_urlsafe(32)
157
+ try:
158
+ # Firebase checks the token against SHA-256(raw nonce), as for Apple
159
+ configuration = (
160
+ objc_class("FBSDKLoginConfiguration")
161
+ .alloc()
162
+ .initWithPermissions_tracking_nonce_(
163
+ ns_from_py(list(permissions)),
164
+ FACEBOOK_LOGIN_TRACKING_LIMITED
165
+ if limited
166
+ else FACEBOOK_LOGIN_TRACKING_ENABLED,
167
+ hashlib.sha256(raw_nonce.encode()).hexdigest(),
168
+ )
169
+ )
170
+ if configuration is None:
171
+ raise ValueError(
172
+ f"Facebook rejected the login configuration (permissions {list(permissions)})"
173
+ )
174
+ manager = objc_class("FBSDKLoginManager").alloc().init()
175
+ presenter = root_view_controller()
176
+ except Exception as e: # noqa: BLE001 - must always answer the caller
177
+ callback(None, simple_auth_error(str(e)))
178
+ return
179
+
180
+ def done(result_ptr: Any, error_ptr: Any) -> None:
181
+ _pending.discard(manager)
182
+ if error_ptr:
183
+ callback(None, auth_error_from_nserror(objc_instance(error_ptr)))
184
+ return
185
+ if not result_ptr:
186
+ callback(None, simple_auth_error("Facebook login gave no result"))
187
+ return
188
+ try:
189
+ outcome = _credential_from_result(objc_instance(result_ptr), raw_nonce)
190
+ except Exception as e: # noqa: BLE001
191
+ callback(
192
+ None, simple_auth_error(f"could not read the Facebook result: {e!r}")
193
+ )
194
+ return
195
+ if isinstance(outcome, AuthCredential):
196
+ callback(outcome, None)
197
+ else:
198
+ callback(None, outcome)
199
+
200
+ _pending.add(manager)
201
+ manager.logInFromViewController_configuration_completion_(
202
+ presenter, configuration, one_shot_block(done, objc_id, objc_id)
203
+ )
204
+
205
+
206
+ def start_facebook_sign_in(
207
+ callback: DataResultCallback,
208
+ permissions: Sequence[str] = DEFAULT_PERMISSIONS,
209
+ limited: bool = False,
210
+ auth: Auth | None = None,
211
+ ) -> None:
212
+ """Show Facebook Login, then sign in to Firebase.
213
+
214
+ Args:
215
+ callback: gets ``(AuthDataResult, None)``, or ``(None, error)``.
216
+ ``error.cancelled`` is True when the user cancelled: not a failure
217
+ to report. ``ACCOUNT_EXISTS_WITH_DIFFERENT_CREDENTIAL`` means the
218
+ email already has an account with another provider.
219
+ permissions: the Facebook permissions to ask for.
220
+ limited: True for Limited Login (see ``get_facebook_credential``).
221
+ auth: the ``Auth`` to sign in to (default: ``Auth.auth()``).
222
+ """
223
+
224
+ def got_credential(
225
+ credential: AuthCredential | None, error: AuthError | None
226
+ ) -> None:
227
+ if credential is None:
228
+ callback(None, error)
229
+ return
230
+ (auth or Auth.auth()).sign_in_with_credential(credential, callback)
231
+
232
+ get_facebook_credential(got_credential, permissions, limited)
233
+
234
+
235
+ def sign_out_of_facebook() -> None:
236
+ """Log out of Facebook Login (the next login asks again).
237
+
238
+ Call it with ``Auth.sign_out()``, which signs out of Firebase only.
239
+ """
240
+ objc_class("FBSDKLoginManager").alloc().init().logOut()