@teamvortexsoftware/vortex-react-native 0.0.2 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -0
- package/eslint.config.mjs +4 -0
- package/package.json +27 -12
- package/src/components/ShareButtons.tsx +172 -0
- package/src/hooks/useThemeStyles.ts +42 -0
- package/src/hooks/useVortexInvite.ts +289 -0
- package/src/index.tsx +2 -0
- package/src/shared/api.ts +71 -0
- package/src/utils/themeUtils.ts +85 -0
- package/src/vortexInvite.tsx +193 -0
- package/tsconfig.json +9 -0
package/README.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# @teamvortexsoftware/vortex-react-native
|
|
2
|
+
|
|
3
|
+
React Native components for Vortex applications.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @teamvortexsoftware/vortex-react-native
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Development
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# Install dependencies
|
|
15
|
+
pnpm install
|
|
16
|
+
|
|
17
|
+
# Build the package
|
|
18
|
+
pnpm run build
|
|
19
|
+
|
|
20
|
+
# Run type checking
|
|
21
|
+
pnpm run type-check
|
|
22
|
+
|
|
23
|
+
# Run linting
|
|
24
|
+
pnpm run lint
|
|
25
|
+
|
|
26
|
+
# Watch mode for development
|
|
27
|
+
pnpm run dev
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
import { VortexInvite } from '@teamvortexsoftware/vortex-react-native';
|
|
34
|
+
|
|
35
|
+
// Your component code here
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Requirements
|
|
39
|
+
|
|
40
|
+
- React 18.3.1 or higher
|
|
41
|
+
- React Native 0.74.5 or higher
|
|
42
|
+
- pnpm 10.8.1 or higher
|
package/package.json
CHANGED
|
@@ -2,28 +2,43 @@
|
|
|
2
2
|
"name": "@teamvortexsoftware/vortex-react-native",
|
|
3
3
|
"description": "",
|
|
4
4
|
"author": "@teamvortexsoftware",
|
|
5
|
-
"version": "0.0.
|
|
5
|
+
"version": "0.0.4",
|
|
6
6
|
"publishConfig": {
|
|
7
7
|
"access": "restricted"
|
|
8
8
|
},
|
|
9
|
-
"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
"main": "./dist/index.js",
|
|
13
|
-
"types": "./dist/index.d.ts",
|
|
9
|
+
"main": "src/index.tsx",
|
|
10
|
+
"types": "src/index.tsx",
|
|
11
|
+
"react-native": "src/index.tsx",
|
|
14
12
|
"devDependencies": {
|
|
13
|
+
"@eslint/js": "^9.24.0",
|
|
15
14
|
"@types/react": "18.2.14",
|
|
15
|
+
"@types/react-native-vector-icons": "^6.4.18",
|
|
16
|
+
"@typescript-eslint/eslint-plugin": "^7.3.1",
|
|
17
|
+
"@typescript-eslint/parser": "^7.3.1",
|
|
18
|
+
"eslint": "^9.24.0",
|
|
19
|
+
"eslint-plugin-react-native": "^4.1.0",
|
|
16
20
|
"tsup": "8.0.1",
|
|
17
|
-
"typescript": "5.
|
|
21
|
+
"typescript": "5.8.3",
|
|
22
|
+
"typescript-eslint": "^8.30.1",
|
|
23
|
+
"@teamvortexsoftware/eslint-config": "0.0.0",
|
|
18
24
|
"@teamvortexsoftware/typescript-config": "0.0.0"
|
|
19
25
|
},
|
|
20
26
|
"dependencies": {
|
|
21
|
-
"react": "18.
|
|
22
|
-
"react-native": "0.
|
|
27
|
+
"react": "18.3.1",
|
|
28
|
+
"react-native": "0.76.9",
|
|
29
|
+
"react-native-vector-icons": "^10.2.0",
|
|
30
|
+
"@teamvortexsoftware/vortex-core": "0.0.1"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"react": "*",
|
|
34
|
+
"react-native": "*",
|
|
35
|
+
"react-native-qrcode-svg": "*",
|
|
36
|
+
"react-native-svg": "*"
|
|
23
37
|
},
|
|
24
38
|
"scripts": {
|
|
25
|
-
"build": "
|
|
26
|
-
"
|
|
27
|
-
"
|
|
39
|
+
"build": "echo \"no build\"",
|
|
40
|
+
"prepublish": "pnpm run build",
|
|
41
|
+
"lint": "eslint src/",
|
|
42
|
+
"type-check": "tsc --noEmit"
|
|
28
43
|
}
|
|
29
44
|
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { Pressable, StyleSheet, Text, View } from "react-native";
|
|
3
|
+
import Icon from "react-native-vector-icons/FontAwesome6";
|
|
4
|
+
import { ThemeColors } from "../utils/themeUtils";
|
|
5
|
+
import QRCode from "react-native-qrcode-svg";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* ShareButton component for rendering a single share option button
|
|
9
|
+
*/
|
|
10
|
+
interface ShareButtonProps {
|
|
11
|
+
iconName: string;
|
|
12
|
+
label: string;
|
|
13
|
+
onPress: () => void;
|
|
14
|
+
themeColors: ThemeColors;
|
|
15
|
+
themeStyles: any;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function ShareButton({
|
|
19
|
+
iconName,
|
|
20
|
+
label,
|
|
21
|
+
onPress,
|
|
22
|
+
themeColors,
|
|
23
|
+
themeStyles,
|
|
24
|
+
}: ShareButtonProps) {
|
|
25
|
+
return (
|
|
26
|
+
<View style={styles.shareButtonWrapper}>
|
|
27
|
+
<Pressable
|
|
28
|
+
style={[styles.shareButton, themeStyles.secondaryButton]}
|
|
29
|
+
onPress={onPress}
|
|
30
|
+
>
|
|
31
|
+
<View style={styles.buttonContentContainer}>
|
|
32
|
+
<Icon
|
|
33
|
+
name={iconName}
|
|
34
|
+
size={24}
|
|
35
|
+
color={themeColors.secondaryButtonForeground}
|
|
36
|
+
/>
|
|
37
|
+
<Text
|
|
38
|
+
style={[styles.shareButtonText, themeStyles.secondaryButtonText]}
|
|
39
|
+
>
|
|
40
|
+
{label}
|
|
41
|
+
</Text>
|
|
42
|
+
</View>
|
|
43
|
+
</Pressable>
|
|
44
|
+
</View>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* ShareButtons component for rendering all available share options
|
|
50
|
+
*/
|
|
51
|
+
export interface ShareButtonsProps {
|
|
52
|
+
has: any;
|
|
53
|
+
themeColors: ThemeColors;
|
|
54
|
+
themeStyles: any;
|
|
55
|
+
handleShareLink: () => void;
|
|
56
|
+
handleCopyLink: () => void;
|
|
57
|
+
shareableLink?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function ShareButtons({
|
|
61
|
+
has,
|
|
62
|
+
themeColors,
|
|
63
|
+
themeStyles,
|
|
64
|
+
handleShareLink,
|
|
65
|
+
handleCopyLink,
|
|
66
|
+
shareableLink,
|
|
67
|
+
}: ShareButtonsProps) {
|
|
68
|
+
const [showQRCode, setShowQRCode] = useState(false);
|
|
69
|
+
// Define button configurations
|
|
70
|
+
const buttonConfigs = [
|
|
71
|
+
{
|
|
72
|
+
key: "shareOptionsNativeShareSheet",
|
|
73
|
+
iconName: "share",
|
|
74
|
+
label: "Share",
|
|
75
|
+
onPress: handleShareLink,
|
|
76
|
+
isAvailable: has.shareOptionsNativeShareSheet,
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
key: "shareOptionsCopyLink",
|
|
80
|
+
iconName: "copy",
|
|
81
|
+
label: "Copy Link",
|
|
82
|
+
onPress: handleCopyLink,
|
|
83
|
+
isAvailable: has.shareOptionsCopyLink,
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
key: "shareOptionsQRCode",
|
|
87
|
+
iconName: "qrcode",
|
|
88
|
+
label: "QR Code",
|
|
89
|
+
onPress: () => setShowQRCode(!showQRCode),
|
|
90
|
+
isAvailable: true,
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
key: "shareOptionsWhatsApp",
|
|
94
|
+
iconName: "whatsapp",
|
|
95
|
+
label: "WhatsApp",
|
|
96
|
+
onPress: () => {},
|
|
97
|
+
isAvailable: has.shareOptionsWhatsApp,
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
key: "shareOptionsSms",
|
|
101
|
+
iconName: "message",
|
|
102
|
+
label: "SMS",
|
|
103
|
+
onPress: () => {},
|
|
104
|
+
isAvailable: has.shareOptionsSms,
|
|
105
|
+
},
|
|
106
|
+
// Add more share options as needed
|
|
107
|
+
// Example:
|
|
108
|
+
// {
|
|
109
|
+
// key: 'shareOptionsFacebookMessenger',
|
|
110
|
+
// iconName: 'facebook-messenger',
|
|
111
|
+
// label: 'Messenger',
|
|
112
|
+
// onPress: () => {},
|
|
113
|
+
// isAvailable: has.shareOptionsFacebookMessenger,
|
|
114
|
+
// },
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<>
|
|
119
|
+
{showQRCode && shareableLink && (
|
|
120
|
+
<View style={styles.qrCodeContainer}>
|
|
121
|
+
<QRCode
|
|
122
|
+
value={shareableLink}
|
|
123
|
+
size={200}
|
|
124
|
+
color={themeColors.containerForeground}
|
|
125
|
+
backgroundColor={themeColors.containerBackground}
|
|
126
|
+
/>
|
|
127
|
+
</View>
|
|
128
|
+
)}
|
|
129
|
+
{buttonConfigs
|
|
130
|
+
.filter((config) => config.isAvailable)
|
|
131
|
+
.map((config) => (
|
|
132
|
+
<ShareButton
|
|
133
|
+
key={config.key}
|
|
134
|
+
iconName={config.iconName}
|
|
135
|
+
label={config.label}
|
|
136
|
+
onPress={config.onPress}
|
|
137
|
+
themeColors={themeColors}
|
|
138
|
+
themeStyles={themeStyles}
|
|
139
|
+
/>
|
|
140
|
+
))}
|
|
141
|
+
</>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const styles = StyleSheet.create({
|
|
146
|
+
shareButtonWrapper: {
|
|
147
|
+
width: "48%",
|
|
148
|
+
margin: 5,
|
|
149
|
+
},
|
|
150
|
+
shareButton: {
|
|
151
|
+
flexDirection: "row",
|
|
152
|
+
alignItems: "center",
|
|
153
|
+
padding: 10,
|
|
154
|
+
borderWidth: 1,
|
|
155
|
+
borderRadius: 5,
|
|
156
|
+
justifyContent: "center",
|
|
157
|
+
},
|
|
158
|
+
buttonContentContainer: {
|
|
159
|
+
flexDirection: "row",
|
|
160
|
+
alignItems: "center",
|
|
161
|
+
},
|
|
162
|
+
shareButtonText: {
|
|
163
|
+
marginLeft: 10,
|
|
164
|
+
},
|
|
165
|
+
qrCodeContainer: {
|
|
166
|
+
width: "100%",
|
|
167
|
+
alignItems: "center",
|
|
168
|
+
justifyContent: "center",
|
|
169
|
+
marginBottom: 20,
|
|
170
|
+
padding: 10,
|
|
171
|
+
},
|
|
172
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { ThemeColors } from "../utils/themeUtils";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Hook to generate dynamic styles based on theme colors
|
|
6
|
+
* @param themeColors The theme colors object
|
|
7
|
+
* @returns Object containing styled components based on the theme
|
|
8
|
+
*/
|
|
9
|
+
export function useThemeStyles(themeColors: ThemeColors) {
|
|
10
|
+
const themeStyles = useMemo(
|
|
11
|
+
() => ({
|
|
12
|
+
primaryButton: {
|
|
13
|
+
backgroundColor: themeColors.primaryButtonBackground,
|
|
14
|
+
borderColor: themeColors.primaryButtonBorder,
|
|
15
|
+
},
|
|
16
|
+
primaryButtonText: {
|
|
17
|
+
color: themeColors.primaryButtonForeground,
|
|
18
|
+
},
|
|
19
|
+
secondaryButton: {
|
|
20
|
+
backgroundColor: themeColors.secondaryButtonBackground,
|
|
21
|
+
borderColor: themeColors.secondaryButtonBorder,
|
|
22
|
+
},
|
|
23
|
+
secondaryButtonText: {
|
|
24
|
+
color: themeColors.secondaryButtonForeground,
|
|
25
|
+
},
|
|
26
|
+
containerStyles: {
|
|
27
|
+
backgroundColor: themeColors.containerBackground,
|
|
28
|
+
},
|
|
29
|
+
textStyles: {
|
|
30
|
+
color: themeColors.containerForeground,
|
|
31
|
+
},
|
|
32
|
+
inputStyles: {
|
|
33
|
+
borderColor: themeColors.containerBorder,
|
|
34
|
+
color: themeColors.containerForeground,
|
|
35
|
+
backgroundColor: themeColors.containerBackground,
|
|
36
|
+
},
|
|
37
|
+
}),
|
|
38
|
+
[themeColors],
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
return themeStyles;
|
|
42
|
+
}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { WidgetConfiguration } from "@teamvortexsoftware/vortex-core";
|
|
2
|
+
import { useEffect, useMemo, useState } from "react";
|
|
3
|
+
import { Animated, Share, Clipboard } from "react-native";
|
|
4
|
+
import VortexClient from "../shared/api";
|
|
5
|
+
import { extractFeatureFlags, extractThemeColors } from "../utils/themeUtils";
|
|
6
|
+
import { useThemeStyles } from "./useThemeStyles";
|
|
7
|
+
|
|
8
|
+
export type VortexActionType = "invite" | "share" | string;
|
|
9
|
+
|
|
10
|
+
export interface VortexActionResult {
|
|
11
|
+
type: VortexActionType;
|
|
12
|
+
data: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface UseVortexInviteOptions {
|
|
16
|
+
widgetId: string;
|
|
17
|
+
environmentId: string;
|
|
18
|
+
vortexApiHost: string;
|
|
19
|
+
isLoading?: boolean;
|
|
20
|
+
jwt?: string;
|
|
21
|
+
onSuccess?: (result: VortexActionResult) => void;
|
|
22
|
+
onError?: (error: Error, type: VortexActionType) => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function useVortexInvite({
|
|
26
|
+
widgetId,
|
|
27
|
+
environmentId,
|
|
28
|
+
vortexApiHost,
|
|
29
|
+
isLoading = false,
|
|
30
|
+
jwt,
|
|
31
|
+
onSuccess,
|
|
32
|
+
onError,
|
|
33
|
+
}: UseVortexInviteOptions) {
|
|
34
|
+
const vortexClient = useMemo(
|
|
35
|
+
() => new VortexClient(vortexApiHost),
|
|
36
|
+
[vortexApiHost],
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const [widgetConfiguration, setWidgetConfiguration] = useState<
|
|
40
|
+
WidgetConfiguration | undefined
|
|
41
|
+
>();
|
|
42
|
+
const [error, setError] = useState<{ message: string } | null>(null);
|
|
43
|
+
const [fetching, setFetching] = useState(isLoading);
|
|
44
|
+
const [loading, setLoading] = useState(true);
|
|
45
|
+
const [email, setEmail] = useState("");
|
|
46
|
+
const opacity = new Animated.Value(0.3);
|
|
47
|
+
|
|
48
|
+
// Extract theme colors and feature flags using utility functions
|
|
49
|
+
const themeColors = useMemo(
|
|
50
|
+
() => extractThemeColors(widgetConfiguration),
|
|
51
|
+
[widgetConfiguration],
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
// Get theme styles based on theme colors
|
|
55
|
+
const themeStyles = useThemeStyles(themeColors);
|
|
56
|
+
|
|
57
|
+
const options = useMemo(() => {
|
|
58
|
+
return widgetConfiguration?.configuration?.props || {};
|
|
59
|
+
}, [widgetConfiguration]);
|
|
60
|
+
|
|
61
|
+
const has = useMemo(() => {
|
|
62
|
+
const flags = extractFeatureFlags(widgetConfiguration);
|
|
63
|
+
console.debug("[Vortex] Invite has", flags);
|
|
64
|
+
return flags;
|
|
65
|
+
}, [widgetConfiguration]);
|
|
66
|
+
|
|
67
|
+
// Log the host only once when the hook mounts
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
if (vortexApiHost.indexOf("localhost") > -1) {
|
|
70
|
+
console.warn("[Vortex] Invite Using localhost as host");
|
|
71
|
+
}
|
|
72
|
+
}, [vortexApiHost]);
|
|
73
|
+
|
|
74
|
+
// Loading animation effect
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
const animation = Animated.loop(
|
|
77
|
+
Animated.sequence([
|
|
78
|
+
Animated.timing(opacity, {
|
|
79
|
+
toValue: 1,
|
|
80
|
+
duration: 1000,
|
|
81
|
+
useNativeDriver: false,
|
|
82
|
+
}),
|
|
83
|
+
Animated.timing(opacity, {
|
|
84
|
+
toValue: 0.3,
|
|
85
|
+
duration: 1000,
|
|
86
|
+
useNativeDriver: false,
|
|
87
|
+
}),
|
|
88
|
+
]),
|
|
89
|
+
);
|
|
90
|
+
animation.start();
|
|
91
|
+
|
|
92
|
+
setTimeout(() => {
|
|
93
|
+
setLoading(false);
|
|
94
|
+
animation.stop();
|
|
95
|
+
}, 2000);
|
|
96
|
+
}, []);
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
// Fetch widget configuration if needed
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
// Case: If `widgetConfiguration` is already set, do nothing
|
|
102
|
+
if (widgetConfiguration) {
|
|
103
|
+
console.debug(
|
|
104
|
+
"[Vortex] Invite Already has widgetConfiguration, skipping fetch",
|
|
105
|
+
);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (!jwt) {
|
|
109
|
+
console.debug("[Vortex] Invite JWT is required");
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const fetchData = async () => {
|
|
114
|
+
console.debug("[Vortex] Invite Fetching Data...");
|
|
115
|
+
setFetching(true);
|
|
116
|
+
setLoading(true);
|
|
117
|
+
setError(null);
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const result = await vortexClient.getWidgetConfiguration(
|
|
121
|
+
widgetId,
|
|
122
|
+
jwt,
|
|
123
|
+
environmentId,
|
|
124
|
+
);
|
|
125
|
+
if (result?.data?.widgetConfiguration) {
|
|
126
|
+
setWidgetConfiguration(result.data.widgetConfiguration);
|
|
127
|
+
console.debug(
|
|
128
|
+
"[Vortex] Invite Successfully fetched widgetConfiguration",
|
|
129
|
+
JSON.stringify(result.data.widgetConfiguration),
|
|
130
|
+
);
|
|
131
|
+
} else {
|
|
132
|
+
const error = result?.error || "No configuration data found.";
|
|
133
|
+
console.error(`[Vortex] Invite ${error}`);
|
|
134
|
+
throw new Error(error);
|
|
135
|
+
}
|
|
136
|
+
} catch (err) {
|
|
137
|
+
console.error("[Vortex] Invite Error", err);
|
|
138
|
+
setError({
|
|
139
|
+
message: (err as Error).message || "Something went wrong!",
|
|
140
|
+
});
|
|
141
|
+
} finally {
|
|
142
|
+
setFetching(false);
|
|
143
|
+
setLoading(false);
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
fetchData();
|
|
148
|
+
}, [widgetId, jwt, environmentId]);
|
|
149
|
+
|
|
150
|
+
const handleInviteClick = async () => {
|
|
151
|
+
try {
|
|
152
|
+
if (!widgetConfiguration?.id) {
|
|
153
|
+
console.debug("[Vortex Invite] Widget configuration ID is required");
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
// const url = `${host}/api/v1/no-auth/components/widget-configuration/${widgetConfigurationId}/invite`; // TODO should we use options.widgetHost?.value ?
|
|
157
|
+
console.debug("[Vortex] Invite Sending invitation", {
|
|
158
|
+
email,
|
|
159
|
+
widgetId,
|
|
160
|
+
environmentId,
|
|
161
|
+
});
|
|
162
|
+
if (!jwt) {
|
|
163
|
+
throw new Error("[Vortex Invite] JWT is required");
|
|
164
|
+
}
|
|
165
|
+
const body = await vortexClient.createInvite(
|
|
166
|
+
jwt,
|
|
167
|
+
widgetConfiguration?.id,
|
|
168
|
+
environmentId,
|
|
169
|
+
{
|
|
170
|
+
email: {
|
|
171
|
+
value: email,
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
);
|
|
175
|
+
// const response = await fetch(url, {
|
|
176
|
+
// method: "POST",
|
|
177
|
+
// headers: {
|
|
178
|
+
// "Content-Type": "application/json",
|
|
179
|
+
// },
|
|
180
|
+
// body: JSON.stringify({ to: [{ email }] }),
|
|
181
|
+
// });
|
|
182
|
+
// if (!response.ok) {
|
|
183
|
+
// const body = await response.json();
|
|
184
|
+
// console.error("[Vortex] Invite Error", body);
|
|
185
|
+
// const error = body?.message || body?.error || "Something went wrong!";
|
|
186
|
+
// onError?.(new Error(error), "invite");
|
|
187
|
+
// return;
|
|
188
|
+
// }
|
|
189
|
+
// const body = await response.json();
|
|
190
|
+
console.log("[Vortex] Invite Response", body);
|
|
191
|
+
if (onSuccess) {
|
|
192
|
+
onSuccess({
|
|
193
|
+
type: "invite",
|
|
194
|
+
data: "Email invite sent",
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
} catch (error) {
|
|
198
|
+
console.error("[Vortex]", error);
|
|
199
|
+
if (onError) {
|
|
200
|
+
onError(
|
|
201
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
202
|
+
"invite",
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const getShareableLink = (): string | undefined => {
|
|
209
|
+
const result =
|
|
210
|
+
widgetConfiguration?.slug != null
|
|
211
|
+
? vortexClient.getShareableLinkFormatted(widgetConfiguration.slug)
|
|
212
|
+
: undefined;
|
|
213
|
+
return result;
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const handleShareLink = async () => {
|
|
217
|
+
try {
|
|
218
|
+
const shareableLink = getShareableLink();
|
|
219
|
+
if (!shareableLink) {
|
|
220
|
+
throw new Error("No shareable link available");
|
|
221
|
+
}
|
|
222
|
+
await Share.share({
|
|
223
|
+
message: shareableLink,
|
|
224
|
+
title: "Share Invite Link",
|
|
225
|
+
});
|
|
226
|
+
console.debug("[Vortex] Link Shared", "The invite link has been shared.");
|
|
227
|
+
if (onSuccess) {
|
|
228
|
+
onSuccess({
|
|
229
|
+
type: "share",
|
|
230
|
+
data: "Sharable link used",
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
} catch (error) {
|
|
234
|
+
console.error("[Vortex] Failed to share link:", error);
|
|
235
|
+
if (onError) {
|
|
236
|
+
onError(
|
|
237
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
238
|
+
"share",
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const handleCopyLink = async () => {
|
|
245
|
+
try {
|
|
246
|
+
const shareableLink = getShareableLink();
|
|
247
|
+
if (!shareableLink) {
|
|
248
|
+
throw new Error("No shareable link available");
|
|
249
|
+
}
|
|
250
|
+
await Clipboard.setString(shareableLink);
|
|
251
|
+
console.debug(
|
|
252
|
+
"[Vortex] Link Copied to clipboard",
|
|
253
|
+
"The invite link has been copied.",
|
|
254
|
+
);
|
|
255
|
+
if (onSuccess) {
|
|
256
|
+
onSuccess({
|
|
257
|
+
type: "share",
|
|
258
|
+
data: "Sharable copied",
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
} catch (error) {
|
|
262
|
+
console.error("[Vortex] Failed to copy link:", error);
|
|
263
|
+
if (onError) {
|
|
264
|
+
onError(
|
|
265
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
266
|
+
"share",
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
widgetConfiguration,
|
|
274
|
+
error,
|
|
275
|
+
fetching,
|
|
276
|
+
loading,
|
|
277
|
+
email,
|
|
278
|
+
setEmail,
|
|
279
|
+
opacity,
|
|
280
|
+
themeColors,
|
|
281
|
+
themeStyles,
|
|
282
|
+
has,
|
|
283
|
+
options,
|
|
284
|
+
handleInviteClick,
|
|
285
|
+
handleShareLink,
|
|
286
|
+
handleCopyLink,
|
|
287
|
+
getShareableLink,
|
|
288
|
+
};
|
|
289
|
+
}
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
class VortexClient {
|
|
2
|
+
private baseUrl: string;
|
|
3
|
+
|
|
4
|
+
constructor(baseUrl: string) {
|
|
5
|
+
// trim ending /
|
|
6
|
+
this.baseUrl = baseUrl.replace(/\/+$/, "");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async getWidgetConfiguration(
|
|
10
|
+
widgetId: string,
|
|
11
|
+
jwt: string,
|
|
12
|
+
environmentId: string,
|
|
13
|
+
) {
|
|
14
|
+
const url = `${this.baseUrl}/api/v1/environment/${environmentId}/widgets/${widgetId}`;
|
|
15
|
+
console.debug("[VortexClient] getWidgetConfiguration", { url });
|
|
16
|
+
|
|
17
|
+
const response = await fetch(url, {
|
|
18
|
+
headers: { Authorization: `Bearer ${jwt}` },
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
if (!response.ok) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
`Error fetching widget configuration: ${response.status}`,
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const data = await response.json();
|
|
28
|
+
console.debug("[VortexClient] getWidgetConfiguration", { data });
|
|
29
|
+
return data;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async createInvite(
|
|
33
|
+
jwt: string,
|
|
34
|
+
widgetConfigurationId: string,
|
|
35
|
+
environmentId: string,
|
|
36
|
+
payload: any,
|
|
37
|
+
) {
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
const response = await fetch(
|
|
41
|
+
`${this.baseUrl}/api/v1/environment/${environmentId}/widget-configuration/${widgetConfigurationId}/invite`,
|
|
42
|
+
{
|
|
43
|
+
method: "POST",
|
|
44
|
+
headers: {
|
|
45
|
+
"Content-Type": "application/json",
|
|
46
|
+
Authorization: `Bearer ${jwt}`,
|
|
47
|
+
},
|
|
48
|
+
body: JSON.stringify({
|
|
49
|
+
data: {
|
|
50
|
+
payload,
|
|
51
|
+
},
|
|
52
|
+
}),
|
|
53
|
+
},
|
|
54
|
+
);
|
|
55
|
+
if (!response.ok) {
|
|
56
|
+
const body = await response.text();
|
|
57
|
+
console.error("[VortexClient] createInvite", { status: response.status, body });
|
|
58
|
+
throw new Error(`Error POSTing widget invite: ${response.status}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const data = await response.json();
|
|
62
|
+
console.log("[VortexClient] createInvite", data);
|
|
63
|
+
return data;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
getShareableLinkFormatted(slug: string) {
|
|
67
|
+
return `${this.baseUrl}/api/v1/no-auth/components/share/${slug}`;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export default VortexClient;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { WidgetConfiguration } from "@teamvortexsoftware/vortex-core";
|
|
2
|
+
|
|
3
|
+
export interface ThemeColors {
|
|
4
|
+
containerBackground: string;
|
|
5
|
+
containerForeground: string;
|
|
6
|
+
containerBorder: string;
|
|
7
|
+
primaryButtonBackground: string;
|
|
8
|
+
primaryButtonForeground: string;
|
|
9
|
+
primaryButtonBorder: string;
|
|
10
|
+
secondaryButtonBackground: string;
|
|
11
|
+
secondaryButtonForeground: string;
|
|
12
|
+
secondaryButtonBorder: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface FeatureFlags {
|
|
16
|
+
shareableLinks: boolean;
|
|
17
|
+
emailInvitations: boolean;
|
|
18
|
+
shareOptionsCopyLink: boolean;
|
|
19
|
+
shareOptionsSms: boolean;
|
|
20
|
+
shareOptionsFacebookMessenger: boolean;
|
|
21
|
+
shareOptionsInstagramDms: boolean;
|
|
22
|
+
shareOptionsLinkedInMessaging: boolean;
|
|
23
|
+
shareOptionsTwitterDms: boolean;
|
|
24
|
+
shareOptionsWhatsApp: boolean;
|
|
25
|
+
shareOptionsNativeShareSheet: boolean;
|
|
26
|
+
shareOptionsQrCode: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Extracts theme colors from widget configuration
|
|
31
|
+
*/
|
|
32
|
+
export function extractThemeColors(widgetConfiguration?: WidgetConfiguration): ThemeColors {
|
|
33
|
+
const options = widgetConfiguration?.configuration?.props;
|
|
34
|
+
const colors = options?.["vortex.theme.colors"]?.value || [];
|
|
35
|
+
const colorMap: Record<string, string> = {};
|
|
36
|
+
|
|
37
|
+
colors.forEach((color: any) => {
|
|
38
|
+
if (color.key && color.value) {
|
|
39
|
+
colorMap[color.key] = color.value;
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
containerBackground: colorMap["--container-background"] || "#ffffff",
|
|
45
|
+
containerForeground: colorMap["--container-foreground-color"] || "#666666",
|
|
46
|
+
containerBorder: colorMap["--container-border-color"] || "#c4c4c4",
|
|
47
|
+
primaryButtonBackground: colorMap["--primary-button-background"] || "#197af3",
|
|
48
|
+
primaryButtonForeground: colorMap["--primary-button-foreground-color"] || "#ffffff",
|
|
49
|
+
primaryButtonBorder: colorMap["--primary-button-border-color"] || "#000000",
|
|
50
|
+
secondaryButtonBackground: colorMap["--secondary-button-background"] || "#dfdfdf",
|
|
51
|
+
secondaryButtonForeground: colorMap["--secondary-button-foreground-color"] || "#000000",
|
|
52
|
+
secondaryButtonBorder: colorMap["--secondary-button-border-color"] || "#c4c4c4",
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Extracts feature flags from widget configuration
|
|
58
|
+
*/
|
|
59
|
+
export function extractFeatureFlags(widgetConfiguration?: WidgetConfiguration): FeatureFlags {
|
|
60
|
+
const features: string[] = Array.isArray(
|
|
61
|
+
widgetConfiguration?.configuration?.props?.["vortex.components"]?.value,
|
|
62
|
+
)
|
|
63
|
+
? widgetConfiguration?.configuration?.props?.["vortex.components"]?.value
|
|
64
|
+
: [];
|
|
65
|
+
|
|
66
|
+
const shareOptions: string[] = Array.isArray(
|
|
67
|
+
widgetConfiguration?.configuration?.props?.["vortex.components.share.options"]?.value,
|
|
68
|
+
)
|
|
69
|
+
? widgetConfiguration?.configuration?.props?.["vortex.components.share.options"]?.value
|
|
70
|
+
: [];
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
shareableLinks: features.includes("vortex.components.emailinvitations"),
|
|
74
|
+
emailInvitations: features.includes("vortex.components.share"),
|
|
75
|
+
shareOptionsCopyLink: shareOptions.includes("copyLink"),
|
|
76
|
+
shareOptionsFacebookMessenger: shareOptions.includes("facebookMessenger"),
|
|
77
|
+
shareOptionsInstagramDms: shareOptions.includes("instagramDms"),
|
|
78
|
+
shareOptionsLinkedInMessaging: shareOptions.includes("linkedInMessaging"),
|
|
79
|
+
shareOptionsNativeShareSheet: shareOptions.includes("nativeShareSheet"),
|
|
80
|
+
shareOptionsQrCode: shareOptions.includes("qrCode"),
|
|
81
|
+
shareOptionsSms: shareOptions.includes("sms"),
|
|
82
|
+
shareOptionsTwitterDms: shareOptions.includes("twitterDms"),
|
|
83
|
+
shareOptionsWhatsApp: shareOptions.includes("whatsApp"),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import {
|
|
3
|
+
Animated,
|
|
4
|
+
Pressable,
|
|
5
|
+
StyleSheet,
|
|
6
|
+
Text,
|
|
7
|
+
TextInput,
|
|
8
|
+
View,
|
|
9
|
+
} from "react-native";
|
|
10
|
+
import {
|
|
11
|
+
useVortexInvite,
|
|
12
|
+
VortexActionResult,
|
|
13
|
+
VortexActionType,
|
|
14
|
+
} from "./hooks/useVortexInvite";
|
|
15
|
+
import { ShareButtons } from "./components/ShareButtons";
|
|
16
|
+
|
|
17
|
+
export interface VortexInviteProps {
|
|
18
|
+
environmentId: string;
|
|
19
|
+
widgetId: string;
|
|
20
|
+
vortexApiHost: string;
|
|
21
|
+
isLoading: boolean;
|
|
22
|
+
jwt?: string;
|
|
23
|
+
onSuccess?: (result: VortexActionResult) => void;
|
|
24
|
+
onError?: (error: Error, type: VortexActionType) => void;
|
|
25
|
+
// host: string;
|
|
26
|
+
// widgetConfigurationId?: string;
|
|
27
|
+
// widgetConfiguration?: WidgetConfiguration;
|
|
28
|
+
// isLoading?: boolean;
|
|
29
|
+
// onSuccess?: (result: VortexActionResult) => void;
|
|
30
|
+
// onError?: (error: Error, type: VortexActionType) => void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function VortexInvite({
|
|
34
|
+
widgetId,
|
|
35
|
+
environmentId,
|
|
36
|
+
vortexApiHost,
|
|
37
|
+
isLoading = false,
|
|
38
|
+
jwt,
|
|
39
|
+
onSuccess,
|
|
40
|
+
onError,
|
|
41
|
+
}: VortexInviteProps) {
|
|
42
|
+
const {
|
|
43
|
+
error,
|
|
44
|
+
fetching,
|
|
45
|
+
loading,
|
|
46
|
+
email,
|
|
47
|
+
setEmail,
|
|
48
|
+
opacity,
|
|
49
|
+
themeColors,
|
|
50
|
+
themeStyles,
|
|
51
|
+
has,
|
|
52
|
+
handleInviteClick,
|
|
53
|
+
handleShareLink,
|
|
54
|
+
handleCopyLink,
|
|
55
|
+
getShareableLink,
|
|
56
|
+
} = useVortexInvite({
|
|
57
|
+
widgetId,
|
|
58
|
+
environmentId,
|
|
59
|
+
vortexApiHost,
|
|
60
|
+
isLoading,
|
|
61
|
+
jwt,
|
|
62
|
+
onSuccess,
|
|
63
|
+
onError,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
if (error?.message) {
|
|
67
|
+
return <Text data-testid="error">{error.message}</Text>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (fetching || loading) {
|
|
71
|
+
return <Animated.View style={[styles.skeleton, { opacity }]} />;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Theme styles are now provided by the hook
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<View style={[styles.container, themeStyles.containerStyles]}>
|
|
78
|
+
{has?.emailInvitations && (
|
|
79
|
+
<View style={styles.inviteContainer}>
|
|
80
|
+
<Text style={[styles.introText, themeStyles.textStyles]}>
|
|
81
|
+
Invite People
|
|
82
|
+
</Text>
|
|
83
|
+
<TextInput
|
|
84
|
+
style={[styles.input, themeStyles.inputStyles]}
|
|
85
|
+
placeholder="Enter email"
|
|
86
|
+
placeholderTextColor={themeColors.containerForeground}
|
|
87
|
+
value={email}
|
|
88
|
+
onChangeText={setEmail}
|
|
89
|
+
autoCapitalize="none"
|
|
90
|
+
/>
|
|
91
|
+
<View style={styles.buttonContainer}>
|
|
92
|
+
<Pressable
|
|
93
|
+
style={[styles.submitButton, themeStyles.primaryButton]}
|
|
94
|
+
onPress={handleInviteClick}
|
|
95
|
+
>
|
|
96
|
+
<Text
|
|
97
|
+
style={[styles.submitButtonText, themeStyles.primaryButtonText]}
|
|
98
|
+
>
|
|
99
|
+
Invite
|
|
100
|
+
</Text>
|
|
101
|
+
</Pressable>
|
|
102
|
+
</View>
|
|
103
|
+
</View>
|
|
104
|
+
)}
|
|
105
|
+
|
|
106
|
+
{has?.shareableLinks && (
|
|
107
|
+
<View style={styles.shareContainer}>
|
|
108
|
+
{has?.emailInvitations && (
|
|
109
|
+
<Text style={[styles.divider, themeStyles.textStyles]}>OR</Text>
|
|
110
|
+
)}
|
|
111
|
+
|
|
112
|
+
<View style={styles.shareButtonsContainer}>
|
|
113
|
+
<ShareButtons
|
|
114
|
+
has={has}
|
|
115
|
+
themeColors={themeColors}
|
|
116
|
+
themeStyles={themeStyles}
|
|
117
|
+
handleShareLink={handleShareLink}
|
|
118
|
+
handleCopyLink={handleCopyLink}
|
|
119
|
+
shareableLink={getShareableLink()}
|
|
120
|
+
/>
|
|
121
|
+
</View>
|
|
122
|
+
</View>
|
|
123
|
+
)}
|
|
124
|
+
</View>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const styles = StyleSheet.create({
|
|
129
|
+
container: {
|
|
130
|
+
padding: 15,
|
|
131
|
+
display: "flex",
|
|
132
|
+
flexDirection: "column",
|
|
133
|
+
borderRadius: 8,
|
|
134
|
+
borderWidth: 1,
|
|
135
|
+
},
|
|
136
|
+
inviteContainer: {
|
|
137
|
+
display: "flex",
|
|
138
|
+
flexDirection: "column",
|
|
139
|
+
justifyContent: "center",
|
|
140
|
+
alignItems: "center",
|
|
141
|
+
marginTop: 8,
|
|
142
|
+
},
|
|
143
|
+
introText: {
|
|
144
|
+
marginBottom: 8,
|
|
145
|
+
fontSize: 24,
|
|
146
|
+
fontWeight: "bold",
|
|
147
|
+
textAlign: "center",
|
|
148
|
+
},
|
|
149
|
+
input: {
|
|
150
|
+
width: "100%",
|
|
151
|
+
borderWidth: 1,
|
|
152
|
+
padding: 10,
|
|
153
|
+
borderRadius: 5,
|
|
154
|
+
marginBottom: 8,
|
|
155
|
+
},
|
|
156
|
+
buttonContainer: {
|
|
157
|
+
marginTop: 16,
|
|
158
|
+
marginBottom: 8,
|
|
159
|
+
width: "100%",
|
|
160
|
+
},
|
|
161
|
+
submitButton: {
|
|
162
|
+
padding: 12,
|
|
163
|
+
borderRadius: 5,
|
|
164
|
+
alignItems: "center",
|
|
165
|
+
borderWidth: 1,
|
|
166
|
+
},
|
|
167
|
+
submitButtonText: {
|
|
168
|
+
fontWeight: "500",
|
|
169
|
+
textTransform: "none",
|
|
170
|
+
},
|
|
171
|
+
skeleton: {
|
|
172
|
+
width: "100%",
|
|
173
|
+
height: 50,
|
|
174
|
+
backgroundColor: "#e0e0e0",
|
|
175
|
+
borderRadius: 5,
|
|
176
|
+
},
|
|
177
|
+
shareContainer: {
|
|
178
|
+
marginTop: 16,
|
|
179
|
+
alignItems: "center",
|
|
180
|
+
},
|
|
181
|
+
divider: {
|
|
182
|
+
marginVertical: 10,
|
|
183
|
+
fontSize: 16,
|
|
184
|
+
fontWeight: "bold",
|
|
185
|
+
textAlign: "center",
|
|
186
|
+
},
|
|
187
|
+
shareButtonsContainer: {
|
|
188
|
+
flexDirection: "row",
|
|
189
|
+
flexWrap: "wrap",
|
|
190
|
+
justifyContent: "center",
|
|
191
|
+
},
|
|
192
|
+
// Share button styles moved to ShareButtons.tsx
|
|
193
|
+
});
|