vibefast-cli 0.4.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.
Files changed (89) hide show
  1. package/FEATURE-DEPENDENCY-SPEC.md +338 -0
  2. package/dist/__tests__/integration.test.d.ts +2 -0
  3. package/dist/__tests__/integration.test.d.ts.map +1 -0
  4. package/dist/__tests__/integration.test.js +219 -0
  5. package/dist/__tests__/integration.test.js.map +1 -0
  6. package/dist/__tests__/recipes.test.d.ts +2 -0
  7. package/dist/__tests__/recipes.test.d.ts.map +1 -0
  8. package/dist/__tests__/recipes.test.js +143 -0
  9. package/dist/__tests__/recipes.test.js.map +1 -0
  10. package/dist/commands/__tests__/init.test.d.ts +2 -0
  11. package/dist/commands/__tests__/init.test.d.ts.map +1 -0
  12. package/dist/commands/__tests__/init.test.js +95 -0
  13. package/dist/commands/__tests__/init.test.js.map +1 -0
  14. package/dist/commands/__tests__/platform.test.d.ts +2 -0
  15. package/dist/commands/__tests__/platform.test.d.ts.map +1 -0
  16. package/dist/commands/__tests__/platform.test.js +123 -0
  17. package/dist/commands/__tests__/platform.test.js.map +1 -0
  18. package/dist/commands/add.d.ts.map +1 -1
  19. package/dist/commands/add.js +4 -5
  20. package/dist/commands/add.js.map +1 -1
  21. package/dist/commands/init.d.ts.map +1 -1
  22. package/dist/commands/init.js +12 -12
  23. package/dist/commands/init.js.map +1 -1
  24. package/dist/commands/platform.d.ts +3 -0
  25. package/dist/commands/platform.d.ts.map +1 -0
  26. package/dist/commands/platform.js +245 -0
  27. package/dist/commands/platform.js.map +1 -0
  28. package/dist/core/journal.d.ts.map +1 -1
  29. package/dist/core/journal.js +36 -19
  30. package/dist/core/journal.js.map +1 -1
  31. package/dist/core/recipes.d.ts.map +1 -1
  32. package/dist/core/recipes.js +8 -39
  33. package/dist/core/recipes.js.map +1 -1
  34. package/dist/index.js +2 -0
  35. package/dist/index.js.map +1 -1
  36. package/package.json +1 -1
  37. package/recipes/ios-widget/recipe.json +78 -0
  38. package/recipes/ios-widget/targets/widget/AppIntent.swift +46 -0
  39. package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-20x20@1x.png +0 -0
  40. package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-20x20@2x.png +0 -0
  41. package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-20x20@3x.png +0 -0
  42. package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-29x29@1x.png +0 -0
  43. package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-29x29@2x.png +0 -0
  44. package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-29x29@3x.png +0 -0
  45. package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-40x40@1x.png +0 -0
  46. package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-40x40@2x.png +0 -0
  47. package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-40x40@3x.png +0 -0
  48. package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-60x60@2x.png +0 -0
  49. package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-60x60@3x.png +0 -0
  50. package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-76x76@1x.png +0 -0
  51. package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-76x76@2x.png +0 -0
  52. package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-83.5x83.5@2x.png +0 -0
  53. package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/Contents.json +122 -0
  54. package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png +0 -0
  55. package/recipes/ios-widget/targets/widget/CalorieTrackerWidget.swift +424 -0
  56. package/recipes/ios-widget/targets/widget/HabitTrackerWidget.swift +305 -0
  57. package/recipes/ios-widget/targets/widget/Info.plist +11 -0
  58. package/recipes/ios-widget/targets/widget/WidgetLiveActivity.swift +75 -0
  59. package/recipes/ios-widget/targets/widget/expo-target.config.js +10 -0
  60. package/recipes/ios-widget/targets/widget/generated.entitlements +5 -0
  61. package/recipes/ios-widget/targets/widget/index.swift +18 -0
  62. package/recipes/ios-widget/targets/widget/widgets.swift +96 -0
  63. package/recipes/ios-widget@latest.zip +0 -0
  64. package/recipes/payments/apps/native/src/app/(root)/(protected)/paywall/index.tsx +74 -0
  65. package/recipes/payments/apps/native/src/app/(root)/(protected)/paywall/local.tsx +25 -0
  66. package/recipes/payments/apps/native/src/app/(root)/(protected)/paywall/remote.tsx +23 -0
  67. package/recipes/payments/apps/native/src/features/payments/README.md +200 -0
  68. package/recipes/payments/apps/native/src/features/payments/app/local-paywall.tsx +194 -0
  69. package/recipes/payments/apps/native/src/features/payments/app/remote-paywall.tsx +79 -0
  70. package/recipes/payments/apps/native/src/features/payments/components/payment-initializer.tsx +95 -0
  71. package/recipes/payments/apps/native/src/features/payments/components/paywall-error-state.tsx +60 -0
  72. package/recipes/payments/apps/native/src/features/payments/components/paywall-local-mode.tsx +116 -0
  73. package/recipes/payments/apps/native/src/features/payments/components/paywall-product-card.tsx +133 -0
  74. package/recipes/payments/apps/native/src/features/payments/components/paywall-remote-mode.tsx +146 -0
  75. package/recipes/payments/apps/native/src/features/payments/hooks/use-entitlement.ts +63 -0
  76. package/recipes/payments/apps/native/src/features/payments/index.ts +8 -0
  77. package/recipes/payments/apps/native/src/features/payments/services/revenuecat-adapter.ts +407 -0
  78. package/recipes/payments/recipe.json +58 -0
  79. package/recipes/payments@latest.zip +0 -0
  80. package/src/__tests__/integration.test.ts +249 -0
  81. package/src/__tests__/recipes.test.ts +168 -0
  82. package/src/commands/__tests__/init.test.ts +112 -0
  83. package/src/commands/__tests__/platform.test.ts +141 -0
  84. package/src/commands/add.ts +4 -5
  85. package/src/commands/init.ts +14 -15
  86. package/src/commands/platform.ts +309 -0
  87. package/src/core/journal.ts +42 -25
  88. package/src/core/recipes.ts +8 -40
  89. package/src/index.ts +2 -0
@@ -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,5 @@
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
+ </plist>
@@ -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
+ }