376 lines
11 KiB
JavaScript
376 lines
11 KiB
JavaScript
import React, {
|
|
createContext,
|
|
useContext,
|
|
useReducer,
|
|
useEffect,
|
|
useMemo,
|
|
useRef,
|
|
useCallback,
|
|
} from 'react';
|
|
import { useAppStatus } from './appStatus';
|
|
import {
|
|
bulkDeleteGiftsLocal,
|
|
bulkSaveGiftsLocal,
|
|
deleteGiftLocal,
|
|
getAllLocalGifts,
|
|
saveGiftLocal,
|
|
updateGiftLocal,
|
|
} from '../app/functions/gift/giftsStorage';
|
|
import {
|
|
addGiftToDatabase,
|
|
bulkAddGiftsToDatabase,
|
|
bulkDeleteGiftsFromDatabase,
|
|
deleteGift,
|
|
handleGiftCheck,
|
|
reloadGiftsOnDomesday,
|
|
updateGiftInDatabase,
|
|
} from '../db';
|
|
import { useGlobalContextProvider } from './context';
|
|
import { getLocalStorageItem, setLocalStorageItem } from '../app/functions';
|
|
import { deriveKeyFromMnemonic } from '../app/functions/seed';
|
|
import { useKeysContext } from './keys';
|
|
import { randomBytes } from 'react-native-quick-crypto';
|
|
import { getPublicKey } from 'nostr-tools';
|
|
import { encriptMessage } from '../app/functions/messaging/encodingAndDecodingMessages';
|
|
import { createGiftUrl } from '../app/functions/gift/encodeDecodeSecret';
|
|
import { deriveSparkGiftMnemonic } from '../app/functions/gift/deriveGiftWallet';
|
|
import { GIFT_DERIVE_PATH_CUTOFF } from '../app/constants';
|
|
|
|
const initialState = {
|
|
gifts: {},
|
|
currentDerivedGiftIndex: 0,
|
|
userUuid: null,
|
|
};
|
|
|
|
function giftReducer(state, action) {
|
|
switch (action.type) {
|
|
case 'LOAD_LOCAL_GIFTS':
|
|
return {
|
|
...state,
|
|
gifts: action.payload.reduce((map, g) => {
|
|
map[g.uuid] = g;
|
|
return map;
|
|
}, {}),
|
|
};
|
|
|
|
case 'ADD_OR_UPDATE_GIFT':
|
|
return {
|
|
...state,
|
|
gifts: { ...state.gifts, [action.payload.uuid]: action.payload },
|
|
};
|
|
case 'BULK_ADD_GIFTS':
|
|
return {
|
|
...state,
|
|
gifts: {
|
|
...state.gifts,
|
|
...action.payload.reduce((map, g) => {
|
|
map[g.uuid] = g;
|
|
return map;
|
|
}, {}),
|
|
},
|
|
};
|
|
case 'BULK_DELETE_GIFTS': {
|
|
const remaining = { ...state.gifts };
|
|
action.payload.forEach(uuid => delete remaining[uuid]);
|
|
return { ...state, gifts: remaining };
|
|
}
|
|
default:
|
|
return state;
|
|
}
|
|
}
|
|
|
|
const GiftContext = createContext(null);
|
|
|
|
export function GiftProvider({ children }) {
|
|
const { accountMnemoinc } = useKeysContext();
|
|
const { masterInfoObject } = useGlobalContextProvider();
|
|
const { didGetToHomepage } = useAppStatus();
|
|
const [state, dispatch] = useReducer(giftReducer, initialState);
|
|
const isCheckingRefunds = useRef(null);
|
|
|
|
const updateGiftList = useCallback(async () => {
|
|
const updatedList = await getAllLocalGifts();
|
|
dispatch({ type: 'LOAD_LOCAL_GIFTS', payload: updatedList });
|
|
return updatedList;
|
|
}, []);
|
|
|
|
const saveGiftToCloud = useCallback(async giftObj => {
|
|
try {
|
|
const localObject = JSON.parse(JSON.stringify(giftObj));
|
|
const localResponse = await saveGiftLocal(localObject);
|
|
delete giftObj.claimURL;
|
|
const serverResponse = await addGiftToDatabase(giftObj);
|
|
|
|
if (!serverResponse || !localResponse)
|
|
throw new Error('Unable to save gift');
|
|
dispatch({ type: 'ADD_OR_UPDATE_GIFT', payload: localObject });
|
|
return true;
|
|
} catch (err) {
|
|
console.log('error saving gift to cloud');
|
|
return false;
|
|
}
|
|
}, []);
|
|
|
|
const bulkSaveGiftsToCloud = useCallback(async gifts => {
|
|
try {
|
|
const localObjects = gifts.map(g => JSON.parse(JSON.stringify(g)));
|
|
const [localResponse, serverResponse] = await Promise.all([
|
|
bulkSaveGiftsLocal(localObjects),
|
|
bulkAddGiftsToDatabase(gifts),
|
|
]);
|
|
if (!localResponse || !serverResponse)
|
|
throw new Error('Unable to bulk save gifts');
|
|
dispatch({ type: 'BULK_ADD_GIFTS', payload: localObjects });
|
|
return true;
|
|
} catch (err) {
|
|
console.log('error bulk saving gifts to cloud', err);
|
|
return false;
|
|
}
|
|
}, []);
|
|
|
|
const bulkDeleteGiftsFromCloudAndLocal = useCallback(async uuids => {
|
|
try {
|
|
const [localResponse, serverResponse] = await Promise.all([
|
|
bulkDeleteGiftsLocal(uuids),
|
|
bulkDeleteGiftsFromDatabase(uuids),
|
|
]);
|
|
if (!localResponse || !serverResponse)
|
|
throw new Error('Unable to bulk delete gifts');
|
|
dispatch({
|
|
type: 'BULK_DELETE_GIFTS',
|
|
payload: uuids,
|
|
});
|
|
return true;
|
|
} catch (err) {
|
|
console.log('error bulk deleting gifts from cloud and local', err);
|
|
return false;
|
|
}
|
|
}, []);
|
|
|
|
const deleteGiftFromCloudAndLocal = useCallback(
|
|
async UUID => {
|
|
try {
|
|
await deleteGiftLocal(UUID);
|
|
const response = await deleteGift(UUID);
|
|
|
|
if (!response) throw new Error('Unable to delete gift from remote ');
|
|
await updateGiftList();
|
|
return true;
|
|
} catch (err) {
|
|
console.log('error saving gift to cloud');
|
|
return false;
|
|
}
|
|
},
|
|
[updateGiftList],
|
|
);
|
|
|
|
const checkForRefunds = useCallback(async giftList => {
|
|
try {
|
|
if (isCheckingRefunds.current) return;
|
|
isCheckingRefunds.current = true;
|
|
const localGifts = await (giftList
|
|
? Promise.resolve(giftList)
|
|
: getAllLocalGifts());
|
|
|
|
const giftArray = Object.values(localGifts);
|
|
const now = Date.now();
|
|
|
|
const expiredGifts = giftArray.filter(item => {
|
|
return item.state === 'Unclaimed' && now >= item.expireTime;
|
|
});
|
|
console.log(expiredGifts, 'expired gifts');
|
|
|
|
if (expiredGifts.length === 0) {
|
|
console.log('No expired gifts to check');
|
|
return;
|
|
}
|
|
|
|
console.log(`Checking ${expiredGifts.length} expired gifts...`);
|
|
|
|
const checkPromises = expiredGifts.map(card =>
|
|
handleGiftCheck(card.uuid)
|
|
.then(response => ({ card, response }))
|
|
.catch(error => {
|
|
console.error(`Error checking gift ${card.uuid}:`, error);
|
|
return { card, response: null };
|
|
}),
|
|
);
|
|
|
|
const results = await Promise.all(checkPromises);
|
|
|
|
// Batch database updates
|
|
const updatePromises = results
|
|
.filter(({ response }) => response?.didWork)
|
|
.map(async ({ card, response }) => {
|
|
console.log(card, response);
|
|
try {
|
|
if (response.wasClaimed) {
|
|
await deleteGift(card.uuid);
|
|
}
|
|
|
|
await updateGiftLocal(card.uuid, {
|
|
state: response.wasClaimed ? 'Claimed' : 'Expired',
|
|
});
|
|
|
|
console.log(
|
|
`Updated gift ${card.uuid}:`,
|
|
response.wasClaimed ? 'Claimed' : 'Expired',
|
|
);
|
|
} catch (error) {
|
|
console.error(`Error updating gift ${card.uuid}:`, error);
|
|
}
|
|
});
|
|
|
|
await Promise.all(updatePromises);
|
|
await updateGiftList();
|
|
console.log(`Processed ${updatePromises.length} gift updates`);
|
|
} catch (err) {
|
|
console.log('error checking for gift refunds', err.message);
|
|
} finally {
|
|
isCheckingRefunds.current = false;
|
|
}
|
|
}, [updateGiftList]);
|
|
|
|
const handleGiftRestoreOnDomeseday = useCallback(async giftList => {
|
|
// If we have gifts that means we are not restoring and do not need to get gifts in database
|
|
if (giftList?.length) return;
|
|
|
|
const didCheckDBForGifts = JSON.parse(
|
|
await getLocalStorageItem('checkForOutstandingGifts'),
|
|
);
|
|
|
|
// We already checked for outstanding gifts and none exist so don't check again
|
|
if (didCheckDBForGifts) return;
|
|
|
|
const outstandingGifts = await reloadGiftsOnDomesday(masterInfoObject.uuid);
|
|
|
|
// If no gifts exist, mark as checked and return
|
|
if (!outstandingGifts.length) {
|
|
await setLocalStorageItem(
|
|
'checkForOutstandingGifts',
|
|
JSON.stringify(true),
|
|
);
|
|
return;
|
|
}
|
|
|
|
const now = Date.now();
|
|
|
|
// Process all gifts in parallel and wait for completion
|
|
const reconstructedGifts = await Promise.all(
|
|
outstandingGifts.map(async item => {
|
|
let giftWalletMnemonic;
|
|
|
|
if (item.createdTime > GIFT_DERIVE_PATH_CUTOFF) {
|
|
giftWalletMnemonic = await deriveSparkGiftMnemonic(
|
|
accountMnemoinc,
|
|
item.giftNum,
|
|
);
|
|
} else {
|
|
giftWalletMnemonic = await deriveKeyFromMnemonic(
|
|
accountMnemoinc,
|
|
item.giftNum,
|
|
);
|
|
}
|
|
|
|
// Gift is expired - just add restore key
|
|
if (item.expireTime < now) {
|
|
const expiredGift = {
|
|
...item,
|
|
restoreKey: giftWalletMnemonic.derivedMnemonic,
|
|
};
|
|
await saveGiftLocal(expiredGift);
|
|
return expiredGift;
|
|
} else {
|
|
// Active gift - update with new secret for sharing
|
|
const randomSecret = randomBytes(32);
|
|
const randomPubkey = getPublicKey(randomSecret);
|
|
const encryptedMnemonic = encriptMessage(
|
|
randomSecret,
|
|
randomPubkey,
|
|
giftWalletMnemonic.derivedMnemonic,
|
|
);
|
|
const urls = createGiftUrl(item.uuid, randomSecret);
|
|
|
|
const updatedGift = {
|
|
...item,
|
|
claimURL: urls.webUrl,
|
|
encryptedText: encryptedMnemonic,
|
|
};
|
|
|
|
await Promise.all([
|
|
updateGiftInDatabase(updatedGift),
|
|
saveGiftLocal(updatedGift),
|
|
]);
|
|
|
|
return updatedGift;
|
|
}
|
|
}),
|
|
);
|
|
|
|
dispatch({ type: 'BULK_ADD_GIFTS', payload: reconstructedGifts });
|
|
await setLocalStorageItem('checkForOutstandingGifts', JSON.stringify(true));
|
|
}, [accountMnemoinc, masterInfoObject.uuid]);
|
|
|
|
useEffect(() => {
|
|
if (!didGetToHomepage) return;
|
|
(async () => {
|
|
const giftList = await updateGiftList();
|
|
await checkForRefunds(giftList);
|
|
await handleGiftRestoreOnDomeseday(giftList);
|
|
})();
|
|
}, [didGetToHomepage]);
|
|
|
|
const { giftsArray, expiredGiftsArray } = useMemo(() => {
|
|
const now = Date.now();
|
|
const all = [];
|
|
const expired = [];
|
|
|
|
Object.values(state.gifts).forEach(gift => {
|
|
all.push(gift);
|
|
if (
|
|
now >= gift.expireTime &&
|
|
gift.state?.toLowerCase() !== 'claimed' &&
|
|
gift.state?.toLowerCase() !== 'reclaimed'
|
|
) {
|
|
expired.push(gift);
|
|
}
|
|
});
|
|
|
|
return {
|
|
giftsArray: all.sort((a, b) => b.giftNum - a.giftNum),
|
|
expiredGiftsArray: expired.sort((a, b) => b.giftNum - a.giftNum),
|
|
};
|
|
}, [state.gifts]);
|
|
|
|
const contextValue = useMemo(
|
|
() => ({
|
|
...state,
|
|
saveGiftToCloud,
|
|
bulkSaveGiftsToCloud,
|
|
checkForRefunds,
|
|
deleteGiftFromCloudAndLocal,
|
|
bulkDeleteGiftsFromCloudAndLocal,
|
|
updateGiftList,
|
|
giftsArray,
|
|
expiredGiftsArray,
|
|
}),
|
|
[
|
|
state,
|
|
saveGiftToCloud,
|
|
bulkSaveGiftsToCloud,
|
|
checkForRefunds,
|
|
deleteGiftFromCloudAndLocal,
|
|
bulkDeleteGiftsFromCloudAndLocal,
|
|
updateGiftList,
|
|
giftsArray,
|
|
expiredGiftsArray,
|
|
],
|
|
);
|
|
|
|
return (
|
|
<GiftContext.Provider value={contextValue}>{children}</GiftContext.Provider>
|
|
);
|
|
}
|
|
|
|
export const useGifts = () => useContext(GiftContext);
|