diff --git a/package-lock.json b/package-lock.json index 8e3eb38..d74ad0d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@mantine/core": "^7.14.1", "@mantine/hooks": "^7.14.1", "@radix-ui/react-accordion": "^1.1.2", + "@radix-ui/react-checkbox": "^1.3.1", "@radix-ui/react-context-menu": "^2.2.1", "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dropdown-menu": "^2.0.6", @@ -34,6 +35,7 @@ "@radix-ui/react-toggle-group": "^1.1.0", "@radix-ui/react-tooltip": "^1.0.7", "@tanstack/react-router": "^1.46.4", + "@tanstack/react-table": "^8.21.3", "@uidotdev/usehooks": "^2.4.1", "axios": "^1.7.7", "axios-retry": "^4.5.0", @@ -2774,6 +2776,204 @@ } } }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.1.tgz", + "integrity": "sha512-xTaLKAO+XXMPK/BpVTSaAAhlefmvMSACjIhK9mGsImvX2ljcTDm8VGR1CuS1uYcNdR5J+oiOhoJZc5un6bh3VQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.2", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/primitive": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz", + "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-presence": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz", + "integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-primitive": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.2.tgz", + "integrity": "sha512-uHa+l/lKfxuDD2zjN/0peM/RhhSmRjr5YWdk/37EnSv1nJ88uvG85DPexSm8HdFQROd2VdERJ6ynXbkCFi+APw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-slot": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.2.tgz", + "integrity": "sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-collapsible": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.0.3.tgz", @@ -5403,6 +5603,39 @@ } } }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-escape-keydown": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.3.tgz", @@ -6005,6 +6238,26 @@ "react-dom": "^17.0.0 || ^18.0.0" } }, + "node_modules/@tanstack/react-table": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz", + "integrity": "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==", + "license": "MIT", + "dependencies": { + "@tanstack/table-core": "8.21.3" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/@tanstack/router-generator": { "version": "1.30.0", "resolved": "https://registry.npmjs.org/@tanstack/router-generator/-/router-generator-1.30.0.tgz", @@ -6062,6 +6315,19 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@tanstack/table-core": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz", + "integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", diff --git a/package.json b/package.json index a988e0b..34a4821 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "@mantine/core": "^7.14.1", "@mantine/hooks": "^7.14.1", "@radix-ui/react-accordion": "^1.1.2", + "@radix-ui/react-checkbox": "^1.3.1", "@radix-ui/react-context-menu": "^2.2.1", "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dropdown-menu": "^2.0.6", @@ -54,6 +55,7 @@ "@radix-ui/react-toggle-group": "^1.1.0", "@radix-ui/react-tooltip": "^1.0.7", "@tanstack/react-router": "^1.46.4", + "@tanstack/react-table": "^8.21.3", "@uidotdev/usehooks": "^2.4.1", "axios": "^1.7.7", "axios-retry": "^4.5.0", diff --git a/src/components/menu/sidebar.tsx b/src/components/menu/sidebar.tsx index 5e45e6f..c9c1f21 100644 --- a/src/components/menu/sidebar.tsx +++ b/src/components/menu/sidebar.tsx @@ -270,6 +270,23 @@ export function SidebarMenu({ )} + {getMenuOptionVisibility('friendsManagement') && ( +
  • + + {t( + 'account-management.options.friends-management' + )} + +
  • + )} {getMenuOptionVisibility('redeemCodes') && (
  • , + ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + +)) +Checkbox.displayName = CheckboxPrimitive.Root.displayName + +export { Checkbox } diff --git a/src/components/ui/table.tsx b/src/components/ui/table.tsx new file mode 100644 index 0000000..0f88d6a --- /dev/null +++ b/src/components/ui/table.tsx @@ -0,0 +1,130 @@ +import type { + HTMLAttributes, + TdHTMLAttributes, + ThHTMLAttributes, +} from 'react' + +import { forwardRef } from 'react' + +import { cn } from '../../lib/utils' + +const Table = forwardRef< + HTMLTableElement, + HTMLAttributes +>(({ className, ...props }, ref) => ( +
    + + +)) +Table.displayName = 'Table' + +const TableHeader = forwardRef< + HTMLTableSectionElement, + HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableHeader.displayName = 'TableHeader' + +const TableBody = forwardRef< + HTMLTableSectionElement, + HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableBody.displayName = 'TableBody' + +const TableFooter = forwardRef< + HTMLTableSectionElement, + HTMLAttributes +>(({ className, ...props }, ref) => ( + tr]:last:border-b-0', + className + )} + {...props} + /> +)) +TableFooter.displayName = 'TableFooter' + +const TableRow = forwardRef< + HTMLTableRowElement, + HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableRow.displayName = 'TableRow' + +const TableHead = forwardRef< + HTMLTableCellElement, + ThHTMLAttributes +>(({ className, ...props }, ref) => ( +
    +)) +TableHead.displayName = 'TableHead' + +const TableCell = forwardRef< + HTMLTableCellElement, + TdHTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableCell.displayName = 'TableCell' + +const TableCaption = forwardRef< + HTMLTableCaptionElement, + HTMLAttributes +>(({ className, ...props }, ref) => ( +
    +)) +TableCaption.displayName = 'TableCaption' + +export { + Table, + TableHeader, + TableBody, + TableFooter, + TableHead, + TableRow, + TableCell, + TableCaption, +} diff --git a/src/kernel/startup/auto-llamas.ts b/src/kernel/startup/auto-llamas.ts index 4145c5a..53873bd 100644 --- a/src/kernel/startup/auto-llamas.ts +++ b/src/kernel/startup/auto-llamas.ts @@ -3,7 +3,11 @@ import type { AutoLlamasData, AutoLlamasRecord, } from '../../types/auto-llamas' -import type { MCPQueryProfileProfileChangesPrerollData } from '../../types/services/mcp' +import type { + MCPQueryProfileChanges, + MCPQueryProfileProfileChangesPrerollData, +} from '../../types/services/mcp' +import type { StorefrontCatalogResponse } from '../../types/services/storefront' import type { RewardsNotification } from '../../types/notifications' import { Collection } from '@discordjs/collection' @@ -292,53 +296,33 @@ export class ProcessAutoLlamas { } } - // eslint-disable-next-line no-constant-condition - while (true) { - let success = true + const accountId = account.accountId - try { - const accessToken = - await Authentication.verifyAccessToken(account) + if (type === ProcessLlamaType.Survivor) { + // eslint-disable-next-line no-constant-condition + while (true) { + try { + const accessToken = + await Authentication.verifyAccessToken(account) - if (!accessToken) { - break - } + if (!accessToken) { + break + } - try { - await populatePrerolledOffers({ + await ProcessAutoLlamas.populateOffers({ accessToken, - accountId: account.accountId, + accountId, }) - // eslint-disable-next-line @typescript-eslint/no-unused-vars - } catch (error) { - // - } - - const queryProfile = await getQueryProfile({ - accessToken, - accountId: account.accountId, - }) - const profileChanges = - queryProfile.data.profileChanges[0] ?? null - - const xRayTickets = - Object.values(profileChanges?.profile.items).find( - (item) => - (item.templateId as string) === - 'AccountResource:currency_xrayllama' - )?.quantity ?? 0 - const llamaTokens = - Object.values(profileChanges?.profile.items).find( - (item) => - (item.templateId as string) === - 'AccountResource:voucher_cardpack_bronze' - )?.quantity ?? 0 + const { llamaTokens, profile, xRayTickets } = + await ProcessAutoLlamas.getProfileData({ + accessToken, + accountId, + }) - let currencyTotal: number | null = null - let currencySubType = 'AccountResource:currency_xrayllama' + let currencyTotal: number | null = null + let currencySubType = 'AccountResource:currency_xrayllama' - if (type === ProcessLlamaType.Survivor) { if (current.actions.survivors) { if (current.actions['use-token']) { if (llamaTokens > 0) { @@ -354,215 +338,404 @@ export class ProcessAutoLlamas { } } } - } else if (type === ProcessLlamaType.FreeUpgrade) { - if (current.actions['free-llamas']) { - currencyTotal = 0 + + const currencyIsToken = + currencySubType === + 'AccountResource:voucher_cardpack_bronze' + + if (currencyTotal === null) { + break } - } - const currencyIsToken = - currencySubType === - 'AccountResource:voucher_cardpack_bronze' + const cardPacks = + await ProcessAutoLlamas.getCurrentCatalog({ + accessToken, + }) - if (currencyTotal === null) { - break - } + if (!cardPacks) { + break + } - const catalog = await getCatalog({ - accessToken, - }) - const cardPacks = catalog.data.storefronts.find( - (item) => item.name === 'CardPackStorePreroll' - ) + const llama = cardPacks.catalogEntries.find((item) => { + return currencyIsToken + ? item.devName === 'Always.UpgradePack.02' + : item.devName === 'Always.UpgradePack.01' + }) - if (!cardPacks) { - break - } + if (!llama) { + break + } - const llama = cardPacks.catalogEntries.find((item) => { - if (type === ProcessLlamaType.Survivor) { - if (currencyIsToken) { - return item.devName === 'Always.UpgradePack.02' - } + const { maxPurchases } = + await ProcessAutoLlamas.checkDailyLimit({ + accessToken, + accountId, + currencyIsToken, + llama, + }) - return ( - item.devName === 'Always.UpgradePack.01' && - item.dailyLimit === 50 && - item.prices[0]?.regularPrice === 50 && - item.prices[0]?.finalPrice === 50 - ) + if (maxPurchases) { + break } - return ( - (item.devName?.toLowerCase().includes('free') || - item.title?.toLowerCase().includes('free')) && - item.prices[0]?.regularPrice === 50 && - item.prices[0]?.finalPrice === 0 - ) - }) + const prerollData = Object.values( + profile.items ?? {} + ).find( + (item) => + isMCPQueryProfileChangesPrerollData(item) && + item.attributes.offerId === llama.offerId + ) as MCPQueryProfileProfileChangesPrerollData | undefined - if (!llama) { - break - } + if (!prerollData) { + break + } - const mainProfile = await getQueryProfileMainProfile({ - accessToken, - accountId: account.accountId, - }) - const totalPurchases = - mainProfile.data.profileChanges[0]?.profile.stats - .attributes.daily_purchases.purchaseList[ - llama.offerId - ] ?? 0 - const dailyLimit = - currencyIsToken && llama.dailyLimit <= -1 - ? 10 - : llama.dailyLimit ?? 2 - - if (totalPurchases >= dailyLimit) { - break - } + const canPurchase = prerollData.attributes.items.some( + ({ itemType }) => { + const newKey = itemType + .replace( + /_((very)?low|medium|(very)?high|extreme)$/gi, + '' + ) + .replace('AccountResource:', '') + .replace('CardPack:zcp_', '') + const survivor = getKey(newKey, survivorsJson) + const mythicSurvivor = getKey( + newKey, + survivorsMythicLeadsJson + ) + const isWorker = newKey.startsWith('Worker:') + const rarity = parseRarity(newKey) + + return ( + survivor || + mythicSurvivor || + (isWorker && + [RarityType.Legendary, RarityType.Mythic].includes( + rarity.rarity + )) + ) + } + ) - const prerollData = Object.values( - profileChanges.profile.items ?? {} - ).find( - (item) => - isMCPQueryProfileChangesPrerollData(item) && - item.attributes.offerId === llama.offerId - ) as MCPQueryProfileProfileChangesPrerollData | undefined + if (!canPurchase) { + break + } - if (!prerollData) { + await ProcessAutoLlamas.buyAndNotify({ + accessToken, + currencySubType, + profile, + accountId: account.accountId, + expectedTotalPrice: currencyTotal, + offerId: llama.offerId, + }) + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (error) { break } + } - const canPurchase = - type === ProcessLlamaType.FreeUpgrade - ? true - : prerollData.attributes.items.some(({ itemType }) => { - const newKey = itemType - .replace( - /_((very)?low|medium|(very)?high|extreme)$/gi, - '' - ) - .replace('AccountResource:', '') - .replace('CardPack:zcp_', '') - const survivor = getKey(newKey, survivorsJson) - const mythicSurvivor = getKey( - newKey, - survivorsMythicLeadsJson - ) - const isWorker = newKey.startsWith('Worker:') - const rarity = parseRarity(newKey) - - return ( - survivor || - mythicSurvivor || - (isWorker && - [ - RarityType.Legendary, - RarityType.Mythic, - ].includes(rarity.rarity)) - ) - }) + return + } - if (!canPurchase) { - break - } + try { + const accessToken = + await Authentication.verifyAccessToken(account) - const response = await purchaseCatalogEntry({ - accessToken, - currencySubType, - accountId: account.accountId, - offerId: llama.offerId, - expectedTotalPrice: currencyTotal, - }) - const responseNotifications = - response.data.notifications ?? [] - - const notifications: Array<{ - itemType: string - quantity: number - }> = [] - - responseNotifications?.forEach((notification) => { - if (notification.loot) { - if (notification.loot.items) { - notification.loot.items.forEach((loot) => { - notifications.push({ - itemType: loot.itemType, - quantity: loot.quantity, - }) - }) - } else if (notification.loot.lootGranted) { - notification.loot.lootGranted.items.forEach((loot) => { - notifications.push({ - itemType: loot.itemType, - quantity: loot.quantity, - }) - }) - } - } else if (notification.lootGranted) { - notification.lootGranted?.items.forEach((loot) => { - notifications.push({ - itemType: loot.itemType, - quantity: loot.quantity, - }) - }) - } else if (notification.lootResult) { - notification.lootResult?.items.forEach((loot) => { - notifications.push({ - itemType: loot.itemType, - quantity: loot.quantity, - }) - }) - } - }) + if (!accessToken) { + return + } - const rewards: RewardsNotification['rewards'] = {} + await ProcessAutoLlamas.populateOffers({ + accessToken, + accountId, + }) - notifications.forEach(({ itemType, quantity }) => { - if (!itemType.toLowerCase().startsWith('accolades:')) { - const newItemType = - itemType === 'AccountResource:campaign_event_currency' - ? profileChanges.profile.stats.attributes - .event_currency?.templateId ?? itemType - : itemType + const { profile } = await ProcessAutoLlamas.getProfileData({ + accessToken, + accountId, + }) - if (!rewards[newItemType]) { - rewards[newItemType] = 0 - } + const cardPacks = await ProcessAutoLlamas.getCurrentCatalog({ + accessToken, + }) - rewards[newItemType] += quantity - } - }) + if (!cardPacks) { + return + } - const result: RewardsNotification = { - accolades: { - totalMissionXPRedeemed: 0, - totalQuestXPRedeemed: 0, - }, - rewards, - createdAt: getDateWithDefaultFormat(), - id: crypto.randomUUID(), - accountId: account.accountId, - } + const llamas = cardPacks.catalogEntries.filter((item) => { + return item.prices[0]?.finalPrice === 0 + }) - MainWindow.instance.webContents.send( - ElectronAPIEventKeys.ClaimRewardsClientGlobalSyncNotification, - [result] - ) + if (llamas.length <= 0) { + return + } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - } catch (error) { - success = false + const availableLlamas: StorefrontCatalogResponse['storefronts'][number]['catalogEntries'] = + [] + + for (const llama of llamas) { + // const { dailyLimit, maxPurchases, totalPurchases } = + // await ProcessAutoLlamas.checkDailyLimit({ + // accessToken, + // accountId, + // llama, + // currencyIsToken: false, + // }) + + // if (maxPurchases) { + // continue + // } + + // Array.from({ length: dailyLimit - totalPurchases }).forEach( + Array.from({ + length: llama.dailyLimit <= -1 ? 30 : llama.dailyLimit, + }).forEach(() => { + availableLlamas.push(llama) + }) } - if (!success) { - break + if (availableLlamas.length <= 0) { + return } + + availableLlamas.forEach((llama) => { + ProcessAutoLlamas.buyAndNotify({ + accessToken, + profile, + accountId: account.accountId, + currencySubType: 'AccountResource:currency_xrayllama', + expectedTotalPrice: 0, + offerId: llama.offerId, + }).catch(() => {}) + }) + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (error) { + // } + + return } ) }) } + + static async populateOffers({ + accessToken, + accountId, + }: { + accessToken: string + accountId: string + }) { + try { + await populatePrerolledOffers({ + accessToken, + accountId, + }) + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (error) { + // + } + } + + static async getProfileData({ + accessToken, + accountId, + }: { + accessToken: string + accountId: string + }) { + const queryProfile = await getQueryProfile({ + accessToken, + accountId, + }) + const profileChanges = queryProfile.data.profileChanges[0] ?? null + + const xRayTickets = + Object.values(profileChanges?.profile.items).find( + (item) => + (item.templateId as string) === + 'AccountResource:currency_xrayllama' + )?.quantity ?? 0 + const llamaTokens = + Object.values(profileChanges?.profile.items).find( + (item) => + (item.templateId as string) === + 'AccountResource:voucher_cardpack_bronze' + )?.quantity ?? 0 + + return { + llamaTokens, + xRayTickets, + profile: queryProfile.data.profileChanges[0]?.profile, + } + } + + static async getCurrentCatalog({ + accessToken, + }: { + accessToken: string + }) { + const catalog = await getCatalog({ + accessToken, + }) + const cardPacks = catalog.data.storefronts.find( + (item) => item.name === 'CardPackStorePreroll' + ) + + return cardPacks + } + + static async checkDailyLimit({ + accessToken, + accountId, + currencyIsToken, + llama, + }: { + accessToken: string + accountId: string + currencyIsToken: boolean + llama: StorefrontCatalogResponse['storefronts'][number]['catalogEntries'][number] + }) { + try { + const mainProfile = await getQueryProfileMainProfile({ + accessToken, + accountId, + }) + const getDenyRequirement = llama.requirements?.find( + (item) => item.requirementType === 'DenyOnFulfillment' + ) + const totalPurchases = + mainProfile.data.profileChanges[0]?.profile.stats.attributes + .daily_purchases?.purchaseList?.[llama.offerId] ?? + mainProfile.data.profileChanges[0]?.profile.stats.attributes + .in_app_purchases?.fulfillmentCounts?.[ + getDenyRequirement?.requiredId ?? -1 + ] ?? + 0 + const dailyLimit = + currencyIsToken && llama.dailyLimit <= -1 + ? 30 + : llama.dailyLimit ?? 30 + + return { + maxPurchases: totalPurchases >= dailyLimit, + totalPurchases, + dailyLimit, + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (error) { + // + } + + return { + maxPurchases: false, + totalPurchases: 0, + dailyLimit: 0, + } + } + + static async buyAndNotify({ + accessToken, + accountId, + currencySubType, + expectedTotalPrice, + offerId, + profile, + }: { + accessToken: string + accountId: string + currencySubType?: string + offerId: string + expectedTotalPrice: number + profile: MCPQueryProfileChanges['profile'] + }) { + const response = await purchaseCatalogEntry({ + accessToken, + accountId, + currencySubType, + offerId, + expectedTotalPrice, + }) + const responseNotifications = response.data.notifications ?? [] + + const notifications: Array<{ + itemType: string + quantity: number + }> = [] + + responseNotifications?.forEach((notification) => { + if (notification.loot) { + if (notification.loot.items) { + notification.loot.items.forEach((loot) => { + notifications.push({ + itemType: loot.itemType, + quantity: loot.quantity, + }) + }) + } else if (notification.loot.lootGranted) { + notification.loot.lootGranted.items.forEach((loot) => { + notifications.push({ + itemType: loot.itemType, + quantity: loot.quantity, + }) + }) + } + } else if (notification.lootGranted) { + notification.lootGranted?.items.forEach((loot) => { + notifications.push({ + itemType: loot.itemType, + quantity: loot.quantity, + }) + }) + } else if (notification.lootResult) { + notification.lootResult?.items.forEach((loot) => { + notifications.push({ + itemType: loot.itemType, + quantity: loot.quantity, + }) + }) + } + }) + + const rewards: RewardsNotification['rewards'] = {} + + notifications.forEach(({ itemType, quantity }) => { + if (!itemType.toLowerCase().startsWith('accolades:')) { + const newItemType = + itemType === 'AccountResource:campaign_event_currency' + ? profile.stats.attributes.event_currency?.templateId ?? + itemType + : itemType + + if (!rewards[newItemType]) { + rewards[newItemType] = 0 + } + + rewards[newItemType] += quantity + } + }) + + const result: RewardsNotification = { + accountId, + rewards, + accolades: { + totalMissionXPRedeemed: 0, + totalQuestXPRedeemed: 0, + }, + createdAt: getDateWithDefaultFormat(), + id: crypto.randomUUID(), + } + + MainWindow.instance.webContents.send( + ElectronAPIEventKeys.ClaimRewardsClientGlobalSyncNotification, + [result] + ) + } } diff --git a/src/lib/validations/schemas/settings.ts b/src/lib/validations/schemas/settings.ts index 6132e32..8ac4b26 100644 --- a/src/lib/validations/schemas/settings.ts +++ b/src/lib/validations/schemas/settings.ts @@ -60,6 +60,7 @@ export const customizableMenuSettingsSchema = z accountManagement: z.boolean().default(true), vbucksInformation: z.boolean().default(true), + friendsManagement: z.boolean().default(true), redeemCodes: z.boolean().default(true), devicesAuth: z.boolean().default(true), epicGamesSettings: z.boolean().default(true), diff --git a/src/lib/validations/schemas/storefront.ts b/src/lib/validations/schemas/storefront.ts index da6d400..d321a36 100644 --- a/src/lib/validations/schemas/storefront.ts +++ b/src/lib/validations/schemas/storefront.ts @@ -48,7 +48,18 @@ export const storefrontCatalogSchema = z.object({ monthlyLimit: z.number(), refundable: z.boolean(), // appStoreId: z.array(z.string()), - // requirements: z.array(z.unknown()), + requirements: z + .array( + z.object({ + requirementType: z.enum([ + 'DenyOnFulfillment', + 'RequireFulfillment', + ]), + requiredId: z.string(), + minQuantity: z.number(), + }) + ) + .optional(), meta: z .object({ PurchaseLimitingEventId: z.string(), diff --git a/src/locales/en-US/account-management/friends-management.json b/src/locales/en-US/account-management/friends-management.json new file mode 100644 index 0000000..addc766 --- /dev/null +++ b/src/locales/en-US/account-management/friends-management.json @@ -0,0 +1,3 @@ +{ + "coming-soon": "Coming Soon!" +} diff --git a/src/locales/en-US/sidebar.json b/src/locales/en-US/sidebar.json index 8e2e425..91754bd 100644 --- a/src/locales/en-US/sidebar.json +++ b/src/locales/en-US/sidebar.json @@ -20,6 +20,7 @@ "title": "Account Management", "options": { "vbucks-information": "V-Bucks Information", + "friends-management": "Friends Management", "redeem-codes": "Redeem Codes", "devices-auth": "Devices Auth", "epic-settings": "Epic Games Settings" diff --git a/src/locales/resources.ts b/src/locales/resources.ts index 250e2fa..92a8743 100644 --- a/src/locales/resources.ts +++ b/src/locales/resources.ts @@ -18,6 +18,7 @@ import enUS_stwOperations_XPBoosts from './en-US/stw-operations/xpboosts.json' import enUS_stwOperations_Llamas from './en-US/stw-operations/llamas.json' import enUS_stwOperations_Unlock from './en-US/stw-operations/unlock.json' import enUS_accountManagement_VBucksInformation from './en-US/account-management/vbucks-information.json' +import enUS_accountManagement_FriendsManagement from './en-US/account-management/friends-management.json' import enUS_accountManagement_EULA from './en-US/account-management/eula.json' import enUS_accountManagement_RedeemCodes from './en-US/account-management/redeem-codes.json' import enUS_accountManagement_DevicesAuth from './en-US/account-management/devices-auth.json' @@ -49,6 +50,7 @@ const enUS = { }, 'account-management': { 'vbucks-information': enUS_accountManagement_VBucksInformation, + 'friends-management': enUS_accountManagement_FriendsManagement, 'redeem-codes': enUS_accountManagement_RedeemCodes, 'devices-auth': enUS_accountManagement_DevicesAuth, 'epic-settings': enUS_accountManagement_EpicSettings, diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index aad52b3..9bd49fd 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -27,6 +27,7 @@ import { Route as AdvancedModeMatchmakingTrackRouteImport } from './routes/advan import { Route as AccountsRemoveRouteImport } from './routes/accounts/remove/route' import { Route as AccountManagementVbucksInformationRouteImport } from './routes/account-management/vbucks-information/route' import { Route as AccountManagementRedeemCodesRouteImport } from './routes/account-management/redeem-codes/route' +import { Route as AccountManagementFriendsManagementRouteImport } from './routes/account-management/friends-management/route' import { Route as AccountManagementEulaRouteImport } from './routes/account-management/eula/route' import { Route as AccountManagementEpicGamesSettingsRouteImport } from './routes/account-management/epic-games-settings/route' import { Route as AccountManagementDevicesAuthRouteImport } from './routes/account-management/devices-auth/route' @@ -125,6 +126,12 @@ const AccountManagementRedeemCodesRouteRoute = getParentRoute: () => rootRoute, } as any) +const AccountManagementFriendsManagementRouteRoute = + AccountManagementFriendsManagementRouteImport.update({ + path: '/account-management/friends-management', + getParentRoute: () => rootRoute, + } as any) + const AccountManagementEulaRouteRoute = AccountManagementEulaRouteImport.update( { path: '/account-management/eula', @@ -173,6 +180,10 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AccountManagementEulaRouteImport parentRoute: typeof rootRoute } + '/account-management/friends-management': { + preLoaderRoute: typeof AccountManagementFriendsManagementRouteImport + parentRoute: typeof rootRoute + } '/account-management/redeem-codes': { preLoaderRoute: typeof AccountManagementRedeemCodesRouteImport parentRoute: typeof rootRoute @@ -244,6 +255,7 @@ export const routeTree = rootRoute.addChildren([ AccountManagementDevicesAuthRouteRoute, AccountManagementEpicGamesSettingsRouteRoute, AccountManagementEulaRouteRoute, + AccountManagementFriendsManagementRouteRoute, AccountManagementRedeemCodesRouteRoute, AccountManagementVbucksInformationRouteRoute, AccountsRemoveRouteRoute, diff --git a/src/routes/account-management/friends-management/route.tsx b/src/routes/account-management/friends-management/route.tsx new file mode 100644 index 0000000..4aa4ff3 --- /dev/null +++ b/src/routes/account-management/friends-management/route.tsx @@ -0,0 +1,565 @@ +import { createRoute } from '@tanstack/react-router' +import { + ColumnDef, + // ColumnFiltersState, + // Row, + // SortingState, + // VisibilityState, + flexRender, + getCoreRowModel, + getFacetedRowModel, + getFacetedUniqueValues, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, +} from '@tanstack/react-table' +import { + BanIcon, + ChevronLeftIcon, + ChevronRightIcon, + ChevronsLeftIcon, + ChevronsRightIcon, + Trash2Icon, +} from 'lucide-react' +import { useState } from 'react' +import { useTranslation } from 'react-i18next' +import { z } from 'zod' + +import { Route as RootRoute } from '../../__root' + +import { HomeBreadcrumb } from '../../../components/navigations/breadcrumb/home' +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator, +} from '../../../components/ui/breadcrumb' +import { Button } from '../../../components/ui/button' +import { Checkbox } from '../../../components/ui/checkbox' +import { Combobox } from '../../../components/ui/extended/combobox' +import { Input } from '../../../components/ui/input' +// import { Label } from '../../../components/ui/label' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '../../../components/ui/select' +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '../../../components/ui/table' +import { + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from '../../../components/ui/tabs' +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from '../../../components/ui/tooltip' + +import { useCustomizableMenuSettingsVisibility } from '../../../hooks/settings' + +import { numberWithCommaSeparator } from '../../../lib/parsers/numbers' +import { cn } from '../../../lib/utils' + +export const Route = createRoute({ + getParentRoute: () => RootRoute, + path: '/account-management/friends-management', + component: () => { + const { t } = useTranslation(['sidebar'], { + keyPrefix: 'account-management', + }) + + return ( + <> + + + + + + {t('title')} + + + + + {t('options.friends-management')} + + + + + + + ) + }, +}) + +function Content() { + const { t } = useTranslation(['account-management', 'general']) + + const [data] = useState>>(() => [ + { + accountId: 'Andhord', + alias: 'Anjor', + created: '2020-01-01T02:00:00.000Z', + favorite: false, + groups: [], + note: '', + }, + { + accountId: 'Kuva_.', + alias: 'Copia de Kuda', + created: '2020-01-01T02:00:00.000Z', + favorite: false, + groups: [], + note: '', + }, + { + accountId: 'Yo Fist', + alias: '', + created: '2020-01-01T02:00:00.000Z', + favorite: false, + groups: [], + note: '', + }, + ]) + const [rowSelection, setRowSelection] = useState({}) + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 10, + }) + + const table = useReactTable({ + data, + columns, + state: { + // sorting, + // columnVisibility, + rowSelection, + // columnFilters, + pagination, + }, + getRowId: (row) => row.accountId, + enableRowSelection: true, + onRowSelectionChange: setRowSelection, + // onSortingChange: setSorting, + // onColumnFiltersChange: setColumnFilters, + // onColumnVisibilityChange: setColumnVisibility, + onPaginationChange: setPagination, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFacetedRowModel: getFacetedRowModel(), + getFacetedUniqueValues: getFacetedUniqueValues(), + }) + + const { getMenuOptionVisibility } = + useCustomizableMenuSettingsVisibility() + + return ( +
    +
    +
    +
    + {}} + // onSelectItem={onSelectItem} + emptyContentClassname="py-6 text-center text-sm" + // disabled={accountSelectorIsDisabled} + // disabledItem={accountSelectorIsDisabled} + // inputSearchIsDisabled={accountSelectorIsDisabled} + hideInputSearchWhenOnlyOneOptionIsAvailable + hideSelectorOnSelectItem + /> + +
    +
    + +
    + +
    + + Friends + Incoming + Outgoing + Blocklist + +
    + + Content + + + Content + + + Content + + + Content + +
    +
    + +
    +
    + Total friends +
    + {numberWithCommaSeparator(1234)} +
    +
    +
    + +
    + +
    + +
    + + + {table.getHeaderGroups().map((headerGroup) => ( + th:first-child]:w-10', + '[&>th:nth-child(2)]:w-44' + )} + key={headerGroup.id} + > + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ) + })} + + ))} + + + {table.getRowModel().rows.map((row, index) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + ))} + +
    +
    + +
    + {/*
    + {table.getFilteredSelectedRowModel().rows.length} of{' '} + {table.getFilteredRowModel().rows.length} row(s) selected. +
    */} +
    +
    + {/* */} + +
    +
    + Page {table.getState().pagination.pageIndex + 1} of{' '} + {table.getPageCount()} +
    +
    + + + + +
    +
    +
    +
    +
    + ) +} + +export const schema = z.object({ + accountId: z.string(), + groups: z.array(z.any()), + alias: z.string(), + note: z.string(), + favorite: z.boolean(), + created: z.string(), +}) + +const columns: Array>> = [ + { + id: 'select', + header: ({ table }) => ( +
    + + table.toggleAllPageRowsSelected(!!value) + } + aria-label="select all" + /> +
    + ), + cell: ({ row }) => ( +
    + row.toggleSelected(!!value)} + aria-label="select row" + /> +
    + ), + }, + { + accessorKey: 'header', + header: () => { + return
    Name
    + }, + cell: ({ row }) => { + return ( +
    + +
    + ) + }, + }, + { + id: 'actions', + header: 'Actions', + cell: () => ( +
    + {/* */} + + + + + + + + + +

    Block

    +
    +
    +
    + + + + + + + +

    Remove

    +
    +
    +
    +
    + ), + }, +] diff --git a/src/state/settings/customizable-menu.ts b/src/state/settings/customizable-menu.ts index 36162b5..4823a57 100644 --- a/src/state/settings/customizable-menu.ts +++ b/src/state/settings/customizable-menu.ts @@ -36,6 +36,7 @@ export const customizableMenuSettingsRelations: Record< ], accountManagement: [ 'vbucksInformation', + 'friendsManagement', 'redeemCodes', 'devicesAuth', 'epicGamesSettings', diff --git a/src/types/services/mcp/query-profile-main.d.ts b/src/types/services/mcp/query-profile-main.d.ts index a946d6c..30e5f52 100644 --- a/src/types/services/mcp/query-profile-main.d.ts +++ b/src/types/services/mcp/query-profile-main.d.ts @@ -23,14 +23,14 @@ export type MCPQueryProfileMainProfile = { lastInterval: string purchaseList: Record } - daily_purchases: { + daily_purchases?: { lastInterval: string - purchaseList: Record + purchaseList?: Record } - in_app_purchases: { + in_app_purchases?: { receipts: Array ignoredReceipts: Array - fulfillmentCounts: Record + fulfillmentCounts?: Record refreshTimers: { EpicPurchasingService: { nextEntitlementRefresh: string