Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions Android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
android:name="android.hardware.camera.autofocus"
android:required="false" />

<!-- permissions needed for notifications -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

<!-- permissions needed for using the internet or an embedded WebKit browser -->
<uses-permission android:name="android.permission.INTERNET" />
<!-- <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> -->
Expand All @@ -30,7 +33,16 @@
android:supportsRtl="true"
android:enableOnBackInvokedCallback="true"
android:icon="@mipmap/ic_launcher">

<!-- Notification Icon -->
<meta-data
android:name="showcase.module.default_notification_icon"
android:resource="@drawable/ic_notification" />

<!-- Google Maps -->
<meta-data android:name="com.google.android.geo.API_KEY" android:value="AIzaSyBdt_bhz4zi6WAUelkVkEnxMs9pkbEKXKM"/>

<!-- Main Activity -->
<activity
android:name=".MainActivity"
android:exported="true"
Expand All @@ -49,6 +61,7 @@
<data android:scheme="org.appfair.app.showcaselite" android:host="auth" />
</intent-filter>
</activity>

<!-- needed in order for the camera to be able to share the photo with the app -->
<provider
android:name="androidx.core.content.FileProvider"
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 6 additions & 1 deletion Darwin/Entitlements.plist
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
<dict>
<key>aps-environment</key>
<string>production</string>
<key>com.apple.developer.aps-environment</key>
<string>production</string>
</dict>
</plist>
8 changes: 7 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ let package = Package(
],
dependencies: [
.package(url: "https://source.skip.tools/skip.git", from: "1.4.0"),
.package(url: "https://source.skip.tools/skip-ui.git", from: "1.26.0"),

// TODO: Update skip ui package URL and version after skip ui PR has been merged
.package(url: "https://source.skip.tools/skip-ui.git", branch: "main"),
//.package(url: "https://source.skip.tools/skip-ui.git", from: "1.26.0"),

.package(url: "https://source.skip.tools/skip-av.git", "0.0.0"..<"2.0.0"),
.package(url: "https://source.skip.tools/skip-kit.git", "0.0.0"..<"2.0.0"),
.package(url: "https://source.skip.tools/skip-sql.git", "0.12.1"..<"2.0.0"),
Expand All @@ -20,6 +24,7 @@ let package = Package(
.package(url: "https://source.skip.tools/skip-keychain.git", "0.3.0"..<"2.0.0"),
.package(url: "https://source.skip.tools/skip-marketplace.git", "0.0.0"..<"2.0.0"),
.package(url: "https://source.skip.tools/skip-authentication-services.git", "0.0.0"..<"2.0.0"),
.package(url: "https://source.skip.tools/skip-notify.git", "0.0.0"..<"2.0.0"),
],
targets: [
.target(name: "Showcase", dependencies: [
Expand All @@ -33,6 +38,7 @@ let package = Package(
.product(name: "SkipKeychain", package: "skip-keychain"),
.product(name: "SkipMarketplace", package: "skip-marketplace"),
.product(name: "SkipAuthenticationServices", package: "skip-authentication-services"),
.product(name: "SkipNotify", package: "skip-notify"),
], resources: [.process("Resources")], plugins: [.plugin(name: "skipstone", package: "skip")]),
]
)
2 changes: 1 addition & 1 deletion Project.xcworkspace/contents.xcworkspacedata

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

187 changes: 187 additions & 0 deletions Sources/Showcase/NotificationPlayground.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
// Copyright 2023–2026 Skip
import SwiftUI
import SkipKit
import SkipNotify

struct NotificationPlayground: View {
@State var notificationPermission: String = ""

var body: some View {
List {
Section {
Text("Permission Status: \(self.notificationPermission)")
.task {
self.notificationPermission = await PermissionManager.queryPostNotificationPermission().rawValue
}
.foregroundStyle(self.notificationPermission == "authorized" ? .green : .red)

Button("Request Push Notification Permission") {
Task { @MainActor in
do {
self.notificationPermission = try await PermissionManager.requestPostNotificationPermission(alert: true, sound: false, badge: true).rawValue
logger.log("obtained push notification permission: \(self.notificationPermission)")
} catch {
logger.error("error obtaining push notification permission: \(error)")
self.notificationPermission = "error: \(error)"
}
}
}
.buttonStyle(.plain)
}

Section {
NavigationLink("Skip Notify") {
SkipNotifyNotificationPlaygroundView()
}

NavigationLink("Local Notifications") {
LocalNotificationPlaygroundView()
}
}
}
.navigationTitle("Notification")
}
}

struct SkipNotifyNotificationPlaygroundView: View {
@State var token: String = ""
var body: some View {
VStack {
HStack {
TextField("Push Notification Client Token", text: $token)
.textFieldStyle(.roundedBorder)
Button("Copy") {
UIPasteboard.general.string = token
}
.buttonStyle(.automatic)
}

Button("Generate Push Notification Token") {
Task { @MainActor in
do {
self.token = try await SkipNotify.shared.fetchNotificationToken()
logger.log("obtained push notification token: \(self.token)")
} catch {
logger.error("error obtaining push notification token: \(error)")
}
}
}
.buttonStyle(.borderedProminent)
}
.navigationTitle("Skip Notify")
.padding()
}
}

struct LocalNotificationPlaygroundView: View {
@State var timerDate: Date?
@State var nextTriggerDate: Date?
let timer = Timer.publish(every: 1.0, on: .main, in: .default).autoconnect()
private var secondsUntilNextTrigger: Int? {
guard let timerDate, let nextTriggerDate else { return nil }
let seconds = Int(nextTriggerDate.timeIntervalSince(timerDate))
return seconds > 0 ? seconds : nil
}

private let notificationCenterDelegate = NotificationCenterDelegate()

init() {
let notificationCenter = UNUserNotificationCenter.current()
notificationCenter.delegate = self.notificationCenterDelegate
}

var body: some View {
VStack(spacing: 20) {
VStack(spacing: 10) {
Button("Trigger Immediate Push Notification") {
Task {
await self.addNotificationRequest(
title: "Title",
body: "Body",
identifier: UUID().uuidString
)
}
}
.backgroundStyle(.blue)
.buttonStyle(.borderedProminent)

Button("Trigger Scheduled Push Notification") {
Task {
await self.addNotificationRequest(
title: "Title",
body: "Body",
identifier: UUID().uuidString,
trigger: {
let result = UNTimeIntervalNotificationTrigger(
timeInterval: 10,
repeats: false
)
self.nextTriggerDate = result.nextTriggerDate()
return result
}()
)
}
}
.backgroundStyle(.blue)
.buttonStyle(.borderedProminent)
.disabled(self.secondsUntilNextTrigger != nil)
}

if let seconds = self.secondsUntilNextTrigger {
Text("Next notification in \(seconds) s")
.foregroundStyle(.secondary)

Button("Remove all pending notifications") {
self.removeAllPendingNotifications()
self.nextTriggerDate = nil
}
.foregroundStyle(.red)
} else {
Text("No scheduled notification")
.foregroundStyle(.secondary)
}
}
.navigationTitle("Local Notifications")
.onReceive(self.timer) { date in
self.timerDate = Date()
}
}

private func addNotificationRequest(
title: String,
body: String,
identifier: String,
trigger: UNNotificationTrigger? = nil
) async {

let content = UNMutableNotificationContent()
content.title = title
content.body = body
content.userInfo = [
"identifier": UUID().uuidString
]

let request = UNNotificationRequest(
identifier: identifier,
content: content,
trigger: trigger
)

let notificationCenter = UNUserNotificationCenter.current()
try? await notificationCenter.add(request)
}

private func removeAllPendingNotifications() {
let notificationCenter = UNUserNotificationCenter.current()
notificationCenter.removeAllPendingNotificationRequests()
}
}

private final class NotificationCenterDelegate: NSObject, UNUserNotificationCenterDelegate {
func userNotificationCenter(
_ center: UNUserNotificationCenter,
willPresent notification: UNNotification
) async -> UNNotificationPresentationOptions {
return [.banner]
}
}
5 changes: 5 additions & 0 deletions Sources/Showcase/PlaygroundListView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ enum PlaygroundType: CaseIterable, View {
case menu
case modifier
case navigationStack
case notification
case observable
case offsetPosition
case onSubmit
Expand Down Expand Up @@ -164,6 +165,8 @@ enum PlaygroundType: CaseIterable, View {
return LocalizedStringResource("Modifiers")
case .navigationStack:
return LocalizedStringResource("NavigationStack")
case .notification:
return LocalizedStringResource("Notification")
case .observable:
return LocalizedStringResource("Observable")
case .offsetPosition:
Expand Down Expand Up @@ -329,6 +332,8 @@ enum PlaygroundType: CaseIterable, View {
ModifierPlayground()
case .navigationStack:
NavigationStackPlayground()
case .notification:
NotificationPlayground()
case .observable:
ObservablePlayground()
case .offsetPosition:
Expand Down
Loading
Loading