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,97 @@
1
+ """Keeps the name Apple shares at a user's FIRST authorization until Firebase
2
+ has it.
3
+
4
+ Sign in with Apple sends the name once per user per app. If the Firebase
5
+ sign-in that should carry it fails (for example a flaky network), every retry
6
+ gets no name. So the name is saved here, keyed by Apple's stable user ID
7
+ (``credential.user``), as soon as Apple sends it, reused by a retry that gets
8
+ none, and deleted once Firebase sign-in works.
9
+
10
+ The store is NSUserDefaults on iOS; tests can pass a dict.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ __all__ = ("AppleNameCache", "NameStore", "UserDefaultsStore")
16
+
17
+ from collections.abc import Sequence
18
+ from typing import Any, Final, Protocol
19
+
20
+ DEFAULTS_KEY: Final = "pyios_firebase.apple_sign_in.pending_names"
21
+
22
+
23
+ class NameStore(Protocol):
24
+ """Where names are kept: ``{apple_user: [given, family]}``. A ``dict`` works."""
25
+
26
+ def get(self, apple_user: str, default: Any = None, /) -> Any: ...
27
+ def __setitem__(self, apple_user: str, value: list[str], /) -> None: ...
28
+ def pop(self, apple_user: str, default: Any = None, /) -> Any: ...
29
+
30
+
31
+ class UserDefaultsStore:
32
+ """A ``NameStore`` kept in one NSUserDefaults entry, so it survives restarts."""
33
+
34
+ def __init__(self, key: str = DEFAULTS_KEY) -> None:
35
+ from rubicon.objc import ns_from_py
36
+
37
+ from ..util import objc_class, to_python
38
+
39
+ self._key = key
40
+ self._defaults = objc_class("NSUserDefaults").standardUserDefaults
41
+ self._ns, self._py = ns_from_py, to_python
42
+
43
+ def _load(self) -> dict[str, list[str]]:
44
+ value = self._defaults.objectForKey_(self._key)
45
+ return dict(self._py(value)) if value is not None else {}
46
+
47
+ def _save(self, data: dict[str, list[str]]) -> None:
48
+ if data:
49
+ self._defaults.setObject_forKey_(self._ns(data), self._key)
50
+ else:
51
+ self._defaults.removeObjectForKey_(self._key)
52
+
53
+ def get(self, apple_user: str, default: Any = None, /) -> Any:
54
+ return self._load().get(apple_user, default)
55
+
56
+ def __setitem__(self, apple_user: str, value: list[str], /) -> None:
57
+ data = self._load()
58
+ data[apple_user] = list(value)
59
+ self._save(data)
60
+
61
+ def pop(self, apple_user: str, default: Any = None, /) -> Any:
62
+ data = self._load()
63
+ value = data.pop(apple_user, default)
64
+ self._save(data)
65
+ return value
66
+
67
+
68
+ class AppleNameCache:
69
+ """Remembers the name Apple sent until a Firebase sign-in carries it."""
70
+
71
+ def __init__(self, store: NameStore | None = None) -> None:
72
+ self._store: NameStore = store if store is not None else UserDefaultsStore()
73
+
74
+ def resolve(
75
+ self, apple_user: str, first: str | None, last: str | None
76
+ ) -> tuple[str, str]:
77
+ """The name to use for this authorization: Apple's when it sent one
78
+ (which is then saved), else the one saved by an earlier attempt.
79
+
80
+ Returns:
81
+ ``(first_name, last_name)``; ``""`` for parts nobody knows.
82
+ """
83
+ first, last = first or "", last or ""
84
+ if not apple_user:
85
+ return first, last
86
+ if first or last:
87
+ self._store[apple_user] = [first, last]
88
+ return first, last
89
+ saved: Sequence[str | None] | None = self._store.get(apple_user)
90
+ if saved:
91
+ return str(saved[0] or ""), str(saved[1] or "")
92
+ return "", ""
93
+
94
+ def forget(self, apple_user: str | None) -> None:
95
+ """Drop the saved name: Firebase has it now (call after sign-in works)."""
96
+ if apple_user:
97
+ self._store.pop(apple_user, None)
@@ -0,0 +1,121 @@
1
+ """Helpers shared by the auth modules: block builders and value readers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ from typing import Any, TypeVar
7
+
8
+ from rubicon.objc import Block, objc_id
9
+
10
+ from ..types import ObjCObject
11
+ from ..util import objc_class, objc_instance, one_shot_block, to_python
12
+ from .errors import AuthError, auth_error_from_pointer, simple_auth_error
13
+
14
+ T = TypeVar("T")
15
+
16
+
17
+ def flag(obj: ObjCObject, name: str) -> bool:
18
+ """Read a Swift ``Bool`` property, whichever way Rubicon exposes it.
19
+
20
+ A plain getter is an attribute; a custom getter (``getter=isX``) is a
21
+ method, and reading it without calling it would always be truthy.
22
+ """
23
+ value = getattr(obj, name)
24
+ return bool(value() if callable(value) else value)
25
+
26
+
27
+ def text(value: object) -> str | None:
28
+ """``str(value)``, or ``None`` for ``None``."""
29
+ return None if value is None else str(value)
30
+
31
+
32
+ def url_text(url: ObjCObject | None) -> str | None:
33
+ """An ``NSURL`` as a string, or ``None``."""
34
+ return None if url is None else str(url.absoluteString)
35
+
36
+
37
+ def result_block(
38
+ callback: Callable[[T | None, AuthError | None], None] | None,
39
+ wrap: Callable[[ObjCObject], T],
40
+ ) -> Block:
41
+ """Completion block for ``(result, NSError)``: ``callback(wrap(result), error)``."""
42
+
43
+ def done(result_ptr: Any, error_ptr: Any) -> None:
44
+ if callback is None:
45
+ return
46
+ error = auth_error_from_pointer(error_ptr)
47
+ if error is not None or not result_ptr:
48
+ callback(None, error)
49
+ return
50
+ try:
51
+ result = wrap(objc_instance(result_ptr))
52
+ except Exception as e: # noqa: BLE001 - must always answer the caller
53
+ # an exception here would be swallowed by the SDK's block, and the
54
+ # caller would wait forever
55
+ callback(None, simple_auth_error(f"could not read the result: {e!r}"))
56
+ return
57
+ callback(result, None)
58
+
59
+ return one_shot_block(done, objc_id, objc_id)
60
+
61
+
62
+ def error_block(callback: Callable[[AuthError | None], None] | None) -> Block:
63
+ """Completion block for ``(NSError)``: ``callback(error)``."""
64
+
65
+ def done(error_ptr: Any) -> None:
66
+ if callback is not None:
67
+ callback(auth_error_from_pointer(error_ptr))
68
+
69
+ return one_shot_block(done, objc_id)
70
+
71
+
72
+ def string_block(callback: Callable[[str | None, AuthError | None], None]) -> Block:
73
+ """Completion block for ``(NSString, NSError)``."""
74
+ return result_block(callback, lambda value: str(value))
75
+
76
+
77
+ def registered_url_schemes() -> set[str]:
78
+ """The URL schemes in the app's ``Info.plist`` (``CFBundleURLTypes``), lower case."""
79
+ url_types = objc_class("NSBundle").mainBundle.objectForInfoDictionaryKey_(
80
+ "CFBundleURLTypes"
81
+ )
82
+ schemes: set[str] = set()
83
+ for url_type in to_python(url_types) or []:
84
+ for scheme in (
85
+ url_type.get("CFBundleURLSchemes", []) if isinstance(url_type, dict) else []
86
+ ):
87
+ schemes.add(str(scheme).lower())
88
+ return schemes
89
+
90
+
91
+ def missing_callback_scheme(auth_native: ObjCObject) -> str | None:
92
+ """Why phone and web sign-in cannot start, or ``None`` if they can.
93
+
94
+ The Firebase SDK stops the whole app (``fatalError``) when the callback URL
95
+ scheme is not in ``Info.plist``. This checks the same thing first: the
96
+ reversed client ID if it is registered, else ``app-<GOOGLE_APP_ID>``.
97
+ """
98
+ app = auth_native.app
99
+ options = app.options if app is not None else None
100
+ if options is None:
101
+ return "Firebase is not configured: call core.configure() first"
102
+ schemes = registered_url_schemes()
103
+ reversed_client_id = None
104
+ if options.clientID is not None:
105
+ reversed_client_id = ".".join(reversed(str(options.clientID).split(".")))
106
+ if reversed_client_id.lower() in schemes:
107
+ return None
108
+ if options.googleAppID is None:
109
+ return "the Firebase options have no GOOGLE_APP_ID"
110
+ app_scheme = "app-" + str(options.googleAppID).replace(":", "-")
111
+ if app_scheme.lower() in schemes:
112
+ return None
113
+ wanted = (
114
+ app_scheme
115
+ if reversed_client_id is None
116
+ else f"{reversed_client_id} (or {app_scheme})"
117
+ )
118
+ return (
119
+ f"add the URL scheme {wanted} to CFBundleURLTypes in Info.plist: Firebase Auth "
120
+ "needs it for phone and web sign-in, and stops the app without it"
121
+ )
@@ -0,0 +1,21 @@
1
+ from __future__ import annotations
2
+
3
+ from ..types import ObjCObject
4
+ from ..util import objc_class
5
+
6
+
7
+ def key_window() -> ObjCObject | None:
8
+ """The app's key window (scene-based apps first, then the old ``keyWindow``)."""
9
+ app = objc_class("UIApplication").sharedApplication
10
+ for scene in app.connectedScenes.allObjects():
11
+ for window in getattr(scene, "windows", None) or []:
12
+ if window.isKeyWindow(): # a getter with a custom name: must be called
13
+ return window
14
+ fallback: ObjCObject | None = app.keyWindow
15
+ return fallback
16
+
17
+
18
+ def root_view_controller() -> ObjCObject | None:
19
+ """The key window's root view controller, to present sign-in sheets from."""
20
+ window = key_window()
21
+ return window.rootViewController if window is not None else None
@@ -0,0 +1,326 @@
1
+ """Sign in with Apple, then Firebase Auth.
2
+
3
+ from pyios_firebase.auth.apple import start_apple_sign_in
4
+
5
+ def on_signed_in(result, error):
6
+ if error is not None:
7
+ if not error.cancelled:
8
+ print(error.message or error.description)
9
+ return
10
+ print("Signed in:", result.user.uid, result.first_name)
11
+
12
+ start_apple_sign_in(on_signed_in)
13
+
14
+ Apple shares the name (and a real or relay email) only the FIRST time a user
15
+ signs in to the app; later sign-ins give ``""`` for those. The name is kept on
16
+ the device until a Firebase sign-in carries it.
17
+
18
+ Needs the ``com.apple.developer.applesignin`` entitlement and the Apple
19
+ provider enabled in the Firebase console (Authentication, Sign-in method).
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ __all__ = (
25
+ "AUTHORIZATION_ERROR_CANCELED",
26
+ "AUTHORIZATION_ERROR_DOMAIN",
27
+ "AUTHORIZATION_ERROR_UNKNOWN",
28
+ "AppleCredential",
29
+ "AppleSignInResult",
30
+ "get_apple_credential",
31
+ "start_apple_sign_in",
32
+ )
33
+
34
+ import hashlib
35
+ import secrets
36
+ from collections.abc import Callable
37
+ from dataclasses import dataclass
38
+ from typing import Any, Final, cast
39
+
40
+ from rubicon.objc import NSObject, ObjCInstance, ObjCProtocol, ns_from_py, objc_method
41
+ from rubicon.objc.runtime import load_library, objc_id
42
+
43
+ from ..types import ObjCObject
44
+ from ..util import objc_class
45
+ from ._apple_name_cache import AppleNameCache
46
+ from ._ui import key_window
47
+ from .auth import Auth
48
+ from .credentials import OAuthCredential, OAuthProvider
49
+ from .errors import AuthError, auth_error_from_nserror, simple_auth_error
50
+ from .results import AuthDataResult
51
+
52
+ _auth_services = load_library("AuthenticationServices")
53
+
54
+
55
+ def _string_constant(name: str, fallback: str) -> str:
56
+ """An NSString constant exported by AuthenticationServices."""
57
+ try:
58
+ value = objc_id.in_dll(_auth_services, name)
59
+ except ValueError:
60
+ return fallback
61
+ return str(ObjCInstance(value)) if value.value else fallback
62
+
63
+
64
+ SCOPE_FULL_NAME: Final = _string_constant("ASAuthorizationScopeFullName", "full_name")
65
+ SCOPE_EMAIL: Final = _string_constant("ASAuthorizationScopeEmail", "email")
66
+ AUTHORIZATION_ERROR_DOMAIN: Final = _string_constant(
67
+ "ASAuthorizationErrorDomain", "com.apple.AuthenticationServices.AuthorizationError"
68
+ )
69
+ """The error domain of Apple's sign-in errors."""
70
+ AUTHORIZATION_ERROR_UNKNOWN: Final = 1000
71
+ """Set-up problems, or no Apple Account on the device (also: the user closed
72
+ iOS's "sign in to your Apple Account" alert)."""
73
+ AUTHORIZATION_ERROR_CANCELED: Final = 1001
74
+ """``ASAuthorizationErrorCanceled``: the user closed the Apple sheet."""
75
+
76
+ NOT_SIGNED_IN_MESSAGE: Final = (
77
+ "Sign in with Apple didn't finish. Check that you're signed in to your "
78
+ "Apple Account in Settings, then try again"
79
+ )
80
+
81
+ NSUTF8StringEncoding: Final = 4
82
+
83
+
84
+ @dataclass(frozen=True)
85
+ class AppleCredential:
86
+ """What Sign in with Apple gave, ready for Firebase."""
87
+
88
+ credential: OAuthCredential
89
+ """The Firebase credential (provider ``"apple.com"``)."""
90
+ apple_user: str
91
+ """Apple's stable ID for the user in this app."""
92
+ first_name: str
93
+ """The given name: only on the first sign-in to the app, else ``""``
94
+ (or the name kept from an earlier, failed attempt)."""
95
+ last_name: str
96
+ """The family name: same rules as ``first_name``."""
97
+ email: str
98
+ """The email (real or private relay): only on the first sign-in, else ``""``."""
99
+ authorization_code: str
100
+ """A short-lived code: pass it to ``Auth.revoke_token`` before deleting the
101
+ account (App Store rule)."""
102
+
103
+
104
+ class AppleSignInResult(AuthDataResult):
105
+ """A Firebase sign-in with Apple: an ``AuthDataResult`` plus what Apple shared.
106
+
107
+ Args:
108
+ native: the native ``FIRAuthDataResult``.
109
+ apple: what Apple gave for this sign-in.
110
+ """
111
+
112
+ def __init__(self, native: ObjCObject, apple: AppleCredential) -> None:
113
+ super().__init__(native)
114
+ self.apple = apple
115
+ """What Apple gave (names, email, authorization code)."""
116
+
117
+ @property
118
+ def first_name(self) -> str:
119
+ """The given name Apple shared (first sign-in only), else ``""``."""
120
+ return self.apple.first_name
121
+
122
+ @property
123
+ def last_name(self) -> str:
124
+ """The family name Apple shared (first sign-in only), else ``""``."""
125
+ return self.apple.last_name
126
+
127
+
128
+ AppleCredentialCallback = Callable[[AppleCredential | None, AuthError | None], None]
129
+ AppleSignInCallback = Callable[[AppleSignInResult | None, AuthError | None], None]
130
+
131
+ _names: AppleNameCache | None = None # made on first use (NSUserDefaults)
132
+ # the sign-ins in progress, by delegate: UIKit keeps the delegate only weakly
133
+ _pending: dict[int, dict[str, Any]] = {}
134
+
135
+
136
+ def _key(delegate: ObjCObject) -> int:
137
+ """The delegate's address: the key of its sign-in in ``_pending``."""
138
+ return int(delegate.ptr.value or 0)
139
+
140
+
141
+ def _name_cache() -> AppleNameCache:
142
+ global _names
143
+ if _names is None:
144
+ _names = AppleNameCache()
145
+ return _names
146
+
147
+
148
+ def _protocols(*names: str) -> list[ObjCProtocol]:
149
+ found = []
150
+ for name in names:
151
+ try:
152
+ found.append(ObjCProtocol(name))
153
+ except NameError:
154
+ pass
155
+ return found
156
+
157
+
158
+ def _apple_error(error: ObjCObject) -> AuthError:
159
+ result = auth_error_from_nserror(error)
160
+ apple = result.domain == AUTHORIZATION_ERROR_DOMAIN
161
+ result.cancelled = result.cancelled or (
162
+ apple and result.code == AUTHORIZATION_ERROR_CANCELED
163
+ )
164
+ # 1000 covers "no Apple Account on this device" (and set-up problems):
165
+ # not a network failure, so don't let the app say it is
166
+ if apple and result.code == AUTHORIZATION_ERROR_UNKNOWN:
167
+ result.message = NOT_SIGNED_IN_MESSAGE
168
+ return result
169
+
170
+
171
+ def _text(value: object) -> str:
172
+ return "" if value is None else str(value)
173
+
174
+
175
+ def _utf8(data: ObjCObject | None) -> str:
176
+ if data is None:
177
+ return ""
178
+ return str(
179
+ objc_class("NSString")
180
+ .alloc()
181
+ .initWithData_encoding_(data, NSUTF8StringEncoding)
182
+ )
183
+
184
+
185
+ class PyAppleSignInDelegate( # type: ignore[call-arg] # Rubicon's class keyword
186
+ NSObject,
187
+ protocols=_protocols(
188
+ "ASAuthorizationControllerDelegate",
189
+ "ASAuthorizationControllerPresentationContextProviding",
190
+ ),
191
+ ):
192
+ """The ``ASAuthorizationController`` delegate and presentation context.
193
+
194
+ Rubicon reads these methods' annotations to build their Objective-C
195
+ signatures: parameters must be annotated with ctypes types (``objc_id``),
196
+ not Python types.
197
+ """
198
+
199
+ @objc_method
200
+ def presentationAnchorForAuthorizationController_(
201
+ self, controller: objc_id
202
+ ) -> objc_id:
203
+ """The window the Apple sheet is shown over: the key window."""
204
+ # objc_id is what Rubicon needs for the signature; the value is an object
205
+ return cast("objc_id", key_window())
206
+
207
+ @objc_method
208
+ def authorizationController_didCompleteWithAuthorization_(
209
+ self, controller: objc_id, authorization: objc_id
210
+ ) -> None:
211
+ """Apple signed the user in: turn the identity token into a credential."""
212
+ state = _pending.pop(_key(self), None)
213
+ if state is None:
214
+ return
215
+ callback: AppleCredentialCallback = state["callback"]
216
+ auth: ObjCObject = (
217
+ authorization # an object at runtime (see the class docstring)
218
+ )
219
+ try:
220
+ credential = auth.credential
221
+ if not credential.isKindOfClass(
222
+ objc_class("ASAuthorizationAppleIDCredential")
223
+ ):
224
+ callback(None, simple_auth_error("Unexpected credential type"))
225
+ return
226
+ id_token = _utf8(credential.identityToken)
227
+ if not id_token:
228
+ callback(None, simple_auth_error("Apple returned no identity token"))
229
+ return
230
+ apple_user = _text(credential.user)
231
+ name = credential.fullName
232
+ sent_first = _text(name.givenName) if name is not None else ""
233
+ sent_last = _text(name.familyName) if name is not None else ""
234
+ # Apple sends the name only once: reuse the one an earlier,
235
+ # failed attempt saved
236
+ first, last = _name_cache().resolve(apple_user, sent_first, sent_last)
237
+ result = AppleCredential(
238
+ credential=OAuthProvider.apple_credential(
239
+ id_token, state["raw_nonce"], first, last
240
+ ),
241
+ apple_user=apple_user,
242
+ first_name=first,
243
+ last_name=last,
244
+ email=_text(credential.email),
245
+ authorization_code=_utf8(credential.authorizationCode),
246
+ )
247
+ except Exception as e: # noqa: BLE001 - must always answer the caller
248
+ callback(None, simple_auth_error(str(e)))
249
+ return
250
+ callback(result, None)
251
+
252
+ @objc_method
253
+ def authorizationController_didCompleteWithError_(
254
+ self, controller: objc_id, error: objc_id
255
+ ) -> None:
256
+ """Apple sign-in failed or was cancelled: report it."""
257
+ state = _pending.pop(_key(self), None)
258
+ if state is not None:
259
+ state["callback"](None, _apple_error(error))
260
+
261
+
262
+ def get_apple_credential(callback: AppleCredentialCallback) -> None:
263
+ """Show the Sign in with Apple sheet and give back a Firebase credential,
264
+ without signing in to Firebase (for example to link Apple to the current
265
+ user, or to re-authenticate before deleting the account).
266
+
267
+ Args:
268
+ callback: gets ``(AppleCredential, None)``, or ``(None, error)``.
269
+ ``error.cancelled`` is True when the user closed the sheet;
270
+ ``error.message``, when not ``""``, is a sentence for the user.
271
+ """
272
+ # Apple signs SHA-256(nonce); Firebase checks it against the raw nonce
273
+ raw_nonce = secrets.token_urlsafe(32)
274
+ request = (
275
+ objc_class("ASAuthorizationAppleIDProvider").alloc().init().createRequest()
276
+ )
277
+ request.requestedScopes = ns_from_py([SCOPE_FULL_NAME, SCOPE_EMAIL])
278
+ request.nonce = hashlib.sha256(raw_nonce.encode()).hexdigest()
279
+
280
+ delegate = PyAppleSignInDelegate.alloc().init()
281
+ controller = (
282
+ objc_class("ASAuthorizationController")
283
+ .alloc()
284
+ .initWithAuthorizationRequests_(ns_from_py([request]))
285
+ )
286
+ controller.delegate = delegate # weak in UIKit: _pending keeps both alive
287
+ controller.presentationContextProvider = delegate
288
+ _pending[_key(delegate)] = {
289
+ "callback": callback,
290
+ "raw_nonce": raw_nonce,
291
+ "delegate": delegate,
292
+ "controller": controller,
293
+ }
294
+ controller.performRequests()
295
+
296
+
297
+ def start_apple_sign_in(
298
+ callback: AppleSignInCallback, auth: Auth | None = None
299
+ ) -> None:
300
+ """Show the Sign in with Apple sheet, then sign in to Firebase.
301
+
302
+ Args:
303
+ callback: gets ``(AppleSignInResult, None)``, or ``(None, error)``.
304
+ ``error.cancelled`` is True when the user closed the sheet: not a
305
+ failure to report. ``error.message``, when not ``""``, is a
306
+ sentence to show to the user.
307
+ auth: the ``Auth`` to sign in to (default: ``Auth.auth()``).
308
+ """
309
+
310
+ def got_credential(apple: AppleCredential | None, error: AuthError | None) -> None:
311
+ if apple is None:
312
+ callback(None, error)
313
+ return
314
+
315
+ def signed_in(
316
+ result: AuthDataResult | None, sign_in_error: AuthError | None
317
+ ) -> None:
318
+ if result is None:
319
+ callback(None, sign_in_error)
320
+ return
321
+ _name_cache().forget(apple.apple_user) # Firebase has the name now
322
+ callback(AppleSignInResult(result._native, apple), None)
323
+
324
+ (auth or Auth.auth()).sign_in_with_credential(apple.credential, signed_in)
325
+
326
+ get_apple_credential(got_credential)