@solucx/react-native-solucx-widget 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.intern.md +476 -0
- package/README.md +260 -0
- package/package.json +13 -0
- package/src/SoluCXWidget.tsx +117 -0
- package/src/__tests__/urlUtils.test.ts +56 -0
- package/src/__tests__/useWidgetState.test.ts +190 -0
- package/src/components/CloseButton.tsx +37 -0
- package/src/components/InlineWidget.tsx +25 -0
- package/src/components/ModalWidget.tsx +35 -0
- package/src/components/OverlayWidget.tsx +70 -0
- package/src/constants/webViewConstants.ts +14 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useWidgetState.ts +72 -0
- package/src/index.ts +8 -0
- package/src/interfaces/WidgetData.ts +20 -0
- package/src/interfaces/WidgetOptions.ts +9 -0
- package/src/interfaces/WidgetResponse.ts +15 -0
- package/src/interfaces/WidgetSamplerLog.ts +6 -0
- package/src/interfaces/index.ts +24 -0
- package/src/services/storage.ts +21 -0
- package/src/services/widgetEventService.ts +111 -0
- package/src/services/widgetValidationService.ts +86 -0
- package/src/styles/widgetStyles.ts +59 -0
- package/src/utils/urlUtils.ts +13 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Modal, SafeAreaView, View } from 'react-native';
|
|
3
|
+
import { styles, getWidgetVisibility } from '../styles/widgetStyles';
|
|
4
|
+
import { CloseButton } from './CloseButton';
|
|
5
|
+
|
|
6
|
+
interface ModalWidgetProps {
|
|
7
|
+
visible: boolean;
|
|
8
|
+
children?: React.ReactNode;
|
|
9
|
+
onClose?: () => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const ModalWidget: React.FC<ModalWidgetProps> = ({ visible, children, onClose }) => {
|
|
13
|
+
const [isWidgetVisible, setIsWidgetVisible] = useState<boolean>(true);
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<SafeAreaView>
|
|
17
|
+
<Modal transparent visible={isWidgetVisible} animationType="fade" hardwareAccelerated>
|
|
18
|
+
<View style={[styles.modalOverlay, getWidgetVisibility(visible)]}>
|
|
19
|
+
<View style={[styles.modalContent, getWidgetVisibility(visible)]}>
|
|
20
|
+
{children}
|
|
21
|
+
<CloseButton
|
|
22
|
+
visible={visible}
|
|
23
|
+
onPress={() => {
|
|
24
|
+
setIsWidgetVisible(false);
|
|
25
|
+
if (onClose) {
|
|
26
|
+
onClose();
|
|
27
|
+
}
|
|
28
|
+
}}
|
|
29
|
+
/>
|
|
30
|
+
</View>
|
|
31
|
+
</View>
|
|
32
|
+
</Modal>
|
|
33
|
+
</SafeAreaView>
|
|
34
|
+
);
|
|
35
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { View, ViewStyle, Modal } from 'react-native';
|
|
3
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
4
|
+
import { getWidgetStyles, getWidgetVisibility } from '../styles/widgetStyles';
|
|
5
|
+
import { FIXED_Z_INDEX } from '../constants/webViewConstants';
|
|
6
|
+
import { CloseButton } from './CloseButton';
|
|
7
|
+
|
|
8
|
+
interface OverlayWidgetProps {
|
|
9
|
+
visible: boolean;
|
|
10
|
+
width: number;
|
|
11
|
+
height: number;
|
|
12
|
+
position: 'top' | 'bottom';
|
|
13
|
+
children?: React.ReactNode;
|
|
14
|
+
onClose?: () => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const OverlayWidget: React.FC<OverlayWidgetProps> = ({ visible, width, height, position, children, onClose }) => {
|
|
18
|
+
const insets = useSafeAreaInsets();
|
|
19
|
+
const [isWidgetVisible, setIsWidgetVisible] = useState<boolean>(true);
|
|
20
|
+
|
|
21
|
+
const containerStyle: ViewStyle = {
|
|
22
|
+
position: 'absolute',
|
|
23
|
+
top: 0,
|
|
24
|
+
left: 0,
|
|
25
|
+
right: 0,
|
|
26
|
+
bottom: 0,
|
|
27
|
+
width: '100%',
|
|
28
|
+
height: '100%',
|
|
29
|
+
zIndex: FIXED_Z_INDEX,
|
|
30
|
+
backgroundColor: 'transparent',
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const contentStyle = [
|
|
34
|
+
getWidgetStyles(position).content,
|
|
35
|
+
{
|
|
36
|
+
width,
|
|
37
|
+
height,
|
|
38
|
+
...(position === 'top' && {
|
|
39
|
+
top: insets.top,
|
|
40
|
+
}),
|
|
41
|
+
...(position === 'bottom' && {
|
|
42
|
+
bottom: insets.bottom,
|
|
43
|
+
}),
|
|
44
|
+
}
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<Modal
|
|
49
|
+
transparent
|
|
50
|
+
visible={isWidgetVisible}
|
|
51
|
+
animationType="fade"
|
|
52
|
+
hardwareAccelerated
|
|
53
|
+
>
|
|
54
|
+
<View style={[containerStyle, getWidgetVisibility(visible)]}>
|
|
55
|
+
<View style={[contentStyle, getWidgetVisibility(visible)]}>
|
|
56
|
+
{children}
|
|
57
|
+
<CloseButton
|
|
58
|
+
visible={visible}
|
|
59
|
+
onPress={() => {
|
|
60
|
+
setIsWidgetVisible(false);
|
|
61
|
+
if (onClose) {
|
|
62
|
+
onClose();
|
|
63
|
+
}
|
|
64
|
+
}}
|
|
65
|
+
/>
|
|
66
|
+
</View>
|
|
67
|
+
</View>
|
|
68
|
+
</Modal>
|
|
69
|
+
);
|
|
70
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const BASE_URL = 'https://survey-link.solucx.com.br/link';
|
|
2
|
+
export const STORAGE_KEY = '@solucxWidgetLog';
|
|
3
|
+
export const DEFAULT_CHANNEL_NUMBER = 1;
|
|
4
|
+
export const DEFAULT_CHANNEL = 'widget';
|
|
5
|
+
export const DEFAULT_WIDTH = 380;
|
|
6
|
+
export const MIN_HEIGHT = 200;
|
|
7
|
+
export const FIXED_Z_INDEX = 9999;
|
|
8
|
+
export const MODAL_Z_INDEX = 10000;
|
|
9
|
+
export const DESIGN_HEIGHT = 700;
|
|
10
|
+
export const WEB_VIEW_MESSAGE_LISTENER = `
|
|
11
|
+
window.addEventListener('message', function(event) {
|
|
12
|
+
window.ReactNativeWebView.postMessage(event.data);
|
|
13
|
+
});
|
|
14
|
+
`;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useWidgetState } from './useWidgetState';
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { useState, useCallback } from 'react';
|
|
2
|
+
import { Dimensions } from 'react-native';
|
|
3
|
+
import { WidgetSamplerLog, WidgetData, WidgetType } from '../interfaces';
|
|
4
|
+
import { StorageService } from '../services/storage';
|
|
5
|
+
|
|
6
|
+
function getUserId(widgetData: WidgetData): string {
|
|
7
|
+
return widgetData.customer_id ?? widgetData.document ?? widgetData.email ?? "";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const useWidgetState = (data: WidgetData, type?: WidgetType) => {
|
|
11
|
+
const [savedData, setSavedData] = useState<WidgetSamplerLog | null>(null);
|
|
12
|
+
const [widgetHeight, setWidgetHeight] = useState<number>(0);
|
|
13
|
+
const [isWidgetVisible, setIsWidgetVisible] = useState<boolean>(false);
|
|
14
|
+
|
|
15
|
+
const userId = getUserId(data);
|
|
16
|
+
const storageService = new StorageService(userId);
|
|
17
|
+
const screenHeight = Dimensions.get('screen').height;
|
|
18
|
+
|
|
19
|
+
const loadSavedData = useCallback(async () => {
|
|
20
|
+
try {
|
|
21
|
+
const jsonValue = await storageService.read();
|
|
22
|
+
setSavedData(jsonValue);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
console.error('Error loading storage data:', error);
|
|
25
|
+
}
|
|
26
|
+
}, [storageService]);
|
|
27
|
+
|
|
28
|
+
const saveData = useCallback(async (data: WidgetSamplerLog) => {
|
|
29
|
+
try {
|
|
30
|
+
await storageService.write(data);
|
|
31
|
+
setSavedData(data);
|
|
32
|
+
} catch (error) {
|
|
33
|
+
console.error('Error saving storage data:', error);
|
|
34
|
+
}
|
|
35
|
+
}, [storageService]);
|
|
36
|
+
|
|
37
|
+
const open = useCallback(async () => {
|
|
38
|
+
const userLogs = await storageService.read();
|
|
39
|
+
userLogs.attempts++;
|
|
40
|
+
userLogs.lastAttempt = Date.now();
|
|
41
|
+
try {
|
|
42
|
+
await storageService.write(userLogs);
|
|
43
|
+
setSavedData(userLogs);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.error('Error saving storage data:', error);
|
|
46
|
+
}
|
|
47
|
+
setIsWidgetVisible(true);
|
|
48
|
+
}, []);
|
|
49
|
+
|
|
50
|
+
const close = useCallback(() => {
|
|
51
|
+
setIsWidgetVisible(false);
|
|
52
|
+
}, []);
|
|
53
|
+
|
|
54
|
+
const resize = useCallback((value: string) => {
|
|
55
|
+
const receivedHeight = Number(value);
|
|
56
|
+
setWidgetHeight(receivedHeight);
|
|
57
|
+
}, [screenHeight]);
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
savedData,
|
|
61
|
+
widgetHeight,
|
|
62
|
+
isWidgetVisible,
|
|
63
|
+
setIsWidgetVisible,
|
|
64
|
+
loadSavedData,
|
|
65
|
+
saveData,
|
|
66
|
+
open,
|
|
67
|
+
close,
|
|
68
|
+
resize,
|
|
69
|
+
userId,
|
|
70
|
+
screenHeight
|
|
71
|
+
};
|
|
72
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { SoluCXWidget } from './SoluCXWidget';
|
|
2
|
+
export { useWidgetState } from './hooks/useWidgetState';
|
|
3
|
+
export { WidgetEventService } from './services/widgetEventService';
|
|
4
|
+
export { StorageService } from './services/storage';
|
|
5
|
+
export { buildWidgetURL } from './utils/urlUtils';
|
|
6
|
+
export { ModalWidget } from './components/ModalWidget';
|
|
7
|
+
export { getWidgetStyles, styles } from './styles/widgetStyles';
|
|
8
|
+
export * from './interfaces';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface WidgetData {
|
|
2
|
+
transaction_id?: string;
|
|
3
|
+
attempt_id?: string;
|
|
4
|
+
form_id?: string;
|
|
5
|
+
customer_id?: string;
|
|
6
|
+
name?: string;
|
|
7
|
+
email?: string;
|
|
8
|
+
phone?: string;
|
|
9
|
+
phone2?: string;
|
|
10
|
+
document?: string;
|
|
11
|
+
birth_date?: string;
|
|
12
|
+
store_id?: string;
|
|
13
|
+
store_name?: string;
|
|
14
|
+
employee_id?: string;
|
|
15
|
+
employee_name?: string;
|
|
16
|
+
amount?: number;
|
|
17
|
+
score?: number;
|
|
18
|
+
journey?: string;
|
|
19
|
+
[key: string]: string | number | undefined;
|
|
20
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface WidgetResponse {
|
|
2
|
+
status?: "success" | "error";
|
|
3
|
+
message?: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export class WidgetError extends Error implements WidgetResponse {
|
|
7
|
+
status: "error";
|
|
8
|
+
message: string;
|
|
9
|
+
|
|
10
|
+
constructor(message: string) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.status = "error";
|
|
13
|
+
this.message = message;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export type SoluCXKey = string;
|
|
2
|
+
export type WidgetType = "bottom" | "top" | "inline" | "modal";
|
|
3
|
+
export type EventKey =
|
|
4
|
+
| "FORM_OPENED"
|
|
5
|
+
| "FORM_CLOSE"
|
|
6
|
+
| "FORM_ERROR"
|
|
7
|
+
| "FORM_PAGECHANGED"
|
|
8
|
+
| "QUESTION_ANSWERED"
|
|
9
|
+
| "FORM_COMPLETED"
|
|
10
|
+
| "FORM_PARTIALCOMPLETED"
|
|
11
|
+
| "FORM_RESIZE";
|
|
12
|
+
export type SurveyEventKey =
|
|
13
|
+
| "closeSoluCXWidget"
|
|
14
|
+
| "dismissSoluCXWidget"
|
|
15
|
+
| "completeSoluCXWidget"
|
|
16
|
+
| "partialSoluCXWidget"
|
|
17
|
+
| "resizeSoluCXWidget"
|
|
18
|
+
| "openSoluCXWidget"
|
|
19
|
+
| `errorSoluCXWidget`;
|
|
20
|
+
export { WidgetResponse } from './WidgetResponse';
|
|
21
|
+
export { WidgetData } from './WidgetData';
|
|
22
|
+
export { WidgetOptions } from './WidgetOptions';
|
|
23
|
+
export { WidgetSamplerLog } from './WidgetSamplerLog';
|
|
24
|
+
export { WidgetError } from './WidgetResponse';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
2
|
+
import { WidgetSamplerLog } from '../interfaces';
|
|
3
|
+
import { STORAGE_KEY } from '../constants/webViewConstants';
|
|
4
|
+
|
|
5
|
+
export class StorageService {
|
|
6
|
+
private key: string;
|
|
7
|
+
|
|
8
|
+
constructor(key: string) {
|
|
9
|
+
this.key = `${STORAGE_KEY}_${key}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async write(data: WidgetSamplerLog): Promise<void> {
|
|
13
|
+
const json = JSON.stringify(data);
|
|
14
|
+
await AsyncStorage.setItem(this.key, json);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async read(): Promise<WidgetSamplerLog> {
|
|
18
|
+
const json = await AsyncStorage.getItem(this.key);
|
|
19
|
+
return json ? JSON.parse(json) as WidgetSamplerLog : {} as WidgetSamplerLog;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { EventKey, SurveyEventKey, WidgetResponse, WidgetOptions } from '../interfaces';
|
|
2
|
+
import { WidgetValidationService } from './widgetValidationService';
|
|
3
|
+
|
|
4
|
+
export class WidgetEventService {
|
|
5
|
+
private setIsWidgetVisible: (visible: boolean) => void;
|
|
6
|
+
private resize: (value: string) => void;
|
|
7
|
+
private open: () => void;
|
|
8
|
+
private validationService: WidgetValidationService;
|
|
9
|
+
private widgetOptions: WidgetOptions;
|
|
10
|
+
|
|
11
|
+
constructor(
|
|
12
|
+
setIsWidgetVisible: (visible: boolean) => void,
|
|
13
|
+
resize: (value: string) => void,
|
|
14
|
+
open: () => void,
|
|
15
|
+
userId: string,
|
|
16
|
+
widgetOptions: WidgetOptions
|
|
17
|
+
) {
|
|
18
|
+
this.setIsWidgetVisible = setIsWidgetVisible;
|
|
19
|
+
this.resize = resize;
|
|
20
|
+
this.open = open;
|
|
21
|
+
this.validationService = new WidgetValidationService(userId);
|
|
22
|
+
this.widgetOptions = widgetOptions;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async handleMessage(message: string, isForm: boolean): Promise<WidgetResponse> {
|
|
26
|
+
const [eventKey, value = ""] = message.split("-");
|
|
27
|
+
const processedKey = isForm
|
|
28
|
+
? eventKey as EventKey
|
|
29
|
+
: this.adaptSurveyKeyToWidgetKey(eventKey as SurveyEventKey);
|
|
30
|
+
|
|
31
|
+
return await this.executeEvent(processedKey, value);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private async executeEvent(eventKey: EventKey, value: string): Promise<WidgetResponse> {
|
|
35
|
+
const eventHandlers = {
|
|
36
|
+
FORM_OPENED: () => this.handleFormOpened(),
|
|
37
|
+
FORM_CLOSE: () => this.handleFormClose(),
|
|
38
|
+
FORM_ERROR: (value: string) => this.handleFormError(value),
|
|
39
|
+
FORM_PAGECHANGED: (value: string) => this.handlePageChanged(value),
|
|
40
|
+
QUESTION_ANSWERED: () => this.handleQuestionAnswered(),
|
|
41
|
+
FORM_COMPLETED: () => this.handleFormCompleted(),
|
|
42
|
+
FORM_PARTIALCOMPLETED: () => this.handlePartialCompleted(),
|
|
43
|
+
FORM_RESIZE: (value: string) => this.handleResize(value),
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const handler = eventHandlers[eventKey];
|
|
47
|
+
return await (handler?.(value) || { status: "error", message: "Unknown event" });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private async handleFormOpened(): Promise<WidgetResponse> {
|
|
51
|
+
const canDisplay = await this.validationService.shouldDisplayWidget(this.widgetOptions);
|
|
52
|
+
|
|
53
|
+
if (!canDisplay) {
|
|
54
|
+
return { status: "error", message: "Widget not allowed" };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
this.open();
|
|
58
|
+
this.setIsWidgetVisible(true);
|
|
59
|
+
return { status: "success" };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private handleFormClose(): WidgetResponse {
|
|
63
|
+
this.setIsWidgetVisible(false);
|
|
64
|
+
return { status: "success" };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private handleFormError(value: string): WidgetResponse {
|
|
68
|
+
this.setIsWidgetVisible(false);
|
|
69
|
+
return { status: "error", message: value };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private handlePageChanged(value: string): WidgetResponse {
|
|
73
|
+
console.log("Page changed:", value);
|
|
74
|
+
return { status: "success" };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private handleQuestionAnswered(): WidgetResponse {
|
|
78
|
+
console.log("Question answered");
|
|
79
|
+
return { status: "success" };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private handleFormCompleted(): WidgetResponse {
|
|
83
|
+
// TODO: Implement completion logic
|
|
84
|
+
return { status: "success" };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private handlePartialCompleted(): WidgetResponse {
|
|
88
|
+
// TODO: Implement partial completion logic
|
|
89
|
+
return { status: "success" };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private handleResize(value: string): WidgetResponse {
|
|
93
|
+
this.setIsWidgetVisible(true);
|
|
94
|
+
this.resize(value);
|
|
95
|
+
return { status: "success" };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private adaptSurveyKeyToWidgetKey(key: SurveyEventKey): EventKey {
|
|
99
|
+
const keyMapping = {
|
|
100
|
+
closeSoluCXWidget: "FORM_CLOSE",
|
|
101
|
+
dismissSoluCXWidget: "FORM_CLOSE",
|
|
102
|
+
completeSoluCXWidget: "FORM_COMPLETED",
|
|
103
|
+
partialSoluCXWidget: "FORM_PARTIALCOMPLETED",
|
|
104
|
+
resizeSoluCXWidget: "FORM_RESIZE",
|
|
105
|
+
openSoluCXWidget: "FORM_OPENED",
|
|
106
|
+
errorSoluCXWidget: "FORM_ERROR",
|
|
107
|
+
} as const;
|
|
108
|
+
|
|
109
|
+
return keyMapping[key] as EventKey;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { WidgetOptions, WidgetSamplerLog } from '../interfaces';
|
|
2
|
+
import { StorageService } from './storage';
|
|
3
|
+
|
|
4
|
+
export class WidgetValidationService {
|
|
5
|
+
private storageService: StorageService;
|
|
6
|
+
|
|
7
|
+
constructor(userId: string) {
|
|
8
|
+
this.storageService = new StorageService(userId);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async shouldDisplayWidget(widgetOptions: WidgetOptions): Promise<boolean> {
|
|
12
|
+
const { retry, waitDelayAfterRating = 60 } = widgetOptions;
|
|
13
|
+
const { attempts = 5, interval = 1 } = retry || {};
|
|
14
|
+
const userLog = await this.getLog();
|
|
15
|
+
const now = Date.now();
|
|
16
|
+
const dayInMilliseconds = 86400000;
|
|
17
|
+
|
|
18
|
+
if (this.isWithinCollectInterval(userLog, waitDelayAfterRating, now, dayInMilliseconds)) return false;
|
|
19
|
+
if (this.isWithinCollectPartialInterval(userLog, waitDelayAfterRating, now, dayInMilliseconds)) return false;
|
|
20
|
+
if (this.isWithinRetryInterval(userLog, interval, attempts, now, dayInMilliseconds)) return false;
|
|
21
|
+
|
|
22
|
+
await this.resetAttemptsIfNeeded(userLog, attempts);
|
|
23
|
+
await this.setLog(userLog);
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
private async getLog(): Promise<WidgetSamplerLog> {
|
|
28
|
+
try {
|
|
29
|
+
return await this.storageService.read();
|
|
30
|
+
} catch (error) {
|
|
31
|
+
console.error('Error reading widget log:', error);
|
|
32
|
+
return {
|
|
33
|
+
attempts: 0,
|
|
34
|
+
lastAttempt: 0,
|
|
35
|
+
lastRating: 0,
|
|
36
|
+
lastParcial: 0
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private async setLog(userLog: WidgetSamplerLog): Promise<void> {
|
|
42
|
+
try {
|
|
43
|
+
await this.storageService.write(userLog);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.error('Error writing widget log:', error);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private isWithinCollectInterval(
|
|
50
|
+
userLog: WidgetSamplerLog,
|
|
51
|
+
waitDelayAfterRating: number,
|
|
52
|
+
now: number,
|
|
53
|
+
dayInMilliseconds: number
|
|
54
|
+
): boolean {
|
|
55
|
+
const timeSinceLastRating = now - userLog.lastRating;
|
|
56
|
+
return userLog.lastRating > 0 && timeSinceLastRating < waitDelayAfterRating * dayInMilliseconds;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private isWithinCollectPartialInterval(
|
|
60
|
+
userLog: WidgetSamplerLog,
|
|
61
|
+
waitDelayAfterRating: number,
|
|
62
|
+
now: number,
|
|
63
|
+
dayInMilliseconds: number
|
|
64
|
+
): boolean {
|
|
65
|
+
const timeSinceLastPartial = now - userLog.lastParcial;
|
|
66
|
+
return userLog.lastParcial > 0 && timeSinceLastPartial < waitDelayAfterRating * dayInMilliseconds;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private isWithinRetryInterval(
|
|
70
|
+
userLog: WidgetSamplerLog,
|
|
71
|
+
interval: number,
|
|
72
|
+
attempts: number,
|
|
73
|
+
now: number,
|
|
74
|
+
dayInMilliseconds: number
|
|
75
|
+
): boolean {
|
|
76
|
+
if (userLog.attempts < attempts) return false;
|
|
77
|
+
const timeSinceLastAttempt = now - userLog.lastAttempt;
|
|
78
|
+
return timeSinceLastAttempt < interval * dayInMilliseconds;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private async resetAttemptsIfNeeded(userLog: WidgetSamplerLog, maxAttempts: number): Promise<void> {
|
|
82
|
+
if (userLog.attempts >= maxAttempts) {
|
|
83
|
+
userLog.attempts = 0;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { StyleSheet } from 'react-native';
|
|
2
|
+
import { WidgetType } from '../interfaces';
|
|
3
|
+
|
|
4
|
+
export const styles = StyleSheet.create({
|
|
5
|
+
wrapper: {
|
|
6
|
+
flex: 1,
|
|
7
|
+
justifyContent: 'center',
|
|
8
|
+
alignItems: 'center',
|
|
9
|
+
},
|
|
10
|
+
inlineWrapper: {
|
|
11
|
+
justifyContent: 'center',
|
|
12
|
+
alignItems: 'center',
|
|
13
|
+
},
|
|
14
|
+
bottom: {
|
|
15
|
+
position: 'absolute',
|
|
16
|
+
bottom: 0,
|
|
17
|
+
justifyContent: 'center',
|
|
18
|
+
alignItems: 'center',
|
|
19
|
+
},
|
|
20
|
+
top: {
|
|
21
|
+
position: 'absolute',
|
|
22
|
+
top: 0,
|
|
23
|
+
justifyContent: 'center',
|
|
24
|
+
alignItems: 'center',
|
|
25
|
+
},
|
|
26
|
+
inline: {
|
|
27
|
+
justifyContent: 'center',
|
|
28
|
+
alignItems: 'center',
|
|
29
|
+
},
|
|
30
|
+
modalOverlay: {
|
|
31
|
+
flex: 1,
|
|
32
|
+
justifyContent: 'center',
|
|
33
|
+
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
|
34
|
+
alignItems: 'center',
|
|
35
|
+
},
|
|
36
|
+
modalContent: {
|
|
37
|
+
backgroundColor: 'white',
|
|
38
|
+
borderRadius: 10,
|
|
39
|
+
maxHeight: '60%',
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
export const getWidgetVisibility = (visibility: boolean) => {
|
|
44
|
+
return {
|
|
45
|
+
opacity: visibility ? 1 : 0,
|
|
46
|
+
pointerEvents: visibility ? 'auto' as const : 'none' as const,
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const getWidgetStyles = (type: WidgetType) => {
|
|
51
|
+
const styleMap = {
|
|
52
|
+
'bottom': { container: styles.wrapper, content: styles.bottom },
|
|
53
|
+
'top': { container: styles.wrapper, content: styles.top },
|
|
54
|
+
'inline': { container: styles.inlineWrapper, content: styles.inline },
|
|
55
|
+
'modal': { container: styles.wrapper, content: styles.inline }
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
return styleMap[type] || styleMap.bottom;
|
|
59
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { BASE_URL } from '../constants/webViewConstants';
|
|
2
|
+
import { WidgetData, SoluCXKey } from '../interfaces';
|
|
3
|
+
|
|
4
|
+
export function buildWidgetURL(key: SoluCXKey, data: WidgetData): string {
|
|
5
|
+
const params = new URLSearchParams(data as Record<string, string>);
|
|
6
|
+
const baseURL = `${BASE_URL}/${key}/?mode=widget`;
|
|
7
|
+
|
|
8
|
+
if (data.transaction_id) {
|
|
9
|
+
return `${baseURL}&${params.toString()}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return `${baseURL}&transaction_id=&${params.toString()}`;
|
|
13
|
+
}
|