@opndev/react-native-events 0.0.10
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/Changes +64 -0
- package/LICENSE +4 -0
- package/LICENSES/GPL-3.0-or-later.txt +232 -0
- package/LICENSES/LicenseRef-OPNDEV-exceptions.txt +35 -0
- package/LICENSES/LicenseRef-Opndev-Proprietary.txt +0 -0
- package/README.md +320 -0
- package/lib/actions/news.js +100 -0
- package/lib/actions/qrcode.js +115 -0
- package/lib/components/gradient-tile.jsx +49 -0
- package/lib/components/hero-screen.jsx +109 -0
- package/lib/components/parallax-scroll-view.jsx +144 -0
- package/lib/components/qr-code-form.jsx +436 -0
- package/lib/components/tile-base.jsx +123 -0
- package/lib/components/tile.jsx +44 -0
- package/lib/components.js +15 -0
- package/lib/hero-screen-registery.js +26 -0
- package/lib/index.js +10 -0
- package/lib/notifications/fcm.js +63 -0
- package/lib/notifications.js +74 -0
- package/lib/screen-registery.js +68 -0
- package/lib/screens/food-menu-screen.jsx +139 -0
- package/lib/screens/food-vendor-screen.jsx +80 -0
- package/lib/screens/news-item-screen.jsx +154 -0
- package/lib/screens/news-list-screen.jsx +196 -0
- package/lib/screens/qr-code-screen.jsx +56 -0
- package/lib/screens.js +8 -0
- package/lib/testing/react-native.js +94 -0
- package/lib/utils/colors.js +82 -0
- package/lib/utils/format-price.js +19 -0
- package/lib/utils/header-picker.js +38 -0
- package/lib/utils/launch.js +94 -0
- package/package.json +37 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Wesley Schwengle <wesleys@opperschaap.net>
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later WITH LicenseRef-OPNDEV-exceptions
|
|
4
|
+
|
|
5
|
+
import React, {
|
|
6
|
+
useEffect,
|
|
7
|
+
useState,
|
|
8
|
+
} from 'react';
|
|
9
|
+
import {
|
|
10
|
+
ActivityIndicator,
|
|
11
|
+
Pressable,
|
|
12
|
+
RefreshControl,
|
|
13
|
+
ScrollView,
|
|
14
|
+
StyleSheet,
|
|
15
|
+
Text,
|
|
16
|
+
} from 'react-native';
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
import { fetchNewsJson } from '../actions/news';
|
|
20
|
+
import Markdown from 'react-native-markdown-display';
|
|
21
|
+
|
|
22
|
+
const defaultStyle = StyleSheet.create({
|
|
23
|
+
container: {
|
|
24
|
+
flex: 1,
|
|
25
|
+
},
|
|
26
|
+
content: {
|
|
27
|
+
padding: 16,
|
|
28
|
+
gap: 12,
|
|
29
|
+
},
|
|
30
|
+
item: {
|
|
31
|
+
padding: 16,
|
|
32
|
+
borderRadius: 12,
|
|
33
|
+
gap: 8,
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* NewsListScreen
|
|
39
|
+
*
|
|
40
|
+
* Fetches and renders a vertically scrollable list of news items.
|
|
41
|
+
*
|
|
42
|
+
* @param {object} props
|
|
43
|
+
* @param {string} props.uri
|
|
44
|
+
* @param {string} [props.bearerToken]
|
|
45
|
+
* @param {React.ComponentType} [props.TextComponent]
|
|
46
|
+
* @param {Function} [props.onPressItem]
|
|
47
|
+
* @param {string} [props.backgroundColor]
|
|
48
|
+
* @param {string} [props.cardBackgroundColor]
|
|
49
|
+
* @param {object} [props.containerStyle]
|
|
50
|
+
* @param {object} [props.contentStyle]
|
|
51
|
+
* @param {object} [props.itemStyle]
|
|
52
|
+
* @param {object} [props.titleStyle]
|
|
53
|
+
* @param {object} [props.summaryStyle]
|
|
54
|
+
* @param {object} [props.refreshButtonStyle]
|
|
55
|
+
* @param {object} [props.refreshTextStyle]
|
|
56
|
+
* @param {object} [props.errorTextStyle]
|
|
57
|
+
* @param {string} [props.refreshLabel]
|
|
58
|
+
* @param {string} [props.emptyLabel]
|
|
59
|
+
* @param {string} [props.errorLabel]
|
|
60
|
+
* @param {boolean} [props.showRefresh]
|
|
61
|
+
*
|
|
62
|
+
* @returns {JSX.Element}
|
|
63
|
+
*/
|
|
64
|
+
export default function NewsListScreen({
|
|
65
|
+
uri,
|
|
66
|
+
bearerToken,
|
|
67
|
+
TextComponent = Text,
|
|
68
|
+
MarkdownComponent = Markdown,
|
|
69
|
+
onPressItem,
|
|
70
|
+
|
|
71
|
+
backgroundColor,
|
|
72
|
+
cardBackgroundColor,
|
|
73
|
+
|
|
74
|
+
containerStyle,
|
|
75
|
+
contentStyle,
|
|
76
|
+
itemStyle,
|
|
77
|
+
titleStyle,
|
|
78
|
+
summaryStyle,
|
|
79
|
+
refreshButtonStyle,
|
|
80
|
+
refreshTextStyle,
|
|
81
|
+
errorTextStyle,
|
|
82
|
+
|
|
83
|
+
refreshLabel = 'Refresh',
|
|
84
|
+
emptyLabel = 'No news items.',
|
|
85
|
+
errorLabel = 'Unable to load news.',
|
|
86
|
+
showRefresh = true,
|
|
87
|
+
}) {
|
|
88
|
+
const [items, setItems] = useState([]);
|
|
89
|
+
const [loading, setLoading] = useState(true);
|
|
90
|
+
const [refreshing, setRefreshing] = useState(false);
|
|
91
|
+
const [error, setError] = useState(null);
|
|
92
|
+
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
load();
|
|
95
|
+
}, [uri, bearerToken]);
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Loads news items from the configured URI.
|
|
99
|
+
*
|
|
100
|
+
* @param {boolean} [force]
|
|
101
|
+
*
|
|
102
|
+
* @returns {Promise<void>}
|
|
103
|
+
*/
|
|
104
|
+
async function load(force = false) {
|
|
105
|
+
setError(null);
|
|
106
|
+
|
|
107
|
+
if (force) {
|
|
108
|
+
setRefreshing(true);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
setLoading(true);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const data = await fetchNewsJson({
|
|
116
|
+
uri,
|
|
117
|
+
bearerToken,
|
|
118
|
+
force,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
setItems(data.items);
|
|
122
|
+
}
|
|
123
|
+
catch (e) {
|
|
124
|
+
setError(e.message || errorLabel);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (force) {
|
|
128
|
+
setRefreshing(false);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
setLoading(false);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<ScrollView
|
|
137
|
+
style={[
|
|
138
|
+
defaultStyle.container,
|
|
139
|
+
backgroundColor ? { backgroundColor } : null,
|
|
140
|
+
containerStyle,
|
|
141
|
+
]}
|
|
142
|
+
contentContainerStyle={[
|
|
143
|
+
defaultStyle.content,
|
|
144
|
+
contentStyle,
|
|
145
|
+
]}
|
|
146
|
+
refreshControl={
|
|
147
|
+
<RefreshControl
|
|
148
|
+
refreshing={refreshing}
|
|
149
|
+
onRefresh={() => load(true)}
|
|
150
|
+
/>
|
|
151
|
+
}
|
|
152
|
+
>
|
|
153
|
+
{loading ? (
|
|
154
|
+
<ActivityIndicator />
|
|
155
|
+
) : null}
|
|
156
|
+
|
|
157
|
+
{!loading && error ? (
|
|
158
|
+
<TextComponent style={errorTextStyle}>
|
|
159
|
+
{error}
|
|
160
|
+
</TextComponent>
|
|
161
|
+
) : null}
|
|
162
|
+
|
|
163
|
+
{!loading && !error && !items.length ? (
|
|
164
|
+
<TextComponent style={errorTextStyle}>
|
|
165
|
+
{emptyLabel}
|
|
166
|
+
</TextComponent>
|
|
167
|
+
) : null}
|
|
168
|
+
|
|
169
|
+
{!loading && !error && items.map((item) => {
|
|
170
|
+
return (
|
|
171
|
+
<Pressable
|
|
172
|
+
key={item.id}
|
|
173
|
+
style={[
|
|
174
|
+
defaultStyle.item,
|
|
175
|
+
cardBackgroundColor
|
|
176
|
+
? { backgroundColor: cardBackgroundColor }
|
|
177
|
+
: null,
|
|
178
|
+
itemStyle,
|
|
179
|
+
]}
|
|
180
|
+
onPress={() => onPressItem?.(item)}
|
|
181
|
+
>
|
|
182
|
+
<TextComponent style={titleStyle}>
|
|
183
|
+
{item.title}
|
|
184
|
+
</TextComponent>
|
|
185
|
+
|
|
186
|
+
{item.summary ? (
|
|
187
|
+
<MarkdownComponent style={summaryStyle}>
|
|
188
|
+
{item.summary}
|
|
189
|
+
</MarkdownComponent>
|
|
190
|
+
) : null}
|
|
191
|
+
</Pressable>
|
|
192
|
+
);
|
|
193
|
+
})}
|
|
194
|
+
</ScrollView>
|
|
195
|
+
);
|
|
196
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Wesley Schwengle <wesleys@opperschaap.net>
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later WITH LicenseRef-OPNDEV-exceptions
|
|
4
|
+
|
|
5
|
+
import React, { useMemo } from 'react';
|
|
6
|
+
|
|
7
|
+
import QRCodeForm from '../components/qr-code-form';
|
|
8
|
+
import QRCodeAction from '../actions/qrcode';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* QRCodeScreen
|
|
12
|
+
*
|
|
13
|
+
* QR code feature content.
|
|
14
|
+
*
|
|
15
|
+
* @param {object} props
|
|
16
|
+
* @param {React.ComponentType} props.TextComponent
|
|
17
|
+
* @param {{url: string, method?: string, headers?: object}} props.endpoint
|
|
18
|
+
* @param {Array<object>} props.fields
|
|
19
|
+
* @param {string} [props.storageKey]
|
|
20
|
+
* @param {(values: object) => any} [props.buildPayload]
|
|
21
|
+
* @param {(response: any) => string} [props.extractToken]
|
|
22
|
+
*
|
|
23
|
+
* @returns {JSX.Element}
|
|
24
|
+
*/
|
|
25
|
+
export default function QRCodeScreen({
|
|
26
|
+
TextComponent,
|
|
27
|
+
endpoint,
|
|
28
|
+
fields,
|
|
29
|
+
storageKey,
|
|
30
|
+
buildPayload,
|
|
31
|
+
extractToken,
|
|
32
|
+
...rest
|
|
33
|
+
}) {
|
|
34
|
+
const action = useMemo(() => {
|
|
35
|
+
return new QRCodeAction({
|
|
36
|
+
endpoint,
|
|
37
|
+
storageKey,
|
|
38
|
+
buildPayload,
|
|
39
|
+
extractToken,
|
|
40
|
+
});
|
|
41
|
+
}, [
|
|
42
|
+
endpoint,
|
|
43
|
+
storageKey,
|
|
44
|
+
buildPayload,
|
|
45
|
+
extractToken,
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<QRCodeForm
|
|
50
|
+
action={action}
|
|
51
|
+
fields={fields}
|
|
52
|
+
TextComponent={TextComponent}
|
|
53
|
+
{...rest}
|
|
54
|
+
/>
|
|
55
|
+
);
|
|
56
|
+
}
|
package/lib/screens.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Wesley Schwengle <wesleys@opperschaap.net>
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later WITH LicenseRef-OPNDEV-exceptions
|
|
4
|
+
|
|
5
|
+
export { createScreens } from './globals.js';
|
|
6
|
+
export { default as FoodVendorScreen } from './screens/food-vendor-screen.jsx';
|
|
7
|
+
export { default as FoodMenuScreen } from './screens/food-menu-screen.jsx';
|
|
8
|
+
export { default as QRCodeScreen } from './screens/qr-code-screen.jsx';
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Wesley Schwengle <wesleys@opperschaap.net>
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later WITH LicenseRef-OPNDEV-exceptions
|
|
4
|
+
|
|
5
|
+
import Module from 'module';
|
|
6
|
+
|
|
7
|
+
const reactNativeMock = {
|
|
8
|
+
Linking: {
|
|
9
|
+
async openURL() {
|
|
10
|
+
return true;
|
|
11
|
+
},
|
|
12
|
+
async canOpenURL() {
|
|
13
|
+
return true;
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
Platform: {
|
|
17
|
+
OS: 'android',
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
let originalLoad;
|
|
22
|
+
let isInstalled = false;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Install a Node-level mock for `react-native`.
|
|
26
|
+
*
|
|
27
|
+
* This is intended for TAP / Node-based unit tests where React Native
|
|
28
|
+
* is not available as a runtime module.
|
|
29
|
+
*
|
|
30
|
+
* @returns {object}
|
|
31
|
+
* Returns the active mock object so tests can override behavior.
|
|
32
|
+
*/
|
|
33
|
+
export function mockReactNative() {
|
|
34
|
+
if (isInstalled) {
|
|
35
|
+
return reactNativeMock;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
originalLoad = Module._load;
|
|
39
|
+
|
|
40
|
+
Module._load = function (request, parent, isMain) {
|
|
41
|
+
if (request === 'react-native') {
|
|
42
|
+
return reactNativeMock;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return originalLoad.call(this, request, parent, isMain);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
isInstalled = true;
|
|
49
|
+
|
|
50
|
+
return reactNativeMock;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Restore the original Node module loader.
|
|
55
|
+
*/
|
|
56
|
+
export function unmockReactNative() {
|
|
57
|
+
if (!isInstalled) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
Module._load = originalLoad;
|
|
62
|
+
originalLoad = null;
|
|
63
|
+
isInstalled = false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Reset the mock back to defaults between tests.
|
|
68
|
+
*
|
|
69
|
+
* @returns {object}
|
|
70
|
+
* Returns the reset mock object.
|
|
71
|
+
*/
|
|
72
|
+
export function resetReactNativeMock() {
|
|
73
|
+
reactNativeMock.Linking.openURL = async function () {
|
|
74
|
+
return true;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
reactNativeMock.Linking.canOpenURL = async function () {
|
|
78
|
+
return true;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
reactNativeMock.Platform.OS = 'android';
|
|
82
|
+
|
|
83
|
+
return reactNativeMock;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get the active mock object.
|
|
88
|
+
*
|
|
89
|
+
* @returns {object}
|
|
90
|
+
*/
|
|
91
|
+
export function getReactNativeMock() {
|
|
92
|
+
return reactNativeMock;
|
|
93
|
+
}
|
|
94
|
+
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Wesley Schwengle <wesleys@opperschaap.net>
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later WITH LicenseRef-OPNDEV-exceptions
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Mix two hex colors.
|
|
7
|
+
*
|
|
8
|
+
* @param {string} a
|
|
9
|
+
* First hex color, like "#ff0000".
|
|
10
|
+
* @param {string} b
|
|
11
|
+
* Second hex color, like "#0000ff".
|
|
12
|
+
* @param {number} amount
|
|
13
|
+
* Amount of b to mix in, from 0 to 1.
|
|
14
|
+
*
|
|
15
|
+
* @returns {string}
|
|
16
|
+
* Mixed hex color.
|
|
17
|
+
*/
|
|
18
|
+
export function mixHexColors(a, b, amount = 0.5) {
|
|
19
|
+
const ah = a.replace('#', '');
|
|
20
|
+
const bh = b.replace('#', '');
|
|
21
|
+
|
|
22
|
+
const ar = parseInt(ah.slice(0, 2), 16);
|
|
23
|
+
const ag = parseInt(ah.slice(2, 4), 16);
|
|
24
|
+
const ab = parseInt(ah.slice(4, 6), 16);
|
|
25
|
+
|
|
26
|
+
const br = parseInt(bh.slice(0, 2), 16);
|
|
27
|
+
const bg = parseInt(bh.slice(2, 4), 16);
|
|
28
|
+
const bb = parseInt(bh.slice(4, 6), 16);
|
|
29
|
+
|
|
30
|
+
const r = Math.round(ar + (br - ar) * amount);
|
|
31
|
+
const g = Math.round(ag + (bg - ag) * amount);
|
|
32
|
+
const b2 = Math.round(ab + (bb - ab) * amount);
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
'#' +
|
|
36
|
+
r.toString(16).padStart(2, '0') +
|
|
37
|
+
g.toString(16).padStart(2, '0') +
|
|
38
|
+
b2.toString(16).padStart(2, '0')
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Return black or white depending on which has better contrast.
|
|
44
|
+
*
|
|
45
|
+
* @param {string} hex
|
|
46
|
+
* Background color as "#rrggbb".
|
|
47
|
+
*
|
|
48
|
+
* @returns {string}
|
|
49
|
+
* "#000000" or "#ffffff".
|
|
50
|
+
*/
|
|
51
|
+
export function contrastColor(hex) {
|
|
52
|
+
const value = hex.replace('#', '');
|
|
53
|
+
|
|
54
|
+
const r = parseInt(value.slice(0, 2), 16) / 255;
|
|
55
|
+
const g = parseInt(value.slice(2, 4), 16) / 255;
|
|
56
|
+
const b = parseInt(value.slice(4, 6), 16) / 255;
|
|
57
|
+
|
|
58
|
+
const luminance =
|
|
59
|
+
0.2126 * toLinear(r) +
|
|
60
|
+
0.7152 * toLinear(g) +
|
|
61
|
+
0.0722 * toLinear(b);
|
|
62
|
+
|
|
63
|
+
return luminance > 0.179 ? '#000000' : '#ffffff';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Convert sRGB channel to linear RGB.
|
|
68
|
+
*
|
|
69
|
+
* @param {number} channel
|
|
70
|
+
* sRGB channel from 0 to 1.
|
|
71
|
+
*
|
|
72
|
+
* @returns {number}
|
|
73
|
+
* Linear RGB channel.
|
|
74
|
+
*/
|
|
75
|
+
export function toLinear(channel) {
|
|
76
|
+
if (channel <= 0.03928) {
|
|
77
|
+
return channel / 12.92;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return ((channel + 0.055) / 1.055) ** 2.4;
|
|
81
|
+
}
|
|
82
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Wesley Schwengle <wesleys@opperschaap.net>
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later WITH LicenseRef-OPNDEV-exceptions
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Format a numeric price for display.
|
|
7
|
+
*
|
|
8
|
+
* @param {number|string} price
|
|
9
|
+
* @returns {string}
|
|
10
|
+
*/
|
|
11
|
+
export function formatPrice(price) {
|
|
12
|
+
const num = parseFloat(price);
|
|
13
|
+
|
|
14
|
+
if (Number.isNaN(num)) {
|
|
15
|
+
return '';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return num % 1 === 0 ? num.toString() : num.toFixed(2);
|
|
19
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Wesley Schwengle <wesleys@opperschaap.net>
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later WITH LicenseRef-OPNDEV-exceptions
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Pick a different header image every day
|
|
7
|
+
*
|
|
8
|
+
*
|
|
9
|
+
* const headerImages = [
|
|
10
|
+
* require('@/assets/images/main.webp'),
|
|
11
|
+
* require('@/assets/images/main-2.webp'),
|
|
12
|
+
* require('@/assets/images/main-3.webp'),
|
|
13
|
+
* ];
|
|
14
|
+
*
|
|
15
|
+
* @param {images} price
|
|
16
|
+
* @returns {image}
|
|
17
|
+
*/
|
|
18
|
+
export function pickDailyImage(images) {
|
|
19
|
+
const now = new Date();
|
|
20
|
+
const start = new Date(now.getFullYear(), 0, 0);
|
|
21
|
+
const day = Math.floor((now - start) / 86400000);
|
|
22
|
+
|
|
23
|
+
return images[day % images.length];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Pick a random image.
|
|
29
|
+
*
|
|
30
|
+
* @param {Array} images
|
|
31
|
+
* List of static require() image values.
|
|
32
|
+
*
|
|
33
|
+
* @returns {*}
|
|
34
|
+
* Selected image.
|
|
35
|
+
*/
|
|
36
|
+
export function pickRandomImage(images) {
|
|
37
|
+
return images[Math.floor(Math.random() * images.length)];
|
|
38
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Wesley Schwengle <wesleys@opperschaap.net>
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later WITH LicenseRef-OPNDEV-exceptions
|
|
4
|
+
|
|
5
|
+
import { Linking, Platform } from 'react-native';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Open a plain URL.
|
|
9
|
+
*/
|
|
10
|
+
export async function openUrl(url) {
|
|
11
|
+
if (!url) return false;
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
await Linking.openURL(url);
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
catch (e) {
|
|
18
|
+
console.error('openUrl failed:', e);
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Open an app via deep link with fallback to store.
|
|
25
|
+
*
|
|
26
|
+
* @param {object} opts
|
|
27
|
+
* @param {string} [opts.url] Deep link (optional)
|
|
28
|
+
* @param {string} [opts.packageName] Android package
|
|
29
|
+
* @param {string} [opts.appStoreUrl] iOS fallback
|
|
30
|
+
*/
|
|
31
|
+
export async function openApp({
|
|
32
|
+
url,
|
|
33
|
+
packageName,
|
|
34
|
+
appStoreUrl,
|
|
35
|
+
}) {
|
|
36
|
+
try {
|
|
37
|
+
if (Platform.OS === 'android') {
|
|
38
|
+
if (!packageName) {
|
|
39
|
+
if (url) return openUrl(url);
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const appUrl = url || `${packageName}://`;
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const canOpen = await Linking.canOpenURL(appUrl);
|
|
47
|
+
|
|
48
|
+
if (canOpen) {
|
|
49
|
+
await Linking.openURL(appUrl);
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch (_) {
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// fallback → Play Store
|
|
57
|
+
await Linking.openURL(
|
|
58
|
+
`market://details?id=${packageName}`
|
|
59
|
+
);
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (Platform.OS === 'ios') {
|
|
64
|
+
if (url) {
|
|
65
|
+
await Linking.openURL(url);
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (appStoreUrl) {
|
|
70
|
+
await Linking.openURL(appStoreUrl);
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
catch (e) {
|
|
78
|
+
console.error('openApp failed:', e);
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Resolve + open whatever makes sense.
|
|
85
|
+
*
|
|
86
|
+
* Use this as your "one call".
|
|
87
|
+
*/
|
|
88
|
+
export async function openExternal(opts) {
|
|
89
|
+
if (opts.packageName || opts.appStoreUrl) {
|
|
90
|
+
return openApp(opts);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return openUrl(opts.url);
|
|
94
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"author": {
|
|
3
|
+
"email": "wesley@opndev.io",
|
|
4
|
+
"name": "Wesley Schwengle"
|
|
5
|
+
},
|
|
6
|
+
"description": "React Native code for events",
|
|
7
|
+
"devDependencies": {
|
|
8
|
+
"jsdoc": "latest",
|
|
9
|
+
"tap": "latest"
|
|
10
|
+
},
|
|
11
|
+
"exports": {
|
|
12
|
+
".": "./lib/index.js",
|
|
13
|
+
"./components": "./lib/components.js",
|
|
14
|
+
"./globals": "./lib/screen-registery.js",
|
|
15
|
+
"./hero-screen-registry": "./lib/hero-screen-registery.js",
|
|
16
|
+
"./lite": "./lib/hero-screen-registery.js",
|
|
17
|
+
"./notifications": "./lib/notifications.js",
|
|
18
|
+
"./notifications-fcm": "./lib/notifications/fcm.js",
|
|
19
|
+
"./screen-registery": "./lib/screen-registery.js",
|
|
20
|
+
"./screens": "./lib/screens.js"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [],
|
|
23
|
+
"license": "GPL-3.0-or-later WITH LicenseRef-OPNDEV-exceptions",
|
|
24
|
+
"main": "lib/index.js",
|
|
25
|
+
"name": "@opndev/react-native-events",
|
|
26
|
+
"private": false,
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "rzil build",
|
|
29
|
+
"jsdoc": "jsdoc -c .jsdoc.json",
|
|
30
|
+
"pkg": "rzil pkg",
|
|
31
|
+
"release": "rzil release",
|
|
32
|
+
"test": "tap"
|
|
33
|
+
},
|
|
34
|
+
"sideEffects": false,
|
|
35
|
+
"type": "module",
|
|
36
|
+
"version": "0.0.10"
|
|
37
|
+
}
|