Updating lnvpn (#160)
* upating vpn countries list data to new endpoint * update durations text * updating vpn api endpoints + new response * fixing vpn duration picker sizing * standardize file hash name by hashing config data * updated purchase style * using flag insted of name
This commit is contained in:
@@ -6,16 +6,13 @@ import VPNDurationSlider from './components/durationSlider';
|
||||
import CustomButton from '../../../../../functions/CustomElements/button';
|
||||
import FullLoadingScreen from '../../../../../functions/CustomElements/loadingScreen';
|
||||
import {useNavigation} from '@react-navigation/native';
|
||||
import {SATSPERBITCOIN} from '../../../../../constants/math';
|
||||
import GeneratedFile from './pages/generatedFile';
|
||||
import {encriptMessage} from '../../../../../functions/messaging/encodingAndDecodingMessages';
|
||||
import {useGlobalAppData} from '../../../../../../context-store/appData';
|
||||
import GetThemeColors from '../../../../../hooks/themeColors';
|
||||
import {useNodeContext} from '../../../../../../context-store/nodeContext';
|
||||
import {useKeysContext} from '../../../../../../context-store/keys';
|
||||
import sendStorePayment from '../../../../../functions/apps/payments';
|
||||
import {useGlobalContextProvider} from '../../../../../../context-store/context';
|
||||
import {sparkPaymenWrapper} from '../../../../../functions/spark/payments';
|
||||
import {useSparkWallet} from '../../../../../../context-store/sparkContext';
|
||||
import {useGlobalInsets} from '../../../../../../context-store/insetsProvider';
|
||||
import {useActiveCustodyAccount} from '../../../../../../context-store/activeAccount';
|
||||
@@ -24,13 +21,13 @@ import CountryFlag from 'react-native-country-flag';
|
||||
import {useGlobalThemeContext} from '../../../../../../context-store/theme';
|
||||
import {decode} from 'bolt11';
|
||||
|
||||
export default function VPNPlanPage({countryList}) {
|
||||
export default function VPNPlanPage({vpnInformation}) {
|
||||
const countryList = vpnInformation.countries;
|
||||
const {theme, darkModeType} = useGlobalThemeContext();
|
||||
const [searchInput, setSearchInput] = useState('');
|
||||
const {currentWalletMnemoinc} = useActiveCustodyAccount();
|
||||
const {sparkInformation} = useSparkWallet();
|
||||
const {contactsPrivateKey, publicKey} = useKeysContext();
|
||||
const {fiatStats} = useNodeContext();
|
||||
const {decodedVPNS, toggleGlobalAppDataInformation} = useGlobalAppData();
|
||||
const {masterInfoObject} = useGlobalContextProvider();
|
||||
const [selectedDuration, setSelectedDuration] = useState('week');
|
||||
@@ -47,25 +44,25 @@ export default function VPNPlanPage({countryList}) {
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
if (item.country === searchInput) {
|
||||
if (item.name === searchInput) {
|
||||
setSearchInput('');
|
||||
return;
|
||||
}
|
||||
setSearchInput(item.country);
|
||||
setSearchInput(item.name);
|
||||
}}
|
||||
style={[
|
||||
styles.countryItem,
|
||||
{
|
||||
borderWidth: 2,
|
||||
borderColor:
|
||||
searchInput === item.country
|
||||
searchInput === item.name
|
||||
? theme && darkModeType
|
||||
? COLORS.darkModeText
|
||||
: COLORS.primary
|
||||
: 'transparent',
|
||||
},
|
||||
]}
|
||||
key={item.country}>
|
||||
key={item.name}>
|
||||
<CountryFlag
|
||||
style={{marginBottom: 5, borderRadius: 8}}
|
||||
size={50}
|
||||
@@ -74,7 +71,7 @@ export default function VPNPlanPage({countryList}) {
|
||||
<ThemeText
|
||||
CustomNumberOfLines={1}
|
||||
styles={styles.countryText}
|
||||
content={item.country
|
||||
content={item.name
|
||||
.replace(/[\u{1F1E6}-\u{1F1FF}]{2}\s*/gu, '')
|
||||
.replace(/-/g, ' ')}
|
||||
/>
|
||||
@@ -86,7 +83,7 @@ export default function VPNPlanPage({countryList}) {
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
const didAddLocation = countryList.filter(item => {
|
||||
return item.country === searchInput;
|
||||
return item.name === searchInput;
|
||||
});
|
||||
|
||||
if (didAddLocation.length === 0) {
|
||||
@@ -97,27 +94,13 @@ export default function VPNPlanPage({countryList}) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [{cc, country}] = didAddLocation;
|
||||
|
||||
const cost = Math.round(
|
||||
(SATSPERBITCOIN / fiatStats.value) *
|
||||
(selectedDuration === 'hour'
|
||||
? 0.1
|
||||
: selectedDuration === 'day'
|
||||
? 0.5
|
||||
: selectedDuration === 'week'
|
||||
? 1.5
|
||||
: selectedDuration === 'month'
|
||||
? 4
|
||||
: 9),
|
||||
);
|
||||
const [{name}] = didAddLocation;
|
||||
|
||||
navigate.navigate('CustomHalfModal', {
|
||||
wantedContent: 'confirmVPN',
|
||||
country: country,
|
||||
country: name,
|
||||
duration: selectedDuration,
|
||||
createVPN: createVPN,
|
||||
price: cost,
|
||||
sliderHight: 0.5,
|
||||
});
|
||||
}, [countryList, navigate, selectedDuration, createVPN]);
|
||||
@@ -148,6 +131,7 @@ export default function VPNPlanPage({countryList}) {
|
||||
<VPNDurationSlider
|
||||
setSelectedDuration={setSelectedDuration}
|
||||
selectedDuration={selectedDuration}
|
||||
vpnInformation={vpnInformation}
|
||||
/>
|
||||
|
||||
<FlatList
|
||||
@@ -161,6 +145,7 @@ export default function VPNPlanPage({countryList}) {
|
||||
data={countryList}
|
||||
renderItem={flatListElement}
|
||||
keyExtractor={item => item.isoCode}
|
||||
showsVerticalScrollIndicator={false}
|
||||
/>
|
||||
|
||||
<CustomButton
|
||||
@@ -177,77 +162,31 @@ export default function VPNPlanPage({countryList}) {
|
||||
setIsPaying(true);
|
||||
let savedVPNConfigs = JSON.parse(JSON.stringify(decodedVPNS));
|
||||
|
||||
const [{cc, country}] = countryList.filter(item => {
|
||||
return item.country === searchInput;
|
||||
const [{code, name, isoCode}] = countryList.filter(item => {
|
||||
return item.name === searchInput;
|
||||
});
|
||||
|
||||
try {
|
||||
let invoice = '';
|
||||
let invoice = invoiceInformation;
|
||||
|
||||
if (
|
||||
invoiceInformation.payment_request &&
|
||||
invoiceInformation.payment_hash
|
||||
invoice.payment_hash &&
|
||||
invoice.payment_request &&
|
||||
invoice.paymentIdentifier
|
||||
) {
|
||||
invoice = invoiceInformation;
|
||||
} else {
|
||||
const response = await fetch('https://lnvpn.net/api/v1/getInvoice', {
|
||||
method: 'POST',
|
||||
body: new URLSearchParams({
|
||||
duration:
|
||||
selectedDuration === 'hour'
|
||||
? 0.1
|
||||
: selectedDuration === 'day'
|
||||
? 0.5
|
||||
: selectedDuration === 'week'
|
||||
? 1.5
|
||||
: selectedDuration === 'month'
|
||||
? 4
|
||||
: 9,
|
||||
}).toString(),
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
});
|
||||
const responseData = await response.json();
|
||||
const cost = Math.round(
|
||||
(SATSPERBITCOIN / fiatStats.value) *
|
||||
(selectedDuration === 'week'
|
||||
? 1.5
|
||||
: selectedDuration === 'month'
|
||||
? 4
|
||||
: 9),
|
||||
);
|
||||
|
||||
const fee = await sparkPaymenWrapper({
|
||||
getFee: true,
|
||||
address: responseData.payment_request,
|
||||
paymentType: 'lightning',
|
||||
amountSats: cost,
|
||||
masterInfoObject,
|
||||
sparkInformation,
|
||||
userBalance: sparkInformation.balance,
|
||||
mnemonic: currentWalletMnemoinc,
|
||||
});
|
||||
if (!fee.didWork) throw new Error(fee.error);
|
||||
invoice = {
|
||||
...responseData,
|
||||
supportFee: fee.supportFee,
|
||||
fee: fee.fee,
|
||||
};
|
||||
}
|
||||
|
||||
if (invoice.payment_hash && invoice.payment_request) {
|
||||
savedVPNConfigs.push({
|
||||
payment_hash: invoice.payment_hash,
|
||||
payment_request: invoice.payment_request,
|
||||
createdTime: new Date(),
|
||||
duration: selectedDuration,
|
||||
country: country,
|
||||
});
|
||||
setLoadingMessage(
|
||||
t('apps.VPN.VPNPlanPage.payingInvoiceLoadingMessage'),
|
||||
);
|
||||
savedVPNConfigs.push({
|
||||
payment_hash: invoice.payment_hash,
|
||||
payment_request: invoice.payment_request,
|
||||
paymentIdentifier: invoice.paymentIdentifier,
|
||||
createdTime: new Date(),
|
||||
duration: selectedDuration,
|
||||
country: name,
|
||||
countryCode: code,
|
||||
isoCode: isoCode,
|
||||
});
|
||||
saveVPNConfigsToDB(savedVPNConfigs);
|
||||
const parsedInvoice = decode(invoice.payment_request);
|
||||
|
||||
@@ -276,7 +215,8 @@ export default function VPNPlanPage({countryList}) {
|
||||
}
|
||||
getVPNConfig({
|
||||
paymentHash: invoice.payment_hash,
|
||||
location: cc,
|
||||
paymentIdentifier: invoice.paymentIdentifier,
|
||||
location: code,
|
||||
savedVPNConfigs,
|
||||
});
|
||||
} else {
|
||||
@@ -293,9 +233,14 @@ export default function VPNPlanPage({countryList}) {
|
||||
}
|
||||
}
|
||||
|
||||
async function getVPNConfig({paymentHash, location, savedVPNConfigs}) {
|
||||
async function getVPNConfig({
|
||||
paymentHash,
|
||||
paymentIdentifier,
|
||||
location,
|
||||
savedVPNConfigs,
|
||||
}) {
|
||||
let didSettleInvoice = false;
|
||||
let runCount = 0;
|
||||
let runCount = 1;
|
||||
|
||||
while (!didSettleInvoice && runCount < 10) {
|
||||
try {
|
||||
@@ -307,38 +252,45 @@ export default function VPNPlanPage({countryList}) {
|
||||
);
|
||||
|
||||
runCount += 1;
|
||||
const response = await fetch(
|
||||
'https://lnvpn.net/api/v1/getTunnelConfig',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
paymentHash,
|
||||
location: `${location}`,
|
||||
partnerCode: 'BlitzWallet',
|
||||
}).toString(),
|
||||
const apiResponse = await fetch(process.env.LNVPN_CONFIG_DOWNLOAD, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: '*/*',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
);
|
||||
body: JSON.stringify({
|
||||
paymentIdentifier: paymentIdentifier,
|
||||
paymentMethod: 'lightning',
|
||||
country: location,
|
||||
partnerCode: 'BlitzWallet',
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
const contentType = apiResponse.headers.get('content-type');
|
||||
|
||||
if (data.WireguardConfig) {
|
||||
didSettleInvoice = true;
|
||||
setGeneratedFile(data.WireguardConfig);
|
||||
|
||||
const updatedList = savedVPNConfigs.map(item => {
|
||||
if (item.payment_hash === paymentHash) {
|
||||
return {...item, config: data.WireguardConfig};
|
||||
} else return item;
|
||||
});
|
||||
saveVPNConfigsToDB(updatedList);
|
||||
let dataResponse;
|
||||
if (contentType && contentType.includes('application/json')) {
|
||||
dataResponse = await apiResponse.json();
|
||||
} else {
|
||||
console.log('Wating for confirmation...');
|
||||
await new Promise(resolve => setTimeout(resolve, 12000));
|
||||
dataResponse = await apiResponse.text();
|
||||
}
|
||||
|
||||
if (dataResponse?.error || apiResponse.status !== 200)
|
||||
throw new Error('Error with backend', dataResponse?.error);
|
||||
|
||||
didSettleInvoice = true;
|
||||
const configFile =
|
||||
typeof dataResponse === 'string'
|
||||
? dataResponse
|
||||
: dataResponse.data.config; // make sure to match with actual api
|
||||
|
||||
const updatedList = savedVPNConfigs.map(item => {
|
||||
if (item.payment_hash === paymentHash) {
|
||||
return {...item, config: configFile};
|
||||
} else return item;
|
||||
});
|
||||
await saveVPNConfigsToDB(updatedList);
|
||||
setGeneratedFile(configFile);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
console.log('Wating for confirmation...');
|
||||
@@ -350,7 +302,6 @@ export default function VPNPlanPage({countryList}) {
|
||||
errorMessage: t('apps.VPN.VPNPlanPage.configError'),
|
||||
});
|
||||
setIsPaying(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,34 +31,31 @@ export default function ConfirmVPNPage(props) {
|
||||
useEffect(() => {
|
||||
async function fetchInvoice() {
|
||||
try {
|
||||
const response = await fetch('https://lnvpn.net/api/v1/getInvoice', {
|
||||
const response = await fetch(process.env.LNVPN_PURCHASE_REQUEST, {
|
||||
method: 'POST',
|
||||
body: new URLSearchParams({
|
||||
duration:
|
||||
duration === 'hour'
|
||||
? 0.1
|
||||
: duration === 'day'
|
||||
? 0.5
|
||||
: duration === 'week'
|
||||
? 1.5
|
||||
: duration === 'month'
|
||||
? 4
|
||||
: 9,
|
||||
}).toString(),
|
||||
body: JSON.stringify({
|
||||
duration: duration,
|
||||
paymentMethod: 'lightning',
|
||||
refCode: 'BlitzWallet',
|
||||
}),
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Accept: '*/*',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
const invoice = await response.json();
|
||||
if (!invoice || !invoice.payment_hash || !invoice.payment_request)
|
||||
|
||||
if (!invoice.success)
|
||||
throw new Error(t('apps.VPN.confirmationSlideUp.invoiceInfoError'));
|
||||
const {data} = invoice;
|
||||
if (!data.payment_hash || !data.payment_request)
|
||||
throw new Error(t('apps.VPN.confirmationSlideUp.invoiceInfoError'));
|
||||
|
||||
const parsedInvoice = decode(invoice.payment_request);
|
||||
const parsedInvoice = decode(data.payment_request);
|
||||
|
||||
const fee = await sparkPaymenWrapper({
|
||||
getFee: true,
|
||||
address: invoice.payment_request,
|
||||
address: data.payment_request,
|
||||
paymentType: 'lightning',
|
||||
amountSats: parsedInvoice.satoshis,
|
||||
masterInfoObject,
|
||||
@@ -74,8 +71,9 @@ export default function ConfirmVPNPage(props) {
|
||||
}
|
||||
|
||||
setInvoiceInformation({
|
||||
payment_hash: invoice.payment_hash,
|
||||
payment_request: invoice.payment_request,
|
||||
payment_hash: data.payment_hash,
|
||||
payment_request: data.payment_request,
|
||||
paymentIdentifier: data.paymentIdentifier,
|
||||
supportFee: fee.supportFee,
|
||||
fee: fee.fee,
|
||||
price: parsedInvoice.satoshis,
|
||||
@@ -121,7 +119,7 @@ export default function ConfirmVPNPage(props) {
|
||||
/>
|
||||
<ThemeText
|
||||
styles={{marginTop: 5}}
|
||||
content={'1 ' + t(`constants.${duration.toLowerCase()}`)}
|
||||
content={'1 ' + t(`apps.VPN.durationSlider.${duration}`)}
|
||||
/>
|
||||
<FormattedSatText
|
||||
neverHideBalance={true}
|
||||
|
||||
@@ -9,76 +9,49 @@ import {useTranslation} from 'react-i18next';
|
||||
export default function VPNDurationSlider({
|
||||
setSelectedDuration,
|
||||
selectedDuration,
|
||||
vpnInformation,
|
||||
}) {
|
||||
const {theme, darkModeType} = useGlobalThemeContext();
|
||||
const {textColor} = GetThemeColors();
|
||||
const {t} = useTranslation();
|
||||
const durations = vpnInformation.durations || [];
|
||||
|
||||
const durationOption = useMemo(() => {
|
||||
return [
|
||||
[
|
||||
t('apps.VPN.durationSlider.durationOption', {
|
||||
duration: t('constants.hour'),
|
||||
}),
|
||||
'hour',
|
||||
],
|
||||
[
|
||||
t('apps.VPN.durationSlider.durationOption', {
|
||||
duration: t('constants.day'),
|
||||
}),
|
||||
'day',
|
||||
],
|
||||
[
|
||||
t('apps.VPN.durationSlider.durationOption', {
|
||||
duration: t('constants.week'),
|
||||
}),
|
||||
'week',
|
||||
],
|
||||
[
|
||||
t('apps.VPN.durationSlider.durationOption', {
|
||||
duration: t('constants.month'),
|
||||
}),
|
||||
'month',
|
||||
],
|
||||
[
|
||||
t('apps.VPN.durationSlider.durationOption', {
|
||||
duration: t('constants.quarter'),
|
||||
}),
|
||||
'quarter',
|
||||
],
|
||||
].map(item => {
|
||||
const [name, itemSelector] = item;
|
||||
return durations.map(item => {
|
||||
const {duration} = item;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={() => setSelectedDuration(itemSelector)}
|
||||
onPress={() => setSelectedDuration(duration)}
|
||||
style={{
|
||||
...styles.durationButton,
|
||||
borderColor: theme ? COLORS.darkModeText : COLORS.primary,
|
||||
backgroundColor:
|
||||
selectedDuration === itemSelector
|
||||
selectedDuration === duration
|
||||
? theme
|
||||
? COLORS.darkModeText
|
||||
: COLORS.primary
|
||||
: 'transparent',
|
||||
}}
|
||||
key={name}>
|
||||
key={duration}>
|
||||
<ThemeText
|
||||
styles={{
|
||||
padding: 10,
|
||||
color:
|
||||
selectedDuration === itemSelector
|
||||
selectedDuration === duration
|
||||
? theme
|
||||
? COLORS.lightModeText
|
||||
: COLORS.darkModeText
|
||||
: textColor,
|
||||
includeFontPadding: false,
|
||||
}}
|
||||
content={name}
|
||||
content={t('apps.VPN.durationSlider.durationOption', {
|
||||
duration: t(`apps.VPN.durationSlider.${duration}`),
|
||||
})}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
});
|
||||
}, [selectedDuration, theme]);
|
||||
}, [selectedDuration, theme, durations]);
|
||||
|
||||
return (
|
||||
<View style={styles.durationContainer}>
|
||||
@@ -86,7 +59,13 @@ export default function VPNDurationSlider({
|
||||
styles={{...styles.infoHeaders}}
|
||||
content={t('apps.VPN.durationSlider.duration')}
|
||||
/>
|
||||
<View style={styles.durationInnerContianer}>{durationOption}</View>
|
||||
<View style={styles.durationInnerContianer}>
|
||||
{durationOption.length ? (
|
||||
durationOption
|
||||
) : (
|
||||
<ThemeText content={t('apps.VPN.durationSlider.noDurations')} />
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -97,14 +76,14 @@ const styles = StyleSheet.create({
|
||||
alignItems: 'center',
|
||||
},
|
||||
durationInnerContianer: {
|
||||
columnGap: 10,
|
||||
rowGap: 10,
|
||||
gap: 10,
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
},
|
||||
durationButton: {
|
||||
borderWidth: 2,
|
||||
flexGrow: 1,
|
||||
minWidth: '45%',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: 8,
|
||||
|
||||
@@ -3,8 +3,8 @@ import {
|
||||
GlobalThemeView,
|
||||
ThemeText,
|
||||
} from '../../../../../functions/CustomElements';
|
||||
import {WINDOWWIDTH} from '../../../../../constants/theme';
|
||||
import {CENTER} from '../../../../../constants';
|
||||
import {COLORS, SIZES} from '../../../../../constants/theme';
|
||||
import {CENTER, CONTENT_KEYBOARD_OFFSET} from '../../../../../constants';
|
||||
import {useNavigation} from '@react-navigation/native';
|
||||
import {useEffect, useState} from 'react';
|
||||
import {copyToClipboard, getLocalStorageItem} from '../../../../../functions';
|
||||
@@ -17,6 +17,9 @@ import FullLoadingScreen from '../../../../../functions/CustomElements/loadingSc
|
||||
import openWebBrowser from '../../../../../functions/openWebBrowser';
|
||||
import {useToast} from '../../../../../../context-store/toastManager';
|
||||
import {useTranslation} from 'react-i18next';
|
||||
import {useGlobalThemeContext} from '../../../../../../context-store/theme';
|
||||
import GetThemeColors from '../../../../../hooks/themeColors';
|
||||
import CountryFlag from 'react-native-country-flag';
|
||||
|
||||
export default function HistoricalVPNPurchases() {
|
||||
const {showToast} = useToast();
|
||||
@@ -26,6 +29,8 @@ export default function HistoricalVPNPurchases() {
|
||||
const {contactsPrivateKey, publicKey} = useKeysContext();
|
||||
const [isRetryingConfig, setIsRetryingConfig] = useState(false);
|
||||
const {t} = useTranslation();
|
||||
const {theme} = useGlobalThemeContext();
|
||||
const {backgroundOffset, backgroundColor} = GetThemeColors();
|
||||
|
||||
useEffect(() => {
|
||||
async function getSavedPurchases() {
|
||||
@@ -43,7 +48,10 @@ export default function HistoricalVPNPurchases() {
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={item.createdTime}
|
||||
style={styles.container}
|
||||
style={[
|
||||
styles.purchaseCard,
|
||||
{backgroundColor: theme ? backgroundOffset : COLORS.darkModeText},
|
||||
]}
|
||||
onPress={() => handleConfigClick(item)}
|
||||
onLongPress={() => {
|
||||
navigate.navigate('ConfirmActionPage', {
|
||||
@@ -53,86 +61,95 @@ export default function HistoricalVPNPurchases() {
|
||||
confirmFunction: () => removeVPNFromList(item.payment_hash),
|
||||
});
|
||||
}}>
|
||||
<View style={styles.infoContainer}>
|
||||
<View style={styles.cardHeader}>
|
||||
<View style={[styles.countryBadge, {backgroundColor}]}>
|
||||
{item.isoCode ? (
|
||||
<CountryFlag size={15} isoCode={item.isoCode} />
|
||||
) : (
|
||||
<ThemeText
|
||||
styles={styles.dateText}
|
||||
content={
|
||||
item.country
|
||||
?.replace(/[\u{1F1E6}-\u{1F1FF}]{2}\s*/gu, '')
|
||||
?.replace(/-/g, ' ') || ''
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<ThemeText
|
||||
styles={{...styles.label}}
|
||||
content={t('apps.VPN.historicalPurchasesPage.country')}
|
||||
/>
|
||||
<ThemeText styles={{...styles.value}} content={item.country} />
|
||||
</View>
|
||||
<View style={styles.infoContainer}>
|
||||
<ThemeText
|
||||
styles={{...styles.label}}
|
||||
content={t('apps.VPN.historicalPurchasesPage.createdAt')}
|
||||
/>
|
||||
<ThemeText
|
||||
styles={{...styles.value}}
|
||||
content={new Date(item.createdTime).toLocaleString()}
|
||||
styles={styles.dateText}
|
||||
content={new Date(item.createdTime).toLocaleDateString()}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.infoContainer}>
|
||||
|
||||
<View style={styles.durationContainer}>
|
||||
<ThemeText
|
||||
styles={{...styles.label}}
|
||||
content={t('apps.VPN.historicalPurchasesPage.duration')}
|
||||
/>
|
||||
<ThemeText
|
||||
styles={{...styles.value}}
|
||||
content={t(t(`constants.${item.duration?.toLowerCase()}`))}
|
||||
styles={styles.durationText}
|
||||
content={
|
||||
typeof item.duration === 'string'
|
||||
? t(`constants.${item.duration?.toLowerCase()}`)
|
||||
: t(`apps.VPN.durationSlider.${item.duration}`)
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
copyToClipboard(item.payment_hash, showToast);
|
||||
}}
|
||||
style={styles.infoContainer}>
|
||||
style={styles.hashContainer}>
|
||||
<ThemeText
|
||||
styles={{...styles.label}}
|
||||
styles={styles.hashLabel}
|
||||
content={t('apps.VPN.historicalPurchasesPage.paymentHash')}
|
||||
/>
|
||||
<ThemeText
|
||||
CustomNumberOfLines={2}
|
||||
styles={{...styles.value}}
|
||||
content={`${item.payment_hash}`}
|
||||
/>
|
||||
<View style={[styles.hashBox, {backgroundColor}]}>
|
||||
<ThemeText
|
||||
CustomNumberOfLines={1}
|
||||
styles={styles.hashText}
|
||||
content={`${item.payment_hash}`}
|
||||
/>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
return (
|
||||
<GlobalThemeView>
|
||||
<View style={styles.globalContainer}>
|
||||
<CustomSettingsTopBar
|
||||
containerStyles={{
|
||||
marginBottom: 0,
|
||||
}}
|
||||
label={t('apps.VPN.historicalPurchasesPage.title')}
|
||||
<GlobalThemeView useStandardWidth={true}>
|
||||
<CustomSettingsTopBar
|
||||
containerStyles={styles.topBarStyle}
|
||||
label={t('apps.VPN.historicalPurchasesPage.title')}
|
||||
/>
|
||||
|
||||
{isRetryingConfig ? (
|
||||
<FullLoadingScreen
|
||||
text={t('apps.VPN.historicalPurchasesPage.retryClaim')}
|
||||
/>
|
||||
{isRetryingConfig ? (
|
||||
<FullLoadingScreen
|
||||
text={t('apps.VPN.historicalPurchasesPage.retryClaim')}
|
||||
) : purchaseElements.length === 0 ? (
|
||||
<View style={styles.emptyState}>
|
||||
<ThemeText
|
||||
styles={styles.emptyText}
|
||||
content={t('apps.VPN.historicalPurchasesPage.noPurchases')}
|
||||
/>
|
||||
) : purchaseElements.length === 0 ? (
|
||||
<View
|
||||
style={{flex: 1, alignItems: 'center', justifyContent: 'center'}}>
|
||||
</View>
|
||||
) : (
|
||||
<>
|
||||
<ScrollView
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={styles.scrollContainer}
|
||||
style={styles.scrollView}>
|
||||
{purchaseElements}
|
||||
</ScrollView>
|
||||
|
||||
<View style={styles.bottomSection}>
|
||||
<ThemeText
|
||||
content={t('apps.VPN.historicalPurchasesPage.noPurchases')}
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<>
|
||||
<ScrollView
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={{paddingVertical: 30}}
|
||||
style={{width: '90%', ...CENTER}}>
|
||||
{purchaseElements}
|
||||
</ScrollView>
|
||||
<ThemeText
|
||||
styles={{textAlign: 'center', paddingTop: 5}}
|
||||
styles={styles.assistanceText}
|
||||
content={t('apps.VPN.historicalPurchasesPage.assistanceText')}
|
||||
/>
|
||||
<CustomButton
|
||||
buttonStyles={{...CENTER, marginTop: 10}}
|
||||
buttonStyles={styles.contactButton}
|
||||
textContent={t('apps.VPN.historicalPurchasesPage.contact')}
|
||||
actionFunction={async () => {
|
||||
await openWebBrowser({
|
||||
@@ -141,9 +158,9 @@ export default function HistoricalVPNPurchases() {
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</GlobalThemeView>
|
||||
);
|
||||
|
||||
@@ -154,96 +171,123 @@ export default function HistoricalVPNPurchases() {
|
||||
});
|
||||
else {
|
||||
setIsRetryingConfig(true);
|
||||
(async () => {
|
||||
const response = await getConfig(item.payment_hash, item.country);
|
||||
if (response.didWork) {
|
||||
const newCardsList = decodedVPNS
|
||||
?.map(vpn => {
|
||||
if (vpn.payment_hash === item.payment_hash) {
|
||||
return {...vpn, config: response.config};
|
||||
} else return vpn;
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
const em = encriptMessage(
|
||||
contactsPrivateKey,
|
||||
publicKey,
|
||||
JSON.stringify(newCardsList),
|
||||
);
|
||||
toggleGlobalAppDataInformation({VPNplans: em}, true);
|
||||
const response = await getConfig(
|
||||
item.paymentIdentifier || item.payment_hash,
|
||||
item.country,
|
||||
item.countryCode,
|
||||
);
|
||||
if (response.didWork) {
|
||||
const newCardsList = decodedVPNS
|
||||
?.map(vpn => {
|
||||
if (vpn.payment_hash === item.payment_hash) {
|
||||
return {...vpn, config: response.config};
|
||||
} else return vpn;
|
||||
})
|
||||
.filter(Boolean);
|
||||
const em = encriptMessage(
|
||||
contactsPrivateKey,
|
||||
publicKey,
|
||||
JSON.stringify(newCardsList),
|
||||
);
|
||||
toggleGlobalAppDataInformation({VPNplans: em}, true);
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
navigate.navigate('GeneratedVPNFile', {
|
||||
generatedFile: response.config,
|
||||
});
|
||||
navigate.navigate('GeneratedVPNFile', {
|
||||
generatedFile: response.config,
|
||||
});
|
||||
});
|
||||
setIsRetryingConfig(false);
|
||||
return;
|
||||
}
|
||||
});
|
||||
setIsRetryingConfig(false);
|
||||
} else {
|
||||
setIsRetryingConfig(false);
|
||||
navigate.navigate('ErrorScreen', {
|
||||
errorMessage: response.error,
|
||||
});
|
||||
})();
|
||||
}
|
||||
}
|
||||
}
|
||||
async function getConfig(paymentHash, location) {
|
||||
|
||||
async function getConfig(payment_hash, location, countryCode) {
|
||||
try {
|
||||
const countriesListResponse = await fetch(
|
||||
'https://lnvpn.net/api/v1/countryList',
|
||||
{
|
||||
method: 'GET',
|
||||
},
|
||||
);
|
||||
|
||||
const countriesList = await countriesListResponse.json();
|
||||
console.log(countriesList);
|
||||
const [{cc}] = countriesList.filter(item => {
|
||||
console.log(item.country, location);
|
||||
return isCountryMatch(item.country, location);
|
||||
});
|
||||
console.log(cc);
|
||||
if (!cc) {
|
||||
return {
|
||||
didWork: false,
|
||||
error: t('apps.VPN.historicalPurchasesPage.noValidCountryCodeError'),
|
||||
};
|
||||
let countryCodeIdentifier = '';
|
||||
if (!countryCode) {
|
||||
const countriesListResponse = await fetch(
|
||||
process.env.LNVPN_COUNTRY_LIST,
|
||||
{
|
||||
method: 'GET',
|
||||
},
|
||||
);
|
||||
const countriesList = await countriesListResponse.json();
|
||||
if (
|
||||
countriesListResponse.status !== 200 ||
|
||||
!countriesList?.data?.countries
|
||||
) {
|
||||
return {
|
||||
didWork: false,
|
||||
error: t(
|
||||
'apps.VPN.historicalPurchasesPage.noValidCountryCodeError',
|
||||
),
|
||||
};
|
||||
}
|
||||
const [{code}] = countriesList.data.countries.filter(item => {
|
||||
console.log(item, location);
|
||||
return isCountryMatch(item.name, location);
|
||||
});
|
||||
if (!code) {
|
||||
return {
|
||||
didWork: false,
|
||||
error: t(
|
||||
'apps.VPN.historicalPurchasesPage.noValidCountryCodeError',
|
||||
),
|
||||
};
|
||||
}
|
||||
countryCodeIdentifier = code;
|
||||
} else {
|
||||
countryCodeIdentifier = countryCode;
|
||||
}
|
||||
|
||||
const response = await fetch('https://lnvpn.net/api/v1/getTunnelConfig', {
|
||||
const response = await fetch(process.env.LNVPN_CONFIG_DOWNLOAD, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Accept: '*/*',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
paymentHash,
|
||||
location: `${cc}`,
|
||||
body: JSON.stringify({
|
||||
paymentIdentifier: payment_hash,
|
||||
paymentMethod: 'lightning',
|
||||
country: countryCodeIdentifier,
|
||||
partnerCode: 'BlitzWallet',
|
||||
}).toString(),
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (!data.WireguardConfig) {
|
||||
const contentType = response.headers.get('content-type');
|
||||
let dataResponse;
|
||||
if (contentType && contentType.includes('application/json')) {
|
||||
dataResponse = await response.json();
|
||||
} else {
|
||||
dataResponse = await response.text();
|
||||
}
|
||||
if (dataResponse?.error || response.status !== 200) {
|
||||
return {
|
||||
didWork: false,
|
||||
error:
|
||||
data?.error ||
|
||||
dataResponse?.error ||
|
||||
t('apps.VPN.historicalPurchasesPage.claimConfigError'),
|
||||
};
|
||||
}
|
||||
return {didWork: true, config: data.WireguardConfig};
|
||||
const configFile =
|
||||
typeof dataResponse === 'string'
|
||||
? dataResponse
|
||||
: dataResponse.data.config;
|
||||
return {didWork: true, config: configFile};
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
return {didWork: false, error: String(err)};
|
||||
}
|
||||
}
|
||||
|
||||
function removeVPNFromList(selctedVPN) {
|
||||
const newCardsList = decodedVPNS?.filter(
|
||||
vpn => vpn.payment_hash !== selctedVPN,
|
||||
);
|
||||
|
||||
const em = encriptMessage(
|
||||
contactsPrivateKey,
|
||||
publicKey,
|
||||
@@ -252,6 +296,7 @@ export default function HistoricalVPNPurchases() {
|
||||
toggleGlobalAppDataInformation({VPNplans: em}, true);
|
||||
}
|
||||
}
|
||||
|
||||
function isCountryMatch(selected, text) {
|
||||
// Normalize by removing flags, making lowercase, and replacing hyphens with spaces
|
||||
const normalize = str =>
|
||||
@@ -259,29 +304,89 @@ function isCountryMatch(selected, text) {
|
||||
.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g, '') // Remove emoji flags
|
||||
.toLowerCase()
|
||||
.replace(/[-\s]+/g, ' '); // Normalize spaces and hyphens
|
||||
|
||||
return normalize(text).includes(normalize(selected));
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
globalContainer: {
|
||||
topBarStyle: {
|
||||
marginBottom: 0,
|
||||
},
|
||||
scrollView: {
|
||||
width: '100%',
|
||||
flex: 1,
|
||||
width: WINDOWWIDTH,
|
||||
...CENTER,
|
||||
},
|
||||
container: {
|
||||
marginVertical: 10,
|
||||
scrollContainer: {
|
||||
paddingHorizontal: 20,
|
||||
paddingTop: 24,
|
||||
paddingBottom: 100,
|
||||
},
|
||||
infoContainer: {
|
||||
|
||||
// Purchase Card Styles
|
||||
purchaseCard: {
|
||||
borderRadius: 16,
|
||||
marginBottom: 16,
|
||||
padding: 20,
|
||||
},
|
||||
cardHeader: {
|
||||
flexDirection: 'row',
|
||||
marginBottom: 10,
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 16,
|
||||
},
|
||||
label: {
|
||||
fontWeight: 'bold',
|
||||
countryBadge: {
|
||||
flexShrink: 1,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 20,
|
||||
marginRight: 10,
|
||||
},
|
||||
value: {
|
||||
countryText: {},
|
||||
dateText: {
|
||||
fontSize: SIZES.small,
|
||||
},
|
||||
durationContainer: {
|
||||
marginBottom: 16,
|
||||
},
|
||||
durationText: {
|
||||
fontSize: 14,
|
||||
},
|
||||
hashContainer: {
|
||||
marginTop: 4,
|
||||
},
|
||||
hashLabel: {
|
||||
fontSize: 12,
|
||||
marginBottom: 6,
|
||||
},
|
||||
hashBox: {
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 8,
|
||||
},
|
||||
hashText: {
|
||||
fontSize: SIZES.small,
|
||||
opacity: 0.7,
|
||||
},
|
||||
|
||||
// Empty State Styles
|
||||
emptyState: {
|
||||
flex: 1,
|
||||
flexWrap: 'wrap',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
|
||||
emptyText: {
|
||||
textAlign: 'center',
|
||||
},
|
||||
|
||||
// Bottom Section Styles
|
||||
bottomSection: {marginTop: CONTENT_KEYBOARD_OFFSET},
|
||||
assistanceText: {
|
||||
textAlign: 'center',
|
||||
opacity: 0.7,
|
||||
marginBottom: 16,
|
||||
lineHeight: 20,
|
||||
},
|
||||
contactButton: {
|
||||
...CENTER,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -29,23 +29,24 @@ export default function VPNHome() {
|
||||
const navigate = useNavigation();
|
||||
const {theme, darkModeType} = useGlobalThemeContext();
|
||||
const [selectedPage, setSelectedPage] = useState(null);
|
||||
const [countryList, setCountriesList] = useState([]);
|
||||
const [vpnInformation, setVpnInformation] = useState({
|
||||
countries: [],
|
||||
durations: [],
|
||||
});
|
||||
const {t} = useTranslation();
|
||||
useEffect(() => {
|
||||
async function getAvailableCountries() {
|
||||
try {
|
||||
const response = await fetch('https://lnvpn.net/api/v1/countryList', {
|
||||
const response = await fetch(process.env.LNVPN_COUNTRY_LIST, {
|
||||
method: 'GET',
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
setCountriesList(data);
|
||||
if (data.success) {
|
||||
setVpnInformation(data.data);
|
||||
} else throw new Error('Unable to fetch vpn information');
|
||||
} catch (err) {
|
||||
navigate.navigate('ErrorScreen', {
|
||||
errorMessage: t('apps.VPN.home.apiConnectionError'),
|
||||
customNavigator: () => {
|
||||
navigate.popTo('HomeAdmin');
|
||||
},
|
||||
});
|
||||
console.log(err);
|
||||
}
|
||||
@@ -112,15 +113,22 @@ export default function VPNHome() {
|
||||
<CustomButton
|
||||
buttonStyles={{width: '80%', marginTop: 50}}
|
||||
actionFunction={() => {
|
||||
if (!countryList.length) return;
|
||||
if (
|
||||
!vpnInformation.countries.length ||
|
||||
!vpnInformation.durations.length
|
||||
)
|
||||
return;
|
||||
setSelectedPage('Select Plan');
|
||||
}}
|
||||
textContent={t('constants.continue')}
|
||||
useLoading={!countryList.length}
|
||||
useLoading={
|
||||
!vpnInformation.countries.length ||
|
||||
!vpnInformation.durations.length
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<VPNPlanPage countryList={countryList} />
|
||||
<VPNPlanPage vpnInformation={vpnInformation} />
|
||||
)}
|
||||
</View>
|
||||
</CustomKeyboardAvoidingView>
|
||||
|
||||
@@ -17,13 +17,14 @@ import {
|
||||
INSET_WINDOW_WIDTH,
|
||||
WINDOWWIDTH,
|
||||
} from '../../../../../../constants/theme';
|
||||
import {backArrow} from '../../../../../../constants/styles';
|
||||
import GetThemeColors from '../../../../../../hooks/themeColors';
|
||||
import QrCodeWrapper from '../../../../../../functions/CustomElements/QrWrapper';
|
||||
import writeAndShareFileToFilesystem from '../../../../../../functions/writeFileToFilesystem';
|
||||
import {useToast} from '../../../../../../../context-store/toastManager';
|
||||
import {useTranslation} from 'react-i18next';
|
||||
import CustomSettingsTopBar from '../../../../../../functions/CustomElements/settingsTopBar';
|
||||
import customUUID from '../../../../../../functions/customUUID';
|
||||
import sha256Hash from '../../../../../../functions/hash';
|
||||
|
||||
export default function GeneratedVPNFile(props) {
|
||||
const generatedFile =
|
||||
@@ -52,7 +53,12 @@ function VPNFileDisplay({generatedFile}) {
|
||||
const navigate = useNavigation();
|
||||
const {backgroundOffset} = GetThemeColors();
|
||||
const {t} = useTranslation();
|
||||
console.log(generatedFile);
|
||||
console.log(generatedFile, typeof generatedFile);
|
||||
|
||||
const configData =
|
||||
typeof generatedFile === 'string'
|
||||
? generatedFile
|
||||
: generatedFile.join('\n');
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -63,9 +69,9 @@ function VPNFileDisplay({generatedFile}) {
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
copyToClipboard(generatedFile.join('\n'), showToast);
|
||||
copyToClipboard(configData, showToast);
|
||||
}}>
|
||||
<QrCodeWrapper QRData={generatedFile.join('\n')} />
|
||||
<QrCodeWrapper QRData={configData} />
|
||||
</TouchableOpacity>
|
||||
|
||||
<View style={styles.copyButtonsContainer}>
|
||||
@@ -73,14 +79,14 @@ function VPNFileDisplay({generatedFile}) {
|
||||
buttonStyles={styles.buttonContainer}
|
||||
textContent={t('constants.download')}
|
||||
actionFunction={() => {
|
||||
downloadVPNFile({generatedFile, navigate});
|
||||
downloadVPNFile({generatedFile: configData, navigate});
|
||||
}}
|
||||
/>
|
||||
<CustomButton
|
||||
buttonStyles={styles.buttonContainer}
|
||||
textContent={t('constants.copy')}
|
||||
actionFunction={() => {
|
||||
copyToClipboard(generatedFile.join('\n'), showToast);
|
||||
copyToClipboard(configData, showToast);
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
@@ -97,8 +103,9 @@ function VPNFileDisplay({generatedFile}) {
|
||||
}
|
||||
|
||||
async function downloadVPNFile({generatedFile, navigate}) {
|
||||
const content = generatedFile.join('\n');
|
||||
const fileName = `blitzVPN.conf`;
|
||||
const content = generatedFile;
|
||||
const fileHash = sha256Hash(content);
|
||||
const fileName = `blitzVPN-${fileHash?.slice(0, 8) || customUUID()}.conf`;
|
||||
|
||||
const response = await writeAndShareFileToFilesystem(
|
||||
content,
|
||||
|
||||
@@ -316,7 +316,7 @@ async function handleSupportPayment(masterInfoObject, supportFee, mnemonic) {
|
||||
});
|
||||
await Promise.race([
|
||||
txPromise,
|
||||
new Promise(res => setTimeout(res, 10000)),
|
||||
new Promise(res => setTimeout(res, 30000)),
|
||||
]);
|
||||
txPromise.catch(err =>
|
||||
console.log('Error sending support payment (late)', err),
|
||||
|
||||
@@ -290,7 +290,14 @@
|
||||
"durationSlider": {
|
||||
"durationOption": "1 {{duration}}",
|
||||
"price": "Price: ",
|
||||
"duration": "Duration"
|
||||
"noDurations": "No available durations",
|
||||
"duration": "Duration",
|
||||
"0.1": "hour",
|
||||
"0.5": "day",
|
||||
"1.5": "week",
|
||||
"3": "month",
|
||||
"8": "quarter",
|
||||
"30": "year"
|
||||
},
|
||||
"historicalPurchasesPage": {
|
||||
"deleteVPNConfirmMessage": "Are you sure you want to remove this VPN.",
|
||||
|
||||
@@ -290,7 +290,14 @@
|
||||
"durationSlider": {
|
||||
"durationOption": "1 {{duration}}",
|
||||
"price": "Prezzo: ",
|
||||
"duration": "Durata"
|
||||
"noDurations": "Nessuna durata disponibile",
|
||||
"duration": "Durata",
|
||||
"0.1": "hora",
|
||||
"0.5": "día",
|
||||
"1.5": "semana",
|
||||
"3": "mes",
|
||||
"8": "trimestre",
|
||||
"30": "año"
|
||||
},
|
||||
"historicalPurchasesPage": {
|
||||
"deleteVPNConfirmMessage": "Sei sicuro di voler rimuovere questa VPN?",
|
||||
|
||||
@@ -284,7 +284,14 @@
|
||||
"durationSlider": {
|
||||
"durationOption": "1 {{duration}}",
|
||||
"price": "Precio: ",
|
||||
"duration": "Duración"
|
||||
"noDurations": "No hay duraciones disponibles",
|
||||
"duration": "Duración",
|
||||
"0.1": "ora",
|
||||
"0.5": "giorno",
|
||||
"1.5": "settimana",
|
||||
"3": "mese",
|
||||
"8": "trimestre",
|
||||
"30": "anno"
|
||||
},
|
||||
"historicalPurchasesPage": {
|
||||
"deleteVPNConfirmMessage": "¿Estás seguro de que quieres eliminar este VPN?",
|
||||
|
||||
Reference in New Issue
Block a user