@webority/mobile-insights 0.0.0-stage → 0.130.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 +9 -0
- package/README.md +69 -2
- package/lib/commonjs/FeedbackButton.js +1 -0
- package/lib/commonjs/InsightsFeedbackProvider.js +1 -0
- package/lib/commonjs/NpsPrompt.js +1 -0
- package/lib/commonjs/client.js +1 -0
- package/lib/commonjs/index.js +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/types.js +1 -0
- package/lib/module/FeedbackButton.js +1 -0
- package/lib/module/InsightsFeedbackProvider.js +1 -0
- package/lib/module/NpsPrompt.js +1 -0
- package/lib/module/client.js +1 -0
- package/lib/module/index.js +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/types.js +1 -0
- package/lib/typescript/commonjs/FeedbackButton.d.ts +12 -0
- package/lib/typescript/commonjs/InsightsFeedbackProvider.d.ts +13 -0
- package/lib/typescript/commonjs/NpsPrompt.d.ts +12 -0
- package/lib/typescript/commonjs/client.d.ts +16 -0
- package/lib/typescript/commonjs/index.d.ts +9 -0
- package/lib/typescript/commonjs/package.json +1 -0
- package/lib/typescript/commonjs/types.d.ts +49 -0
- package/lib/typescript/module/FeedbackButton.d.ts +12 -0
- package/lib/typescript/module/InsightsFeedbackProvider.d.ts +13 -0
- package/lib/typescript/module/NpsPrompt.d.ts +12 -0
- package/lib/typescript/module/client.d.ts +16 -0
- package/lib/typescript/module/index.d.ts +9 -0
- package/lib/typescript/module/package.json +1 -0
- package/lib/typescript/module/types.d.ts +49 -0
- package/package.json +102 -3
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
|
-
#
|
|
1
|
+
# @webority/mobile-insights
|
|
2
2
|
|
|
3
|
-
|
|
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),[m,b]=(0,_react.useState)(""),[_,x]=(0,_react.useState)(!1),[j,h]=(0,_react.useState)(!1),[g,p]=(0,_react.useState)(null),v=m.trim(),y=v.length-1e3,R=v.length>0&&y<=0;return(0,_jsxRuntime.jsxs)(_jsxRuntime.Fragment,{children:[(0,_jsxRuntime.jsx)(_mobileUi.Button,{label:e,onPress:()=>{h(!1),p(null),u(!0)}}),(0,_jsxRuntime.jsx)(_mobileUi.Modal,{open: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:m,onChangeText:b,disabled:_,helperText:`${v.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),p(null);try{await r.leaveComment({comment:v,pagePath:l}),b(""),h(!0)}catch(e){p((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:o})=>{const{client:i}=useInsightsFeedback(),r=useTheme(),c=useMemo(()=>buildStyles(r),[r]),[d,m]=useState(!1),[u,h]=useState(""),[b,g]=useState(!1),[p,x]=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:()=>{x(!1),j(null),m(!0)}}),_jsx(Modal,{open:d,onClose:()=>m(!1),accessibilityLabel:t,children:_jsxs(KeyboardAwareScrollView,{contentContainerStyle:c.content,safeAreaEdges:[],children:[_jsx(Text,{variant:"title",children:t}),p?_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 i.leaveComment({comment:S,pagePath:o}),h(""),x(!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.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.130.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.130.0",
|
|
44
|
+
"@webority/mobile-ui": "0.130.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.130.0",
|
|
85
|
+
"@webority/mobile-dev": "0.0.0",
|
|
86
|
+
"@webority/mobile-ui": "0.130.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
|
}
|