@loopaware/react-native 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/LICENSE ADDED
@@ -0,0 +1,109 @@
1
+ LoopAware Source Available License
2
+ Version 1.0, April 2026
3
+
4
+ Copyright (c) 2025-2026 Marco Polo Research Lab
5
+ https://mprlab.com
6
+
7
+ TERMS AND CONDITIONS
8
+
9
+ 1. Definitions
10
+
11
+ "Licensor" means Marco Polo Research Lab, the entity offering the
12
+ Software under these terms.
13
+
14
+ "Software" means the LoopAware software made available under this
15
+ license, including all source code, object code, documentation, and
16
+ associated files.
17
+
18
+ "You" means the individual or entity exercising rights under this
19
+ license.
20
+
21
+ "Non-Commercial Use" means use of the Software for any purpose that
22
+ does not generate revenue, directly or indirectly. This includes
23
+ personal projects, open-source projects that are free to use,
24
+ educational use, academic research, and evaluation of the Software.
25
+
26
+ "Commercial Use" means any use of the Software in connection with a
27
+ product, service, or project that generates revenue, directly or
28
+ indirectly. This includes, without limitation: products or services
29
+ sold or licensed for a fee, products or services that generate
30
+ advertising revenue, products or services offered under a paid
31
+ subscription, and internal use by a for-profit entity to support
32
+ revenue-generating operations.
33
+
34
+ "Commercial License" means a separate written agreement between You
35
+ and the Licensor granting rights to Commercial Use.
36
+
37
+ 2. Grant of Rights for Non-Commercial Use
38
+
39
+ Subject to the terms of this license, the Licensor grants You a
40
+ non-exclusive, worldwide, royalty-free license to use, copy, modify,
41
+ and create derivative works of the Software, solely for
42
+ Non-Commercial Use.
43
+
44
+ You may redistribute the Software or derivative works, provided that:
45
+
46
+ (a) You include a copy of this license with every distribution;
47
+ (b) You do not remove or alter any copyright notices; and
48
+ (c) You clearly state any modifications You have made.
49
+
50
+ 3. Commercial Use Requires a Commercial License
51
+
52
+ You may not use the Software for any Commercial Use unless You have
53
+ obtained a Commercial License from the Licensor.
54
+
55
+ Commercial Licenses are available at:
56
+ https://loopaware.mprlab.com/pricing
57
+
58
+ For licensing inquiries, contact: legal@mprlab.com
59
+
60
+ 4. Competing Use Restriction
61
+
62
+ Regardless of whether Your use is commercial or non-commercial, You
63
+ may not use the Software to create, offer, or operate a product or
64
+ service that competes with the Software or any product or service
65
+ offered by the Licensor that incorporates the Software.
66
+
67
+ 5. Patents
68
+
69
+ To the extent Your permitted use would necessarily infringe the
70
+ Licensor's patents, this license includes a patent license for that
71
+ use. If You bring a patent claim against any party alleging that the
72
+ Software infringes a patent, Your rights under this license terminate
73
+ immediately.
74
+
75
+ 6. Trademarks
76
+
77
+ This license does not grant You any rights to use the Licensor's
78
+ trademarks, trade names, service marks, or product names, except as
79
+ necessary to comply with the attribution requirements in Section 2.
80
+
81
+ 7. Disclaimer of Warranty
82
+
83
+ THE SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTIES OF ANY KIND,
84
+ EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF
85
+ FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, TITLE, OR
86
+ NON-INFRINGEMENT.
87
+
88
+ 8. Limitation of Liability
89
+
90
+ IN NO EVENT SHALL THE LICENSOR BE LIABLE FOR ANY INDIRECT, SPECIAL,
91
+ INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF OR RELATED TO
92
+ THE SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
93
+ THE LICENSOR'S TOTAL LIABILITY UNDER THIS LICENSE SHALL NOT EXCEED
94
+ THE AMOUNTS PAID BY YOU TO THE LICENSOR FOR THE SOFTWARE IN THE
95
+ TWELVE MONTHS PRECEDING THE CLAIM.
96
+
97
+ 9. Termination
98
+
99
+ This license terminates automatically if You violate any of its
100
+ terms. Upon termination, You must cease all use of the Software and
101
+ destroy all copies in Your possession.
102
+
103
+ The Licensor may, at its sole discretion, reinstate Your rights under
104
+ this license upon written notice.
105
+
106
+ 10. Governing Law
107
+
108
+ This license is governed by the laws of the State of California,
109
+ United States, without regard to conflict of law principles.
package/README.md ADDED
@@ -0,0 +1,107 @@
1
+ # LoopAware React Native Feedback
2
+
3
+ Native feedback button for Expo and React Native apps. This client submits customer feedback to `/public/mobile-feedback`; it is not part of LA Sentry error capture.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @loopaware/react-native
9
+ ```
10
+
11
+ The package ships built ESM JavaScript and TypeScript declarations from `dist/`. React and React Native are peer
12
+ dependencies and must come from the host app.
13
+
14
+ ## Expo setup
15
+
16
+ Register the mobile app for a LoopAware site through the authenticated API:
17
+
18
+ ```http
19
+ POST /api/sites/{site_id}/mobile-apps
20
+ ```
21
+
22
+ ```json
23
+ {
24
+ "platform": "ios",
25
+ "app_identifier": "com.example.app",
26
+ "display_name": "Example iOS"
27
+ }
28
+ ```
29
+
30
+ The response includes a public `client_id`. Ship that value with the app alongside the site ID.
31
+
32
+ ```tsx
33
+ import * as Application from "expo-application";
34
+ import { LoopAwareFeedbackButton, LoopAwareProvider } from "@loopaware/react-native";
35
+
36
+ export function App() {
37
+ return (
38
+ <LoopAwareProvider
39
+ siteId="SITE_ID"
40
+ mobileClientId="MOBILE_CLIENT_ID"
41
+ apiOrigin="https://loopaware.mprlab.com"
42
+ app={{
43
+ applicationId: Application.applicationId ?? "com.example.app",
44
+ version: Application.nativeApplicationVersion ?? "",
45
+ build: Application.nativeBuildVersion ?? "",
46
+ environment: "production",
47
+ }}
48
+ >
49
+ <CheckoutScreen />
50
+ </LoopAwareProvider>
51
+ );
52
+ }
53
+
54
+ function CheckoutScreen() {
55
+ return (
56
+ <>
57
+ <CheckoutContent />
58
+ <LoopAwareFeedbackButton
59
+ screen={{ name: "Checkout", path: "/checkout/payment" }}
60
+ context={{ step: "payment", plan: "pro" }}
61
+ />
62
+ </>
63
+ );
64
+ }
65
+ ```
66
+
67
+ The `context` object should contain only safe product state. Do not include passwords, payment data, tokens, or free-form private user content.
68
+
69
+ ## Manual submission
70
+
71
+ Apps with their own UI can call `submitLoopAwareFeedback` directly:
72
+
73
+ ```tsx
74
+ await submitLoopAwareFeedback(
75
+ {
76
+ siteId: "SITE_ID",
77
+ mobileClientId: "MOBILE_CLIENT_ID",
78
+ apiOrigin: "https://loopaware.mprlab.com",
79
+ app: {
80
+ platform: "android",
81
+ applicationId: "com.example.app",
82
+ version: "1.2.3",
83
+ build: "44",
84
+ environment: "production",
85
+ },
86
+ },
87
+ {
88
+ contact: "person@example.com",
89
+ message: "The checkout button is confusing.",
90
+ sentiment: "sad",
91
+ screen: { name: "Checkout", path: "/checkout/payment" },
92
+ context: { step: "payment" },
93
+ }
94
+ );
95
+ ```
96
+
97
+ ## Non-React Native apps
98
+
99
+ Swift, Kotlin, and other native clients should call the same `/public/mobile-feedback` REST endpoint directly. Register
100
+ the app with `/api/sites/{site_id}/mobile-apps`, store the public `client_id`, and submit the same JSON shape used by
101
+ `submitLoopAwareFeedback`.
102
+
103
+ ## Package validation
104
+
105
+ Repository CI runs `make client-react-native-check`. That target typechecks the source, builds `dist/`, packs the npm
106
+ tarball, verifies the package contents, installs the tarball into a temporary consumer project, and typechecks a real
107
+ `@loopaware/react-native` import.
@@ -0,0 +1,45 @@
1
+ import type { ReactNode } from "react";
2
+ export type LoopAwareSentiment = "sad" | "neutral" | "happy";
3
+ export type LoopAwareContextValue = string | number | boolean | null | LoopAwareContextValue[] | {
4
+ [key: string]: LoopAwareContextValue;
5
+ };
6
+ export type LoopAwareFeedbackContext = {
7
+ [key: string]: LoopAwareContextValue;
8
+ };
9
+ export type LoopAwareScreen = {
10
+ name: string;
11
+ path?: string;
12
+ };
13
+ export type LoopAwareAppMetadata = {
14
+ platform?: "ios" | "android";
15
+ applicationId: string;
16
+ version?: string;
17
+ build?: string;
18
+ environment?: string;
19
+ };
20
+ export type LoopAwareConfig = {
21
+ siteId: string;
22
+ mobileClientId: string;
23
+ apiOrigin: string;
24
+ app: LoopAwareAppMetadata;
25
+ defaultContext?: LoopAwareFeedbackContext;
26
+ };
27
+ export type LoopAwareProviderProps = LoopAwareConfig & {
28
+ children: ReactNode;
29
+ };
30
+ export type LoopAwareFeedbackInput = {
31
+ contact: string;
32
+ message: string;
33
+ sentiment?: LoopAwareSentiment;
34
+ screen: LoopAwareScreen;
35
+ context?: LoopAwareFeedbackContext;
36
+ };
37
+ export type LoopAwareFeedbackButtonProps = {
38
+ screen: LoopAwareScreen;
39
+ context?: LoopAwareFeedbackContext;
40
+ buttonLabel?: string;
41
+ };
42
+ export declare function LoopAwareProvider({ children, siteId, mobileClientId, apiOrigin, app, defaultContext, }: LoopAwareProviderProps): JSX.Element;
43
+ export declare function useLoopAware(): LoopAwareConfig;
44
+ export declare function submitLoopAwareFeedback(config: LoopAwareConfig, input: LoopAwareFeedbackInput): Promise<void>;
45
+ export declare function LoopAwareFeedbackButton({ screen, context, buttonLabel }: LoopAwareFeedbackButtonProps): JSX.Element;
package/dist/index.js ADDED
@@ -0,0 +1,206 @@
1
+ import React, { createContext, useContext, useMemo, useState } from "react";
2
+ import { Modal, Platform, Pressable, StyleSheet, Text, TextInput, View, } from "react-native";
3
+ const LoopAwareContext = createContext(null);
4
+ export function LoopAwareProvider({ children, siteId, mobileClientId, apiOrigin, app, defaultContext, }) {
5
+ const value = useMemo(() => ({
6
+ siteId,
7
+ mobileClientId,
8
+ apiOrigin: apiOrigin.replace(/\/+$/, ""),
9
+ app,
10
+ defaultContext,
11
+ }), [siteId, mobileClientId, apiOrigin, app, defaultContext]);
12
+ return React.createElement(LoopAwareContext.Provider, { value: value }, children);
13
+ }
14
+ export function useLoopAware() {
15
+ const config = useContext(LoopAwareContext);
16
+ if (!config) {
17
+ throw new Error("loopaware_provider_missing");
18
+ }
19
+ return config;
20
+ }
21
+ export async function submitLoopAwareFeedback(config, input) {
22
+ const mergedContext = {
23
+ ...(config.defaultContext ?? {}),
24
+ ...(input.context ?? {}),
25
+ };
26
+ const platform = config.app.platform ?? (Platform.OS === "ios" || Platform.OS === "android" ? Platform.OS : "ios");
27
+ const response = await fetch(`${config.apiOrigin.replace(/\/+$/, "")}/public/mobile-feedback`, {
28
+ method: "POST",
29
+ headers: { "Content-Type": "application/json" },
30
+ body: JSON.stringify({
31
+ site_id: config.siteId,
32
+ mobile_client_id: config.mobileClientId,
33
+ contact: input.contact,
34
+ message: input.message,
35
+ sentiment: input.sentiment ?? "",
36
+ screen: {
37
+ name: input.screen.name,
38
+ path: input.screen.path ?? "",
39
+ },
40
+ app: {
41
+ platform,
42
+ application_id: config.app.applicationId,
43
+ version: config.app.version ?? "",
44
+ build: config.app.build ?? "",
45
+ environment: config.app.environment ?? "",
46
+ },
47
+ context: mergedContext,
48
+ }),
49
+ });
50
+ if (!response.ok) {
51
+ throw new Error(`loopaware_feedback_failed:${response.status}`);
52
+ }
53
+ }
54
+ export function LoopAwareFeedbackButton({ screen, context, buttonLabel = "Feedback" }) {
55
+ const config = useLoopAware();
56
+ const [open, setOpen] = useState(false);
57
+ const [contact, setContact] = useState("");
58
+ const [message, setMessage] = useState("");
59
+ const [sentiment, setSentiment] = useState();
60
+ const [status, setStatus] = useState("");
61
+ const [submitting, setSubmitting] = useState(false);
62
+ async function handleSubmit() {
63
+ const normalizedContact = contact.trim();
64
+ const normalizedMessage = message.trim();
65
+ if (!normalizedContact || (!normalizedMessage && !sentiment)) {
66
+ setStatus("Add contact and feedback.");
67
+ return;
68
+ }
69
+ setSubmitting(true);
70
+ setStatus("Sending...");
71
+ try {
72
+ await submitLoopAwareFeedback(config, {
73
+ contact: normalizedContact,
74
+ message: normalizedMessage,
75
+ sentiment,
76
+ screen,
77
+ context,
78
+ });
79
+ setContact("");
80
+ setMessage("");
81
+ setSentiment(undefined);
82
+ setStatus("Sent.");
83
+ setOpen(false);
84
+ }
85
+ catch {
86
+ setStatus("Unable to send feedback.");
87
+ }
88
+ finally {
89
+ setSubmitting(false);
90
+ }
91
+ }
92
+ return (React.createElement(React.Fragment, null,
93
+ React.createElement(Pressable, { accessibilityRole: "button", onPress: () => setOpen(true), style: styles.floatingButton },
94
+ React.createElement(Text, { style: styles.floatingButtonText }, buttonLabel)),
95
+ React.createElement(Modal, { animationType: "slide", transparent: true, visible: open, onRequestClose: () => setOpen(false) },
96
+ React.createElement(View, { style: styles.modalBackdrop },
97
+ React.createElement(View, { style: styles.sheet },
98
+ React.createElement(Text, { style: styles.title }, "Send feedback"),
99
+ React.createElement(TextInput, { autoCapitalize: "none", keyboardType: "email-address", onChangeText: setContact, placeholder: "Email or phone", style: styles.input, value: contact }),
100
+ React.createElement(View, { style: styles.sentimentRow }, ["sad", "neutral", "happy"].map((value) => (React.createElement(Pressable, { accessibilityRole: "button", key: value, onPress: () => setSentiment(value), style: [styles.sentimentButton, sentiment === value ? styles.sentimentButtonActive : null] },
101
+ React.createElement(Text, { style: styles.sentimentText }, value))))),
102
+ React.createElement(TextInput, { multiline: true, onChangeText: setMessage, placeholder: "Your message", style: [styles.input, styles.messageInput], value: message }),
103
+ status ? React.createElement(Text, { style: styles.status }, status) : null,
104
+ React.createElement(View, { style: styles.actions },
105
+ React.createElement(Pressable, { accessibilityRole: "button", onPress: () => setOpen(false), style: styles.secondaryButton },
106
+ React.createElement(Text, { style: styles.secondaryButtonText }, "Cancel")),
107
+ React.createElement(Pressable, { accessibilityRole: "button", disabled: submitting, onPress: handleSubmit, style: [styles.primaryButton, submitting ? styles.disabledButton : null] },
108
+ React.createElement(Text, { style: styles.primaryButtonText }, "Send"))))))));
109
+ }
110
+ const styles = StyleSheet.create({
111
+ actions: {
112
+ flexDirection: "row",
113
+ gap: 12,
114
+ justifyContent: "flex-end",
115
+ },
116
+ disabledButton: {
117
+ opacity: 0.6,
118
+ },
119
+ floatingButton: {
120
+ alignItems: "center",
121
+ backgroundColor: "#0d6efd",
122
+ borderRadius: 24,
123
+ bottom: 24,
124
+ elevation: 4,
125
+ paddingHorizontal: 18,
126
+ paddingVertical: 12,
127
+ position: "absolute",
128
+ right: 20,
129
+ },
130
+ floatingButtonText: {
131
+ color: "#ffffff",
132
+ fontWeight: "700",
133
+ },
134
+ input: {
135
+ borderColor: "#ced4da",
136
+ borderRadius: 8,
137
+ borderWidth: 1,
138
+ marginBottom: 12,
139
+ paddingHorizontal: 12,
140
+ paddingVertical: 10,
141
+ },
142
+ messageInput: {
143
+ minHeight: 110,
144
+ textAlignVertical: "top",
145
+ },
146
+ modalBackdrop: {
147
+ backgroundColor: "rgba(0, 0, 0, 0.35)",
148
+ flex: 1,
149
+ justifyContent: "flex-end",
150
+ },
151
+ primaryButton: {
152
+ backgroundColor: "#0d6efd",
153
+ borderRadius: 8,
154
+ paddingHorizontal: 16,
155
+ paddingVertical: 10,
156
+ },
157
+ primaryButtonText: {
158
+ color: "#ffffff",
159
+ fontWeight: "700",
160
+ },
161
+ secondaryButton: {
162
+ borderColor: "#6c757d",
163
+ borderRadius: 8,
164
+ borderWidth: 1,
165
+ paddingHorizontal: 16,
166
+ paddingVertical: 10,
167
+ },
168
+ secondaryButtonText: {
169
+ color: "#343a40",
170
+ fontWeight: "700",
171
+ },
172
+ sentimentButton: {
173
+ borderColor: "#ced4da",
174
+ borderRadius: 8,
175
+ borderWidth: 1,
176
+ paddingHorizontal: 12,
177
+ paddingVertical: 8,
178
+ },
179
+ sentimentButtonActive: {
180
+ backgroundColor: "#e7f1ff",
181
+ borderColor: "#0d6efd",
182
+ },
183
+ sentimentRow: {
184
+ flexDirection: "row",
185
+ gap: 8,
186
+ marginBottom: 12,
187
+ },
188
+ sentimentText: {
189
+ textTransform: "capitalize",
190
+ },
191
+ sheet: {
192
+ backgroundColor: "#ffffff",
193
+ borderTopLeftRadius: 16,
194
+ borderTopRightRadius: 16,
195
+ padding: 20,
196
+ },
197
+ status: {
198
+ color: "#495057",
199
+ marginBottom: 12,
200
+ },
201
+ title: {
202
+ fontSize: 18,
203
+ fontWeight: "700",
204
+ marginBottom: 16,
205
+ },
206
+ });
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@loopaware/react-native",
3
+ "version": "0.1.0",
4
+ "description": "React Native feedback widget client for LoopAware.",
5
+ "license": "SEE LICENSE IN LICENSE",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "react-native": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js",
14
+ "default": "./dist/index.js"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "LICENSE",
20
+ "README.md"
21
+ ],
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/tyemirov/loopaware.git",
25
+ "directory": "clients/react-native"
26
+ },
27
+ "keywords": [
28
+ "loopaware",
29
+ "feedback",
30
+ "react-native",
31
+ "expo"
32
+ ],
33
+ "publishConfig": {
34
+ "access": "public",
35
+ "registry": "https://registry.npmjs.org/"
36
+ },
37
+ "scripts": {
38
+ "build": "node scripts/clean-dist.mjs && tsc -p tsconfig.build.json",
39
+ "prepack": "npm run build",
40
+ "typecheck": "tsc --noEmit -p tsconfig.json",
41
+ "verify-package": "node scripts/verify-package.mjs"
42
+ },
43
+ "peerDependencies": {
44
+ "react": ">=18.0.0",
45
+ "react-native": ">=0.72.0"
46
+ },
47
+ "devDependencies": {
48
+ "typescript": "^5.4.5"
49
+ }
50
+ }