unified-video-framework 1.0.0

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 (129) hide show
  1. package/.github/workflows/ci.yml +253 -0
  2. package/ANDROID_TV_IMPLEMENTATION.md +313 -0
  3. package/COMPLETION_STATUS.md +165 -0
  4. package/CONTRIBUTING.md +376 -0
  5. package/FINAL_STATUS_REPORT.md +170 -0
  6. package/FRAMEWORK_REVIEW.md +247 -0
  7. package/IMPROVEMENTS_SUMMARY.md +168 -0
  8. package/LICENSE +21 -0
  9. package/NATIVE_APP_INTEGRATION_GUIDE.md +903 -0
  10. package/PAYWALL_RENTAL_FLOW.md +499 -0
  11. package/PLATFORM_SETUP_GUIDE.md +1636 -0
  12. package/README.md +315 -0
  13. package/RUN_LOCALLY.md +151 -0
  14. package/apps/demo/cast-sender-min.html +173 -0
  15. package/apps/demo/custom-player.html +883 -0
  16. package/apps/demo/demo.html +990 -0
  17. package/apps/demo/enhanced-player.html +3556 -0
  18. package/apps/demo/index.html +159 -0
  19. package/apps/rental-api/.env.example +24 -0
  20. package/apps/rental-api/README.md +23 -0
  21. package/apps/rental-api/migrations/001_init.sql +35 -0
  22. package/apps/rental-api/migrations/002_videos.sql +10 -0
  23. package/apps/rental-api/migrations/003_add_gateway_subref.sql +4 -0
  24. package/apps/rental-api/migrations/004_update_gateways.sql +4 -0
  25. package/apps/rental-api/migrations/005_seed_demo_video.sql +5 -0
  26. package/apps/rental-api/package-lock.json +2045 -0
  27. package/apps/rental-api/package.json +33 -0
  28. package/apps/rental-api/scripts/run-migration.js +42 -0
  29. package/apps/rental-api/scripts/update-video-currency.js +21 -0
  30. package/apps/rental-api/scripts/update-video-price.js +19 -0
  31. package/apps/rental-api/src/config.ts +14 -0
  32. package/apps/rental-api/src/db.ts +10 -0
  33. package/apps/rental-api/src/routes/cashfree.ts +167 -0
  34. package/apps/rental-api/src/routes/pesapal.ts +92 -0
  35. package/apps/rental-api/src/routes/rentals.ts +242 -0
  36. package/apps/rental-api/src/routes/webhooks.ts +73 -0
  37. package/apps/rental-api/src/server.ts +41 -0
  38. package/apps/rental-api/src/services/entitlements.ts +45 -0
  39. package/apps/rental-api/src/services/payments.ts +22 -0
  40. package/apps/rental-api/tsconfig.json +17 -0
  41. package/check-urls.ps1 +74 -0
  42. package/comparison-report.md +181 -0
  43. package/docs/PAYWALL.md +95 -0
  44. package/docs/PLAYER_UI_VISIBILITY.md +431 -0
  45. package/docs/README.md +7 -0
  46. package/docs/SYSTEM_ARCHITECTURE.md +612 -0
  47. package/docs/VDOCIPHER_CLONE_REQUIREMENTS.md +403 -0
  48. package/examples/android/JavaSampleApp/MainActivity.java +641 -0
  49. package/examples/android/JavaSampleApp/activity_main.xml +226 -0
  50. package/examples/android/SampleApp/MainActivity.kt +430 -0
  51. package/examples/ios/SampleApp/ViewController.swift +337 -0
  52. package/examples/ios/SwiftUISampleApp/ContentView.swift +304 -0
  53. package/iOS_IMPLEMENTATION_OPTIONS.md +470 -0
  54. package/ios/UnifiedVideoPlayer/UnifiedVideoPlayer.podspec +33 -0
  55. package/jest.config.js +33 -0
  56. package/jitpack.yml +5 -0
  57. package/lerna.json +35 -0
  58. package/package.json +69 -0
  59. package/packages/PLATFORM_STATUS.md +163 -0
  60. package/packages/android/build.gradle +135 -0
  61. package/packages/android/src/main/AndroidManifest.xml +36 -0
  62. package/packages/android/src/main/java/com/unifiedvideo/player/PlayerConfiguration.java +221 -0
  63. package/packages/android/src/main/java/com/unifiedvideo/player/UnifiedVideoPlayer.java +1037 -0
  64. package/packages/android/src/main/java/com/unifiedvideo/player/UnifiedVideoPlayer.kt +707 -0
  65. package/packages/android/src/main/java/com/unifiedvideo/player/analytics/AnalyticsProvider.java +9 -0
  66. package/packages/android/src/main/java/com/unifiedvideo/player/cast/CastManager.java +141 -0
  67. package/packages/android/src/main/java/com/unifiedvideo/player/cast/CastOptionsProvider.java +29 -0
  68. package/packages/android/src/main/java/com/unifiedvideo/player/overlay/WatermarkOverlayView.java +88 -0
  69. package/packages/android/src/main/java/com/unifiedvideo/player/pip/PipActionReceiver.java +33 -0
  70. package/packages/android/src/main/java/com/unifiedvideo/player/services/PlaybackService.java +110 -0
  71. package/packages/android/src/main/java/com/unifiedvideo/player/services/PlayerHolder.java +19 -0
  72. package/packages/core/package.json +34 -0
  73. package/packages/core/src/BasePlayer.ts +250 -0
  74. package/packages/core/src/VideoPlayer.ts +237 -0
  75. package/packages/core/src/VideoPlayerFactory.ts +145 -0
  76. package/packages/core/src/index.ts +20 -0
  77. package/packages/core/src/interfaces/IVideoPlayer.ts +184 -0
  78. package/packages/core/src/interfaces.ts +240 -0
  79. package/packages/core/src/utils/EventEmitter.ts +66 -0
  80. package/packages/core/src/utils/PlatformDetector.ts +300 -0
  81. package/packages/core/tsconfig.json +20 -0
  82. package/packages/enact/package.json +51 -0
  83. package/packages/enact/src/VideoPlayer.js +365 -0
  84. package/packages/enact/src/adapters/TizenAdapter.js +354 -0
  85. package/packages/enact/src/index.js +82 -0
  86. package/packages/ios/BUILD_INSTRUCTIONS.md +108 -0
  87. package/packages/ios/FIX_EMBED_ISSUE.md +142 -0
  88. package/packages/ios/GETTING_STARTED.md +100 -0
  89. package/packages/ios/Package.swift +35 -0
  90. package/packages/ios/README.md +84 -0
  91. package/packages/ios/Sources/UnifiedVideoPlayer/Analytics/AnalyticsEmitter.swift +26 -0
  92. package/packages/ios/Sources/UnifiedVideoPlayer/DRM/FairPlayDRMManager.swift +102 -0
  93. package/packages/ios/Sources/UnifiedVideoPlayer/Info.plist +24 -0
  94. package/packages/ios/Sources/UnifiedVideoPlayer/Remote/RemoteCommandCenter.swift +109 -0
  95. package/packages/ios/Sources/UnifiedVideoPlayer/UnifiedVideoPlayer.swift +811 -0
  96. package/packages/ios/Sources/UnifiedVideoPlayer/UnifiedVideoPlayerView.swift +640 -0
  97. package/packages/ios/Sources/UnifiedVideoPlayer/Utilities/Color+Hex.swift +36 -0
  98. package/packages/ios/UnifiedVideoPlayer.podspec +27 -0
  99. package/packages/ios/UnifiedVideoPlayer.xcodeproj/project.pbxproj +385 -0
  100. package/packages/ios/build_framework.sh +55 -0
  101. package/packages/react-native/android/src/main/java/com/unifiedvideo/UnifiedVideoPlayerModule.kt +482 -0
  102. package/packages/react-native/ios/UnifiedVideoPlayer.swift +436 -0
  103. package/packages/react-native/package.json +51 -0
  104. package/packages/react-native/src/ReactNativePlayer.tsx +423 -0
  105. package/packages/react-native/src/VideoPlayer.tsx +224 -0
  106. package/packages/react-native/src/index.ts +28 -0
  107. package/packages/react-native/src/utils/EventEmitter.ts +66 -0
  108. package/packages/react-native/tsconfig.json +31 -0
  109. package/packages/roku/components/UnifiedVideoPlayer.brs +400 -0
  110. package/packages/roku/package.json +44 -0
  111. package/packages/roku/source/VideoPlayer.brs +231 -0
  112. package/packages/roku/source/main.brs +28 -0
  113. package/packages/web/GETTING_STARTED.md +292 -0
  114. package/packages/web/jest.config.js +28 -0
  115. package/packages/web/jest.setup.ts +110 -0
  116. package/packages/web/package.json +50 -0
  117. package/packages/web/src/SecureVideoPlayer.ts +1164 -0
  118. package/packages/web/src/WebPlayer.ts +3110 -0
  119. package/packages/web/src/__tests__/WebPlayer.test.ts +314 -0
  120. package/packages/web/src/index.ts +14 -0
  121. package/packages/web/src/paywall/PaywallController.ts +215 -0
  122. package/packages/web/src/react/WebPlayerView.tsx +177 -0
  123. package/packages/web/tsconfig.json +23 -0
  124. package/packages/web/webpack.config.js +45 -0
  125. package/server.js +131 -0
  126. package/server.py +84 -0
  127. package/test-urls.ps1 +97 -0
  128. package/test-video-urls.ps1 +87 -0
  129. package/tsconfig.json +39 -0
@@ -0,0 +1,431 @@
1
+ # Player UI Visibility API (playerId-driven)
2
+
3
+ This document defines an admin-friendly API for dynamically controlling per-player UI using a playerId. The WebPlayer fetches a JSON config using `playerId` and applies it. Everything defaults to visible and the default layout if not configured.
4
+
5
+
6
+ ## Controls you can toggle
7
+
8
+ All keys are booleans (true = visible, false = hidden). Omit a key to fall back to true.
9
+
10
+ - cast: Cast button + Stop Casting
11
+ - settings: Settings button + Settings menu
12
+ - share: Share button
13
+ - fullscreen: Fullscreen button
14
+ - pip: Picture-in-Picture button
15
+ - playlist: Playlist button
16
+ - skipBack: Back 10s button
17
+ - skipForward: Forward 10s button
18
+ - volume: Volume button + Volume panel
19
+ - time: Time display (00:00 / 00:00)
20
+ - qualityBadge: Quality badge (HD/AUTO)
21
+ - seekbar: Progress bar section (bar, handle, tooltip)
22
+
23
+
24
+ ## DOM ID mapping (WebPlayer)
25
+
26
+ Used internally to show/hide elements.
27
+
28
+ - cast -> uvf-cast-btn, uvf-stop-cast-btn
29
+ - settings -> uvf-settings-btn, uvf-settings-menu
30
+ - share -> uvf-share-btn
31
+ - fullscreen -> uvf-fullscreen-btn
32
+ - pip -> uvf-pip-btn
33
+ - playlist -> uvf-playlist-btn
34
+ - skipBack -> uvf-skip-back
35
+ - skipForward -> uvf-skip-forward
36
+ - volume -> uvf-volume-btn, uvf-volume-panel
37
+ - time -> uvf-time-display
38
+ - qualityBadge -> uvf-quality-badge
39
+ - seekbar -> uvf-progress-section, uvf-progress-bar
40
+
41
+
42
+ ## API design
43
+
44
+ Single source of truth per `playerId`. The player only uses these endpoints—no other fallbacks.
45
+
46
+ - GET /player-ui/{playerId}
47
+ - Returns JSON visibility+layout for that playerId
48
+ - 200 with JSON if configured; 404 if not configured
49
+ - PUT /player-ui/{playerId}
50
+ - Admin upsert of the visibility+layout JSON
51
+ - 200 with the saved JSON
52
+
53
+
54
+ ## Response format (GET)
55
+
56
+ ```json
57
+ {
58
+ "playerId": "landing_hero",
59
+ "v": 1,
60
+ "visibility": {
61
+ "cast": false,
62
+ "settings": true,
63
+ "share": false,
64
+ "fullscreen": true,
65
+ "pip": false,
66
+ "playlist": false,
67
+ "skipBack": true,
68
+ "skipForward": true,
69
+ "volume": true,
70
+ "time": true,
71
+ "qualityBadge": true,
72
+ "seekbar": true
73
+ },
74
+ "layout": {
75
+ "template": "classic",
76
+ "regions": {
77
+ "topLeft": ["playlist"],
78
+ "topRight": ["cast", "share"],
79
+ "bottomLeft": ["playPause", "skipBack", "skipForward"],
80
+ "bottomRight": ["settings", "pip", "fullscreen"],
81
+ "centerOverlay": ["centerPlay"]
82
+ },
83
+ "order": {
84
+ "playPause": 10,
85
+ "skipBack": 20,
86
+ "skipForward": 30,
87
+ "settings": 10,
88
+ "pip": 20,
89
+ "fullscreen": 30
90
+ },
91
+ "breakpoints": {
92
+ "desktop": {
93
+ "regions": {
94
+ "topRight": ["cast", "share"],
95
+ "bottomRight": ["settings", "pip", "fullscreen"]
96
+ }
97
+ },
98
+ "tablet": {
99
+ "regions": {
100
+ "topRight": ["cast"],
101
+ "bottomRight": ["settings", "fullscreen"]
102
+ }
103
+ },
104
+ "mobile": {
105
+ "regions": {
106
+ "topRight": [],
107
+ "bottomRight": ["settings"],
108
+ "topLeft": []
109
+ },
110
+ "overrides": {
111
+ "share": { "visible": false },
112
+ "pip": { "visible": false }
113
+ }
114
+ }
115
+ },
116
+ "overrides": {
117
+ "qualityBadge": { "region": "bottomRight", "order": 5 },
118
+ "time": { "region": "bottomLeft", "order": 40 },
119
+ "seekbar": { "region": "bottomFull", "order": 1 }
120
+ },
121
+ "style": {
122
+ "gap": 10,
123
+ "padding": 16,
124
+ "align": "space-between"
125
+ }
126
+ },
127
+ "meta": {
128
+ "updatedAt": "2025-09-02T09:00:00.000Z",
129
+ "updatedBy": "admin@example.com"
130
+ }
131
+ }
132
+ ```
133
+
134
+ Notes
135
+ - `visibility` and `layout` may omit keys; omitted visibility keys are treated as `true`; omitted layout falls back to the default template.
136
+ - `v` is a schema/version number (start with `1`).
137
+
138
+
139
+ ## Request format (PUT)
140
+
141
+ Admin panels send the parts they modify; backend stores them.
142
+
143
+ ```json
144
+ {
145
+ "visibility": {
146
+ "cast": false,
147
+ "share": false,
148
+ "pip": false,
149
+ "seekbar": true
150
+ },
151
+ "layout": {
152
+ "template": "classic",
153
+ "overrides": {
154
+ "fullscreen": { "region": "topRight", "order": 1 },
155
+ "settings": { "region": "bottomRight", "order": 10 }
156
+ },
157
+ "breakpoints": {
158
+ "mobile": {
159
+ "regions": {
160
+ "topRight": [],
161
+ "bottomRight": ["settings"]
162
+ },
163
+ "overrides": {
164
+ "fullscreen": { "visible": false }
165
+ }
166
+ }
167
+ }
168
+ }
169
+ }
170
+ ```
171
+
172
+ A successful upsert returns the complete stored object.
173
+
174
+
175
+ ## JSON Schema (validation)
176
+
177
+ ```json
178
+ {
179
+ "type": "object",
180
+ "properties": {
181
+ "playerId": { "type": "string" },
182
+ "v": { "type": "integer" },
183
+ "visibility": {
184
+ "type": "object",
185
+ "properties": {
186
+ "cast": { "type": "boolean" },
187
+ "settings": { "type": "boolean" },
188
+ "share": { "type": "boolean" },
189
+ "fullscreen": { "type": "boolean" },
190
+ "pip": { "type": "boolean" },
191
+ "playlist": { "type": "boolean" },
192
+ "skipBack": { "type": "boolean" },
193
+ "skipForward": { "type": "boolean" },
194
+ "volume": { "type": "boolean" },
195
+ "time": { "type": "boolean" },
196
+ "qualityBadge": { "type": "boolean" },
197
+ "seekbar": { "type": "boolean" }
198
+ },
199
+ "additionalProperties": false
200
+ },
201
+ "layout": {
202
+ "type": "object",
203
+ "properties": {
204
+ "template": { "type": "string" },
205
+ "regions": {
206
+ "type": "object",
207
+ "description": "Map of regionName -> array of control keys",
208
+ "additionalProperties": {
209
+ "type": "array",
210
+ "items": { "type": "string" }
211
+ }
212
+ },
213
+ "order": {
214
+ "type": "object",
215
+ "description": "Global z-order within a region (lower comes first)",
216
+ "additionalProperties": { "type": "integer" }
217
+ },
218
+ "overrides": {
219
+ "type": "object",
220
+ "description": "Per-control overrides (region/order/visibility/position)",
221
+ "additionalProperties": {
222
+ "type": "object",
223
+ "properties": {
224
+ "region": { "type": "string" },
225
+ "order": { "type": "integer" },
226
+ "visible": { "type": "boolean" },
227
+ "position": {
228
+ "type": "object",
229
+ "properties": {
230
+ "mode": { "type": "string", "enum": ["region", "absolute"] },
231
+ "top": { "type": "number" },
232
+ "right": { "type": "number" },
233
+ "bottom": { "type": "number" },
234
+ "left": { "type": "number" }
235
+ },
236
+ "additionalProperties": false
237
+ }
238
+ },
239
+ "additionalProperties": true
240
+ }
241
+ },
242
+ "breakpoints": {
243
+ "type": "object",
244
+ "description": "Responsive overrides per breakpoint",
245
+ "properties": {
246
+ "desktop": { "$ref": "#/definitions/bp" },
247
+ "tablet": { "$ref": "#/definitions/bp" },
248
+ "mobile": { "$ref": "#/definitions/bp" }
249
+ },
250
+ "additionalProperties": { "$ref": "#/definitions/bp" }
251
+ },
252
+ "style": {
253
+ "type": "object",
254
+ "properties": {
255
+ "gap": { "type": "number" },
256
+ "padding": { "type": "number" },
257
+ "align": { "type": "string", "enum": ["start", "center", "end", "space-between"] }
258
+ },
259
+ "additionalProperties": true
260
+ }
261
+ },
262
+ "additionalProperties": false,
263
+ "definitions": {
264
+ "bp": {
265
+ "type": "object",
266
+ "properties": {
267
+ "regions": {
268
+ "type": "object",
269
+ "additionalProperties": {
270
+ "type": "array",
271
+ "items": { "type": "string" }
272
+ }
273
+ },
274
+ "overrides": {
275
+ "type": "object",
276
+ "additionalProperties": {
277
+ "type": "object",
278
+ "properties": {
279
+ "region": { "type": "string" },
280
+ "order": { "type": "integer" },
281
+ "visible": { "type": "boolean" },
282
+ "position": {
283
+ "type": "object",
284
+ "properties": {
285
+ "mode": { "type": "string", "enum": ["region", "absolute"] },
286
+ "top": { "type": "number" },
287
+ "right": { "type": "number" },
288
+ "bottom": { "type": "number" },
289
+ "left": { "type": "number" }
290
+ },
291
+ "additionalProperties": false
292
+ }
293
+ },
294
+ "additionalProperties": true
295
+ }
296
+ }
297
+ },
298
+ "additionalProperties": false
299
+ }
300
+ }
301
+ },
302
+ "meta": {
303
+ "type": "object",
304
+ "properties": {
305
+ "updatedAt": { "type": "string", "format": "date-time" },
306
+ "updatedBy": { "type": "string" }
307
+ },
308
+ "additionalProperties": true
309
+ }
310
+ },
311
+ "additionalProperties": false
312
+ }
313
+ ```
314
+
315
+
316
+ ## Database model
317
+
318
+ One record per `playerId`. Store `visibility` and `layout` as JSON/JSONB so you can evolve easily.
319
+
320
+ ```sql
321
+ CREATE TABLE IF NOT EXISTS player_ui_config (
322
+ player_id TEXT PRIMARY KEY,
323
+ v INTEGER NOT NULL DEFAULT 1,
324
+ visibility JSONB NOT NULL DEFAULT '{}'::jsonb,
325
+ layout JSONB NOT NULL DEFAULT '{}'::jsonb,
326
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
327
+ updated_by TEXT
328
+ );
329
+ ```
330
+
331
+
332
+ ## Backend outline (Express-style pseudocode)
333
+
334
+ ```js
335
+ // GET /player-ui/:playerId
336
+ app.get('/player-ui/:playerId', async (req, res) => {
337
+ const playerId = req.params.playerId.trim();
338
+ const row = await db.selectOne('player_ui_config', { player_id: playerId });
339
+ if (!row) return res.status(404).json({ error: 'not_found', playerId });
340
+
341
+ const defaultsVis = {
342
+ cast: true, settings: true, share: true, fullscreen: true, pip: true,
343
+ playlist: true, skipBack: true, skipForward: true, volume: true,
344
+ time: true, qualityBadge: true, seekbar: true
345
+ };
346
+ const vis = { ...defaultsVis, ...(row.visibility || {}) };
347
+ const layout = row.layout || {};
348
+
349
+ res.json({
350
+ playerId,
351
+ v: row.v || 1,
352
+ visibility: vis,
353
+ layout,
354
+ meta: { updatedAt: row.updated_at, updatedBy: row.updated_by || null }
355
+ });
356
+ });
357
+
358
+ // PUT /player-ui/:playerId
359
+ app.put('/player-ui/:playerId', requireAdminAuth, async (req, res) => {
360
+ const playerId = req.params.playerId.trim();
361
+ const incomingVis = req.body?.visibility || {};
362
+ const incomingLayout = req.body?.layout || {};
363
+
364
+ const allowedVis = [
365
+ 'cast','settings','share','fullscreen','pip','playlist',
366
+ 'skipBack','skipForward','volume','time','qualityBadge','seekbar'
367
+ ];
368
+ const cleanedVis = {};
369
+ for (const k of allowedVis) if (typeof incomingVis[k] === 'boolean') cleanedVis[k] = incomingVis[k];
370
+
371
+ await db.upsert('player_ui_config', {
372
+ player_id: playerId,
373
+ v: 1,
374
+ visibility: cleanedVis,
375
+ layout: incomingLayout,
376
+ updated_at: new Date(),
377
+ updated_by: req.user?.email || 'system'
378
+ });
379
+
380
+ res.json({ status: true, playerId, v: 1, visibility: cleanedVis, layout: incomingLayout, meta: { updatedAt: new Date().toISOString(), updatedBy: req.user?.email || 'system' } });
381
+ });
382
+ ```
383
+
384
+
385
+ ## Admin panel UX (suggestion)
386
+
387
+ - Page: Player UI Profiles
388
+ - Table: playerId, Updated, Updated By, Actions (Edit, Duplicate)
389
+ - Tabs in edit form: Visibility, Layout, Preview
390
+ - Visibility tab: toggles for all flags
391
+ - Layout tab:
392
+ - Template select (default/classic/compact/mobile-first)
393
+ - Region editor (drag buttons into regions, set order)
394
+ - Breakpoint editor (desktop/tablet/mobile overrides)
395
+ - Per-control overrides (region/order/visibility, optional absolute position)
396
+ - Preview tab: embedded test player using this `playerId`
397
+
398
+
399
+ ## Player integration (WebPlayer)
400
+
401
+ Pass a `playerId` and a `visibilityEndpoint`. The player calls GET /player-ui/{playerId}, applies `visibility`, then applies `layout` by moving controls into regions and ordering them. If fetch fails or 404, it uses all-visible + default layout.
402
+
403
+ ```ts
404
+ await player.initialize('#player', {
405
+ ui: {
406
+ playerId: 'landing_hero',
407
+ visibilityEndpoint: 'https://your-api.example.com/player-ui/{playerId}'
408
+ }
409
+ } as any);
410
+ ```
411
+
412
+ Region model in the player (suggestion)
413
+ - Predefine region containers in DOM (created by the player) such as:
414
+ - topLeft, topRight, bottomLeft, bottomRight, bottomFull, centerOverlay
415
+ - For each region name in `layout.regions`, the player appends known control nodes by ID.
416
+ - Apply `order` and per-control `overrides` to fine-tune placement.
417
+ - `breakpoints` are applied by listening to resize and reapplying the region mapping.
418
+
419
+
420
+ ## Defaults (no config)
421
+
422
+ If no record exists for a `playerId` or the request fails, the player should:
423
+ - Show all controls (visibility = all true)
424
+ - Use the internal default layout (template = default)
425
+
426
+
427
+ ## Future-proofing
428
+
429
+ - Keep `v` for schema evolution (e.g., adding new regions or properties)
430
+ - Unknown fields should be ignored by the player
431
+
package/docs/README.md ADDED
@@ -0,0 +1,7 @@
1
+ # Documentation Index
2
+
3
+ - Paywall Rental Flow (Stripe, Pesapal, optional Google Pay): [../PAYWALL_RENTAL_FLOW.md](../PAYWALL_RENTAL_FLOW.md)
4
+
5
+ Coming soon:
6
+ - API reference and other guides can be added here.
7
+