@teamvortexsoftware/vortex-react-native 0.0.3 → 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 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
@@ -0,0 +1,4 @@
1
+ import { config } from "@teamvortexsoftware/eslint-config/react-native";
2
+
3
+ /** @type {import("eslint").Linter.Config} */
4
+ export default config;
package/package.json CHANGED
@@ -2,36 +2,43 @@
2
2
  "name": "@teamvortexsoftware/vortex-react-native",
3
3
  "description": "",
4
4
  "author": "@teamvortexsoftware",
5
- "version": "0.0.3",
5
+ "version": "0.0.4",
6
6
  "publishConfig": {
7
7
  "access": "restricted"
8
8
  },
9
- "files": [
10
- "dist"
11
- ],
12
- "exports": {
13
- ".": {
14
- "import": "./dist/index.js",
15
- "require": "./dist/index.js",
16
- "types": "./dist/index.d.ts"
17
- }
18
- },
19
- "main": "./dist/index.js",
20
- "types": "./dist/index.d.ts",
9
+ "main": "src/index.tsx",
10
+ "types": "src/index.tsx",
11
+ "react-native": "src/index.tsx",
21
12
  "devDependencies": {
13
+ "@eslint/js": "^9.24.0",
22
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",
23
20
  "tsup": "8.0.1",
24
- "typescript": "5.5.4",
21
+ "typescript": "5.8.3",
22
+ "typescript-eslint": "^8.30.1",
23
+ "@teamvortexsoftware/eslint-config": "0.0.0",
25
24
  "@teamvortexsoftware/typescript-config": "0.0.0"
26
25
  },
27
26
  "dependencies": {
28
- "react": "18.2.0",
29
- "react-native": "0.74.5"
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": "*"
30
37
  },
31
38
  "scripts": {
32
- "build": "tsup",
33
- "dev": "tsup --watch",
34
- "clean": "rm -rf dist",
35
- "prepublish": "npm run build"
39
+ "build": "echo \"no build\"",
40
+ "prepublish": "pnpm run build",
41
+ "lint": "eslint src/",
42
+ "type-check": "tsc --noEmit"
36
43
  }
37
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,2 @@
1
+ export { VortexInvite, type VortexInviteProps } from "./vortexInvite";
2
+ export { type VortexActionResult, type VortexActionType } from "./hooks/useVortexInvite";
@@ -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
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "@teamvortexsoftware/typescript-config/react-native-library",
3
+ "include": ["src"],
4
+ "exclude": ["node_modules", "dist"],
5
+ "compilerOptions": {
6
+ "jsx": "react-native",
7
+ "strict": true
8
+ }
9
+ }
package/dist/index.d.mts DELETED
@@ -1,24 +0,0 @@
1
- import React from 'react';
2
-
3
- interface ButtonProps {
4
- text: string;
5
- onClick?: (e: any) => void;
6
- }
7
- declare function Button({ text, onClick }: ButtonProps): React.JSX.Element;
8
-
9
- interface WidgetConfiguration {
10
- configuration?: {
11
- props?: any;
12
- styles?: any;
13
- };
14
- slug?: string;
15
- [key: string]: any;
16
- }
17
- interface VortexInviteProps {
18
- widgetConfigurationId?: string;
19
- widgetConfiguration?: WidgetConfiguration;
20
- isLoading?: boolean;
21
- }
22
- declare function VortexInvite({ widgetConfigurationId, widgetConfiguration, isLoading, }: VortexInviteProps): React.JSX.Element;
23
-
24
- export { Button, type ButtonProps, VortexInvite, type VortexInviteProps };
package/dist/index.d.ts DELETED
@@ -1,24 +0,0 @@
1
- import React from 'react';
2
-
3
- interface ButtonProps {
4
- text: string;
5
- onClick?: (e: any) => void;
6
- }
7
- declare function Button({ text, onClick }: ButtonProps): React.JSX.Element;
8
-
9
- interface WidgetConfiguration {
10
- configuration?: {
11
- props?: any;
12
- styles?: any;
13
- };
14
- slug?: string;
15
- [key: string]: any;
16
- }
17
- interface VortexInviteProps {
18
- widgetConfigurationId?: string;
19
- widgetConfiguration?: WidgetConfiguration;
20
- isLoading?: boolean;
21
- }
22
- declare function VortexInvite({ widgetConfigurationId, widgetConfiguration, isLoading, }: VortexInviteProps): React.JSX.Element;
23
-
24
- export { Button, type ButtonProps, VortexInvite, type VortexInviteProps };
package/dist/index.js DELETED
@@ -1,177 +0,0 @@
1
- 'use client'
2
- "use strict";
3
- var __create = Object.create;
4
- var __defProp = Object.defineProperty;
5
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
- var __getOwnPropNames = Object.getOwnPropertyNames;
7
- var __getProtoOf = Object.getPrototypeOf;
8
- var __hasOwnProp = Object.prototype.hasOwnProperty;
9
- var __export = (target, all) => {
10
- for (var name in all)
11
- __defProp(target, name, { get: all[name], enumerable: true });
12
- };
13
- var __copyProps = (to, from, except, desc) => {
14
- if (from && typeof from === "object" || typeof from === "function") {
15
- for (let key of __getOwnPropNames(from))
16
- if (!__hasOwnProp.call(to, key) && key !== except)
17
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
18
- }
19
- return to;
20
- };
21
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
22
- // If the importer is in node compatibility mode or this is not an ESM
23
- // file that has been converted to a CommonJS file using a Babel-
24
- // compatible transform (i.e. "__esModule" has not been set), then set
25
- // "default" to the CommonJS "module.exports" for node compatibility.
26
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
27
- mod
28
- ));
29
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
30
-
31
- // src/index.tsx
32
- var src_exports = {};
33
- __export(src_exports, {
34
- Button: () => Button,
35
- VortexInvite: () => VortexInvite
36
- });
37
- module.exports = __toCommonJS(src_exports);
38
-
39
- // src/button.tsx
40
- var import_react = __toESM(require("react"));
41
- var import_react_native = require("react-native");
42
- function Button({ text, onClick }) {
43
- const [count, setCount] = (0, import_react.useState)(0);
44
- (0, import_react.useEffect)(() => {
45
- console.log("Current count:", count);
46
- }, [count]);
47
- const handlePress = (e) => {
48
- console.log("handlePress called");
49
- setCount((prevCount) => prevCount + 1);
50
- if (onClick) {
51
- onClick(e);
52
- }
53
- };
54
- return /* @__PURE__ */ import_react.default.createElement(import_react_native.Text, { style: styles.text }, text);
55
- }
56
- var styles = import_react_native.StyleSheet.create({
57
- button: {
58
- maxWidth: 200,
59
- textAlign: "center",
60
- borderRadius: 10,
61
- paddingTop: 14,
62
- paddingBottom: 14,
63
- paddingLeft: 30,
64
- paddingRight: 30,
65
- fontSize: 15,
66
- backgroundColor: "#2f80ed"
67
- },
68
- text: {
69
- color: "white"
70
- }
71
- });
72
-
73
- // src/vortexInvite.tsx
74
- var import_react2 = __toESM(require("react"));
75
- var import_react_native2 = require("react-native");
76
- function VortexInvite({
77
- widgetConfigurationId,
78
- widgetConfiguration,
79
- isLoading = false
80
- }) {
81
- return /* @__PURE__ */ import_react2.default.createElement(import_react_native2.View, null, /* @__PURE__ */ import_react2.default.createElement("pre", null, "Chupacabra"));
82
- }
83
- var styles2 = import_react_native2.StyleSheet.create({
84
- container: {
85
- padding: 15,
86
- display: "flex",
87
- flexDirection: "column"
88
- },
89
- inviteContainer: {
90
- display: "flex",
91
- flexDirection: "column",
92
- justifyContent: "center",
93
- alignItems: "center",
94
- marginTop: 8
95
- },
96
- introText: {
97
- marginBottom: 8,
98
- fontSize: 24,
99
- fontWeight: "bold",
100
- textAlign: "center"
101
- },
102
- input: {
103
- width: "100%",
104
- borderWidth: 1,
105
- borderColor: "#ccc",
106
- padding: 10,
107
- borderRadius: 5,
108
- marginBottom: 8
109
- },
110
- buttonContainer: {
111
- marginTop: 16,
112
- marginBottom: 8,
113
- width: "100%"
114
- },
115
- submitButton: {
116
- // backgroundColor: "#007bff",
117
- padding: 12,
118
- borderRadius: 5,
119
- alignItems: "center",
120
- shadowColor: "rgba(0, 0, 0, 0.2)",
121
- shadowOffset: { width: 0, height: 2 },
122
- shadowOpacity: 0.5,
123
- shadowRadius: 2
124
- },
125
- submitButtonText: {
126
- color: "white",
127
- fontWeight: "500",
128
- textTransform: "none"
129
- },
130
- skeleton: {
131
- width: "100%",
132
- height: 50,
133
- backgroundColor: "#e0e0e0",
134
- borderRadius: 5
135
- },
136
- shareContainer: {
137
- marginTop: 16,
138
- alignItems: "center"
139
- },
140
- divider: {
141
- marginVertical: 10,
142
- fontSize: 16,
143
- fontWeight: "bold",
144
- textAlign: "center"
145
- },
146
- shareButtonsContainer: {
147
- flexDirection: "row",
148
- flexWrap: "wrap",
149
- justifyContent: "center"
150
- },
151
- shareButtonWrapper: {
152
- width: "48%",
153
- margin: 5
154
- },
155
- centeredShareButton: {
156
- width: "100%",
157
- alignItems: "center"
158
- },
159
- shareButton: {
160
- flexDirection: "row",
161
- alignItems: "center",
162
- padding: 10,
163
- borderWidth: 1,
164
- borderColor: "#007bff",
165
- borderRadius: 5,
166
- justifyContent: "center"
167
- },
168
- shareButtonText: {
169
- paddingLeft: 10,
170
- color: "#007bff"
171
- }
172
- });
173
- // Annotate the CommonJS export names for ESM import in node:
174
- 0 && (module.exports = {
175
- Button,
176
- VortexInvite
177
- });
package/dist/index.mjs DELETED
@@ -1,143 +0,0 @@
1
- 'use client'
2
-
3
- // src/button.tsx
4
- import React, { useEffect, useState } from "react";
5
- import { StyleSheet, Text } from "react-native";
6
- function Button({ text, onClick }) {
7
- const [count, setCount] = useState(0);
8
- useEffect(() => {
9
- console.log("Current count:", count);
10
- }, [count]);
11
- const handlePress = (e) => {
12
- console.log("handlePress called");
13
- setCount((prevCount) => prevCount + 1);
14
- if (onClick) {
15
- onClick(e);
16
- }
17
- };
18
- return /* @__PURE__ */ React.createElement(Text, { style: styles.text }, text);
19
- }
20
- var styles = StyleSheet.create({
21
- button: {
22
- maxWidth: 200,
23
- textAlign: "center",
24
- borderRadius: 10,
25
- paddingTop: 14,
26
- paddingBottom: 14,
27
- paddingLeft: 30,
28
- paddingRight: 30,
29
- fontSize: 15,
30
- backgroundColor: "#2f80ed"
31
- },
32
- text: {
33
- color: "white"
34
- }
35
- });
36
-
37
- // src/vortexInvite.tsx
38
- import React2 from "react";
39
- import {
40
- View,
41
- StyleSheet as StyleSheet2
42
- } from "react-native";
43
- function VortexInvite({
44
- widgetConfigurationId,
45
- widgetConfiguration,
46
- isLoading = false
47
- }) {
48
- return /* @__PURE__ */ React2.createElement(View, null, /* @__PURE__ */ React2.createElement("pre", null, "Chupacabra"));
49
- }
50
- var styles2 = StyleSheet2.create({
51
- container: {
52
- padding: 15,
53
- display: "flex",
54
- flexDirection: "column"
55
- },
56
- inviteContainer: {
57
- display: "flex",
58
- flexDirection: "column",
59
- justifyContent: "center",
60
- alignItems: "center",
61
- marginTop: 8
62
- },
63
- introText: {
64
- marginBottom: 8,
65
- fontSize: 24,
66
- fontWeight: "bold",
67
- textAlign: "center"
68
- },
69
- input: {
70
- width: "100%",
71
- borderWidth: 1,
72
- borderColor: "#ccc",
73
- padding: 10,
74
- borderRadius: 5,
75
- marginBottom: 8
76
- },
77
- buttonContainer: {
78
- marginTop: 16,
79
- marginBottom: 8,
80
- width: "100%"
81
- },
82
- submitButton: {
83
- // backgroundColor: "#007bff",
84
- padding: 12,
85
- borderRadius: 5,
86
- alignItems: "center",
87
- shadowColor: "rgba(0, 0, 0, 0.2)",
88
- shadowOffset: { width: 0, height: 2 },
89
- shadowOpacity: 0.5,
90
- shadowRadius: 2
91
- },
92
- submitButtonText: {
93
- color: "white",
94
- fontWeight: "500",
95
- textTransform: "none"
96
- },
97
- skeleton: {
98
- width: "100%",
99
- height: 50,
100
- backgroundColor: "#e0e0e0",
101
- borderRadius: 5
102
- },
103
- shareContainer: {
104
- marginTop: 16,
105
- alignItems: "center"
106
- },
107
- divider: {
108
- marginVertical: 10,
109
- fontSize: 16,
110
- fontWeight: "bold",
111
- textAlign: "center"
112
- },
113
- shareButtonsContainer: {
114
- flexDirection: "row",
115
- flexWrap: "wrap",
116
- justifyContent: "center"
117
- },
118
- shareButtonWrapper: {
119
- width: "48%",
120
- margin: 5
121
- },
122
- centeredShareButton: {
123
- width: "100%",
124
- alignItems: "center"
125
- },
126
- shareButton: {
127
- flexDirection: "row",
128
- alignItems: "center",
129
- padding: 10,
130
- borderWidth: 1,
131
- borderColor: "#007bff",
132
- borderRadius: 5,
133
- justifyContent: "center"
134
- },
135
- shareButtonText: {
136
- paddingLeft: 10,
137
- color: "#007bff"
138
- }
139
- });
140
- export {
141
- Button,
142
- VortexInvite
143
- };