@capgo/capacitor-stream-call 0.0.2

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 (39) hide show
  1. package/Package.swift +31 -0
  2. package/README.md +340 -0
  3. package/StreamCall.podspec +19 -0
  4. package/android/build.gradle +74 -0
  5. package/android/src/main/AndroidManifest.xml +2 -0
  6. package/android/src/main/java/ee/forgr/capacitor/streamcall/CallOverlayView.kt +281 -0
  7. package/android/src/main/java/ee/forgr/capacitor/streamcall/CustomNotificationHandler.kt +142 -0
  8. package/android/src/main/java/ee/forgr/capacitor/streamcall/IncomingCallView.kt +147 -0
  9. package/android/src/main/java/ee/forgr/capacitor/streamcall/RingtonePlayer.kt +164 -0
  10. package/android/src/main/java/ee/forgr/capacitor/streamcall/StreamCallPlugin.kt +1014 -0
  11. package/android/src/main/java/ee/forgr/capacitor/streamcall/TouchInterceptWrapper.kt +31 -0
  12. package/android/src/main/java/ee/forgr/capacitor/streamcall/UserRepository.kt +111 -0
  13. package/android/src/main/res/.gitkeep +0 -0
  14. package/android/src/main/res/values/strings.xml +7 -0
  15. package/dist/docs.json +533 -0
  16. package/dist/esm/definitions.d.ts +169 -0
  17. package/dist/esm/definitions.js +2 -0
  18. package/dist/esm/definitions.js.map +1 -0
  19. package/dist/esm/index.d.ts +4 -0
  20. package/dist/esm/index.js +7 -0
  21. package/dist/esm/index.js.map +1 -0
  22. package/dist/esm/web.d.ts +32 -0
  23. package/dist/esm/web.js +323 -0
  24. package/dist/esm/web.js.map +1 -0
  25. package/dist/plugin.cjs.js +337 -0
  26. package/dist/plugin.cjs.js.map +1 -0
  27. package/dist/plugin.js +339 -0
  28. package/dist/plugin.js.map +1 -0
  29. package/ios/Sources/StreamCallPlugin/CallOverlayView.swift +147 -0
  30. package/ios/Sources/StreamCallPlugin/CustomCallParticipantImageView.swift +60 -0
  31. package/ios/Sources/StreamCallPlugin/CustomCallView.swift +257 -0
  32. package/ios/Sources/StreamCallPlugin/CustomVideoParticipantsView.swift +107 -0
  33. package/ios/Sources/StreamCallPlugin/ParticipantsView.swift +206 -0
  34. package/ios/Sources/StreamCallPlugin/StreamCallPlugin.swift +722 -0
  35. package/ios/Sources/StreamCallPlugin/TouchInterceptView.swift +177 -0
  36. package/ios/Sources/StreamCallPlugin/UserRepository.swift +96 -0
  37. package/ios/Sources/StreamCallPlugin/WebviewNavigationDelegate.swift +68 -0
  38. package/ios/Tests/StreamCallPluginTests/StreamCallPluginTests.swift +15 -0
  39. package/package.json +96 -0
@@ -0,0 +1,177 @@
1
+ import UIKit
2
+
3
+ class TouchInterceptView: UIView {
4
+ private var webView: UIView?
5
+ private var overlayView: UIView?
6
+ private var labeledFrames: [ViewFramePreferenceData] = []
7
+
8
+ override init(frame: CGRect) {
9
+ super.init(frame: frame)
10
+ isUserInteractionEnabled = true
11
+ }
12
+
13
+ required init?(coder: NSCoder) {
14
+ super.init(coder: coder)
15
+ isUserInteractionEnabled = true
16
+ }
17
+
18
+ func setupWithWebView(_ webView: UIView, overlayView: UIView) {
19
+ self.webView = webView
20
+ self.overlayView = overlayView
21
+
22
+ // Add both views as subviews
23
+ addSubview(overlayView)
24
+ addSubview(webView)
25
+
26
+ // Ensure both views can receive touches
27
+ webView.isUserInteractionEnabled = true
28
+ overlayView.isUserInteractionEnabled = true
29
+ }
30
+
31
+ override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
32
+ guard let webView = webView,
33
+ let overlayView = overlayView else {
34
+ super.touchesBegan(touches, with: event)
35
+ return
36
+ }
37
+
38
+ // Convert touch locations and check labeled frames
39
+ if let touch = touches.first {
40
+ let point = touch.location(in: self)
41
+ let globalPoint = convert(point, to: nil)
42
+
43
+ // If touch is in a labeled frame, only send to overlay
44
+ if labeledFrames.contains(where: { $0.frame.contains(globalPoint) }) {
45
+ overlayView.touchesBegan(touches, with: event)
46
+ } else {
47
+ // Otherwise broadcast to both views
48
+ webView.touchesBegan(touches, with: event)
49
+ overlayView.touchesBegan(touches, with: event)
50
+ }
51
+ }
52
+
53
+ super.touchesBegan(touches, with: event)
54
+ }
55
+
56
+ override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
57
+ guard let webView = webView,
58
+ let overlayView = overlayView else {
59
+ super.touchesMoved(touches, with: event)
60
+ return
61
+ }
62
+
63
+ if let touch = touches.first {
64
+ let point = touch.location(in: self)
65
+ let globalPoint = convert(point, to: nil)
66
+
67
+ if labeledFrames.contains(where: { $0.frame.contains(globalPoint) }) {
68
+ overlayView.touchesMoved(touches, with: event)
69
+ } else {
70
+ webView.touchesMoved(touches, with: event)
71
+ overlayView.touchesMoved(touches, with: event)
72
+ }
73
+ }
74
+
75
+ super.touchesMoved(touches, with: event)
76
+ }
77
+
78
+ override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
79
+ guard let webView = webView,
80
+ let overlayView = overlayView else {
81
+ super.touchesEnded(touches, with: event)
82
+ return
83
+ }
84
+
85
+ if let touch = touches.first {
86
+ let point = touch.location(in: self)
87
+ let globalPoint = convert(point, to: nil)
88
+
89
+ if labeledFrames.contains(where: { $0.frame.contains(globalPoint) }) {
90
+ overlayView.touchesEnded(touches, with: event)
91
+ } else {
92
+ webView.touchesEnded(touches, with: event)
93
+ overlayView.touchesEnded(touches, with: event)
94
+ }
95
+ }
96
+
97
+ super.touchesEnded(touches, with: event)
98
+ }
99
+
100
+ override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
101
+ guard let webView = webView,
102
+ let overlayView = overlayView else {
103
+ super.touchesCancelled(touches, with: event)
104
+ return
105
+ }
106
+
107
+ if let touch = touches.first {
108
+ let point = touch.location(in: self)
109
+ let globalPoint = convert(point, to: nil)
110
+
111
+ if labeledFrames.contains(where: { $0.frame.contains(globalPoint) }) {
112
+ overlayView.touchesCancelled(touches, with: event)
113
+ } else {
114
+ webView.touchesCancelled(touches, with: event)
115
+ overlayView.touchesCancelled(touches, with: event)
116
+ }
117
+ }
118
+
119
+ super.touchesCancelled(touches, with: event)
120
+ }
121
+
122
+ override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
123
+ return true
124
+ }
125
+
126
+ override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
127
+ guard let webView = webView,
128
+ let overlayView = overlayView else {
129
+ return super.hitTest(point, with: event)
130
+ }
131
+
132
+ // Convert point to global coordinates for labeled frame checking
133
+ let globalPoint = convert(point, to: nil)
134
+
135
+ print("TouchInterceptView - Hit test at global point: \(globalPoint)")
136
+ print("Current labeled frames: \(labeledFrames.map { "\($0.label): \($0.frame)" }.joined(separator: ", "))")
137
+
138
+ // Convert point for both views
139
+ let webViewPoint = convert(point, to: webView)
140
+ let overlayPoint = convert(point, to: overlayView)
141
+
142
+ // First check if the point is inside any labeled frame
143
+ for labeledFrame in labeledFrames {
144
+ if labeledFrame.frame.contains(globalPoint) {
145
+ print("Hit labeled frame: \(labeledFrame.label)")
146
+ // If it's in a labeled frame, let the overlay handle it
147
+ if overlayView.point(inside: overlayPoint, with: event),
148
+ let overlayHitView = overlayView.hitTest(overlayPoint, with: event) {
149
+ return overlayHitView
150
+ }
151
+ }
152
+ }
153
+
154
+ // If not in a labeled frame, let webview try first
155
+ if webView.point(inside: webViewPoint, with: event),
156
+ let webViewHitView = webView.hitTest(webViewPoint, with: event) {
157
+ return webViewHitView
158
+ }
159
+
160
+ // Finally check if overlay wants to handle the touch
161
+ if overlayView.point(inside: overlayPoint, with: event),
162
+ let overlayHitView = overlayView.hitTest(overlayPoint, with: event) {
163
+ return overlayHitView
164
+ }
165
+
166
+ return super.hitTest(point, with: event)
167
+ }
168
+
169
+ func updateLabeledFrames(_ frames: [ViewFramePreferenceData]) {
170
+ print("TouchInterceptView - Updating labeled frames:")
171
+ print("Number of frames: \(frames.count)")
172
+ frames.forEach { frame in
173
+ print("Label: \(frame.label), Frame: \(frame.frame)")
174
+ }
175
+ self.labeledFrames = frames
176
+ }
177
+ }
@@ -0,0 +1,96 @@
1
+ //
2
+ // UserRepository.swift
3
+ // Pods
4
+ //
5
+ // Created by Michał Tremblay on 07/02/2025.
6
+ //
7
+
8
+ import Security
9
+ import StreamVideo
10
+ import Foundation
11
+
12
+ struct UserCredentials: Identifiable, Codable {
13
+ var id: String {
14
+ user.id
15
+ }
16
+ let user: User
17
+ let tokenValue: String
18
+ }
19
+
20
+ protocol UserRepository {
21
+
22
+ func save(user: UserCredentials)
23
+
24
+ func loadCurrentUser() -> UserCredentials?
25
+
26
+ func removeCurrentUser()
27
+
28
+ func save(token: String)
29
+
30
+ }
31
+
32
+ protocol VoipTokenHandler {
33
+
34
+ func save(voipPushToken: String?)
35
+
36
+ func currentVoipPushToken() -> String?
37
+
38
+ }
39
+
40
+ // NOTE: This is just for simplicity. User data shouldn't be kept in `UserDefaults`.
41
+ // NOTE: This is just for simplicity. User data shouldn't be kept in `UserDefaults`.
42
+ class SecureUserRepository: UserRepository, VoipTokenHandler {
43
+
44
+ private let defaults = UserDefaults.standard
45
+ private let userKey = "stream.video.user"
46
+ private let tokenKey = "stream.video.token"
47
+ private let chatTokenKey = "stream.chat.token"
48
+ private let voipPushTokenKey = "stream.video.voip.token"
49
+
50
+ static let shared = SecureUserRepository()
51
+
52
+ private init() {}
53
+
54
+ func save(user: UserCredentials) {
55
+ let encoder = JSONEncoder()
56
+ if let encoded = try? encoder.encode(user.user) {
57
+ defaults.set(encoded, forKey: userKey)
58
+ defaults.set(user.tokenValue, forKey: tokenKey)
59
+ }
60
+ }
61
+
62
+ func save(token: String) {
63
+ defaults.set(token, forKey: tokenKey)
64
+ }
65
+
66
+ func loadCurrentUser() -> UserCredentials? {
67
+ if let savedUser = defaults.object(forKey: userKey) as? Data {
68
+ let decoder = JSONDecoder()
69
+ do {
70
+ let loadedUser = try decoder.decode(User.self, from: savedUser)
71
+ guard let tokenValue = defaults.value(forKey: tokenKey) as? String else {
72
+ throw ClientError.Unexpected()
73
+ }
74
+ return UserCredentials(user: loadedUser, tokenValue: tokenValue)
75
+ } catch {
76
+ log.error("Error while decoding user: \(String(describing: error))")
77
+ }
78
+ }
79
+ return nil
80
+ }
81
+
82
+ func save(voipPushToken: String?) {
83
+ defaults.set(voipPushToken, forKey: voipPushTokenKey)
84
+ }
85
+
86
+ func currentVoipPushToken() -> String? {
87
+ defaults.value(forKey: voipPushTokenKey) as? String
88
+ }
89
+
90
+ func removeCurrentUser() {
91
+ defaults.set(nil, forKey: userKey)
92
+ defaults.set(nil, forKey: tokenKey)
93
+ defaults.set(nil, forKey: voipPushTokenKey)
94
+ }
95
+
96
+ }
@@ -0,0 +1,68 @@
1
+ import Foundation
2
+ import WebKit
3
+
4
+ class WebviewNavigationDelegate: NSObject, WKNavigationDelegate {
5
+ private let wrappedDelegate: WKNavigationDelegate?
6
+ private let onSetupOverlay: () -> Void
7
+
8
+ init(wrappedDelegate: WKNavigationDelegate?, onSetupOverlay: @escaping () -> Void) {
9
+ self.wrappedDelegate = wrappedDelegate
10
+ self.onSetupOverlay = onSetupOverlay
11
+ super.init()
12
+ }
13
+
14
+ func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
15
+ // Then forward to the original delegate
16
+ wrappedDelegate?.webView?(webView, didFinish: navigation)
17
+
18
+ // Call our custom setup
19
+ onSetupOverlay()
20
+ }
21
+
22
+ // Forward all other WKNavigationDelegate methods
23
+
24
+ func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
25
+ if let result = wrappedDelegate?.webView?(webView, decidePolicyFor: navigationAction, decisionHandler: decisionHandler) {
26
+ return result
27
+ }
28
+ decisionHandler(.allow)
29
+ }
30
+
31
+ func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) {
32
+ if let result = wrappedDelegate?.webView?(webView, decidePolicyFor: navigationResponse, decisionHandler: decisionHandler) {
33
+ return result
34
+ }
35
+ decisionHandler(.allow)
36
+ }
37
+
38
+ func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
39
+ wrappedDelegate?.webView?(webView, didStartProvisionalNavigation: navigation)
40
+ }
41
+
42
+ func webView(_ webView: WKWebView, didReceiveServerRedirectForProvisionalNavigation navigation: WKNavigation!) {
43
+ wrappedDelegate?.webView?(webView, didReceiveServerRedirectForProvisionalNavigation: navigation)
44
+ }
45
+
46
+ func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
47
+ wrappedDelegate?.webView?(webView, didFailProvisionalNavigation: navigation, withError: error)
48
+ }
49
+
50
+ func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
51
+ wrappedDelegate?.webView?(webView, didCommit: navigation)
52
+ }
53
+
54
+ func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
55
+ wrappedDelegate?.webView?(webView, didFail: navigation, withError: error)
56
+ }
57
+
58
+ func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
59
+ if let result = wrappedDelegate?.webView?(webView, didReceive: challenge, completionHandler: completionHandler) {
60
+ return result
61
+ }
62
+ completionHandler(.performDefaultHandling, nil)
63
+ }
64
+
65
+ func webViewWebContentProcessDidTerminate(_ webView: WKWebView) {
66
+ wrappedDelegate?.webViewWebContentProcessDidTerminate?(webView)
67
+ }
68
+ }
@@ -0,0 +1,15 @@
1
+ import XCTest
2
+ @testable import StreamCallPlugin
3
+
4
+ class StreamCallTests: XCTestCase {
5
+ func testEcho() {
6
+ // This is an example of a functional test case for a plugin.
7
+ // Use XCTAssert and related functions to verify your tests produce the correct results.
8
+
9
+ let implementation = StreamCall()
10
+ let value = "Hello, World!"
11
+ let result = implementation.echo(value)
12
+
13
+ XCTAssertEqual(value, result)
14
+ }
15
+ }
package/package.json ADDED
@@ -0,0 +1,96 @@
1
+ {
2
+ "name": "@capgo/capacitor-stream-call",
3
+ "version": "0.0.2",
4
+ "description": "Uses the https://getstream.io/ SDK to implement calling in Capacitor",
5
+ "main": "dist/plugin.cjs.js",
6
+ "module": "dist/esm/index.js",
7
+ "types": "dist/esm/index.d.ts",
8
+ "unpkg": "dist/plugin.js",
9
+ "files": [
10
+ "android/src/main/",
11
+ "android/build.gradle",
12
+ "dist/",
13
+ "ios/Sources",
14
+ "ios/Tests",
15
+ "Package.swift",
16
+ "StreamCall.podspec"
17
+ ],
18
+ "author": "Martin Donadieu <martin@capgo.app>",
19
+ "license": "MIT",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/Cap-go/StreamCall.git"
23
+ },
24
+ "bugs": {
25
+ "url": "https://github.com/Cap-go/StreamCall/issues"
26
+ },
27
+ "keywords": [
28
+ "capacitor",
29
+ "plugin",
30
+ "stream",
31
+ "call",
32
+ "video",
33
+ "audio",
34
+ "getstream",
35
+ "stream-video",
36
+ "stream-call",
37
+ "group-call",
38
+ "native",
39
+ "ios",
40
+ "android",
41
+ "web",
42
+ "capacitor-plugin"
43
+ ],
44
+ "scripts": {
45
+ "verify": "npm run verify:ios && npm run verify:android && npm run verify:web",
46
+ "verify:ios": "xcodebuild -scheme StreamCall -destination generic/platform=iOS",
47
+ "verify:android": "cd android && ./gradlew clean build test && cd ..",
48
+ "verify:web": "npm run build",
49
+ "lint": "npm run eslint && npm run prettier -- --check && npm run swiftlint -- lint",
50
+ "fmt": "npm run eslint -- --fix && npm run prettier -- --write && npm run swiftlint -- --fix --format",
51
+ "eslint": "eslint . --ext ts",
52
+ "prettier": "prettier \"**/*.{css,html,ts,js,java}\" --plugin=prettier-plugin-java",
53
+ "swiftlint": "node-swiftlint",
54
+ "docgen": "docgen --api StreamCallPlugin --output-readme README.md --output-json dist/docs.json",
55
+ "build": "npm run clean && npm run docgen && tsc && rollup -c rollup.config.mjs",
56
+ "clean": "rimraf ./dist",
57
+ "watch": "tsc --watch",
58
+ "prepublishOnly": "npm run build"
59
+ },
60
+ "devDependencies": {
61
+ "@capacitor/android": "^7.0.0",
62
+ "@capacitor/core": "^7.0.0",
63
+ "@capacitor/docgen": "^0.3.0",
64
+ "@capacitor/ios": "^7.0.0",
65
+ "@ionic/eslint-config": "^0.4.0",
66
+ "@ionic/prettier-config": "^4.0.0",
67
+ "@ionic/swiftlint-config": "^2.0.0",
68
+ "eslint": "^8.57.0",
69
+ "prettier": "^3.5.0",
70
+ "prettier-plugin-java": "^2.6.7",
71
+ "rimraf": "^6.0.1",
72
+ "rollup": "^4.34.6",
73
+ "swiftlint": "^2.0.0",
74
+ "typescript": "^5.7.3"
75
+ },
76
+ "peerDependencies": {
77
+ "@capacitor/core": ">=7.0.0"
78
+ },
79
+ "prettier": "@ionic/prettier-config",
80
+ "swiftlint": "@ionic/swiftlint-config",
81
+ "eslintConfig": {
82
+ "extends": "@ionic/eslint-config/recommended"
83
+ },
84
+ "capacitor": {
85
+ "ios": {
86
+ "src": "ios"
87
+ },
88
+ "android": {
89
+ "src": "android"
90
+ }
91
+ },
92
+ "dependencies": {
93
+ "@stream-io/video-client": "1.16.7",
94
+ "@stream-io/audio-filters-web": "^0.2.2"
95
+ }
96
+ }