@meetreeve/capacitor-bridge 0.1.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.
- package/README.md +134 -0
- package/compliance/PrivacyInfo.xcprivacy.template +155 -0
- package/compliance/README.md +42 -0
- package/compliance/index.cjs +60 -0
- package/compliance/permission-strings.json +14 -0
- package/dist/bugreport/index.cjs +143 -0
- package/dist/bugreport/index.d.cts +76 -0
- package/dist/bugreport/index.d.ts +76 -0
- package/dist/bugreport/index.js +111 -0
- package/dist/core/index.cjs +160 -0
- package/dist/core/index.d.cts +29 -0
- package/dist/core/index.d.ts +29 -0
- package/dist/core/index.js +124 -0
- package/dist/deeplink/index.cjs +166 -0
- package/dist/deeplink/index.d.cts +81 -0
- package/dist/deeplink/index.d.ts +81 -0
- package/dist/deeplink/index.js +133 -0
- package/dist/paywall/index.cjs +103 -0
- package/dist/paywall/index.d.cts +75 -0
- package/dist/paywall/index.d.ts +75 -0
- package/dist/paywall/index.js +71 -0
- package/dist/react/index.cjs +523 -0
- package/dist/react/index.d.cts +68 -0
- package/dist/react/index.d.ts +68 -0
- package/dist/react/index.js +483 -0
- package/dist/safe-area-B67yGQOn.d.cts +51 -0
- package/dist/safe-area-B67yGQOn.d.ts +51 -0
- package/package.json +78 -0
package/README.md
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# @meetreeve/capacitor-bridge
|
|
2
|
+
|
|
3
|
+
Reeve.Mobile substrate — the Capacitor wrapper bridge every Reeve consumer mobile app imports. One package, six entrypoints, zero per-app reinvention.
|
|
4
|
+
|
|
5
|
+
Tracked in [DEV-1387](https://linear.app/mindfortress/issue/DEV-1387) under the Reeve substrate roadmap [DEV-1376](https://linear.app/mindfortress/issue/DEV-1376).
|
|
6
|
+
|
|
7
|
+
## What it gives you
|
|
8
|
+
|
|
9
|
+
| Entrypoint | What |
|
|
10
|
+
|---|---|
|
|
11
|
+
| `@meetreeve/capacitor-bridge/core` | Platform detection (`isNativePlatform`, `isIOS`, …), safe-area inset reader, splash-screen dismiss. Framework-agnostic. |
|
|
12
|
+
| `@meetreeve/capacitor-bridge/react` | `<CapacitorProvider>`, `useCapacitor()`, `useSafeArea()`. |
|
|
13
|
+
| `@meetreeve/capacitor-bridge/paywall` | Native-aware paywall gate. Auto-routes to RevenueCat on iOS/Android and Stripe on web. Prevents Apple-review rejection from in-app Stripe Checkout. |
|
|
14
|
+
| `@meetreeve/capacitor-bridge/deeplink` | App-URL listener + handler chain for custom-scheme callbacks (Auth0) and universal links (email/iMessage/push). |
|
|
15
|
+
| `@meetreeve/capacitor-bridge/bugreport` | TestFlight bug-report submitter — POSTs payload + screenshot + environment to a consumer-configured endpoint. |
|
|
16
|
+
| `@meetreeve/capacitor-bridge/compliance` | Apple Privacy Manifest template + canonical Info.plist usage-description catalog (Node-only — used by the scaffold). |
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install @meetreeve/capacitor-bridge
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
(Private package — pulled from the org's npm registry. Consumer apps in the MindFortressInc org get it via the standard `@meetreeve/` workspace path.)
|
|
25
|
+
|
|
26
|
+
## Quick start (React)
|
|
27
|
+
|
|
28
|
+
```tsx
|
|
29
|
+
import { CapacitorProvider, useCapacitor } from "@meetreeve/capacitor-bridge/react";
|
|
30
|
+
|
|
31
|
+
function App() {
|
|
32
|
+
return (
|
|
33
|
+
<CapacitorProvider>
|
|
34
|
+
<YourApp />
|
|
35
|
+
</CapacitorProvider>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function YourApp() {
|
|
40
|
+
const { platform, isIOS } = useCapacitor();
|
|
41
|
+
// …
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Quick start (paywall gate)
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import {
|
|
49
|
+
runPaywall,
|
|
50
|
+
registerNativePaywall,
|
|
51
|
+
registerWebPaywall,
|
|
52
|
+
shouldShowWebPaywall,
|
|
53
|
+
} from "@meetreeve/capacitor-bridge/paywall";
|
|
54
|
+
|
|
55
|
+
// At app boot — wire your platform-specific checkouts
|
|
56
|
+
registerNativePaywall(async (product) => {
|
|
57
|
+
// Reeve.Commerce DEV-1388 wires RevenueCat here
|
|
58
|
+
return purchaseViaRevenueCat(product);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
registerWebPaywall(async (product) => {
|
|
62
|
+
// Existing Stripe Checkout redirect
|
|
63
|
+
window.location.href = await getStripeCheckoutUrl(product.webPriceId!);
|
|
64
|
+
return { status: "purchased", sourceChannel: "web_stripe" };
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// In your paywall UI
|
|
68
|
+
if (shouldShowWebPaywall()) {
|
|
69
|
+
// render the Stripe button
|
|
70
|
+
}
|
|
71
|
+
const result = await runPaywall({ productId: "pro-monthly", webPriceId: "price_xxx" });
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Quick start (deep links)
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
import {
|
|
78
|
+
registerDeepLinkHandler,
|
|
79
|
+
startDeepLinkListener,
|
|
80
|
+
} from "@meetreeve/capacitor-bridge/deeplink";
|
|
81
|
+
|
|
82
|
+
registerDeepLinkHandler(async (link) => {
|
|
83
|
+
if (link.scheme.endsWith(".ios") && link.path === "/callback") {
|
|
84
|
+
await handleAuth0Callback(link.query);
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
return false;
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
startDeepLinkListener({
|
|
91
|
+
onUnhandled: (link) => router.push(link.path + (link.hash ? `#${link.hash}` : "")),
|
|
92
|
+
});
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Quick start (bug report)
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
import { configureBugReport, submitBugReport } from "@meetreeve/capacitor-bridge/bugreport";
|
|
99
|
+
|
|
100
|
+
configureBugReport({
|
|
101
|
+
endpoint: "https://api.meetfreya.com/me/bug-report",
|
|
102
|
+
headers: async () => ({ Authorization: `Bearer ${await getToken()}` }),
|
|
103
|
+
appVersion: "1.0.0",
|
|
104
|
+
buildNumber: "42",
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
await submitBugReport({ message: "Push notification opened wrong project", userIdentifier: "user-42" });
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Architecture notes
|
|
111
|
+
|
|
112
|
+
- **Zero hard dependencies.** Peers React and `@capacitor/core` are optional — `core/` works in any browser-like environment, `react/` only loads if React is present, `paywall`/`deeplink`/`bugreport` similarly degrade.
|
|
113
|
+
- **SSR-safe.** Every entrypoint guards on `typeof window === "undefined"` and returns sensible defaults (`platform: "web"`, zero insets, no-op dismiss).
|
|
114
|
+
- **One-time install.** `<CapacitorProvider>` installs the safe-area CSS vars + `data-platform` attribute on mount. Idempotent.
|
|
115
|
+
|
|
116
|
+
## Roadmap
|
|
117
|
+
|
|
118
|
+
| Substrate PR | What | Where it lives |
|
|
119
|
+
|---|---|---|
|
|
120
|
+
| **PR 1 (this)** | core + react + paywall + deeplink + bugreport + compliance pack | This package |
|
|
121
|
+
| PR 2 | `create-reeve-mobile-app` cookie-cutter | Sibling `packages/create-reeve-mobile-app/` |
|
|
122
|
+
| PR 3 | `reeve-mobile-ci` reusable GH workflow | `.github/workflows/reusable-ios-testflight.yml` |
|
|
123
|
+
| PR 4 | Universal-links Vercel edge fn template | Bridge `deeplink/` already covers the JS side; edge fn = separate folder |
|
|
124
|
+
| PR 5 (this) | Bug-report widget + compliance pack consolidated here | This package |
|
|
125
|
+
|
|
126
|
+
PR 1 and PR 5 actually land together in this single package — separating them into multiple commits inside one PR.
|
|
127
|
+
|
|
128
|
+
## Consumers
|
|
129
|
+
|
|
130
|
+
- Freya iOS ([DEV-1390](https://linear.app/mindfortress/issue/DEV-1390)) — first wedge
|
|
131
|
+
- AgentPik mobile (planned, post-Freya validation)
|
|
132
|
+
- Studio mobile (planned)
|
|
133
|
+
- AskClara mobile (planned, requires HIPAA-mode defaults)
|
|
134
|
+
- Reeve Platform v1 mobile (planned)
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!--
|
|
3
|
+
Apple Privacy Manifest template for Reeve consumer mobile apps.
|
|
4
|
+
|
|
5
|
+
Required by App Store review as of Spring 2024 onward. Consumer apps
|
|
6
|
+
copy this file to their iOS project's App/PrivacyInfo.xcprivacy and
|
|
7
|
+
customize the collected-data categories + API reason codes to match
|
|
8
|
+
what the specific app actually does.
|
|
9
|
+
|
|
10
|
+
Baseline assumptions (Reeve substrate defaults):
|
|
11
|
+
- Auth0 collects User ID (email, sub) — already mapped below.
|
|
12
|
+
- Sentry collects Crash Data + Other Diagnostic Data — already mapped.
|
|
13
|
+
- PostHog collects Product Interaction + Other Usage Data — already mapped.
|
|
14
|
+
- No tracking domains (NSPrivacyTracking = false). Reeve apps do not
|
|
15
|
+
cross-app track; PostHog stays first-party.
|
|
16
|
+
|
|
17
|
+
Consumer-specific additions (per app):
|
|
18
|
+
- Camera / microphone access → add NSPrivacyAccessedAPITypes entries
|
|
19
|
+
AND ensure the Info.plist has *UsageDescription strings (see
|
|
20
|
+
compliance/permission-strings.json).
|
|
21
|
+
- Health / financial data → add the relevant
|
|
22
|
+
NSPrivacyCollectedDataType entries.
|
|
23
|
+
|
|
24
|
+
See compliance/README.md for the full reasoning checklist.
|
|
25
|
+
-->
|
|
26
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
27
|
+
<plist version="1.0">
|
|
28
|
+
<dict>
|
|
29
|
+
<key>NSPrivacyTracking</key>
|
|
30
|
+
<false/>
|
|
31
|
+
<key>NSPrivacyTrackingDomains</key>
|
|
32
|
+
<array/>
|
|
33
|
+
<key>NSPrivacyCollectedDataTypes</key>
|
|
34
|
+
<array>
|
|
35
|
+
<!-- Auth0 user identity -->
|
|
36
|
+
<dict>
|
|
37
|
+
<key>NSPrivacyCollectedDataType</key>
|
|
38
|
+
<string>NSPrivacyCollectedDataTypeUserID</string>
|
|
39
|
+
<key>NSPrivacyCollectedDataTypeLinked</key>
|
|
40
|
+
<true/>
|
|
41
|
+
<key>NSPrivacyCollectedDataTypeTracking</key>
|
|
42
|
+
<false/>
|
|
43
|
+
<key>NSPrivacyCollectedDataTypePurposes</key>
|
|
44
|
+
<array>
|
|
45
|
+
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
|
|
46
|
+
<string>NSPrivacyCollectedDataTypePurposeAuthentication</string>
|
|
47
|
+
</array>
|
|
48
|
+
</dict>
|
|
49
|
+
<dict>
|
|
50
|
+
<key>NSPrivacyCollectedDataType</key>
|
|
51
|
+
<string>NSPrivacyCollectedDataTypeEmailAddress</string>
|
|
52
|
+
<key>NSPrivacyCollectedDataTypeLinked</key>
|
|
53
|
+
<true/>
|
|
54
|
+
<key>NSPrivacyCollectedDataTypeTracking</key>
|
|
55
|
+
<false/>
|
|
56
|
+
<key>NSPrivacyCollectedDataTypePurposes</key>
|
|
57
|
+
<array>
|
|
58
|
+
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
|
|
59
|
+
</array>
|
|
60
|
+
</dict>
|
|
61
|
+
<!-- Sentry crash + diagnostic data -->
|
|
62
|
+
<dict>
|
|
63
|
+
<key>NSPrivacyCollectedDataType</key>
|
|
64
|
+
<string>NSPrivacyCollectedDataTypeCrashData</string>
|
|
65
|
+
<key>NSPrivacyCollectedDataTypeLinked</key>
|
|
66
|
+
<false/>
|
|
67
|
+
<key>NSPrivacyCollectedDataTypeTracking</key>
|
|
68
|
+
<false/>
|
|
69
|
+
<key>NSPrivacyCollectedDataTypePurposes</key>
|
|
70
|
+
<array>
|
|
71
|
+
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
|
|
72
|
+
<string>NSPrivacyCollectedDataTypePurposeAnalytics</string>
|
|
73
|
+
</array>
|
|
74
|
+
</dict>
|
|
75
|
+
<dict>
|
|
76
|
+
<key>NSPrivacyCollectedDataType</key>
|
|
77
|
+
<string>NSPrivacyCollectedDataTypeOtherDiagnosticData</string>
|
|
78
|
+
<key>NSPrivacyCollectedDataTypeLinked</key>
|
|
79
|
+
<false/>
|
|
80
|
+
<key>NSPrivacyCollectedDataTypeTracking</key>
|
|
81
|
+
<false/>
|
|
82
|
+
<key>NSPrivacyCollectedDataTypePurposes</key>
|
|
83
|
+
<array>
|
|
84
|
+
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
|
|
85
|
+
<string>NSPrivacyCollectedDataTypePurposeAnalytics</string>
|
|
86
|
+
</array>
|
|
87
|
+
</dict>
|
|
88
|
+
<!-- PostHog product analytics -->
|
|
89
|
+
<dict>
|
|
90
|
+
<key>NSPrivacyCollectedDataType</key>
|
|
91
|
+
<string>NSPrivacyCollectedDataTypeProductInteraction</string>
|
|
92
|
+
<key>NSPrivacyCollectedDataTypeLinked</key>
|
|
93
|
+
<true/>
|
|
94
|
+
<key>NSPrivacyCollectedDataTypeTracking</key>
|
|
95
|
+
<false/>
|
|
96
|
+
<key>NSPrivacyCollectedDataTypePurposes</key>
|
|
97
|
+
<array>
|
|
98
|
+
<string>NSPrivacyCollectedDataTypePurposeAnalytics</string>
|
|
99
|
+
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
|
|
100
|
+
</array>
|
|
101
|
+
</dict>
|
|
102
|
+
<dict>
|
|
103
|
+
<key>NSPrivacyCollectedDataType</key>
|
|
104
|
+
<string>NSPrivacyCollectedDataTypeOtherUsageData</string>
|
|
105
|
+
<key>NSPrivacyCollectedDataTypeLinked</key>
|
|
106
|
+
<true/>
|
|
107
|
+
<key>NSPrivacyCollectedDataTypeTracking</key>
|
|
108
|
+
<false/>
|
|
109
|
+
<key>NSPrivacyCollectedDataTypePurposes</key>
|
|
110
|
+
<array>
|
|
111
|
+
<string>NSPrivacyCollectedDataTypePurposeAnalytics</string>
|
|
112
|
+
</array>
|
|
113
|
+
</dict>
|
|
114
|
+
</array>
|
|
115
|
+
<key>NSPrivacyAccessedAPITypes</key>
|
|
116
|
+
<array>
|
|
117
|
+
<!-- UserDefaults — Capacitor uses for plugin storage -->
|
|
118
|
+
<dict>
|
|
119
|
+
<key>NSPrivacyAccessedAPIType</key>
|
|
120
|
+
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
|
|
121
|
+
<key>NSPrivacyAccessedAPITypeReasons</key>
|
|
122
|
+
<array>
|
|
123
|
+
<string>CA92.1</string>
|
|
124
|
+
</array>
|
|
125
|
+
</dict>
|
|
126
|
+
<!-- File timestamp — bridged JS file APIs -->
|
|
127
|
+
<dict>
|
|
128
|
+
<key>NSPrivacyAccessedAPIType</key>
|
|
129
|
+
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
|
|
130
|
+
<key>NSPrivacyAccessedAPITypeReasons</key>
|
|
131
|
+
<array>
|
|
132
|
+
<string>C617.1</string>
|
|
133
|
+
</array>
|
|
134
|
+
</dict>
|
|
135
|
+
<!-- System boot time — Sentry uses for crash correlation -->
|
|
136
|
+
<dict>
|
|
137
|
+
<key>NSPrivacyAccessedAPIType</key>
|
|
138
|
+
<string>NSPrivacyAccessedAPICategorySystemBootTime</string>
|
|
139
|
+
<key>NSPrivacyAccessedAPITypeReasons</key>
|
|
140
|
+
<array>
|
|
141
|
+
<string>35F9.1</string>
|
|
142
|
+
</array>
|
|
143
|
+
</dict>
|
|
144
|
+
<!-- Disk space — push notification payload pre-flight -->
|
|
145
|
+
<dict>
|
|
146
|
+
<key>NSPrivacyAccessedAPIType</key>
|
|
147
|
+
<string>NSPrivacyAccessedAPICategoryDiskSpace</string>
|
|
148
|
+
<key>NSPrivacyAccessedAPITypeReasons</key>
|
|
149
|
+
<array>
|
|
150
|
+
<string>E174.1</string>
|
|
151
|
+
</array>
|
|
152
|
+
</dict>
|
|
153
|
+
</array>
|
|
154
|
+
</dict>
|
|
155
|
+
</plist>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Apple compliance pack
|
|
2
|
+
|
|
3
|
+
Templates + catalogs every Reeve consumer mobile app uses to satisfy App Store review. Owned by the Reeve.Mobile substrate ([DEV-1387](https://linear.app/mindfortress/issue/DEV-1387)).
|
|
4
|
+
|
|
5
|
+
## What's in here
|
|
6
|
+
|
|
7
|
+
| File | Purpose |
|
|
8
|
+
|---|---|
|
|
9
|
+
| `PrivacyInfo.xcprivacy.template` | Apple Privacy Manifest baseline. Mandatory since Spring 2024. Consumer apps copy + extend per their actual data use. |
|
|
10
|
+
| `permission-strings.json` | Canonical Info.plist `*UsageDescription` strings. Specific copy passes review; vague copy gets flagged. Substitute `{{APP_NAME}}` per consumer. |
|
|
11
|
+
| `index.js` | Node helpers — `renderPrivacyInfo()`, `renderPermissionStrings()`. Used by the create-reeve-mobile-app scaffold (PR 2). |
|
|
12
|
+
|
|
13
|
+
## How consumer apps use this
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
const { renderPrivacyInfo, renderPermissionStrings } = require("@meetreeve/capacitor-bridge/compliance");
|
|
17
|
+
|
|
18
|
+
// Generate Freya's privacy manifest
|
|
19
|
+
const xml = renderPrivacyInfo();
|
|
20
|
+
fs.writeFileSync("ios/App/App/PrivacyInfo.xcprivacy", xml);
|
|
21
|
+
|
|
22
|
+
// Generate Freya's usage-description strings
|
|
23
|
+
const strings = renderPermissionStrings(
|
|
24
|
+
["NSCameraUsageDescription", "NSMicrophoneUsageDescription", "NSFaceIDUsageDescription"],
|
|
25
|
+
"Freya"
|
|
26
|
+
);
|
|
27
|
+
// → merge into Info.plist
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Apple review checklist
|
|
31
|
+
|
|
32
|
+
- [ ] `PrivacyInfo.xcprivacy` present at `ios/App/App/PrivacyInfo.xcprivacy`
|
|
33
|
+
- [ ] Every native capability used has a corresponding `*UsageDescription` Info.plist key
|
|
34
|
+
- [ ] `NSPrivacyTracking = false` unless a third-party SDK forces tracking (rare)
|
|
35
|
+
- [ ] No collected data type marked `NSPrivacyCollectedDataTypeTracking = true`
|
|
36
|
+
- [ ] No payment URLs (Stripe Checkout) reachable from inside the app — paywall gate routes to RevenueCat IAP via [DEV-1388](https://linear.app/mindfortress/issue/DEV-1388)
|
|
37
|
+
- [ ] Sign-out option visible if a sign-in option exists (App Store guideline 5.1.1(v))
|
|
38
|
+
- [ ] Account deletion path visible if accounts are supported (App Store guideline 5.1.1(v))
|
|
39
|
+
|
|
40
|
+
## When to add a new data type
|
|
41
|
+
|
|
42
|
+
If the consumer app collects a new category Apple recognizes (e.g. Health, Financial Info), open a PR adding it to `PrivacyInfo.xcprivacy.template` so the next consumer doesn't have to re-discover it.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* compliance/ helpers. CommonJS — small enough to ship without a TS build,
|
|
3
|
+
* and the cookie-cutter (create-reeve-mobile-app) consumes these in a Node
|
|
4
|
+
* scaffold script. Browsers never import this; it's tooling only.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require("node:fs");
|
|
8
|
+
const path = require("node:path");
|
|
9
|
+
|
|
10
|
+
const TEMPLATE_PATH = path.join(__dirname, "PrivacyInfo.xcprivacy.template");
|
|
11
|
+
const PERMISSION_STRINGS_PATH = path.join(__dirname, "permission-strings.json");
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Render the PrivacyInfo template — the template has no substitutions today
|
|
15
|
+
* but we may add them (e.g. extra collected-data types per consumer). For
|
|
16
|
+
* now it's a straight copy. Returns the xml string.
|
|
17
|
+
*/
|
|
18
|
+
function renderPrivacyInfo() {
|
|
19
|
+
return fs.readFileSync(TEMPLATE_PATH, "utf8");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Render Info.plist usage-description strings for the requested permission
|
|
24
|
+
* keys with the consumer's app name substituted in.
|
|
25
|
+
*
|
|
26
|
+
* @param {string[]} keys - e.g. ["NSCameraUsageDescription", "NSFaceIDUsageDescription"]
|
|
27
|
+
* @param {string} appName - the consumer app's display name (e.g. "Freya")
|
|
28
|
+
* @returns {Record<string, string>}
|
|
29
|
+
*/
|
|
30
|
+
function renderPermissionStrings(keys, appName) {
|
|
31
|
+
const catalog = JSON.parse(fs.readFileSync(PERMISSION_STRINGS_PATH, "utf8"));
|
|
32
|
+
const out = {};
|
|
33
|
+
for (const key of keys) {
|
|
34
|
+
const template = catalog[key];
|
|
35
|
+
if (!template) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
`[reeve/capacitor-bridge/compliance] unknown permission key: ${key}. ` +
|
|
38
|
+
`See compliance/permission-strings.json for the catalog.`
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
out[key] = template.replaceAll("{{APP_NAME}}", appName);
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* List every permission key the catalog knows about.
|
|
48
|
+
*/
|
|
49
|
+
function listPermissionKeys() {
|
|
50
|
+
const catalog = JSON.parse(fs.readFileSync(PERMISSION_STRINGS_PATH, "utf8"));
|
|
51
|
+
return Object.keys(catalog).filter((k) => !k.startsWith("$"));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = {
|
|
55
|
+
renderPrivacyInfo,
|
|
56
|
+
renderPermissionStrings,
|
|
57
|
+
listPermissionKeys,
|
|
58
|
+
TEMPLATE_PATH,
|
|
59
|
+
PERMISSION_STRINGS_PATH,
|
|
60
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "Reeve.Mobile substrate — canonical Info.plist usage-description strings. Consumer apps include the keys for capabilities they actually use; Apple rejects builds that declare a capability but omit the matching string. Strings are intentionally specific about WHY we need the capability — vague descriptions also draw rejections.",
|
|
3
|
+
"NSCameraUsageDescription": "{{APP_NAME}} uses your camera so you can capture photos for your projects.",
|
|
4
|
+
"NSPhotoLibraryUsageDescription": "{{APP_NAME}} accesses your photo library so you can attach images to your work.",
|
|
5
|
+
"NSPhotoLibraryAddUsageDescription": "{{APP_NAME}} saves images you create to your photo library.",
|
|
6
|
+
"NSMicrophoneUsageDescription": "{{APP_NAME}} uses your microphone so you can dictate notes and ideas hands-free.",
|
|
7
|
+
"NSSpeechRecognitionUsageDescription": "{{APP_NAME}} processes the audio you record so dictation appears as text.",
|
|
8
|
+
"NSFaceIDUsageDescription": "{{APP_NAME}} uses Face ID to quickly and securely sign you back in.",
|
|
9
|
+
"NSLocationWhenInUseUsageDescription": "{{APP_NAME}} uses your location to personalize nearby suggestions while the app is open.",
|
|
10
|
+
"NSBluetoothAlwaysUsageDescription": "{{APP_NAME}} connects to nearby devices to support hardware integrations.",
|
|
11
|
+
"NSUserTrackingUsageDescription": "{{APP_NAME}} does not track you across other apps or websites — leave this off; we include this string only if a third-party SDK requires it.",
|
|
12
|
+
"NSContactsUsageDescription": "{{APP_NAME}} accesses your contacts only when you choose to share, so you can invite collaborators.",
|
|
13
|
+
"NSCalendarsUsageDescription": "{{APP_NAME}} reads your calendar to suggest times that work for you."
|
|
14
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/bugreport/index.ts
|
|
21
|
+
var bugreport_exports = {};
|
|
22
|
+
__export(bugreport_exports, {
|
|
23
|
+
__resetBugReportConfigForTests: () => __resetBugReportConfigForTests,
|
|
24
|
+
captureScreenshot: () => captureScreenshot,
|
|
25
|
+
configureBugReport: () => configureBugReport,
|
|
26
|
+
getBugReportEnvironment: () => getBugReportEnvironment,
|
|
27
|
+
resetBugReportConfig: () => resetBugReportConfig,
|
|
28
|
+
submitBugReport: () => submitBugReport
|
|
29
|
+
});
|
|
30
|
+
module.exports = __toCommonJS(bugreport_exports);
|
|
31
|
+
|
|
32
|
+
// src/core/platform.ts
|
|
33
|
+
function getCapacitor() {
|
|
34
|
+
if (typeof window === "undefined") return null;
|
|
35
|
+
const cap = window.Capacitor;
|
|
36
|
+
if (!cap || typeof cap.isNativePlatform !== "function") return null;
|
|
37
|
+
return cap;
|
|
38
|
+
}
|
|
39
|
+
function getPlatform() {
|
|
40
|
+
const cap = getCapacitor();
|
|
41
|
+
if (!cap) return "web";
|
|
42
|
+
const p = cap.getPlatform();
|
|
43
|
+
return p === "ios" ? "ios" : p === "android" ? "android" : "web";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// src/bugreport/index.ts
|
|
47
|
+
var GLOBAL_KEY = /* @__PURE__ */ Symbol.for("@meetreeve/capacitor-bridge/bugreport@1");
|
|
48
|
+
function getGlobalState() {
|
|
49
|
+
const g = globalThis;
|
|
50
|
+
if (!g[GLOBAL_KEY]) g[GLOBAL_KEY] = { config: null };
|
|
51
|
+
return g[GLOBAL_KEY];
|
|
52
|
+
}
|
|
53
|
+
function configureBugReport(c) {
|
|
54
|
+
const state = getGlobalState();
|
|
55
|
+
if (state.config && state.config.endpoint !== c.endpoint) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
"[@meetreeve/capacitor-bridge/bugreport] configureBugReport has already been called with a different endpoint. Re-configuration is refused so a later-loaded script cannot redirect bug-report submissions. Call __resetBugReportConfigForTests() in tests."
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
state.config = Object.freeze({ ...c });
|
|
61
|
+
}
|
|
62
|
+
function __resetBugReportConfigForTests() {
|
|
63
|
+
getGlobalState().config = null;
|
|
64
|
+
}
|
|
65
|
+
function resetBugReportConfig() {
|
|
66
|
+
if (typeof console !== "undefined" && typeof console.warn === "function") {
|
|
67
|
+
console.warn(
|
|
68
|
+
"[@meetreeve/capacitor-bridge/bugreport] resetBugReportConfig is deprecated; use __resetBugReportConfigForTests."
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
__resetBugReportConfigForTests();
|
|
72
|
+
}
|
|
73
|
+
function getBugReportEnvironment() {
|
|
74
|
+
const config = getGlobalState().config;
|
|
75
|
+
if (typeof window === "undefined") {
|
|
76
|
+
return {
|
|
77
|
+
platform: getPlatform(),
|
|
78
|
+
userAgent: "",
|
|
79
|
+
currentUrl: "",
|
|
80
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
platform: getPlatform(),
|
|
85
|
+
userAgent: window.navigator?.userAgent ?? "",
|
|
86
|
+
appVersion: config?.appVersion,
|
|
87
|
+
buildNumber: config?.buildNumber,
|
|
88
|
+
currentUrl: window.location?.href ?? "",
|
|
89
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
async function submitBugReport(payload) {
|
|
93
|
+
const config = getGlobalState().config;
|
|
94
|
+
if (!config) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
"[@meetreeve/capacitor-bridge/bugreport] submitBugReport called without configureBugReport({endpoint})."
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
const submission = {
|
|
100
|
+
...payload,
|
|
101
|
+
environment: getBugReportEnvironment()
|
|
102
|
+
};
|
|
103
|
+
const headers = {
|
|
104
|
+
"Content-Type": "application/json"
|
|
105
|
+
};
|
|
106
|
+
if (config.headers) {
|
|
107
|
+
const extraHeaders = await config.headers();
|
|
108
|
+
Object.assign(headers, extraHeaders);
|
|
109
|
+
}
|
|
110
|
+
const response = await fetch(config.endpoint, {
|
|
111
|
+
method: "POST",
|
|
112
|
+
headers,
|
|
113
|
+
body: JSON.stringify(submission),
|
|
114
|
+
credentials: config.credentials ?? "omit"
|
|
115
|
+
});
|
|
116
|
+
if (!response.ok) {
|
|
117
|
+
throw new Error(
|
|
118
|
+
`[@meetreeve/capacitor-bridge/bugreport] bug-report submission failed: ${response.status} ${response.statusText}`
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
async function captureScreenshot() {
|
|
123
|
+
if (typeof window === "undefined") return null;
|
|
124
|
+
const cap = window.Capacitor;
|
|
125
|
+
const plugin = cap?.Plugins?.Screenshot;
|
|
126
|
+
if (plugin) {
|
|
127
|
+
try {
|
|
128
|
+
const { base64 } = await plugin.take();
|
|
129
|
+
return `data:image/png;base64,${base64}`;
|
|
130
|
+
} catch {
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
136
|
+
0 && (module.exports = {
|
|
137
|
+
__resetBugReportConfigForTests,
|
|
138
|
+
captureScreenshot,
|
|
139
|
+
configureBugReport,
|
|
140
|
+
getBugReportEnvironment,
|
|
141
|
+
resetBugReportConfig,
|
|
142
|
+
submitBugReport
|
|
143
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bug-report widget.
|
|
3
|
+
*
|
|
4
|
+
* TestFlight beta users hit "Report a bug" → small modal → text + optional
|
|
5
|
+
* screenshot → POST to a consumer-configured endpoint (typically a thin
|
|
6
|
+
* Slack-relay route on the consumer's backend). Sentry user feedback works
|
|
7
|
+
* too but the social UX of a Slack channel + screenshot is what beta cohorts
|
|
8
|
+
* actually respond to.
|
|
9
|
+
*
|
|
10
|
+
* This module owns the submission contract + a tiny snapshot helper. The
|
|
11
|
+
* React widget itself ships in src/react/BugReportButton.tsx (next step) —
|
|
12
|
+
* consumer apps that don't use React can call submitBugReport() directly.
|
|
13
|
+
*/
|
|
14
|
+
interface BugReportPayload {
|
|
15
|
+
message: string;
|
|
16
|
+
/** Optional screenshot — data URL or remote URL — uploaded as part of report. */
|
|
17
|
+
screenshot?: string;
|
|
18
|
+
/** Consumer-supplied user identifier (subscriber_id, email — whatever's stable). */
|
|
19
|
+
userIdentifier?: string;
|
|
20
|
+
/** Free-form metadata; merged with platform info before send. */
|
|
21
|
+
extra?: Record<string, unknown>;
|
|
22
|
+
}
|
|
23
|
+
interface BugReportEnvironment {
|
|
24
|
+
platform: string;
|
|
25
|
+
userAgent: string;
|
|
26
|
+
appVersion?: string;
|
|
27
|
+
buildNumber?: string;
|
|
28
|
+
currentUrl: string;
|
|
29
|
+
timestamp: string;
|
|
30
|
+
}
|
|
31
|
+
interface BugReportSubmission extends BugReportPayload {
|
|
32
|
+
environment: BugReportEnvironment;
|
|
33
|
+
}
|
|
34
|
+
interface BugReportConfig {
|
|
35
|
+
endpoint: string;
|
|
36
|
+
/** Headers to attach (auth token, host-app key, etc.). */
|
|
37
|
+
headers?: () => Record<string, string> | Promise<Record<string, string>>;
|
|
38
|
+
/** Consumer-supplied version metadata. */
|
|
39
|
+
appVersion?: string;
|
|
40
|
+
buildNumber?: string;
|
|
41
|
+
/**
|
|
42
|
+
* fetch credentials mode. Defaults to `"omit"` because the Capacitor
|
|
43
|
+
* WebView origin (`capacitor://localhost`) differs from the consumer's API
|
|
44
|
+
* origin, so cookie-based auth wouldn't flow anyway, and `omit` avoids
|
|
45
|
+
* accidentally turning the bug-report endpoint into a CSRF target if the
|
|
46
|
+
* consumer enables permissive CORS. Set to `"include"` only when you
|
|
47
|
+
* understand the consequences for the consumer's endpoint.
|
|
48
|
+
*/
|
|
49
|
+
credentials?: "omit" | "same-origin" | "include";
|
|
50
|
+
}
|
|
51
|
+
declare function configureBugReport(c: BugReportConfig): void;
|
|
52
|
+
/** Internal — for tests only. */
|
|
53
|
+
declare function __resetBugReportConfigForTests(): void;
|
|
54
|
+
/** @deprecated use __resetBugReportConfigForTests; prod calls now warn. */
|
|
55
|
+
declare function resetBugReportConfig(): void;
|
|
56
|
+
declare function getBugReportEnvironment(): BugReportEnvironment;
|
|
57
|
+
/**
|
|
58
|
+
* Submit a bug report to the configured endpoint. Throws if not configured.
|
|
59
|
+
*
|
|
60
|
+
* The post body always includes a top-level `environment` object — consumer
|
|
61
|
+
* backends can route on `environment.platform === "ios"` to tag with a
|
|
62
|
+
* Slack emoji, etc.
|
|
63
|
+
*/
|
|
64
|
+
declare function submitBugReport(payload: BugReportPayload): Promise<void>;
|
|
65
|
+
/**
|
|
66
|
+
* Capture the current screen as a data URL using the Capacitor Screenshot
|
|
67
|
+
* plugin (if present) or a DOM-canvas fallback. Returns null if no capture
|
|
68
|
+
* method is available — the bug-report modal then submits without an image.
|
|
69
|
+
*
|
|
70
|
+
* Note: many WebView screenshot plugins can't capture native chrome (push
|
|
71
|
+
* banners, system alerts). For TestFlight QA that's fine — the bug message
|
|
72
|
+
* almost always describes the chrome anyway.
|
|
73
|
+
*/
|
|
74
|
+
declare function captureScreenshot(): Promise<string | null>;
|
|
75
|
+
|
|
76
|
+
export { type BugReportEnvironment, type BugReportPayload, type BugReportSubmission, __resetBugReportConfigForTests, captureScreenshot, configureBugReport, getBugReportEnvironment, resetBugReportConfig, submitBugReport };
|