Selaa lähdekoodia

Remove Calendar page; open Manage Subscription in App Store

- Drop Calendar sidebar page, UI, Google Calendar client, and ScheduledMeeting model; remove calendar.events OAuth scope.
- Simplify loadSchedule to Classroom to-dos only; remove dead add-meeting UI.
- Open subscription management with NSWorkspace (macappstore://, then https) instead of the in-app browser.

Made-with: Cursor
huzaifahayat12 1 viikko sitten
vanhempi
commit
dd764f6d28

+ 1 - 2
classroom_app/Auth/GoogleOAuthService.swift

@@ -41,12 +41,11 @@ final class GoogleOAuthService: NSObject {
41 41
     private let bundledClientSecret = "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"
42 42
     private let legacyMeetingClientId = "824412072260-m9g5g6mlemnb0o079rtuqnh0e1unmelc.apps.googleusercontent.com"
43 43
 
44
-    // Calendar is needed for schedule. Profile/email make login feel complete in-app.
44
+    // Profile/email make login feel complete in-app.
45 45
     private let scopes = [
46 46
         "openid",
47 47
         "email",
48 48
         "profile",
49
-        "https://www.googleapis.com/auth/calendar.events",
50 49
         // Classroom To-do (assignments/quizzes)
51 50
         "https://www.googleapis.com/auth/classroom.courses.readonly",
52 51
         "https://www.googleapis.com/auth/classroom.coursework.me.readonly",

+ 0 - 323
classroom_app/Google/GoogleCalendarClient.swift

@@ -1,323 +0,0 @@
1
-import Foundation
2
-
3
-enum GoogleCalendarClientError: Error {
4
-    case invalidResponse
5
-    case httpStatus(Int, String)
6
-    case decodeFailed(String)
7
-}
8
-
9
-final class GoogleCalendarClient {
10
-    struct Options: Sendable {
11
-        var daysAhead: Int
12
-        var maxResults: Int
13
-        var includeNonMeetEvents: Bool
14
-
15
-        init(daysAhead: Int = 180, maxResults: Int = 200, includeNonMeetEvents: Bool = true) {
16
-            self.daysAhead = daysAhead
17
-            self.maxResults = maxResults
18
-            self.includeNonMeetEvents = includeNonMeetEvents
19
-        }
20
-    }
21
-
22
-    private let session: URLSession
23
-
24
-    init(session: URLSession = .shared) {
25
-        self.session = session
26
-    }
27
-
28
-    func fetchUpcomingMeetings(accessToken: String) async throws -> [ScheduledMeeting] {
29
-        try await fetchUpcomingMeetings(accessToken: accessToken, options: Options())
30
-    }
31
-
32
-    func fetchUpcomingMeetings(accessToken: String, options: Options) async throws -> [ScheduledMeeting] {
33
-        let now = Date()
34
-        let end = Calendar.current.date(byAdding: .day, value: max(1, options.daysAhead), to: now) ?? now.addingTimeInterval(180 * 24 * 60 * 60)
35
-        let formatter = ISO8601DateFormatter()
36
-        formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
37
-        let totalLimit = max(1, options.maxResults)
38
-        let pageSize = min(250, totalLimit)
39
-        var nextPageToken: String?
40
-        var meetings: [ScheduledMeeting] = []
41
-
42
-        repeat {
43
-            var components = URLComponents(string: "https://www.googleapis.com/calendar/v3/calendars/primary/events")!
44
-            var queryItems = [
45
-                URLQueryItem(name: "timeMin", value: formatter.string(from: now)),
46
-                URLQueryItem(name: "timeMax", value: formatter.string(from: end)),
47
-                URLQueryItem(name: "singleEvents", value: "true"),
48
-                URLQueryItem(name: "orderBy", value: "startTime"),
49
-                URLQueryItem(name: "maxResults", value: String(pageSize)),
50
-                URLQueryItem(name: "conferenceDataVersion", value: "1")
51
-            ]
52
-            if let nextPageToken {
53
-                queryItems.append(URLQueryItem(name: "pageToken", value: nextPageToken))
54
-            }
55
-            components.queryItems = queryItems
56
-
57
-            var request = URLRequest(url: components.url!)
58
-            request.httpMethod = "GET"
59
-            request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
60
-
61
-            let (data, response) = try await session.data(for: request)
62
-            guard let http = response as? HTTPURLResponse else { throw GoogleCalendarClientError.invalidResponse }
63
-            guard (200..<300).contains(http.statusCode) else {
64
-                let body = String(data: data, encoding: .utf8) ?? "<no body>"
65
-                throw GoogleCalendarClientError.httpStatus(http.statusCode, body)
66
-            }
67
-
68
-            let decoded: EventsList
69
-            do {
70
-                decoded = try JSONDecoder().decode(EventsList.self, from: data)
71
-            } catch {
72
-                let raw = String(data: data, encoding: .utf8) ?? "<unreadable body>"
73
-                throw GoogleCalendarClientError.decodeFailed(raw)
74
-            }
75
-
76
-            let pageMeetings: [ScheduledMeeting] = decoded.items.compactMap { item in
77
-            let title = item.summary?.trimmingCharacters(in: .whitespacesAndNewlines)
78
-            let subtitle = item.organizer?.displayName ?? item.organizer?.email
79
-
80
-            guard let start = item.start?.resolvedDate,
81
-                  let end = item.end?.resolvedDate else { return nil }
82
-
83
-            let isAllDay = item.start?.date != nil
84
-
85
-            let meetURL: URL? = {
86
-                if let hangout = item.hangoutLink, let u = URL(string: hangout) { return u }
87
-                let entry = item.conferenceData?.entryPoints?.first(where: { $0.entryPointType == "video" && ($0.uri?.contains("meet.google.com") ?? false) })
88
-                if let uri = entry?.uri, let u = URL(string: uri) { return u }
89
-                if options.includeNonMeetEvents, let htmlLink = item.htmlLink, let u = URL(string: htmlLink) { return u }
90
-                return nil
91
-            }()
92
-
93
-            if meetURL == nil, options.includeNonMeetEvents == false { return nil }
94
-            guard let meetURL else { return nil }
95
-
96
-            return ScheduledMeeting(
97
-                id: item.id ?? UUID().uuidString,
98
-                title: (title?.isEmpty == false) ? title! : "Untitled meeting",
99
-                subtitle: subtitle,
100
-                startDate: start,
101
-                endDate: end,
102
-                meetURL: meetURL,
103
-                isAllDay: isAllDay
104
-            )
105
-            }
106
-
107
-            meetings.append(contentsOf: pageMeetings)
108
-            nextPageToken = decoded.nextPageToken
109
-        } while nextPageToken != nil && meetings.count < totalLimit
110
-
111
-        if meetings.count > totalLimit {
112
-            meetings = Array(meetings.prefix(totalLimit))
113
-        }
114
-        return meetings
115
-    }
116
-
117
-    func createEvent(accessToken: String,
118
-                     title: String,
119
-                     description: String?,
120
-                     start: Date,
121
-                     end: Date,
122
-                     timeZone: TimeZone = .current) async throws -> ScheduledMeeting {
123
-        var components = URLComponents(string: "https://www.googleapis.com/calendar/v3/calendars/primary/events")!
124
-        components.queryItems = [
125
-            URLQueryItem(name: "conferenceDataVersion", value: "1")
126
-        ]
127
-
128
-        let requestID = UUID().uuidString
129
-        let body = CreateEventRequest(
130
-            summary: title,
131
-            description: description,
132
-            start: CreateEventRequest.EventDateTime(dateTime: ISO8601DateFormatter.fractional.string(from: start), timeZone: timeZone.identifier),
133
-            end: CreateEventRequest.EventDateTime(dateTime: ISO8601DateFormatter.fractional.string(from: end), timeZone: timeZone.identifier),
134
-            conferenceData: CreateEventRequest.ConferenceData(
135
-                createRequest: CreateEventRequest.CreateRequest(
136
-                    requestId: requestID,
137
-                    conferenceSolutionKey: CreateEventRequest.ConferenceSolutionKey(type: "hangoutsMeet")
138
-                )
139
-            )
140
-        )
141
-
142
-        var request = URLRequest(url: components.url!)
143
-        request.httpMethod = "POST"
144
-        request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
145
-        request.setValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type")
146
-        request.httpBody = try JSONEncoder().encode(body)
147
-
148
-        let (data, response) = try await session.data(for: request)
149
-        guard let http = response as? HTTPURLResponse else { throw GoogleCalendarClientError.invalidResponse }
150
-        guard (200..<300).contains(http.statusCode) else {
151
-            let raw = String(data: data, encoding: .utf8) ?? "<no body>"
152
-            throw GoogleCalendarClientError.httpStatus(http.statusCode, raw)
153
-        }
154
-
155
-        let created: EventItem
156
-        do {
157
-            created = try JSONDecoder().decode(EventItem.self, from: data)
158
-        } catch {
159
-            let raw = String(data: data, encoding: .utf8) ?? "<unreadable body>"
160
-            throw GoogleCalendarClientError.decodeFailed(raw)
161
-        }
162
-
163
-        guard let startDate = created.start?.resolvedDate,
164
-              let endDate = created.end?.resolvedDate else {
165
-            throw GoogleCalendarClientError.decodeFailed("Created event missing start/end.")
166
-        }
167
-
168
-        let isAllDay = created.start?.date != nil
169
-        let meetURL: URL? = {
170
-            if let hangout = created.hangoutLink, let u = URL(string: hangout) { return u }
171
-            let entry = created.conferenceData?.entryPoints?.first(where: { $0.entryPointType == "video" && ($0.uri?.contains("meet.google.com") ?? false) })
172
-            if let uri = entry?.uri, let u = URL(string: uri) { return u }
173
-            if let htmlLink = created.htmlLink, let u = URL(string: htmlLink) { return u }
174
-            return nil
175
-        }()
176
-        guard let meetURL else {
177
-            throw GoogleCalendarClientError.decodeFailed("Created event missing Meet/HTML link.")
178
-        }
179
-
180
-        let cleanedTitle = created.summary?.trimmingCharacters(in: .whitespacesAndNewlines)
181
-        let subtitle = created.organizer?.displayName ?? created.organizer?.email
182
-        return ScheduledMeeting(
183
-            id: created.id ?? UUID().uuidString,
184
-            title: (cleanedTitle?.isEmpty == false) ? cleanedTitle! : "Untitled meeting",
185
-            subtitle: subtitle,
186
-            startDate: startDate,
187
-            endDate: endDate,
188
-            meetURL: meetURL,
189
-            isAllDay: isAllDay
190
-        )
191
-    }
192
-}
193
-
194
-extension GoogleCalendarClientError: LocalizedError {
195
-    var errorDescription: String? {
196
-        switch self {
197
-        case .invalidResponse:
198
-            return "Google Calendar returned an invalid response."
199
-        case let .httpStatus(status, body):
200
-            return "Google Calendar API error (\(status)): \(body)"
201
-        case let .decodeFailed(raw):
202
-            return "Failed to parse Google Calendar events: \(raw)"
203
-        }
204
-    }
205
-}
206
-
207
-// MARK: - Calendar API models
208
-
209
-private struct EventsList: Decodable {
210
-    let items: [EventItem]
211
-    let nextPageToken: String?
212
-}
213
-
214
-private struct EventItem: Decodable {
215
-    let id: String?
216
-    let summary: String?
217
-    let hangoutLink: String?
218
-    let htmlLink: String?
219
-    let organizer: Organizer?
220
-    let start: EventDateTime?
221
-    let end: EventDateTime?
222
-    let conferenceData: ConferenceData?
223
-}
224
-
225
-private struct Organizer: Decodable {
226
-    let displayName: String?
227
-    let email: String?
228
-}
229
-
230
-private struct EventDateTime: Decodable {
231
-    let dateTime: String?
232
-    let date: String?
233
-    let timeZone: String?
234
-
235
-    var resolvedDate: Date? {
236
-        if let dateTime, let parsed = Self.parseDateTime(dateTime, timeZone: timeZone) {
237
-            return parsed
238
-        }
239
-        if let date, let parsed = DateFormatter.googleAllDay.date(from: date) {
240
-            return parsed
241
-        }
242
-        return nil
243
-    }
244
-
245
-    private static func parseDateTime(_ raw: String, timeZone: String?) -> Date? {
246
-        if let dt = ISO8601DateFormatter.fractional.date(from: raw) ?? ISO8601DateFormatter.nonFractional.date(from: raw) {
247
-            return dt
248
-        }
249
-
250
-        // Some Calendar payloads provide dateTime without explicit zone and separate timeZone field.
251
-        let formatter = DateFormatter()
252
-        formatter.calendar = Calendar(identifier: .gregorian)
253
-        formatter.locale = Locale(identifier: "en_US_POSIX")
254
-        formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
255
-        if let timeZone, let tz = TimeZone(identifier: timeZone) {
256
-            formatter.timeZone = tz
257
-        } else {
258
-            formatter.timeZone = TimeZone.current
259
-        }
260
-        return formatter.date(from: raw)
261
-    }
262
-}
263
-
264
-private struct ConferenceData: Decodable {
265
-    let entryPoints: [EntryPoint]?
266
-}
267
-
268
-private struct EntryPoint: Decodable {
269
-    let entryPointType: String?
270
-    let uri: String?
271
-}
272
-
273
-private struct CreateEventRequest: Encodable {
274
-    struct EventDateTime: Encodable {
275
-        let dateTime: String
276
-        let timeZone: String
277
-    }
278
-
279
-    struct ConferenceSolutionKey: Encodable {
280
-        let type: String
281
-    }
282
-
283
-    struct CreateRequest: Encodable {
284
-        let requestId: String
285
-        let conferenceSolutionKey: ConferenceSolutionKey
286
-    }
287
-
288
-    struct ConferenceData: Encodable {
289
-        let createRequest: CreateRequest
290
-    }
291
-
292
-    let summary: String
293
-    let description: String?
294
-    let start: EventDateTime
295
-    let end: EventDateTime
296
-    let conferenceData: ConferenceData
297
-}
298
-
299
-private extension ISO8601DateFormatter {
300
-    static let fractional: ISO8601DateFormatter = {
301
-        let f = ISO8601DateFormatter()
302
-        f.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
303
-        return f
304
-    }()
305
-
306
-    static let nonFractional: ISO8601DateFormatter = {
307
-        let f = ISO8601DateFormatter()
308
-        f.formatOptions = [.withInternetDateTime]
309
-        return f
310
-    }()
311
-}
312
-
313
-private extension DateFormatter {
314
-    static let googleAllDay: DateFormatter = {
315
-        let f = DateFormatter()
316
-        f.calendar = Calendar(identifier: .gregorian)
317
-        f.locale = Locale(identifier: "en_US_POSIX")
318
-        f.timeZone = TimeZone(secondsFromGMT: 0)
319
-        f.dateFormat = "yyyy-MM-dd"
320
-        return f
321
-    }()
322
-}
323
-

+ 0 - 12
classroom_app/Models/ScheduledMeeting.swift

@@ -1,12 +0,0 @@
1
-import Foundation
2
-
3
-struct ScheduledMeeting: Identifiable, Equatable {
4
-    let id: String
5
-    let title: String
6
-    let subtitle: String?
7
-    let startDate: Date
8
-    let endDate: Date
9
-    let meetURL: URL
10
-    let isAllDay: Bool
11
-}
12
-

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 13 - 1086
classroom_app/ViewController.swift