vibefast-cli 0.5.0 → 0.5.1
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.
- package/FEATURE-DEPENDENCY-SPEC.md +338 -0
- package/dist/__tests__/integration.test.d.ts +2 -0
- package/dist/__tests__/integration.test.d.ts.map +1 -0
- package/dist/__tests__/integration.test.js +219 -0
- package/dist/__tests__/integration.test.js.map +1 -0
- package/dist/__tests__/recipes.test.d.ts +2 -0
- package/dist/__tests__/recipes.test.d.ts.map +1 -0
- package/dist/__tests__/recipes.test.js +143 -0
- package/dist/__tests__/recipes.test.js.map +1 -0
- package/dist/commands/__tests__/init.test.d.ts +2 -0
- package/dist/commands/__tests__/init.test.d.ts.map +1 -0
- package/dist/commands/__tests__/init.test.js +95 -0
- package/dist/commands/__tests__/init.test.js.map +1 -0
- package/dist/commands/__tests__/platform.test.d.ts +2 -0
- package/dist/commands/__tests__/platform.test.d.ts.map +1 -0
- package/dist/commands/__tests__/platform.test.js +123 -0
- package/dist/commands/__tests__/platform.test.js.map +1 -0
- package/dist/commands/add.d.ts.map +1 -1
- package/dist/commands/add.js +4 -5
- package/dist/commands/add.js.map +1 -1
- package/dist/core/journal.d.ts.map +1 -1
- package/dist/core/journal.js +36 -19
- package/dist/core/journal.js.map +1 -1
- package/dist/core/recipes.d.ts.map +1 -1
- package/dist/core/recipes.js +8 -39
- package/dist/core/recipes.js.map +1 -1
- package/package.json +1 -1
- package/recipes/ios-widget/recipe.json +78 -0
- package/recipes/ios-widget/targets/widget/AppIntent.swift +46 -0
- package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-20x20@1x.png +0 -0
- package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-20x20@2x.png +0 -0
- package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-20x20@3x.png +0 -0
- package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-29x29@1x.png +0 -0
- package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-29x29@2x.png +0 -0
- package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-29x29@3x.png +0 -0
- package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-40x40@1x.png +0 -0
- package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-40x40@2x.png +0 -0
- package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-40x40@3x.png +0 -0
- package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-60x60@2x.png +0 -0
- package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-60x60@3x.png +0 -0
- package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-76x76@1x.png +0 -0
- package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-76x76@2x.png +0 -0
- package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-83.5x83.5@2x.png +0 -0
- package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/Contents.json +122 -0
- package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png +0 -0
- package/recipes/ios-widget/targets/widget/CalorieTrackerWidget.swift +424 -0
- package/recipes/ios-widget/targets/widget/HabitTrackerWidget.swift +305 -0
- package/recipes/ios-widget/targets/widget/Info.plist +11 -0
- package/recipes/ios-widget/targets/widget/WidgetLiveActivity.swift +75 -0
- package/recipes/ios-widget/targets/widget/expo-target.config.js +10 -0
- package/recipes/ios-widget/targets/widget/generated.entitlements +5 -0
- package/recipes/ios-widget/targets/widget/index.swift +18 -0
- package/recipes/ios-widget/targets/widget/widgets.swift +96 -0
- package/recipes/ios-widget@latest.zip +0 -0
- package/recipes/payments/apps/native/src/app/(root)/(protected)/paywall/index.tsx +74 -0
- package/recipes/payments/apps/native/src/app/(root)/(protected)/paywall/local.tsx +25 -0
- package/recipes/payments/apps/native/src/app/(root)/(protected)/paywall/remote.tsx +23 -0
- package/recipes/payments/apps/native/src/features/payments/README.md +200 -0
- package/recipes/payments/apps/native/src/features/payments/app/local-paywall.tsx +194 -0
- package/recipes/payments/apps/native/src/features/payments/app/remote-paywall.tsx +79 -0
- package/recipes/payments/apps/native/src/features/payments/components/payment-initializer.tsx +95 -0
- package/recipes/payments/apps/native/src/features/payments/components/paywall-error-state.tsx +60 -0
- package/recipes/payments/apps/native/src/features/payments/components/paywall-local-mode.tsx +116 -0
- package/recipes/payments/apps/native/src/features/payments/components/paywall-product-card.tsx +133 -0
- package/recipes/payments/apps/native/src/features/payments/components/paywall-remote-mode.tsx +146 -0
- package/recipes/payments/apps/native/src/features/payments/hooks/use-entitlement.ts +63 -0
- package/recipes/payments/apps/native/src/features/payments/index.ts +8 -0
- package/recipes/payments/apps/native/src/features/payments/services/revenuecat-adapter.ts +407 -0
- package/recipes/payments/recipe.json +58 -0
- package/recipes/payments@latest.zip +0 -0
- package/src/__tests__/integration.test.ts +249 -0
- package/src/__tests__/recipes.test.ts +168 -0
- package/src/commands/__tests__/init.test.ts +112 -0
- package/src/commands/__tests__/platform.test.ts +141 -0
- package/src/commands/add.ts +4 -5
- package/src/core/journal.ts +42 -25
- package/src/core/recipes.ts +8 -40
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
import WidgetKit
|
|
3
|
+
|
|
4
|
+
// MARK: - Data Models
|
|
5
|
+
struct HabitItem {
|
|
6
|
+
let id: String
|
|
7
|
+
let name: String
|
|
8
|
+
let color: Color
|
|
9
|
+
let completedDays: [Bool]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
struct HabitEntry: TimelineEntry {
|
|
13
|
+
let date: Date
|
|
14
|
+
let totalStreak: Int
|
|
15
|
+
let completionRate: Int
|
|
16
|
+
let habits: [HabitItem]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// MARK: - Timeline Provider
|
|
20
|
+
struct HabitProvider: TimelineProvider {
|
|
21
|
+
func placeholder(in context: Context) -> HabitEntry {
|
|
22
|
+
HabitEntry(
|
|
23
|
+
date: Date(),
|
|
24
|
+
totalStreak: 12,
|
|
25
|
+
completionRate: 85,
|
|
26
|
+
habits: getHardcodedHabits()
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
func getSnapshot(in context: Context, completion: @escaping (HabitEntry) -> ()) {
|
|
31
|
+
let entry = HabitEntry(
|
|
32
|
+
date: Date(),
|
|
33
|
+
totalStreak: 12,
|
|
34
|
+
completionRate: 85,
|
|
35
|
+
habits: getHardcodedHabits()
|
|
36
|
+
)
|
|
37
|
+
completion(entry)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
|
|
41
|
+
let currentDate = Date()
|
|
42
|
+
let entry = HabitEntry(
|
|
43
|
+
date: currentDate,
|
|
44
|
+
totalStreak: 12,
|
|
45
|
+
completionRate: 85,
|
|
46
|
+
habits: getHardcodedHabits()
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
// Refresh every hour
|
|
50
|
+
let nextUpdate = Calendar.current.date(byAdding: .hour, value: 1, to: currentDate)!
|
|
51
|
+
let timeline = Timeline(entries: [entry], policy: .after(nextUpdate))
|
|
52
|
+
completion(timeline)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private func getHardcodedHabits() -> [HabitItem] {
|
|
56
|
+
return [
|
|
57
|
+
HabitItem(
|
|
58
|
+
id: "exercise",
|
|
59
|
+
name: "Exercise",
|
|
60
|
+
color: Color(red: 1.0, green: 0.42, blue: 0.42),
|
|
61
|
+
completedDays: [true, true, false, true, true, true, false]
|
|
62
|
+
),
|
|
63
|
+
HabitItem(
|
|
64
|
+
id: "read",
|
|
65
|
+
name: "Read",
|
|
66
|
+
color: Color(red: 0.31, green: 0.8, blue: 0.77),
|
|
67
|
+
completedDays: [true, true, true, true, false, true, true]
|
|
68
|
+
),
|
|
69
|
+
HabitItem(
|
|
70
|
+
id: "meditate",
|
|
71
|
+
name: "Meditate",
|
|
72
|
+
color: Color(red: 0.27, green: 0.72, blue: 0.82),
|
|
73
|
+
completedDays: [true, false, true, true, true, true, true]
|
|
74
|
+
),
|
|
75
|
+
HabitItem(
|
|
76
|
+
id: "water",
|
|
77
|
+
name: "Water",
|
|
78
|
+
color: Color(red: 0.59, green: 0.81, blue: 0.71),
|
|
79
|
+
completedDays: [true, true, true, true, true, false, true]
|
|
80
|
+
)
|
|
81
|
+
]
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// MARK: - Widget Views
|
|
86
|
+
struct HabitTrackerWidgetEntryView: View {
|
|
87
|
+
var entry: HabitProvider.Entry
|
|
88
|
+
@Environment(\.widgetFamily) var family
|
|
89
|
+
|
|
90
|
+
var body: some View {
|
|
91
|
+
switch family {
|
|
92
|
+
case .systemSmall:
|
|
93
|
+
SmallHabitView(entry: entry)
|
|
94
|
+
case .systemMedium:
|
|
95
|
+
MediumHabitView(entry: entry)
|
|
96
|
+
case .systemLarge:
|
|
97
|
+
LargeHabitView(entry: entry)
|
|
98
|
+
default:
|
|
99
|
+
MediumHabitView(entry: entry)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
struct SmallHabitView: View {
|
|
105
|
+
let entry: HabitEntry
|
|
106
|
+
|
|
107
|
+
var body: some View {
|
|
108
|
+
VStack(alignment: .leading, spacing: 8) {
|
|
109
|
+
HStack {
|
|
110
|
+
Text("Habits")
|
|
111
|
+
.font(.system(size: 14, weight: .bold))
|
|
112
|
+
.foregroundColor(.primary)
|
|
113
|
+
Spacer()
|
|
114
|
+
HStack(spacing: 2) {
|
|
115
|
+
Image(systemName: "flame.fill")
|
|
116
|
+
.font(.system(size: 12))
|
|
117
|
+
.foregroundColor(.orange)
|
|
118
|
+
Text("\(entry.totalStreak)")
|
|
119
|
+
.font(.system(size: 12, weight: .bold))
|
|
120
|
+
.foregroundColor(.primary)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
Spacer()
|
|
125
|
+
|
|
126
|
+
VStack(spacing: 4) {
|
|
127
|
+
ForEach(entry.habits.prefix(3), id: \.id) { habit in
|
|
128
|
+
HStack(spacing: 2) {
|
|
129
|
+
ForEach(0..<7, id: \.self) { day in
|
|
130
|
+
RoundedRectangle(cornerRadius: 1)
|
|
131
|
+
.fill(habit.completedDays[day] ? habit.color : Color.gray.opacity(0.3))
|
|
132
|
+
.frame(width: 8, height: 8)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
Spacer()
|
|
139
|
+
|
|
140
|
+
Text("\(entry.completionRate)%")
|
|
141
|
+
.font(.system(size: 18, weight: .bold))
|
|
142
|
+
.foregroundColor(.blue)
|
|
143
|
+
}
|
|
144
|
+
.padding(12)
|
|
145
|
+
.containerBackground(Color.clear, for: .widget)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
struct MediumHabitView: View {
|
|
150
|
+
let entry: HabitEntry
|
|
151
|
+
|
|
152
|
+
var body: some View {
|
|
153
|
+
VStack(alignment: .leading, spacing: 10) {
|
|
154
|
+
HStack {
|
|
155
|
+
Text("Habit Tracker")
|
|
156
|
+
.font(.system(size: 14, weight: .bold))
|
|
157
|
+
.foregroundColor(.primary)
|
|
158
|
+
Spacer()
|
|
159
|
+
HStack(spacing: 3) {
|
|
160
|
+
Image(systemName: "flame.fill")
|
|
161
|
+
.font(.system(size: 12))
|
|
162
|
+
.foregroundColor(.orange)
|
|
163
|
+
Text("\(entry.totalStreak)")
|
|
164
|
+
.font(.system(size: 12, weight: .bold))
|
|
165
|
+
.foregroundColor(.primary)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
VStack(alignment: .leading, spacing: 2) {
|
|
170
|
+
Text("\(entry.completionRate)%")
|
|
171
|
+
.font(.system(size: 20, weight: .heavy))
|
|
172
|
+
.foregroundColor(.blue)
|
|
173
|
+
Text("This week")
|
|
174
|
+
.font(.system(size: 9))
|
|
175
|
+
.foregroundColor(.secondary)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
VStack(spacing: 4) {
|
|
179
|
+
ForEach(entry.habits, id: \.id) { habit in
|
|
180
|
+
HStack(alignment: .center, spacing: 6) {
|
|
181
|
+
HStack(spacing: 3) {
|
|
182
|
+
Circle()
|
|
183
|
+
.fill(habit.color)
|
|
184
|
+
.frame(width: 5, height: 5)
|
|
185
|
+
Text(habit.name)
|
|
186
|
+
.font(.system(size: 9, weight: .medium))
|
|
187
|
+
.foregroundColor(.primary)
|
|
188
|
+
.lineLimit(1)
|
|
189
|
+
}
|
|
190
|
+
.frame(width: 50, alignment: .leading)
|
|
191
|
+
|
|
192
|
+
HStack(spacing: 2) {
|
|
193
|
+
ForEach(0..<7, id: \.self) { day in
|
|
194
|
+
RoundedRectangle(cornerRadius: 1.5)
|
|
195
|
+
.fill(habit.completedDays[day] ? habit.color : Color.gray.opacity(0.3))
|
|
196
|
+
.frame(width: 10, height: 10)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
Spacer()
|
|
204
|
+
}
|
|
205
|
+
.padding(14)
|
|
206
|
+
.containerBackground(Color.clear, for: .widget)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
struct LargeHabitView: View {
|
|
211
|
+
let entry: HabitEntry
|
|
212
|
+
|
|
213
|
+
var body: some View {
|
|
214
|
+
VStack(alignment: .leading, spacing: 14) {
|
|
215
|
+
HStack {
|
|
216
|
+
Text("Habit Tracker")
|
|
217
|
+
.font(.system(size: 16, weight: .bold))
|
|
218
|
+
.foregroundColor(.primary)
|
|
219
|
+
Spacer()
|
|
220
|
+
HStack(spacing: 4) {
|
|
221
|
+
Image(systemName: "flame.fill")
|
|
222
|
+
.font(.system(size: 14))
|
|
223
|
+
.foregroundColor(.orange)
|
|
224
|
+
Text("\(entry.totalStreak)")
|
|
225
|
+
.font(.system(size: 14, weight: .bold))
|
|
226
|
+
.foregroundColor(.primary)
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
VStack(alignment: .leading, spacing: 4) {
|
|
231
|
+
Text("\(entry.completionRate)%")
|
|
232
|
+
.font(.system(size: 28, weight: .heavy))
|
|
233
|
+
.foregroundColor(.blue)
|
|
234
|
+
Text("This week")
|
|
235
|
+
.font(.system(size: 11))
|
|
236
|
+
.foregroundColor(.secondary)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
VStack(spacing: 6) {
|
|
240
|
+
ForEach(entry.habits, id: \.id) { habit in
|
|
241
|
+
HStack(alignment: .center, spacing: 10) {
|
|
242
|
+
HStack(spacing: 5) {
|
|
243
|
+
Circle()
|
|
244
|
+
.fill(habit.color)
|
|
245
|
+
.frame(width: 7, height: 7)
|
|
246
|
+
Text(habit.name)
|
|
247
|
+
.font(.system(size: 11, weight: .medium))
|
|
248
|
+
.foregroundColor(.primary)
|
|
249
|
+
}
|
|
250
|
+
.frame(width: 70, alignment: .leading)
|
|
251
|
+
|
|
252
|
+
HStack(spacing: 3) {
|
|
253
|
+
ForEach(0..<7, id: \.self) { day in
|
|
254
|
+
RoundedRectangle(cornerRadius: 2)
|
|
255
|
+
.fill(habit.completedDays[day] ? habit.color : Color.gray.opacity(0.3))
|
|
256
|
+
.frame(width: 14, height: 14)
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
Spacer()
|
|
264
|
+
|
|
265
|
+
HStack {
|
|
266
|
+
VStack(alignment: .leading, spacing: 2) {
|
|
267
|
+
Text("Active habits")
|
|
268
|
+
.font(.system(size: 10))
|
|
269
|
+
.foregroundColor(.secondary)
|
|
270
|
+
Text("\(entry.habits.count)")
|
|
271
|
+
.font(.system(size: 12, weight: .semibold))
|
|
272
|
+
.foregroundColor(.primary)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
Spacer()
|
|
276
|
+
|
|
277
|
+
VStack(alignment: .trailing, spacing: 2) {
|
|
278
|
+
Text("Best streak")
|
|
279
|
+
.font(.system(size: 10))
|
|
280
|
+
.foregroundColor(.secondary)
|
|
281
|
+
Text("\(entry.totalStreak) days")
|
|
282
|
+
.font(.system(size: 12, weight: .semibold))
|
|
283
|
+
.foregroundColor(.primary)
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
.padding(18)
|
|
288
|
+
.containerBackground(Color.clear, for: .widget)
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// MARK: - Widget Configuration
|
|
293
|
+
struct HabitTrackerWidget: Widget {
|
|
294
|
+
let kind: String = "HabitTrackerWidget"
|
|
295
|
+
|
|
296
|
+
var body: some WidgetConfiguration {
|
|
297
|
+
StaticConfiguration(kind: kind, provider: HabitProvider()) { entry in
|
|
298
|
+
HabitTrackerWidgetEntryView(entry: entry)
|
|
299
|
+
.widgetURL(URL(string: "vibefast://tracker-app"))
|
|
300
|
+
}
|
|
301
|
+
.configurationDisplayName("Habit Tracker")
|
|
302
|
+
.description("Track your daily habits with a beautiful grid view.")
|
|
303
|
+
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
|
|
304
|
+
}
|
|
305
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
<key>NSExtension</key>
|
|
6
|
+
<dict>
|
|
7
|
+
<key>NSExtensionPointIdentifier</key>
|
|
8
|
+
<string>com.apple.widgetkit-extension</string>
|
|
9
|
+
</dict>
|
|
10
|
+
</dict>
|
|
11
|
+
</plist>
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import ActivityKit
|
|
2
|
+
import WidgetKit
|
|
3
|
+
import SwiftUI
|
|
4
|
+
|
|
5
|
+
struct WidgetAttributes: ActivityAttributes {
|
|
6
|
+
public struct ContentState: Codable, Hashable {
|
|
7
|
+
// Dynamic stateful properties about your activity go here!
|
|
8
|
+
var emoji: String
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Fixed non-changing properties about your activity go here!
|
|
12
|
+
var name: String
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
struct WidgetLiveActivity: Widget {
|
|
16
|
+
var body: some WidgetConfiguration {
|
|
17
|
+
ActivityConfiguration(for: WidgetAttributes.self) { context in
|
|
18
|
+
// Lock screen/banner UI goes here
|
|
19
|
+
VStack {
|
|
20
|
+
Text("Hello \(context.state.emoji)")
|
|
21
|
+
}
|
|
22
|
+
.activityBackgroundTint(Color.cyan)
|
|
23
|
+
.activitySystemActionForegroundColor(Color.black)
|
|
24
|
+
|
|
25
|
+
} dynamicIsland: { context in
|
|
26
|
+
DynamicIsland {
|
|
27
|
+
// Expanded UI goes here. Compose the expanded UI through
|
|
28
|
+
// various regions, like leading/trailing/center/bottom
|
|
29
|
+
DynamicIslandExpandedRegion(.leading) {
|
|
30
|
+
Text("Leading")
|
|
31
|
+
}
|
|
32
|
+
DynamicIslandExpandedRegion(.trailing) {
|
|
33
|
+
Text("Trailing")
|
|
34
|
+
}
|
|
35
|
+
DynamicIslandExpandedRegion(.bottom) {
|
|
36
|
+
Text("Bottom \(context.state.emoji)")
|
|
37
|
+
// more content
|
|
38
|
+
}
|
|
39
|
+
} compactLeading: {
|
|
40
|
+
Text("L")
|
|
41
|
+
} compactTrailing: {
|
|
42
|
+
Text("T \(context.state.emoji)")
|
|
43
|
+
} minimal: {
|
|
44
|
+
Text(context.state.emoji)
|
|
45
|
+
}
|
|
46
|
+
.widgetURL(URL(string: "https://www.expo.dev"))
|
|
47
|
+
.keylineTint(Color.red)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
extension WidgetAttributes {
|
|
53
|
+
fileprivate static var preview: WidgetAttributes {
|
|
54
|
+
WidgetAttributes(name: "World")
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
extension WidgetAttributes.ContentState {
|
|
59
|
+
fileprivate static var smiley: WidgetAttributes.ContentState {
|
|
60
|
+
WidgetAttributes.ContentState(emoji: "😀")
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
fileprivate static var starEyes: WidgetAttributes.ContentState {
|
|
64
|
+
WidgetAttributes.ContentState(emoji: "🤩")
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
#if swift(>=5.9)
|
|
69
|
+
#Preview("Notification", as: .content, using: WidgetAttributes.preview) {
|
|
70
|
+
WidgetLiveActivity()
|
|
71
|
+
} contentStates: {
|
|
72
|
+
WidgetAttributes.ContentState.smiley
|
|
73
|
+
WidgetAttributes.ContentState.starEyes
|
|
74
|
+
}
|
|
75
|
+
#endif
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** @type {import('@bacons/apple-targets/app.plugin').ConfigFunction} */
|
|
2
|
+
module.exports = (config) => ({
|
|
3
|
+
type: 'widget',
|
|
4
|
+
icon: 'https://github.com/expo.png',
|
|
5
|
+
entitlements: {
|
|
6
|
+
// Mirror the root app's App Group
|
|
7
|
+
'com.apple.security.application-groups':
|
|
8
|
+
config.ios.entitlements['com.apple.developer.associated-groups'],
|
|
9
|
+
},
|
|
10
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import WidgetKit
|
|
2
|
+
import SwiftUI
|
|
3
|
+
|
|
4
|
+
@main
|
|
5
|
+
struct exportWidgets: WidgetBundle {
|
|
6
|
+
var body: some Widget {
|
|
7
|
+
// Conditionally include iOS 17+ AppIntent-based widget
|
|
8
|
+
if #available(iOSApplicationExtension 17.0, *) {
|
|
9
|
+
widget()
|
|
10
|
+
}
|
|
11
|
+
// Other widgets targeting earlier iOS versions
|
|
12
|
+
WidgetLiveActivity()
|
|
13
|
+
HabitTrackerWidget()
|
|
14
|
+
if #available(iOSApplicationExtension 17.0, *) {
|
|
15
|
+
CalorieTrackerWidget()
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import WidgetKit
|
|
2
|
+
import SwiftUI
|
|
3
|
+
import AppIntents
|
|
4
|
+
|
|
5
|
+
@available(iOSApplicationExtension 17.0, *)
|
|
6
|
+
struct Provider: AppIntentTimelineProvider {
|
|
7
|
+
typealias Entry = SimpleEntry
|
|
8
|
+
typealias Intent = ConfigurationAppIntent
|
|
9
|
+
|
|
10
|
+
func placeholder(in context: Context) -> SimpleEntry {
|
|
11
|
+
SimpleEntry(date: Date(), configuration: .smiley)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
func snapshot(for configuration: ConfigurationAppIntent, in context: Context) async -> SimpleEntry {
|
|
15
|
+
SimpleEntry(date: Date(), configuration: configuration)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
func timeline(for configuration: ConfigurationAppIntent, in context: Context) async -> Timeline<SimpleEntry> {
|
|
19
|
+
var entries: [SimpleEntry] = []
|
|
20
|
+
|
|
21
|
+
// Generate a timeline consisting of five entries an hour apart, starting from the current date.
|
|
22
|
+
let currentDate = Date()
|
|
23
|
+
for hourOffset in 0 ..< 5 {
|
|
24
|
+
let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
|
|
25
|
+
let entry = SimpleEntry(date: entryDate, configuration: configuration)
|
|
26
|
+
entries.append(entry)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return Timeline(entries: entries, policy: .atEnd)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
func recommendations() -> [AppIntentRecommendation<ConfigurationAppIntent>] {
|
|
33
|
+
[
|
|
34
|
+
AppIntentRecommendation(intent: .smiley, description: "Smiley"),
|
|
35
|
+
AppIntentRecommendation(intent: .starEyes, description: "Star Eyes")
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@available(iOSApplicationExtension 17.0, *)
|
|
41
|
+
struct SimpleEntry: TimelineEntry {
|
|
42
|
+
let date: Date
|
|
43
|
+
let configuration: ConfigurationAppIntent
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@available(iOSApplicationExtension 17.0, *)
|
|
47
|
+
struct widgetEntryView : View {
|
|
48
|
+
var entry: Provider.Entry
|
|
49
|
+
|
|
50
|
+
var body: some View {
|
|
51
|
+
VStack {
|
|
52
|
+
Text("Time:")
|
|
53
|
+
Text(entry.date, style: .time)
|
|
54
|
+
|
|
55
|
+
Text("Favorite Emoji:")
|
|
56
|
+
Text(entry.configuration.favoriteEmoji)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@available(iOSApplicationExtension 17.0, *)
|
|
62
|
+
struct widget: Widget {
|
|
63
|
+
let kind: String = "widget"
|
|
64
|
+
|
|
65
|
+
var body: some WidgetConfiguration {
|
|
66
|
+
AppIntentConfiguration(kind: kind, intent: ConfigurationAppIntent.self, provider: Provider()) { entry in
|
|
67
|
+
widgetEntryView(entry: entry)
|
|
68
|
+
.containerBackground(Color.clear, for: .widget)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@available(iOSApplicationExtension 17.0, *)
|
|
74
|
+
extension ConfigurationAppIntent {
|
|
75
|
+
fileprivate static var smiley: ConfigurationAppIntent {
|
|
76
|
+
let intent = ConfigurationAppIntent()
|
|
77
|
+
intent.favoriteEmoji = "😀"
|
|
78
|
+
return intent
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
fileprivate static var starEyes: ConfigurationAppIntent {
|
|
82
|
+
let intent = ConfigurationAppIntent()
|
|
83
|
+
intent.favoriteEmoji = "🤩"
|
|
84
|
+
return intent
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
#if swift(>=5.9)
|
|
89
|
+
@available(iOSApplicationExtension 17.0, *)
|
|
90
|
+
#Preview(as: .systemSmall) {
|
|
91
|
+
widget()
|
|
92
|
+
} timeline: {
|
|
93
|
+
SimpleEntry(date: Date.now, configuration: .smiley)
|
|
94
|
+
SimpleEntry(date: Date.now, configuration: .starEyes)
|
|
95
|
+
}
|
|
96
|
+
#endif
|
|
Binary file
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { router, Stack } from 'expo-router';
|
|
2
|
+
import React, { useMemo } from 'react';
|
|
3
|
+
import { ScrollView, View } from 'react-native';
|
|
4
|
+
|
|
5
|
+
import { FeatureButton, FocusAwareStatusBar } from '@/components/ui';
|
|
6
|
+
import { translate } from '@/lib';
|
|
7
|
+
import { useThemeConfig } from '@/lib/use-theme-config';
|
|
8
|
+
|
|
9
|
+
export default function PaywallSelection() {
|
|
10
|
+
const theme = useThemeConfig();
|
|
11
|
+
const optionsTitle = translate('paywall.options_title');
|
|
12
|
+
const screenOptions = useMemo(
|
|
13
|
+
() => ({
|
|
14
|
+
title: optionsTitle,
|
|
15
|
+
headerShown: true,
|
|
16
|
+
headerBackButtonDisplayMode: 'generic' as const,
|
|
17
|
+
}),
|
|
18
|
+
[optionsTitle],
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const paywallOptions = [
|
|
22
|
+
{
|
|
23
|
+
id: 'remote',
|
|
24
|
+
title: translate('paywall.remote_title'),
|
|
25
|
+
icon: '💳',
|
|
26
|
+
color: '#FBBF24', // Amber
|
|
27
|
+
description: translate('paywall.remote_description'),
|
|
28
|
+
route: '/paywall/remote',
|
|
29
|
+
testID: 'remote-paywall-option',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: 'local',
|
|
33
|
+
title: translate('paywall.local_title'),
|
|
34
|
+
icon: '🛒',
|
|
35
|
+
color: '#10B981', // Emerald
|
|
36
|
+
description: translate('paywall.local_description'),
|
|
37
|
+
route: '/paywall/local',
|
|
38
|
+
testID: 'local-paywall-option',
|
|
39
|
+
},
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
const handleOptionPress = (route: string) => {
|
|
43
|
+
router.push(route as any);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<>
|
|
48
|
+
<FocusAwareStatusBar />
|
|
49
|
+
<Stack.Screen options={screenOptions} />
|
|
50
|
+
<ScrollView
|
|
51
|
+
style={{ backgroundColor: theme.colors.background }}
|
|
52
|
+
className="flex-1"
|
|
53
|
+
showsVerticalScrollIndicator={false}
|
|
54
|
+
>
|
|
55
|
+
<View className="px-6 py-8">
|
|
56
|
+
<View className="-mx-2 flex-row flex-wrap">
|
|
57
|
+
{paywallOptions.map((option) => (
|
|
58
|
+
<View key={option.id} className="w-1/2 px-2 pb-4">
|
|
59
|
+
<FeatureButton
|
|
60
|
+
title={option.title}
|
|
61
|
+
icon={option.icon}
|
|
62
|
+
color={option.color}
|
|
63
|
+
description={option.description}
|
|
64
|
+
testID={option.testID}
|
|
65
|
+
onPress={() => handleOptionPress(option.route)}
|
|
66
|
+
/>
|
|
67
|
+
</View>
|
|
68
|
+
))}
|
|
69
|
+
</View>
|
|
70
|
+
</View>
|
|
71
|
+
</ScrollView>
|
|
72
|
+
</>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Stack } from 'expo-router';
|
|
2
|
+
import React, { useMemo } from 'react';
|
|
3
|
+
|
|
4
|
+
import LocalPaywall from '@/features/payments/app/local-paywall';
|
|
5
|
+
import { RevenueCatAdapter } from '@/features/payments/services/revenuecat-adapter';
|
|
6
|
+
import { translate } from '@/lib';
|
|
7
|
+
|
|
8
|
+
export default function LocalPaywallScreen() {
|
|
9
|
+
const paymentService = useMemo(() => new RevenueCatAdapter(), []);
|
|
10
|
+
const localTitle = translate('paywall.local_title');
|
|
11
|
+
const screenOptions = useMemo(
|
|
12
|
+
() => ({
|
|
13
|
+
title: localTitle,
|
|
14
|
+
headerShown: false,
|
|
15
|
+
}),
|
|
16
|
+
[localTitle],
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<>
|
|
21
|
+
<Stack.Screen options={screenOptions} />
|
|
22
|
+
<LocalPaywall paymentService={paymentService} />
|
|
23
|
+
</>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Stack } from 'expo-router';
|
|
2
|
+
import React, { useMemo } from 'react';
|
|
3
|
+
|
|
4
|
+
import RemotePaywall from '@/features/payments/app/remote-paywall';
|
|
5
|
+
import { translate } from '@/lib';
|
|
6
|
+
|
|
7
|
+
export default function RemotePaywallScreen() {
|
|
8
|
+
const remoteTitle = translate('paywall.remote_title');
|
|
9
|
+
const screenOptions = useMemo(
|
|
10
|
+
() => ({
|
|
11
|
+
title: remoteTitle,
|
|
12
|
+
headerShown: true,
|
|
13
|
+
}),
|
|
14
|
+
[remoteTitle],
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<>
|
|
19
|
+
<Stack.Screen options={screenOptions} />
|
|
20
|
+
<RemotePaywall />
|
|
21
|
+
</>
|
|
22
|
+
);
|
|
23
|
+
}
|