@webority/mobile-insights 0.0.0-stage → 0.129.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.txt ADDED
@@ -0,0 +1,9 @@
1
+ Copyright (c) Webority Technologies. All rights reserved.
2
+
3
+ This software and its NuGet packages are the proprietary and confidential property
4
+ of Webority Technologies. They are published publicly only for the convenience of
5
+ Webority's own projects and internal consumption.
6
+
7
+ No license or right to use, copy, modify, merge, publish, distribute, sublicense,
8
+ or create derivative works is granted to any third party. Unauthorized use,
9
+ reproduction, or distribution is prohibited.
package/README.md CHANGED
@@ -1,3 +1,70 @@
1
- # Temporary Holding Version
1
+ # @webority/mobile-insights
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Product feedback for Webority React Native apps. It gives the app the one NPS question and a
4
+ feedback button. Both post to the product's Webority Insights feedback API, which the product
5
+ maps on a prefix (`/api/insights/feedback` by default).
6
+
7
+ ```bash
8
+ npm install @webority/mobile-insights
9
+ ```
10
+
11
+ Builds on `@webority/mobile-core` (the signed-in HTTP client, the error shape) and
12
+ `@webority/mobile-ui` (theme, components). Every visible string is a prop with a plain-English
13
+ default, so the app can translate or reword them.
14
+
15
+ ## Peers
16
+
17
+ | Package | Why | Required |
18
+ |---|---|---|
19
+ | `expo` | mobile-core's modules | yes |
20
+ | `react-native-gesture-handler` | mobile-ui needs it | yes |
21
+
22
+ ## Provider
23
+
24
+ Create the client over the app's own signed-in HTTP client, so every call carries the same
25
+ bearer token, tenant header and refresh handling as the rest of the app.
26
+
27
+ ```tsx
28
+ import { userClient } from '@webority/mobile-core';
29
+ import { createInsightsFeedbackClient, InsightsFeedbackProvider } from '@webority/mobile-insights';
30
+
31
+ const feedbackClient = createInsightsFeedbackClient(userClient, { prefix: '/api/insights/feedback' });
32
+
33
+ export const App = () => (
34
+ <ThemeProvider>
35
+ <InsightsFeedbackProvider client={feedbackClient}>
36
+ <Navigation />
37
+ </InsightsFeedbackProvider>
38
+ </ThemeProvider>
39
+ );
40
+ ```
41
+
42
+ ## Components
43
+
44
+ ```tsx
45
+ import { FeedbackButton, NpsPrompt } from '@webority/mobile-insights';
46
+
47
+ <NpsPrompt />;
48
+
49
+ <FeedbackButton pagePath={route.name} />;
50
+ ```
51
+
52
+ - **NpsPrompt** asks the question once per mount, only when the server says it is due. It renders
53
+ nothing while it loads, when it is not due, and after a load failure. Send is enabled once a
54
+ score is chosen. Not now is a permanent answer, so the question does not come back.
55
+ - **FeedbackButton** opens a sheet with one multiline field. Send is disabled while the message is
56
+ empty or longer than 1000 characters.
57
+
58
+ Failures show the message the server sends, written for the person. Branch on `code` if you need
59
+ to, never show it.
60
+
61
+ ## Wire contract
62
+
63
+ | Route | Body | Response |
64
+ |---|---|---|
65
+ | `GET {prefix}/nps` | | `{ due: boolean, trigger: string \| null }` |
66
+ | `POST {prefix}/nps` | `{ trigger: string, score: number \| null, comment?: string \| null }` | 204. A null score is "not now". |
67
+ | `POST {prefix}/comments` | `{ comment: string, pagePath?: string \| null }` | 204 |
68
+
69
+ Every POST sends the header `X-Webority-Surface: Mobile`. Errors arrive as the standard error body
70
+ `{ code, message }` and are thrown as `InsightsFeedbackApiError`.
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.FeedbackButton=void 0;var _mobileUi=require("@webority/mobile-ui"),_react=require("react"),_reactNative=require("react-native"),_client=require("./client.js"),_InsightsFeedbackProvider=require("./InsightsFeedbackProvider.js"),_jsxRuntime=require("react/jsx-runtime");const MAX_LENGTH=1e3,FeedbackButton=({label:e="Feedback",title:t="Send us feedback",placeholder:i="Tell us what works and what does not",sendLabel:s="Send",cancelLabel:a="Cancel",thanks:n="Thanks, we read every message.",pagePath:l})=>{const{client:r}=(0,_InsightsFeedbackProvider.useInsightsFeedback)(),o=(0,_mobileUi.useTheme)(),c=(0,_react.useMemo)(()=>buildStyles(o),[o]),[d,u]=(0,_react.useState)(!1),[b,m]=(0,_react.useState)(""),[_,x]=(0,_react.useState)(!1),[j,h]=(0,_react.useState)(!1),[g,v]=(0,_react.useState)(null),p=b.trim(),y=p.length-1e3,R=p.length>0&&y<=0;return(0,_jsxRuntime.jsxs)(_jsxRuntime.Fragment,{children:[(0,_jsxRuntime.jsx)(_mobileUi.Button,{label:e,onPress:()=>{h(!1),v(null),u(!0)}}),(0,_jsxRuntime.jsx)(_mobileUi.Modal,{visible:d,onClose:()=>u(!1),accessibilityLabel:t,children:(0,_jsxRuntime.jsxs)(_mobileUi.KeyboardAwareScrollView,{contentContainerStyle:c.content,safeAreaEdges:[],children:[(0,_jsxRuntime.jsx)(_mobileUi.Text,{variant:"title",children:t}),j?(0,_jsxRuntime.jsx)(_mobileUi.Text,{variant:"body",color:"secondary",accessibilityLiveRegion:"polite",children:n}):(0,_jsxRuntime.jsxs)(_jsxRuntime.Fragment,{children:[(0,_jsxRuntime.jsx)(_mobileUi.Input,{accessibilityLabel:i,placeholder:i,multiline:!0,value:b,onChangeText:m,disabled:_,helperText:`${p.length} / 1000`,error:y>0?`Up to 1000 characters; remove ${y} to send.`:void 0}),null!==g&&(0,_jsxRuntime.jsx)(_mobileUi.Text,{variant:"bodySmall",color:"danger",accessibilityLiveRegion:"polite",children:g}),(0,_jsxRuntime.jsxs)(_reactNative.View,{style:c.actions,children:[(0,_jsxRuntime.jsx)(_mobileUi.Button,{label:a,variant:"ghost",onPress:()=>u(!1),disabled:_}),(0,_jsxRuntime.jsx)(_mobileUi.Button,{label:s,onPress:()=>{(async()=>{x(!0),v(null);try{await r.leaveComment({comment:p,pagePath:l}),m(""),h(!0)}catch(e){v((0,_client.toInsightsApiError)(e).message)}finally{x(!1)}})()},disabled:!R||_,loading:_})]})]})]})})]})};exports.FeedbackButton=FeedbackButton;const buildStyles=e=>_reactNative.StyleSheet.create({content:{padding:e.space.md,gap:e.space.md},actions:{flexDirection:"row",justifyContent:"flex-end",gap:e.space.sm}});
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.useInsightsFeedback=exports.InsightsFeedbackProvider=void 0;var _react=require("react"),_jsxRuntime=require("react/jsx-runtime");const InsightsFeedbackContext=(0,_react.createContext)(null),InsightsFeedbackProvider=({client:e,children:t})=>{const s=(0,_react.useMemo)(()=>({client:e}),[e]);return(0,_jsxRuntime.jsx)(InsightsFeedbackContext.Provider,{value:s,children:t})};exports.InsightsFeedbackProvider=InsightsFeedbackProvider;const useInsightsFeedback=()=>{const e=(0,_react.useContext)(InsightsFeedbackContext);if(!e)throw new Error("InsightsFeedbackProvider is missing.");return e};exports.useInsightsFeedback=useInsightsFeedback;
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.NpsPrompt=void 0;var _mobileUi=require("@webority/mobile-ui"),_react=require("react"),_reactNative=require("react-native"),_client=require("./client.js"),_InsightsFeedbackProvider=require("./InsightsFeedbackProvider.js"),_jsxRuntime=require("react/jsx-runtime");const SCORES=[0,1,2,3,4,5,6,7,8,9,10],NpsPrompt=({question:e="How likely are you to recommend us to a colleague?",lowLabel:t="Not likely",highLabel:i="Very likely",commentLabel:r="What is the main reason for your score? (optional)",sendLabel:s="Send",notNowLabel:l="Not now",thanks:n="Thanks for your feedback."})=>{const{client:a}=(0,_InsightsFeedbackProvider.useInsightsFeedback)(),o=(0,_mobileUi.useTheme)(),c=(0,_react.useMemo)(()=>buildStyles(o),[o]),[u,_]=(0,_react.useState)(null),[m,d]=(0,_react.useState)(!1),[x,b]=(0,_react.useState)(null),[j,h]=(0,_react.useState)(""),[p,y]=(0,_react.useState)(!1),[g,v]=(0,_react.useState)(null),[R,f]=(0,_react.useState)(null),S=(0,_react.useRef)(!1),w=(0,_react.useRef)(!1);(0,_react.useEffect)(()=>(S.current=!0,()=>{S.current=!1}),[]),(0,_react.useEffect)(()=>{w.current||(w.current=!0,a.getNps().then(e=>{S.current&&_(e)},()=>{S.current&&d(!0)}))},[a]);const N=u?.due&&null!==u.trigger?u.trigger:null,submit=async e=>{if(null===N||p)return;const t=j.trim(),i=null===e?{trigger:N,score:null}:{trigger:N,score:e,...t?{comment:t}:{}};y(!0),v(null);try{await a.answerNps(i),S.current&&f(null===e?"dismissed":"thanked")}catch(e){S.current&&v((0,_client.toInsightsApiError)(e).message)}finally{S.current&&y(!1)}};return"thanked"===R?(0,_jsxRuntime.jsx)(_reactNative.View,{style:c.root,children:(0,_jsxRuntime.jsx)(_mobileUi.Text,{variant:"body",color:"secondary",accessibilityLiveRegion:"polite",children:n})}):null===N||m||"dismissed"===R?null:(0,_jsxRuntime.jsxs)(_reactNative.View,{style:c.root,children:[(0,_jsxRuntime.jsx)(_mobileUi.Text,{variant:"title",children:e}),(0,_jsxRuntime.jsx)(_reactNative.View,{style:c.scores,children:SCORES.map(e=>(0,_jsxRuntime.jsx)(_mobileUi.Chip,{label:String(e),accessibilityLabel:`Score ${e}`,selected:x===e,onPress:()=>b(e),disabled:p},e))}),(0,_jsxRuntime.jsxs)(_reactNative.View,{style:c.anchors,children:[(0,_jsxRuntime.jsx)(_mobileUi.Text,{variant:"caption",color:"tertiary",children:t}),(0,_jsxRuntime.jsx)(_mobileUi.Text,{variant:"caption",color:"tertiary",children:i})]}),null!==x&&(0,_jsxRuntime.jsx)(_mobileUi.Input,{label:r,accessibilityLabel:r,multiline:!0,value:j,onChangeText:h,disabled:p}),(0,_jsxRuntime.jsxs)(_reactNative.View,{style:c.actions,children:[(0,_jsxRuntime.jsx)(_mobileUi.Button,{label:s,onPress:()=>{submit(x)},disabled:null===x||p,loading:p}),(0,_jsxRuntime.jsx)(_mobileUi.Button,{label:l,variant:"ghost",tone:"neutral",onPress:()=>{submit(null)},disabled:p})]}),null!==g&&(0,_jsxRuntime.jsx)(_mobileUi.Text,{variant:"bodySmall",color:"danger",accessibilityLiveRegion:"polite",children:g})]})};exports.NpsPrompt=NpsPrompt;const buildStyles=e=>_reactNative.StyleSheet.create({root:{gap:e.space.md},scores:{flexDirection:"row",flexWrap:"wrap",gap:e.space.sm},anchors:{flexDirection:"row",justifyContent:"space-between"},actions:{flexDirection:"row",justifyContent:"flex-end",gap:e.space.sm}});
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.toInsightsApiError=exports.createInsightsFeedbackClient=exports.InsightsFeedbackApiError=void 0;var _mobileCore=require("@webority/mobile-core");const DEFAULT_PREFIX="/api/insights/feedback",OFFLINE_MESSAGE="You are offline. Check your connection and try again.",GENERIC_MESSAGE="Something went wrong. Try again in a moment.",SURFACE={headers:{"X-Webority-Surface":"Mobile"}};class InsightsFeedbackApiError extends Error{constructor(e,r,s){super(r),this.name="InsightsFeedbackApiError",this.code=e,this.status=s}}exports.InsightsFeedbackApiError=InsightsFeedbackApiError;const toInsightsApiError=e=>{if(e instanceof InsightsFeedbackApiError)return e;const r=e??{};if("RequestOfflineError"===r.name)return new InsightsFeedbackApiError("offline",OFFLINE_MESSAGE,null);const s="number"==typeof r.response?.status?r.response.status:null,t=r.response?.data;if(t&&"string"==typeof t.message&&t.message.trim()){const e="string"==typeof t.code?t.code:"unknown";return new InsightsFeedbackApiError(e,t.message,s)}return r.response?new InsightsFeedbackApiError("unknown",GENERIC_MESSAGE,s):new InsightsFeedbackApiError("network",OFFLINE_MESSAGE,null)};exports.toInsightsApiError=toInsightsApiError;const createInsightsFeedbackClient=(e=_mobileCore.userClient,r={})=>{const s=(r.prefix??DEFAULT_PREFIX).replace(/\/+$/,""),call=async e=>{try{return(await e()).data}catch(e){throw toInsightsApiError(e)}};return{getNps:()=>call(()=>e.get(`${s}/nps`)),answerNps:async r=>{await call(()=>e.post(`${s}/nps`,r,SURFACE))},leaveComment:async r=>{await call(()=>e.post(`${s}/comments`,r,SURFACE))}}};exports.createInsightsFeedbackClient=createInsightsFeedbackClient;
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),Object.defineProperty(exports,"FeedbackButton",{enumerable:!0,get:function(){return _FeedbackButton.FeedbackButton}}),Object.defineProperty(exports,"InsightsFeedbackApiError",{enumerable:!0,get:function(){return _client.InsightsFeedbackApiError}}),Object.defineProperty(exports,"InsightsFeedbackProvider",{enumerable:!0,get:function(){return _InsightsFeedbackProvider.InsightsFeedbackProvider}}),Object.defineProperty(exports,"NpsPrompt",{enumerable:!0,get:function(){return _NpsPrompt.NpsPrompt}}),Object.defineProperty(exports,"createInsightsFeedbackClient",{enumerable:!0,get:function(){return _client.createInsightsFeedbackClient}}),Object.defineProperty(exports,"useInsightsFeedback",{enumerable:!0,get:function(){return _InsightsFeedbackProvider.useInsightsFeedback}});var _client=require("./client.js"),_FeedbackButton=require("./FeedbackButton.js"),_InsightsFeedbackProvider=require("./InsightsFeedbackProvider.js"),_NpsPrompt=require("./NpsPrompt.js");
@@ -0,0 +1 @@
1
+ {"type":"commonjs"}
@@ -0,0 +1 @@
1
+ "use strict";
@@ -0,0 +1 @@
1
+ "use strict";import{Button,Input,KeyboardAwareScrollView,Modal,Text,useTheme}from"@webority/mobile-ui";import{useMemo,useState}from"react";import{StyleSheet,View}from"react-native";import{toInsightsApiError}from"./client.js";import{useInsightsFeedback}from"./InsightsFeedbackProvider.js";import{jsx as _jsx,jsxs as _jsxs,Fragment as _Fragment}from"react/jsx-runtime";const MAX_LENGTH=1e3;export const FeedbackButton=({label:e="Feedback",title:t="Send us feedback",placeholder:s="Tell us what works and what does not",sendLabel:a="Send",cancelLabel:n="Cancel",thanks:l="Thanks, we read every message.",pagePath:i})=>{const{client:o}=useInsightsFeedback(),r=useTheme(),c=useMemo(()=>buildStyles(r),[r]),[d,m]=useState(!1),[u,h]=useState(""),[b,g]=useState(!1),[x,p]=useState(!1),[y,j]=useState(null),S=u.trim(),_=S.length-1e3,v=S.length>0&&_<=0;return _jsxs(_Fragment,{children:[_jsx(Button,{label:e,onPress:()=>{p(!1),j(null),m(!0)}}),_jsx(Modal,{visible:d,onClose:()=>m(!1),accessibilityLabel:t,children:_jsxs(KeyboardAwareScrollView,{contentContainerStyle:c.content,safeAreaEdges:[],children:[_jsx(Text,{variant:"title",children:t}),x?_jsx(Text,{variant:"body",color:"secondary",accessibilityLiveRegion:"polite",children:l}):_jsxs(_Fragment,{children:[_jsx(Input,{accessibilityLabel:s,placeholder:s,multiline:!0,value:u,onChangeText:h,disabled:b,helperText:`${S.length} / 1000`,error:_>0?`Up to 1000 characters; remove ${_} to send.`:void 0}),null!==y&&_jsx(Text,{variant:"bodySmall",color:"danger",accessibilityLiveRegion:"polite",children:y}),_jsxs(View,{style:c.actions,children:[_jsx(Button,{label:n,variant:"ghost",onPress:()=>m(!1),disabled:b}),_jsx(Button,{label:a,onPress:()=>{(async()=>{g(!0),j(null);try{await o.leaveComment({comment:S,pagePath:i}),h(""),p(!0)}catch(e){j(toInsightsApiError(e).message)}finally{g(!1)}})()},disabled:!v||b,loading:b})]})]})]})})]})};const buildStyles=e=>StyleSheet.create({content:{padding:e.space.md,gap:e.space.md},actions:{flexDirection:"row",justifyContent:"flex-end",gap:e.space.sm}});
@@ -0,0 +1 @@
1
+ "use strict";import{createContext,useContext,useMemo}from"react";import{jsx as _jsx}from"react/jsx-runtime";const InsightsFeedbackContext=createContext(null);export const InsightsFeedbackProvider=({client:e,children:t})=>{const s=useMemo(()=>({client:e}),[e]);return _jsx(InsightsFeedbackContext.Provider,{value:s,children:t})};export const useInsightsFeedback=()=>{const e=useContext(InsightsFeedbackContext);if(!e)throw new Error("InsightsFeedbackProvider is missing.");return e};
@@ -0,0 +1 @@
1
+ "use strict";import{Button,Chip,Input,Text,useTheme}from"@webority/mobile-ui";import{useEffect,useMemo,useRef,useState}from"react";import{StyleSheet,View}from"react-native";import{toInsightsApiError}from"./client.js";import{useInsightsFeedback}from"./InsightsFeedbackProvider.js";import{jsx as _jsx,jsxs as _jsxs}from"react/jsx-runtime";const SCORES=[0,1,2,3,4,5,6,7,8,9,10];export const NpsPrompt=({question:e="How likely are you to recommend us to a colleague?",lowLabel:t="Not likely",highLabel:s="Very likely",commentLabel:r="What is the main reason for your score? (optional)",sendLabel:l="Send",notNowLabel:n="Not now",thanks:i="Thanks for your feedback."})=>{const{client:o}=useInsightsFeedback(),a=useTheme(),c=useMemo(()=>buildStyles(a),[a]),[u,d]=useState(null),[m,h]=useState(!1),[p,x]=useState(null),[y,b]=useState(""),[f,g]=useState(!1),[j,S]=useState(null),[w,_]=useState(null),k=useRef(!1),v=useRef(!1);useEffect(()=>(k.current=!0,()=>{k.current=!1}),[]),useEffect(()=>{v.current||(v.current=!0,o.getNps().then(e=>{k.current&&d(e)},()=>{k.current&&h(!0)}))},[o]);const T=u?.due&&null!==u.trigger?u.trigger:null,submit=async e=>{if(null===T||f)return;const t=y.trim(),s=null===e?{trigger:T,score:null}:{trigger:T,score:e,...t?{comment:t}:{}};g(!0),S(null);try{await o.answerNps(s),k.current&&_(null===e?"dismissed":"thanked")}catch(e){k.current&&S(toInsightsApiError(e).message)}finally{k.current&&g(!1)}};return"thanked"===w?_jsx(View,{style:c.root,children:_jsx(Text,{variant:"body",color:"secondary",accessibilityLiveRegion:"polite",children:i})}):null===T||m||"dismissed"===w?null:_jsxs(View,{style:c.root,children:[_jsx(Text,{variant:"title",children:e}),_jsx(View,{style:c.scores,children:SCORES.map(e=>_jsx(Chip,{label:String(e),accessibilityLabel:`Score ${e}`,selected:p===e,onPress:()=>x(e),disabled:f},e))}),_jsxs(View,{style:c.anchors,children:[_jsx(Text,{variant:"caption",color:"tertiary",children:t}),_jsx(Text,{variant:"caption",color:"tertiary",children:s})]}),null!==p&&_jsx(Input,{label:r,accessibilityLabel:r,multiline:!0,value:y,onChangeText:b,disabled:f}),_jsxs(View,{style:c.actions,children:[_jsx(Button,{label:l,onPress:()=>{submit(p)},disabled:null===p||f,loading:f}),_jsx(Button,{label:n,variant:"ghost",tone:"neutral",onPress:()=>{submit(null)},disabled:f})]}),null!==j&&_jsx(Text,{variant:"bodySmall",color:"danger",accessibilityLiveRegion:"polite",children:j})]})};const buildStyles=e=>StyleSheet.create({root:{gap:e.space.md},scores:{flexDirection:"row",flexWrap:"wrap",gap:e.space.sm},anchors:{flexDirection:"row",justifyContent:"space-between"},actions:{flexDirection:"row",justifyContent:"flex-end",gap:e.space.sm}});
@@ -0,0 +1 @@
1
+ "use strict";import{userClient}from"@webority/mobile-core";const DEFAULT_PREFIX="/api/insights/feedback",OFFLINE_MESSAGE="You are offline. Check your connection and try again.",GENERIC_MESSAGE="Something went wrong. Try again in a moment.",SURFACE={headers:{"X-Webority-Surface":"Mobile"}};export class InsightsFeedbackApiError extends Error{constructor(e,r,t){super(r),this.name="InsightsFeedbackApiError",this.code=e,this.status=t}}export const toInsightsApiError=e=>{if(e instanceof InsightsFeedbackApiError)return e;const r=e??{};if("RequestOfflineError"===r.name)return new InsightsFeedbackApiError("offline",OFFLINE_MESSAGE,null);const t="number"==typeof r.response?.status?r.response.status:null,s=r.response?.data;if(s&&"string"==typeof s.message&&s.message.trim()){const e="string"==typeof s.code?s.code:"unknown";return new InsightsFeedbackApiError(e,s.message,t)}return r.response?new InsightsFeedbackApiError("unknown",GENERIC_MESSAGE,t):new InsightsFeedbackApiError("network",OFFLINE_MESSAGE,null)};export const createInsightsFeedbackClient=(e=userClient,r={})=>{const t=(r.prefix??DEFAULT_PREFIX).replace(/\/+$/,""),call=async e=>{try{return(await e()).data}catch(e){throw toInsightsApiError(e)}};return{getNps:()=>call(()=>e.get(`${t}/nps`)),answerNps:async r=>{await call(()=>e.post(`${t}/nps`,r,SURFACE))},leaveComment:async r=>{await call(()=>e.post(`${t}/comments`,r,SURFACE))}}};
@@ -0,0 +1 @@
1
+ "use strict";export{createInsightsFeedbackClient,InsightsFeedbackApiError}from"./client.js";export{FeedbackButton}from"./FeedbackButton.js";export{InsightsFeedbackProvider,useInsightsFeedback}from"./InsightsFeedbackProvider.js";export{NpsPrompt}from"./NpsPrompt.js";
@@ -0,0 +1 @@
1
+ {"type":"module"}
@@ -0,0 +1 @@
1
+ "use strict";
@@ -0,0 +1,12 @@
1
+ import type React from "react";
2
+ export interface FeedbackButtonProps {
3
+ label?: string;
4
+ title?: string;
5
+ placeholder?: string;
6
+ sendLabel?: string;
7
+ cancelLabel?: string;
8
+ thanks?: string;
9
+ pagePath?: string;
10
+ }
11
+ export declare const FeedbackButton: React.FC<FeedbackButtonProps>;
12
+ //# sourceMappingURL=FeedbackButton.d.ts.map
@@ -0,0 +1,13 @@
1
+ import type React from "react";
2
+ import type { InsightsFeedbackClient } from "./types";
3
+ interface InsightsFeedbackContextValue {
4
+ client: InsightsFeedbackClient;
5
+ }
6
+ export interface InsightsFeedbackProviderProps {
7
+ client: InsightsFeedbackClient;
8
+ children: React.ReactNode;
9
+ }
10
+ export declare const InsightsFeedbackProvider: React.FC<InsightsFeedbackProviderProps>;
11
+ export declare const useInsightsFeedback: () => InsightsFeedbackContextValue;
12
+ export {};
13
+ //# sourceMappingURL=InsightsFeedbackProvider.d.ts.map
@@ -0,0 +1,12 @@
1
+ import type React from "react";
2
+ export interface NpsPromptProps {
3
+ question?: string;
4
+ lowLabel?: string;
5
+ highLabel?: string;
6
+ commentLabel?: string;
7
+ sendLabel?: string;
8
+ notNowLabel?: string;
9
+ thanks?: string;
10
+ }
11
+ export declare const NpsPrompt: React.FC<NpsPromptProps>;
12
+ //# sourceMappingURL=NpsPrompt.d.ts.map
@@ -0,0 +1,16 @@
1
+ import type { InsightsFeedbackClient, InsightsHttpClient } from "./types";
2
+ /**
3
+ * A failed feedback call. `message` is written for the person and is shown as is;
4
+ * `code` is the backend's stable code, for branching only.
5
+ */
6
+ export declare class InsightsFeedbackApiError extends Error {
7
+ readonly code: string;
8
+ readonly status: number | null;
9
+ constructor(code: string, message: string, status: number | null);
10
+ }
11
+ /** Turns whatever the HTTP client threw into an InsightsFeedbackApiError. */
12
+ export declare const toInsightsApiError: (error: unknown) => InsightsFeedbackApiError;
13
+ export declare const createInsightsFeedbackClient: (http?: InsightsHttpClient, options?: {
14
+ prefix?: string;
15
+ }) => InsightsFeedbackClient;
16
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1,9 @@
1
+ export { createInsightsFeedbackClient, InsightsFeedbackApiError } from "./client";
2
+ export type { FeedbackButtonProps } from "./FeedbackButton";
3
+ export { FeedbackButton } from "./FeedbackButton";
4
+ export type { InsightsFeedbackProviderProps } from "./InsightsFeedbackProvider";
5
+ export { InsightsFeedbackProvider, useInsightsFeedback } from "./InsightsFeedbackProvider";
6
+ export type { NpsPromptProps } from "./NpsPrompt";
7
+ export { NpsPrompt } from "./NpsPrompt";
8
+ export type { InsightsFeedbackClient, InsightsFeedbackComment, InsightsHttpClient, InsightsHttpRequestConfig, InsightsNpsAnswer, InsightsNpsPrompt, } from "./types";
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"type":"commonjs"}
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Wire shapes of the Webority Insights feedback HTTP contract, served under the prefix
3
+ * the product maps (`/api/insights/feedback` by default). Field names are the camelCase
4
+ * of the backend's records, so these mirror the server exactly.
5
+ */
6
+ /** `GET {prefix}/nps`: whether the question is due now, and the trigger it answers. */
7
+ export interface InsightsNpsPrompt {
8
+ due: boolean;
9
+ trigger: string | null;
10
+ }
11
+ /**
12
+ * `POST {prefix}/nps`. A null score means "not now", which is a permanent dismissal:
13
+ * the question does not come back.
14
+ */
15
+ export interface InsightsNpsAnswer {
16
+ trigger: string;
17
+ score: number | null;
18
+ comment?: string | null;
19
+ }
20
+ /** `POST {prefix}/comments`: free feedback, with the page it was left on when known. */
21
+ export interface InsightsFeedbackComment {
22
+ comment: string;
23
+ pagePath?: string | null;
24
+ }
25
+ export interface InsightsHttpRequestConfig {
26
+ headers?: Record<string, string>;
27
+ timeout?: number;
28
+ }
29
+ /**
30
+ * The slice of an HTTP client the feedback client needs. The app's signed-in axios
31
+ * instance (mobile-core's `userClient`) satisfies it as is. Declared structurally so no
32
+ * axios type leaks into this package's declarations.
33
+ */
34
+ export interface InsightsHttpClient {
35
+ get<T = unknown>(url: string, config?: InsightsHttpRequestConfig): Promise<{
36
+ data: T;
37
+ }>;
38
+ post<T = unknown>(url: string, body?: unknown, config?: InsightsHttpRequestConfig): Promise<{
39
+ data: T;
40
+ }>;
41
+ }
42
+ export interface InsightsFeedbackClient {
43
+ /** The question's state for the signed-in person. */
44
+ getNps(): Promise<InsightsNpsPrompt>;
45
+ /** An answer, or a "not now" when the score is null. Resolves once the server has it. */
46
+ answerNps(body: InsightsNpsAnswer): Promise<void>;
47
+ leaveComment(body: InsightsFeedbackComment): Promise<void>;
48
+ }
49
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,12 @@
1
+ import type React from "react";
2
+ export interface FeedbackButtonProps {
3
+ label?: string;
4
+ title?: string;
5
+ placeholder?: string;
6
+ sendLabel?: string;
7
+ cancelLabel?: string;
8
+ thanks?: string;
9
+ pagePath?: string;
10
+ }
11
+ export declare const FeedbackButton: React.FC<FeedbackButtonProps>;
12
+ //# sourceMappingURL=FeedbackButton.d.ts.map
@@ -0,0 +1,13 @@
1
+ import type React from "react";
2
+ import type { InsightsFeedbackClient } from "./types.js";
3
+ interface InsightsFeedbackContextValue {
4
+ client: InsightsFeedbackClient;
5
+ }
6
+ export interface InsightsFeedbackProviderProps {
7
+ client: InsightsFeedbackClient;
8
+ children: React.ReactNode;
9
+ }
10
+ export declare const InsightsFeedbackProvider: React.FC<InsightsFeedbackProviderProps>;
11
+ export declare const useInsightsFeedback: () => InsightsFeedbackContextValue;
12
+ export {};
13
+ //# sourceMappingURL=InsightsFeedbackProvider.d.ts.map
@@ -0,0 +1,12 @@
1
+ import type React from "react";
2
+ export interface NpsPromptProps {
3
+ question?: string;
4
+ lowLabel?: string;
5
+ highLabel?: string;
6
+ commentLabel?: string;
7
+ sendLabel?: string;
8
+ notNowLabel?: string;
9
+ thanks?: string;
10
+ }
11
+ export declare const NpsPrompt: React.FC<NpsPromptProps>;
12
+ //# sourceMappingURL=NpsPrompt.d.ts.map
@@ -0,0 +1,16 @@
1
+ import type { InsightsFeedbackClient, InsightsHttpClient } from "./types.js";
2
+ /**
3
+ * A failed feedback call. `message` is written for the person and is shown as is;
4
+ * `code` is the backend's stable code, for branching only.
5
+ */
6
+ export declare class InsightsFeedbackApiError extends Error {
7
+ readonly code: string;
8
+ readonly status: number | null;
9
+ constructor(code: string, message: string, status: number | null);
10
+ }
11
+ /** Turns whatever the HTTP client threw into an InsightsFeedbackApiError. */
12
+ export declare const toInsightsApiError: (error: unknown) => InsightsFeedbackApiError;
13
+ export declare const createInsightsFeedbackClient: (http?: InsightsHttpClient, options?: {
14
+ prefix?: string;
15
+ }) => InsightsFeedbackClient;
16
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1,9 @@
1
+ export { createInsightsFeedbackClient, InsightsFeedbackApiError } from "./client.js";
2
+ export type { FeedbackButtonProps } from "./FeedbackButton.js";
3
+ export { FeedbackButton } from "./FeedbackButton.js";
4
+ export type { InsightsFeedbackProviderProps } from "./InsightsFeedbackProvider.js";
5
+ export { InsightsFeedbackProvider, useInsightsFeedback } from "./InsightsFeedbackProvider.js";
6
+ export type { NpsPromptProps } from "./NpsPrompt.js";
7
+ export { NpsPrompt } from "./NpsPrompt.js";
8
+ export type { InsightsFeedbackClient, InsightsFeedbackComment, InsightsHttpClient, InsightsHttpRequestConfig, InsightsNpsAnswer, InsightsNpsPrompt, } from "./types.js";
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"type":"module"}
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Wire shapes of the Webority Insights feedback HTTP contract, served under the prefix
3
+ * the product maps (`/api/insights/feedback` by default). Field names are the camelCase
4
+ * of the backend's records, so these mirror the server exactly.
5
+ */
6
+ /** `GET {prefix}/nps`: whether the question is due now, and the trigger it answers. */
7
+ export interface InsightsNpsPrompt {
8
+ due: boolean;
9
+ trigger: string | null;
10
+ }
11
+ /**
12
+ * `POST {prefix}/nps`. A null score means "not now", which is a permanent dismissal:
13
+ * the question does not come back.
14
+ */
15
+ export interface InsightsNpsAnswer {
16
+ trigger: string;
17
+ score: number | null;
18
+ comment?: string | null;
19
+ }
20
+ /** `POST {prefix}/comments`: free feedback, with the page it was left on when known. */
21
+ export interface InsightsFeedbackComment {
22
+ comment: string;
23
+ pagePath?: string | null;
24
+ }
25
+ export interface InsightsHttpRequestConfig {
26
+ headers?: Record<string, string>;
27
+ timeout?: number;
28
+ }
29
+ /**
30
+ * The slice of an HTTP client the feedback client needs. The app's signed-in axios
31
+ * instance (mobile-core's `userClient`) satisfies it as is. Declared structurally so no
32
+ * axios type leaks into this package's declarations.
33
+ */
34
+ export interface InsightsHttpClient {
35
+ get<T = unknown>(url: string, config?: InsightsHttpRequestConfig): Promise<{
36
+ data: T;
37
+ }>;
38
+ post<T = unknown>(url: string, body?: unknown, config?: InsightsHttpRequestConfig): Promise<{
39
+ data: T;
40
+ }>;
41
+ }
42
+ export interface InsightsFeedbackClient {
43
+ /** The question's state for the signed-in person. */
44
+ getNps(): Promise<InsightsNpsPrompt>;
45
+ /** An answer, or a "not now" when the score is null. Resolves once the server has it. */
46
+ answerNps(body: InsightsNpsAnswer): Promise<void>;
47
+ leaveComment(body: InsightsFeedbackComment): Promise<void>;
48
+ }
49
+ //# sourceMappingURL=types.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,105 @@
1
1
  {
2
2
  "name": "@webority/mobile-insights",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.129.0",
4
+ "description": "Product feedback for Webority React Native apps: the NPS question and a feedback button that post to the product's Webority.Insights feedback API.",
5
+ "keywords": [
6
+ "react-native",
7
+ "feedback",
8
+ "nps",
9
+ "webority"
10
+ ],
11
+ "homepage": "https://www.webority.com",
12
+ "license": "SEE LICENSE IN LICENSE.txt",
13
+ "author": {
14
+ "name": "Webority Technologies",
15
+ "email": "contact@webority.com",
16
+ "url": "https://www.webority.com"
17
+ },
18
+ "exports": {
19
+ ".": {
20
+ "import": {
21
+ "types": "./lib/typescript/module/index.d.ts",
22
+ "default": "./lib/module/index.js"
23
+ },
24
+ "require": {
25
+ "types": "./lib/typescript/commonjs/index.d.ts",
26
+ "default": "./lib/commonjs/index.js"
27
+ }
28
+ },
29
+ "./package.json": "./package.json"
30
+ },
31
+ "main": "./lib/commonjs/index.js",
32
+ "module": "./lib/module/index.js",
33
+ "react-native": "./lib/module/index.js",
34
+ "types": "./lib/typescript/commonjs/index.d.ts",
35
+ "files": [
36
+ "lib",
37
+ "!**/__tests__",
38
+ "!**/*.test.*",
39
+ "!**/*.spec.*",
40
+ "!**/*.map"
41
+ ],
42
+ "peerDependencies": {
43
+ "@webority/mobile-core": "0.129.0",
44
+ "@webority/mobile-ui": "0.129.0",
45
+ "expo": ">=57.0.0",
46
+ "react": "^19.1.0",
47
+ "react-native": ">=0.81.0",
48
+ "react-native-gesture-handler": "^2.21.0 || ^3.0.0"
49
+ },
50
+ "publishConfig": {
51
+ "access": "public",
52
+ "registry": "https://registry.npmjs.org/"
53
+ },
54
+ "react-native-builder-bob": {
55
+ "source": "src",
56
+ "output": "lib",
57
+ "targets": [
58
+ [
59
+ "commonjs",
60
+ {
61
+ "esm": true,
62
+ "sourceMaps": false
63
+ }
64
+ ],
65
+ [
66
+ "module",
67
+ {
68
+ "esm": true,
69
+ "sourceMaps": false
70
+ }
71
+ ],
72
+ [
73
+ "typescript",
74
+ {
75
+ "project": "tsconfig.build.json"
76
+ }
77
+ ]
78
+ ]
79
+ },
80
+ "devDependencies": {
81
+ "@react-native/metro-config": "0.86.3",
82
+ "@testing-library/react-native": "12.9.0",
83
+ "@types/react": "19.2.18",
84
+ "@webority/mobile-core": "0.129.0",
85
+ "@webority/mobile-dev": "0.0.0",
86
+ "@webority/mobile-ui": "0.129.0",
87
+ "expo": "57.0.31",
88
+ "jest": "29.7.0",
89
+ "react": "19.2.8",
90
+ "react-native": "0.86.3",
91
+ "react-native-builder-bob": "0.43.0",
92
+ "react-native-gesture-handler": "3.2.1",
93
+ "react-test-renderer": "19.2.8",
94
+ "rimraf": "6.1.3",
95
+ "typescript": "5.9.3"
96
+ },
97
+ "scripts": {
98
+ "build": "bob build && node ../mobile-dev/scripts/minify-lib.mjs",
99
+ "clean": "rimraf lib",
100
+ "lint": "biome lint ./src",
101
+ "lint:fix": "biome lint --write ./src",
102
+ "test": "jest",
103
+ "typecheck": "tsc --noEmit"
104
+ }
6
105
  }