pythonnative 0.35.0__py3-none-any.whl → 0.37.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 (176) hide show
  1. pythonnative/__init__.py +6 -4
  2. pythonnative/_ios_log.py +1 -1
  3. pythonnative/alerts.py +34 -88
  4. pythonnative/bootstrap.py +90 -0
  5. pythonnative/bridge/__init__.py +306 -0
  6. pythonnative/bridge/android.py +78 -0
  7. pythonnative/bridge/codec.py +164 -0
  8. pythonnative/bridge/fake.py +236 -0
  9. pythonnative/bridge/ios.py +138 -0
  10. pythonnative/cli/pn.py +24 -6
  11. pythonnative/components/layout.py +65 -3
  12. pythonnative/gestures.py +13 -10
  13. pythonnative/hooks.py +41 -26
  14. pythonnative/hosts/__init__.py +13 -36
  15. pythonnative/hosts/native.py +348 -0
  16. pythonnative/images.py +6 -5
  17. pythonnative/mutations.py +21 -0
  18. pythonnative/native_modules/__init__.py +30 -29
  19. pythonnative/native_modules/app_state.py +10 -6
  20. pythonnative/native_modules/battery.py +20 -84
  21. pythonnative/native_modules/biometrics.py +11 -124
  22. pythonnative/native_modules/camera.py +15 -291
  23. pythonnative/native_modules/clipboard.py +14 -109
  24. pythonnative/native_modules/desktop.py +322 -0
  25. pythonnative/native_modules/file_system.py +15 -21
  26. pythonnative/native_modules/haptics.py +19 -129
  27. pythonnative/native_modules/linking.py +24 -137
  28. pythonnative/native_modules/location.py +28 -180
  29. pythonnative/native_modules/net_info.py +25 -135
  30. pythonnative/native_modules/notifications.py +36 -257
  31. pythonnative/native_modules/permissions.py +20 -184
  32. pythonnative/native_modules/registry.py +513 -0
  33. pythonnative/native_modules/secure_store.py +18 -155
  34. pythonnative/native_modules/share.py +8 -113
  35. pythonnative/native_views/__init__.py +67 -86
  36. pythonnative/native_views/bridge_backend.py +264 -0
  37. pythonnative/platform.py +19 -15
  38. pythonnative/platform_metrics.py +3 -3
  39. pythonnative/project/android.py +0 -3
  40. pythonnative/project/builder.py +38 -16
  41. pythonnative/project/config.py +12 -2
  42. pythonnative/project/plugins.py +366 -0
  43. pythonnative/py.typed +0 -0
  44. pythonnative/runtime.py +35 -203
  45. pythonnative/sdk/__init__.py +48 -33
  46. pythonnative/sdk/_components.py +57 -76
  47. pythonnative/storage.py +18 -243
  48. pythonnative/templates/android_template/app/build.gradle +6 -9
  49. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt +34 -166
  50. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/ScreenFragment.kt +13 -151
  51. pythonnative/templates/android_template/pythonnative/build.gradle +48 -0
  52. pythonnative/templates/android_template/pythonnative/src/main/AndroidManifest.xml +11 -0
  53. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/PNBridge.kt +214 -0
  54. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/PythonHost.kt +20 -0
  55. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/animation/AnimationSpecs.kt +141 -0
  56. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/animation/PNAnimator.kt +249 -0
  57. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/JsonUtil.kt +175 -0
  58. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/MainThread.kt +32 -0
  59. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/PNLog.kt +33 -0
  60. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/PNRegistry.kt +82 -0
  61. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/PNTransaction.kt +80 -0
  62. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/TransactionApplier.kt +115 -0
  63. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/ViewRegistry.kt +74 -0
  64. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/BuiltinComponents.kt +35 -0
  65. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ButtonManager.kt +33 -0
  66. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ComponentManager.kt +213 -0
  67. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ContainerManagers.kt +69 -0
  68. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ControlManagers.kt +186 -0
  69. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/DatePickerManager.kt +123 -0
  70. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ImageLoader.kt +141 -0
  71. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ImageManager.kt +156 -0
  72. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ModalManager.kt +134 -0
  73. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PNColor.kt +201 -0
  74. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PickerManager.kt +87 -0
  75. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PortalManager.kt +55 -0
  76. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PressableManager.kt +122 -0
  77. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ScrollViewManager.kt +214 -0
  78. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/SegmentedControlManager.kt +113 -0
  79. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/StatusBarManager.kt +52 -0
  80. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/TabBarManager.kt +131 -0
  81. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/TextInputManager.kt +245 -0
  82. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/TextManager.kt +233 -0
  83. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ViewStyler.kt +355 -0
  84. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/VirtualListManager.kt +179 -0
  85. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/WebViewManager.kt +149 -0
  86. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/gestures/GestureArbiter.kt +233 -0
  87. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/gestures/GestureCoordinator.kt +127 -0
  88. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/gestures/GestureRecognizers.kt +527 -0
  89. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/BuiltinModules.kt +73 -0
  90. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/DeviceModules.kt +316 -0
  91. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/HostModule.kt +97 -0
  92. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/MediaModules.kt +200 -0
  93. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/NativeModule.kt +135 -0
  94. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/NotificationsModule.kt +100 -0
  95. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/PermissionsModule.kt +88 -0
  96. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/StorageModules.kt +89 -0
  97. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/SystemModules.kt +178 -0
  98. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/plugins/GeneratedPlugins.kt +16 -0
  99. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/screens/Navigator.kt +116 -0
  100. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/screens/PNScreenFragment.kt +223 -0
  101. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/screens/ScreenRegistry.kt +80 -0
  102. pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNAccessibilityDelegate.kt +5 -4
  103. pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNBorderDrawable.kt +12 -3
  104. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/views/PNEditText.kt +18 -0
  105. pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNFrameLayout.kt +9 -10
  106. pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNVirtualListView.java +9 -12
  107. pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/AnimationSpecsTest.kt +75 -0
  108. pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/GestureArbiterTest.kt +197 -0
  109. pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/PNColorTest.kt +47 -0
  110. pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/PNTransactionTest.kt +78 -0
  111. pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/PromiseTest.kt +75 -0
  112. pythonnative/templates/android_template/settings.gradle +1 -0
  113. pythonnative/templates/ios_template/PythonNativeKit/Package.swift +21 -0
  114. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Animation/PNAnimator.swift +301 -0
  115. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNBridge.swift +191 -0
  116. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNRegistry.swift +140 -0
  117. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNTransaction.swift +165 -0
  118. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNViewRegistry.swift +53 -0
  119. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNButtonManagers.swift +206 -0
  120. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNColor.swift +173 -0
  121. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNComponentManager.swift +183 -0
  122. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNContainerView.swift +56 -0
  123. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNControlManagers.swift +281 -0
  124. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNImageManager.swift +221 -0
  125. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNOverlayManagers.swift +243 -0
  126. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNPressableManager.swift +114 -0
  127. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNScrollViewManager.swift +197 -0
  128. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTabBarManager.swift +106 -0
  129. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTextInputManager.swift +289 -0
  130. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTextManager.swift +230 -0
  131. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTransform.swift +82 -0
  132. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNViewManager.swift +86 -0
  133. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNViewState.swift +87 -0
  134. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNViewStyler.swift +340 -0
  135. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNVirtualListManager.swift +257 -0
  136. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNWebViewManager.swift +124 -0
  137. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Gestures/PNGestureCoordinator.swift +260 -0
  138. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/AlertModule.swift +92 -0
  139. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/HostModule.swift +158 -0
  140. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/LifecycleModules.swift +174 -0
  141. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/MediaModules.swift +177 -0
  142. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/PNNativeModule.swift +125 -0
  143. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/PermissionModules.swift +251 -0
  144. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/SystemModules.swift +251 -0
  145. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Plugins/PNPluginRegistration.swift +8 -0
  146. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Screens/PNScreenRegistry.swift +46 -0
  147. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Screens/PNViewController.swift +251 -0
  148. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNCompat.swift +46 -0
  149. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNJSON.swift +173 -0
  150. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNLog.swift +47 -0
  151. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNWindow.swift +51 -0
  152. pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNColorTests.swift +50 -0
  153. pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNGestureTests.swift +101 -0
  154. pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNManagerTests.swift +124 -0
  155. pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNModuleTests.swift +98 -0
  156. pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNTransactionTests.swift +91 -0
  157. pythonnative/templates/ios_template/ios_template/AppDelegate.swift +8 -50
  158. pythonnative/templates/ios_template/ios_template/Info.plist +4 -4
  159. pythonnative/templates/ios_template/ios_template/PythonRuntime.swift +38 -46
  160. pythonnative/templates/ios_template/ios_template/SceneDelegate.swift +11 -16
  161. pythonnative/templates/ios_template/ios_template/ViewController.swift +12 -142
  162. pythonnative/templates/ios_template/ios_template.xcodeproj/project.pbxproj +47 -1
  163. pythonnative/utils.py +4 -82
  164. pythonnative/virtual_rows.py +3 -3
  165. {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/METADATA +3 -5
  166. pythonnative-0.37.0.dist-info/RECORD +263 -0
  167. pythonnative/hosts/android.py +0 -269
  168. pythonnative/hosts/ios.py +0 -404
  169. pythonnative/native_views/android.py +0 -3702
  170. pythonnative/native_views/ios.py +0 -5253
  171. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/Navigator.kt +0 -43
  172. pythonnative-0.35.0.dist-info/RECORD +0 -155
  173. {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/WHEEL +0 -0
  174. {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/entry_points.txt +0 -0
  175. {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/licenses/LICENSE +0 -0
  176. {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,251 @@
1
+ import AVFoundation
2
+ import Contacts
3
+ import CoreLocation
4
+ import LocalAuthentication
5
+ import Photos
6
+ import UIKit
7
+ import UserNotifications
8
+
9
+ /// `Permissions`: `check` (sync) and `request` (async) for camera,
10
+ /// microphone, location, photos, notifications, and contacts.
11
+ public final class PermissionsModule: PNNativeModule {
12
+ public static let name = "Permissions"
13
+
14
+ private var locationRequester: PNLocationPermissionRequester?
15
+
16
+ public init() {}
17
+
18
+ public func call(_ method: String, args: [String: Any], promise: PNPromise) {
19
+ let permission = PNProps.string(args["permission"]) ?? ""
20
+ switch method {
21
+ case "check":
22
+ check(permission) { promise.resolve($0) }
23
+ case "request":
24
+ request(permission) { promise.resolve($0) }
25
+ default:
26
+ promise.reject("Permissions has no method '\(method)'", code: "unknown_method")
27
+ }
28
+ }
29
+
30
+ // MARK: - Check
31
+
32
+ private func check(_ permission: String, _ done: @escaping (String) -> Void) {
33
+ switch permission {
34
+ case "camera":
35
+ done(PermissionsModule.status(AVCaptureDevice.authorizationStatus(for: .video)))
36
+ case "microphone":
37
+ done(PermissionsModule.status(AVCaptureDevice.authorizationStatus(for: .audio)))
38
+ case "photos":
39
+ done(PermissionsModule.status(PHPhotoLibrary.pnReadWriteAuthorizationStatus()))
40
+ case "contacts":
41
+ done(PermissionsModule.status(CNContactStore.authorizationStatus(for: .contacts)))
42
+ case "location":
43
+ done(PermissionsModule.status(CLLocationManager().pnAuthorizationStatus))
44
+ case "notifications":
45
+ UNUserNotificationCenter.current().getNotificationSettings { settings in
46
+ let status: String
47
+ switch settings.authorizationStatus {
48
+ case .authorized, .provisional, .ephemeral: status = "granted"
49
+ case .denied: status = "blocked"
50
+ case .notDetermined: status = "undetermined"
51
+ @unknown default: status = "undetermined"
52
+ }
53
+ DispatchQueue.main.async { done(status) }
54
+ }
55
+ default:
56
+ done("undetermined")
57
+ }
58
+ }
59
+
60
+ // MARK: - Request
61
+
62
+ private func request(_ permission: String, _ done: @escaping (String) -> Void) {
63
+ let finish: (String) -> Void = { status in DispatchQueue.main.async { done(status) } }
64
+ switch permission {
65
+ case "camera":
66
+ AVCaptureDevice.requestAccess(for: .video) { finish($0 ? "granted" : "blocked") }
67
+ case "microphone":
68
+ AVCaptureDevice.requestAccess(for: .audio) { finish($0 ? "granted" : "blocked") }
69
+ case "photos":
70
+ PHPhotoLibrary.pnRequestReadWriteAuthorization { finish(PermissionsModule.status($0)) }
71
+ case "contacts":
72
+ CNContactStore().requestAccess(for: .contacts) { granted, _ in finish(granted ? "granted" : "blocked") }
73
+ case "notifications":
74
+ UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, _ in
75
+ finish(granted ? "granted" : "blocked")
76
+ }
77
+ case "location":
78
+ let requester = PNLocationPermissionRequester { [weak self] status in
79
+ self?.locationRequester = nil
80
+ finish(status)
81
+ }
82
+ locationRequester = requester
83
+ requester.start()
84
+ default:
85
+ done("undetermined")
86
+ }
87
+ }
88
+
89
+ // MARK: - Status mapping
90
+
91
+ static func status(_ s: AVAuthorizationStatus) -> String {
92
+ switch s {
93
+ case .authorized: return "granted"
94
+ case .denied: return "blocked"
95
+ case .restricted: return "blocked"
96
+ case .notDetermined: return "undetermined"
97
+ @unknown default: return "undetermined"
98
+ }
99
+ }
100
+
101
+ static func status(_ s: PHAuthorizationStatus) -> String {
102
+ switch s {
103
+ case .authorized, .limited: return "granted"
104
+ case .denied, .restricted: return "blocked"
105
+ case .notDetermined: return "undetermined"
106
+ @unknown default: return "undetermined"
107
+ }
108
+ }
109
+
110
+ static func status(_ s: CNAuthorizationStatus) -> String {
111
+ switch s {
112
+ case .authorized: return "granted"
113
+ case .denied, .restricted: return "blocked"
114
+ case .notDetermined: return "undetermined"
115
+ // `.limited` (iOS 18) and future cases still permit access.
116
+ default: return "granted"
117
+ }
118
+ }
119
+
120
+ static func status(_ s: CLAuthorizationStatus) -> String {
121
+ switch s {
122
+ case .authorizedAlways, .authorizedWhenInUse: return "granted"
123
+ case .denied, .restricted: return "blocked"
124
+ case .notDetermined: return "undetermined"
125
+ @unknown default: return "undetermined"
126
+ }
127
+ }
128
+ }
129
+
130
+ /// One-shot when-in-use authorization request.
131
+ final class PNLocationPermissionRequester: NSObject, CLLocationManagerDelegate {
132
+ private let manager = CLLocationManager()
133
+ private let done: (String) -> Void
134
+ private var finished = false
135
+
136
+ init(done: @escaping (String) -> Void) {
137
+ self.done = done
138
+ super.init()
139
+ manager.delegate = self
140
+ }
141
+
142
+ func start() {
143
+ let current = manager.pnAuthorizationStatus
144
+ if current != .notDetermined {
145
+ finish(PermissionsModule.status(current))
146
+ return
147
+ }
148
+ manager.requestWhenInUseAuthorization()
149
+ }
150
+
151
+ func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
152
+ let status = manager.pnAuthorizationStatus
153
+ if status != .notDetermined {
154
+ finish(PermissionsModule.status(status))
155
+ }
156
+ }
157
+
158
+ private func finish(_ status: String) {
159
+ if finished { return }
160
+ finished = true
161
+ done(status)
162
+ }
163
+ }
164
+
165
+ /// `Notifications`: local notifications plus APNs registration.
166
+ public final class NotificationsModule: PNNativeModule {
167
+ public static let name = "Notifications"
168
+
169
+ private static var tokenWaiters: [PNPromise] = []
170
+
171
+ public init() {}
172
+
173
+ public func call(_ method: String, args: [String: Any], promise: PNPromise) {
174
+ let center = UNUserNotificationCenter.current()
175
+ switch method {
176
+ case "request_permission":
177
+ center.requestAuthorization(options: [.alert, .badge, .sound]) { granted, _ in
178
+ DispatchQueue.main.async { promise.resolve(granted) }
179
+ }
180
+ case "schedule":
181
+ let content = UNMutableNotificationContent()
182
+ content.title = PNProps.string(args["title"]) ?? ""
183
+ content.body = PNProps.string(args["body"]) ?? ""
184
+ if PNProps.bool(args["sound"]) != false { content.sound = .default }
185
+ if let badge = PNProps.int(args["badge"]) { content.badge = NSNumber(value: badge) }
186
+ let delay = max(0.1, PNProps.double(args["delay_seconds"]) ?? 0)
187
+ let trigger = UNTimeIntervalNotificationTrigger(timeInterval: delay, repeats: false)
188
+ let identifier = PNProps.string(args["identifier"]) ?? "default"
189
+ center.add(UNNotificationRequest(identifier: identifier, content: content, trigger: trigger)) { error in
190
+ DispatchQueue.main.async {
191
+ if let error = error {
192
+ PNLog.modules.error("schedule failed: \(error.localizedDescription)")
193
+ }
194
+ promise.resolve(error == nil)
195
+ }
196
+ }
197
+ case "cancel":
198
+ let identifier = PNProps.string(args["identifier"]) ?? "default"
199
+ center.removePendingNotificationRequests(withIdentifiers: [identifier])
200
+ center.removeDeliveredNotifications(withIdentifiers: [identifier])
201
+ promise.resolve(nil)
202
+ case "get_device_token":
203
+ NotificationsModule.tokenWaiters.append(promise)
204
+ UIApplication.shared.registerForRemoteNotifications()
205
+ default:
206
+ promise.reject("Notifications has no method '\(method)'", code: "unknown_method")
207
+ }
208
+ }
209
+
210
+ /// Called by the app delegate with the APNs token.
211
+ public static func deliverToken(_ token: Data) {
212
+ let hex = token.map { String(format: "%02x", $0) }.joined()
213
+ let waiters = tokenWaiters
214
+ tokenWaiters = []
215
+ for waiter in waiters { waiter.resolve(hex) }
216
+ }
217
+
218
+ /// Called by the app delegate when APNs registration fails.
219
+ public static func deliverError(_ error: Error) {
220
+ let waiters = tokenWaiters
221
+ tokenWaiters = []
222
+ for waiter in waiters { waiter.reject(String(describing: error), code: "apns") }
223
+ }
224
+ }
225
+
226
+ /// `Biometrics`: `LAContext` availability and authentication.
227
+ public final class BiometricsModule: PNNativeModule {
228
+ public static let name = "Biometrics"
229
+
230
+ public init() {}
231
+
232
+ public func call(_ method: String, args: [String: Any], promise: PNPromise) {
233
+ switch method {
234
+ case "is_available":
235
+ let context = LAContext()
236
+ var error: NSError?
237
+ promise.resolve(context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error))
238
+ case "authenticate":
239
+ let context = LAContext()
240
+ let reason = PNProps.string(args["reason"]) ?? "Authenticate"
241
+ var error: NSError?
242
+ let policy: LAPolicy = context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error)
243
+ ? .deviceOwnerAuthenticationWithBiometrics : .deviceOwnerAuthentication
244
+ context.evaluatePolicy(policy, localizedReason: reason) { success, _ in
245
+ DispatchQueue.main.async { promise.resolve(success) }
246
+ }
247
+ default:
248
+ promise.reject("Biometrics has no method '\(method)'", code: "unknown_method")
249
+ }
250
+ }
251
+ }
@@ -0,0 +1,251 @@
1
+ import AudioToolbox
2
+ import Security
3
+ import UIKit
4
+
5
+ /// `Device`: static device and app-path information.
6
+ public final class DeviceModule: PNNativeModule {
7
+ public static let name = "Device"
8
+
9
+ public init() {}
10
+
11
+ public func call(_ method: String, args: [String: Any], promise: PNPromise) {
12
+ switch method {
13
+ case "info":
14
+ promise.resolve(DeviceModule.info())
15
+ default:
16
+ promise.reject("Device has no method '\(method)'", code: "unknown_method")
17
+ }
18
+ }
19
+
20
+ /// The `Device.info()` payload.
21
+ public static func info() -> [String: Any] {
22
+ let paths = FileManager.default
23
+ let documents = paths.urls(for: .documentDirectory, in: .userDomainMask).first?.path ?? NSHomeDirectory() + "/Documents"
24
+ let caches = paths.urls(for: .cachesDirectory, in: .userDomainMask).first?.path ?? NSTemporaryDirectory()
25
+ return [
26
+ "os": "ios",
27
+ "os_version": UIDevice.current.systemVersion,
28
+ "model": UIDevice.current.model,
29
+ "name": UIDevice.current.name,
30
+ "app_dir": documents,
31
+ "cache_dir": caches,
32
+ "locale": Locale.current.identifier,
33
+ "scale": Double(PNWindow.screenScale()),
34
+ "is_simulator": DeviceModule.isSimulator,
35
+ ]
36
+ }
37
+
38
+ static var isSimulator: Bool {
39
+ #if targetEnvironment(simulator)
40
+ return true
41
+ #else
42
+ return false
43
+ #endif
44
+ }
45
+ }
46
+
47
+ /// `Storage`: `AsyncStorage` over the `pn_async_storage` defaults suite.
48
+ public final class StorageModule: PNNativeModule {
49
+ public static let name = "Storage"
50
+ static let suiteName = "pn_async_storage"
51
+
52
+ private let defaults: UserDefaults
53
+
54
+ public init() {
55
+ defaults = UserDefaults(suiteName: StorageModule.suiteName) ?? .standard
56
+ }
57
+
58
+ public func call(_ method: String, args: [String: Any], promise: PNPromise) {
59
+ switch method {
60
+ case "get":
61
+ guard let key = PNProps.string(args["key"]) else { return promise.reject("missing key", code: "bad_args") }
62
+ promise.resolve(defaults.string(forKey: key))
63
+ case "set":
64
+ guard let key = PNProps.string(args["key"]) else { return promise.reject("missing key", code: "bad_args") }
65
+ defaults.set(PNProps.string(args["value"]) ?? "", forKey: key)
66
+ promise.resolve(nil)
67
+ case "delete":
68
+ guard let key = PNProps.string(args["key"]) else { return promise.reject("missing key", code: "bad_args") }
69
+ defaults.removeObject(forKey: key)
70
+ promise.resolve(nil)
71
+ case "all_keys":
72
+ let keys: [String] = defaults.persistentDomain(forName: StorageModule.suiteName)?.keys.map { $0 } ?? []
73
+ promise.resolve(keys.sorted())
74
+ case "clear":
75
+ defaults.removePersistentDomain(forName: StorageModule.suiteName)
76
+ promise.resolve(nil)
77
+ default:
78
+ promise.reject("Storage has no method '\(method)'", code: "unknown_method")
79
+ }
80
+ }
81
+ }
82
+
83
+ /// `SecureStore`: Keychain generic passwords under one service.
84
+ public final class SecureStoreModule: PNNativeModule {
85
+ public static let name = "SecureStore"
86
+ static let service = "com.pythonnative.securestore"
87
+
88
+ public init() {}
89
+
90
+ public func call(_ method: String, args: [String: Any], promise: PNPromise) {
91
+ guard let key = PNProps.string(args["key"]) else {
92
+ promise.reject("missing key", code: "bad_args")
93
+ return
94
+ }
95
+ switch method {
96
+ case "set_item":
97
+ promise.resolve(SecureStoreModule.set(key, PNProps.string(args["value"]) ?? ""))
98
+ case "get_item":
99
+ promise.resolve(SecureStoreModule.get(key))
100
+ case "delete_item":
101
+ promise.resolve(SecureStoreModule.delete(key))
102
+ default:
103
+ promise.reject("SecureStore has no method '\(method)'", code: "unknown_method")
104
+ }
105
+ }
106
+
107
+ private static func query(_ key: String) -> [String: Any] {
108
+ [
109
+ kSecClass as String: kSecClassGenericPassword,
110
+ kSecAttrService as String: service,
111
+ kSecAttrAccount as String: key,
112
+ ]
113
+ }
114
+
115
+ static func set(_ key: String, _ value: String) -> Bool {
116
+ let data = Data(value.utf8)
117
+ var q = query(key)
118
+ let status = SecItemCopyMatching(q as CFDictionary, nil)
119
+ if status == errSecSuccess {
120
+ return SecItemUpdate(q as CFDictionary, [kSecValueData as String: data] as CFDictionary) == errSecSuccess
121
+ }
122
+ q[kSecValueData as String] = data
123
+ q[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
124
+ return SecItemAdd(q as CFDictionary, nil) == errSecSuccess
125
+ }
126
+
127
+ static func get(_ key: String) -> String? {
128
+ var q = query(key)
129
+ q[kSecReturnData as String] = true
130
+ q[kSecMatchLimit as String] = kSecMatchLimitOne
131
+ var result: CFTypeRef?
132
+ guard SecItemCopyMatching(q as CFDictionary, &result) == errSecSuccess, let data = result as? Data else { return nil }
133
+ return String(data: data, encoding: .utf8)
134
+ }
135
+
136
+ static func delete(_ key: String) -> Bool {
137
+ SecItemDelete(query(key) as CFDictionary) == errSecSuccess
138
+ }
139
+ }
140
+
141
+ /// `Clipboard`: the general pasteboard.
142
+ public final class ClipboardModule: PNNativeModule {
143
+ public static let name = "Clipboard"
144
+
145
+ public init() {}
146
+
147
+ public func call(_ method: String, args: [String: Any], promise: PNPromise) {
148
+ switch method {
149
+ case "set_string":
150
+ UIPasteboard.general.string = PNProps.string(args["text"]) ?? ""
151
+ promise.resolve(nil)
152
+ case "get_string":
153
+ promise.resolve(UIPasteboard.general.string ?? "")
154
+ default:
155
+ promise.reject("Clipboard has no method '\(method)'", code: "unknown_method")
156
+ }
157
+ }
158
+ }
159
+
160
+ /// `Share`: `UIActivityViewController`, resolving whether a share completed.
161
+ public final class ShareModule: PNNativeModule {
162
+ public static let name = "Share"
163
+
164
+ public init() {}
165
+
166
+ public func call(_ method: String, args: [String: Any], promise: PNPromise) {
167
+ guard method == "share" else {
168
+ promise.reject("Share has no method '\(method)'", code: "unknown_method")
169
+ return
170
+ }
171
+ var items: [Any] = []
172
+ if let message = PNProps.string(args["message"]), !message.isEmpty { items.append(message) }
173
+ if let url = PNProps.string(args["url"]).flatMap({ URL(string: $0) }) { items.append(url) }
174
+ guard !items.isEmpty, let top = PNWindow.topViewController() else {
175
+ promise.resolve(false)
176
+ return
177
+ }
178
+ let controller = UIActivityViewController(activityItems: items, applicationActivities: nil)
179
+ if let title = PNProps.string(args["title"]) {
180
+ controller.setValue(title, forKey: "subject")
181
+ }
182
+ controller.completionWithItemsHandler = { _, completed, _, _ in
183
+ promise.resolve(completed)
184
+ }
185
+ if let popover = controller.popoverPresentationController {
186
+ popover.sourceView = top.view
187
+ popover.sourceRect = CGRect(x: top.view.bounds.midX, y: top.view.bounds.midY, width: 1, height: 1)
188
+ popover.permittedArrowDirections = []
189
+ }
190
+ top.present(controller, animated: true)
191
+ }
192
+ }
193
+
194
+ /// `Haptics`: feedback generators plus the legacy vibration sound.
195
+ public final class HapticsModule: PNNativeModule {
196
+ public static let name = "Haptics"
197
+
198
+ public init() {}
199
+
200
+ public func call(_ method: String, args: [String: Any], promise: PNPromise) {
201
+ switch method {
202
+ case "impact":
203
+ let generator = UIImpactFeedbackGenerator(style: HapticsModule.impactStyle(PNProps.string(args["style"])))
204
+ generator.prepare()
205
+ generator.impactOccurred()
206
+ promise.resolve(nil)
207
+ case "notification":
208
+ let generator = UINotificationFeedbackGenerator()
209
+ generator.prepare()
210
+ generator.notificationOccurred(HapticsModule.notificationType(PNProps.string(args["type"])))
211
+ promise.resolve(nil)
212
+ case "selection":
213
+ let generator = UISelectionFeedbackGenerator()
214
+ generator.prepare()
215
+ generator.selectionChanged()
216
+ promise.resolve(nil)
217
+ case "vibrate":
218
+ let duration = PNProps.int(args["duration_ms"]) ?? 400
219
+ if duration <= 100 {
220
+ let generator = UIImpactFeedbackGenerator(style: .heavy)
221
+ generator.prepare()
222
+ generator.impactOccurred()
223
+ } else {
224
+ AudioServicesPlaySystemSound(kSystemSoundID_Vibrate)
225
+ }
226
+ promise.resolve(nil)
227
+ case "cancel":
228
+ promise.resolve(nil)
229
+ default:
230
+ promise.reject("Haptics has no method '\(method)'", code: "unknown_method")
231
+ }
232
+ }
233
+
234
+ static func impactStyle(_ name: String?) -> UIImpactFeedbackGenerator.FeedbackStyle {
235
+ switch name {
236
+ case "light": return .light
237
+ case "heavy": return .heavy
238
+ case "soft": return .soft
239
+ case "rigid": return .rigid
240
+ default: return .medium
241
+ }
242
+ }
243
+
244
+ static func notificationType(_ name: String?) -> UINotificationFeedbackGenerator.FeedbackType {
245
+ switch name {
246
+ case "warning": return .warning
247
+ case "error": return .error
248
+ default: return .success
249
+ }
250
+ }
251
+ }
@@ -0,0 +1,8 @@
1
+ // Generated by `pn build`. Do not edit: the PythonNative build tool
2
+ // overwrites this file with one `register(into:)` call per bundled plugin.
3
+
4
+ /// Registration hook for plugins copied into `Sources/Plugins` by `pn build`.
5
+ public enum PNGeneratedPlugins {
6
+ /// Register every generated plugin into `registry`.
7
+ public static func registerAll(into registry: PNRegistry) {}
8
+ }
@@ -0,0 +1,46 @@
1
+ import UIKit
2
+
3
+ /// Screen id <-> `PNViewController` table.
4
+ ///
5
+ /// Python addresses screens by the integer id assigned here; the
6
+ /// controller is held weakly so a popped screen is reclaimed normally.
7
+ public final class PNScreenRegistry {
8
+ public static let shared = PNScreenRegistry()
9
+
10
+ private final class WeakBox {
11
+ weak var controller: PNViewController?
12
+ init(_ controller: PNViewController) { self.controller = controller }
13
+ }
14
+
15
+ private var screens: [Int64: WeakBox] = [:]
16
+ private var nextId: Int64 = 1
17
+
18
+ private init() {}
19
+
20
+ /// Assign a fresh id to `controller`.
21
+ func register(_ controller: PNViewController) -> Int64 {
22
+ let id = nextId
23
+ nextId += 1
24
+ screens[id] = WeakBox(controller)
25
+ return id
26
+ }
27
+
28
+ /// Forget `id` (called from the controller's deinit).
29
+ func unregister(_ id: Int64) {
30
+ screens.removeValue(forKey: id)
31
+ }
32
+
33
+ /// The controller for `id`, if it's still alive.
34
+ public func controller(for id: Int64) -> PNViewController? {
35
+ guard let box = screens[id] else { return nil }
36
+ if box.controller == nil {
37
+ screens.removeValue(forKey: id)
38
+ }
39
+ return box.controller
40
+ }
41
+
42
+ /// Ids of every live screen.
43
+ public var screenIds: [Int64] {
44
+ screens.compactMap { $0.value.controller == nil ? nil : $0.key }.sorted()
45
+ }
46
+ }