import { createHttpClient } from "@d2api/httpclient";
import "./App.css";
import { DefinitionsProvider, verbose, includeTables, loadDefs, setApiKey } from "@d2api/manifest-react";
import { getAllInventoryItemLiteDefs, getInventoryItemLiteDef, getSeasonDef, getStatDef } from "@d2api/manifest-web";
import {
BungieMembershipType,
DestinyCharacterResponse,
DestinyComponentType,
DestinyItemType,
DestinyVendorResponse,
getCharacter,
getProfile,
getVendor,
} from "bungie-api-ts/destiny2";
import { CoreSettingsConfiguration, getCommonSettings } from "bungie-api-ts/core";
import { useEffect, useState } from "react";
import { OAuthSetup, getLatestOAuth, getOAuthHttpClient } from "@d2api/oauth-react";
import { GeneralUser, getBungieNetUserById, getMembershipDataForCurrentUser } from "bungie-api-ts/user";
import { getApplicationApiUsage } from "bungie-api-ts/app";
// import { OAuthSetup } from "@d2api/d2oauth-react";
const { api_key, client_id, client_secret } = BUNGIE_APP_INFO;
// these initiate definitions download.
// they're at the top level of this file, not within the react structure,
// so that things start getting ready as soon as possible.
verbose();
includeTables(["InventoryItemLite", "Season", "Stat"]);
setApiKey(api_key);
// we're not awaiting this promise, just dispatching it to do its thing, while react builds the page
loadDefs();
function App() {
const fallback = hi, definitions are loading...;
return (
<>
you can see this sentence immediately
below this though, is the definitions-reliant area
{/* everything within DefinitionsProvider wrapper
waits for defs to be available before rendering,
otherwise get✕✕✕✕Def functions would fail.
until then, it shows the fallback ↓ "loading" message */}
you can see this once the definitions load
here, have a random weapon:
here, have a specific weapon:
let's hit a few API endpoints
what season is it anyway?
what's Platypie0803 up to?
pssst wanna OAuth?
>
);
}
/** displays an item's icon+name, given its hash. if no item hash was provided, picks a random weapon. */
function D2Item({ itemHash }: { itemHash?: number }) {
const allWeapons = getAllInventoryItemLiteDefs().filter((i) => i.itemType === DestinyItemType.Weapon);
const aRandomWeapon = allWeapons[Math.floor(Math.random() * allWeapons.length)];
const itemDef = itemHash ? getInventoryItemLiteDef(itemHash) : aRandomWeapon;
const name = itemDef?.displayProperties.name;
const icon = itemDef?.displayProperties.icon;
return (
<>
{name}
>
);
}
// create an anonymous httpClient, (anonymous as in: no OAuth)
// which can be used with functions from bungie-api-ts (like getCommonSettings() below)
const httpClient = createHttpClient(api_key);
/** displays which destiny 2 season it is, according to bungie's global settings endpoint */
function ThisSeason() {
const [coreSettings, setCoreSettings] = useState();
useEffect(() => {
getCommonSettings(httpClient).then((s) => setCoreSettings(s.Response));
}, []);
if (!coreSettings) return "still loading settings";
const currentSeason = getSeasonDef(coreSettings.destiny2CoreSettings.currentSeasonHash);
return `it's currently season ${currentSeason?.seasonNumber}: ${currentSeason?.displayProperties.name}`;
}
/**
* gets some example information from Platypie0803's account and displays
* what item the hunter has equipped in the kinetic slot right now
*
* the CharacterEquipment component shows equipped items, even if
* someone has the majority of their inventory set to private.
* so we can do this with an anonymous httpClient
*/
function KineticEquipment() {
const [characterResponse, setCharacterResponse] = useState();
useEffect(() => {
const getCharacterParams = {
membershipType: BungieMembershipType.TigerXbox, // console players.........
destinyMembershipId: "4611686018455948551", // Platypie0803
characterId: "2305843009572044204",
components: [DestinyComponentType.CharacterEquipment],
};
getCharacter(httpClient, getCharacterParams).then((s) => setCharacterResponse(s.Response));
}, []);
if (!characterResponse) return "still loading equipment";
const kineticWeapon = characterResponse.equipment.data?.items.find((i) => i.bucketHash === 1498876634); // InventoryBucket [1498876634] "Kinetic Weapons"
return (
<>
Platypie's hunter is currently holding a...
>
);
}
/**
* show oauth setup, then later, do some stuff that demonstrates we have working oauth
*/
function OAuthStuff() {
const [completedAuthBnetUser, setCompletedAuthBnetUser] = useState("");
if (!client_id)
return (
<>
you'll need to set your app information in vite.config.ts
set up or get that information here
>
);
// this tells use whether there's currently a token loaded up and ready
const latestAuthed = getLatestOAuth(client_id)?.token.membership_id;
return (
<>
{/* unless we just completed auth, show information for performing auth */}
{!completedAuthBnetUser ? (
<>
fyi:
your client_id is {client_id}
your api_key is {api_key ? {api_key} : "missing"}
your client_secret is {client_secret ? {client_secret} : "missing"}
{(!api_key || !client_secret) && (
<>
client_id is all that's required to establish OAuth, but:
{!api_key && (
<>
• api_key will be required to make API requests using OAuth
>
)}
{!client_secret && (
<>
• without client_secret, OAuth expires after 1 hour
(longer requires a "Confidential" OAuth Client Type{" "}
here
)
>
)}
>
)}
{latestAuthed ? (
<>
you already have a token set up, but if you want, you can
>
) : null}
setTimeout(() => {
setCompletedAuthBnetUser(str);
}, 10)
}
/>
>
) : (
<>
looks like OAuth flow was just completed for Bungie.net user {completedAuthBnetUser}
API reports that {completedAuthBnetUser} belongs to:{" "}
>
)}
{latestAuthed && (
<>
so:
{completedAuthBnetUser ? "also, " : ""}we recently acquired an OAuth token for Bungie.net user{" "}
{latestAuthed}
API reports {latestAuthed} belongs to
{completedAuthBnetUser && completedAuthBnetUser !== latestAuthed && (
<>
weird that they don't match...
>
)}
ok now, what's something we can only do as a logged-in user....
>
)}
>
);
}
/** displays which destiny 2 season it is, according to bungie's global settings endpoint */
function BungieName({ bnetMembershipId }: { bnetMembershipId: string }) {
const [bnetUser, setBnetUser] = useState();
useEffect(() => {
getBungieNetUserById(httpClient, { id: bnetMembershipId }).then((s) => setBnetUser(s.Response));
}, [bnetMembershipId]);
if (!bnetUser) return "loading user...";
return `${bnetUser.cachedBungieGlobalDisplayName}#${bnetUser.cachedBungieGlobalDisplayNameCode}`;
}
/** do some stuff that demonstrates we have working oauth */
function AuthenticatedTask() {
const latestAuthed = getLatestOAuth(client_id)!.token.membership_id!;
const authedClient = getOAuthHttpClient(api_key, client_id, client_secret, latestAuthed, { verbose: true });
// we'll store a string if there's an error. again, a little silly, just doing this for fewer lines of code.
const [vendorResponse, setVendorResponse] = useState();
useEffect(() => {
(async () => {
// we know who we are (the Bungie.net user this OAuth is for (latestAuthed)) but we haven't looked up who we are in Destiny 2
const membershipInfo = (await getMembershipDataForCurrentUser(authedClient)).Response;
getApplicationApiUsage(authedClient, { applicationId: 16281 });
// a single bnet account have have multiple attached destiny accounts. this is the id of the main cross-saved one.
const primaryId = membershipInfo.primaryMembershipId;
// using a few !s to make assumptions. don't log into a bnet account with no destiny profile :)
const primaryMembership = membershipInfo.destinyMemberships.find((m) => m.membershipId === primaryId)!;
// a destiny account has an id, and a type (platform)
const { membershipId, membershipType } = primaryMembership;
// getProfile is the main endpoint: inventory, triumphs, characters, everything's here.
// which components you request, determines which information will be sent back
const profileResponse = (
await getProfile(httpClient, {
destinyMembershipId: membershipId,
membershipType: membershipType,
components: [DestinyComponentType.Profiles], // here, we ask for the most basic component
})
).Response;
// the goal here is simply to find the ID of one of our characters, so we can make a vendor call
// pick the first char in the list
const characterId = profileResponse.profile.data!.characterIds[0];
// getVendor only works with user authentication. you can't check someone else's vendors
const fetchedVendorResponse = (
await getVendor(authedClient, {
vendorHash: 350061650, // let's check on ada-1's armor
characterId,
membershipType,
destinyMembershipId: membershipId,
// we'll find out what she's selling, and what the stats are for any items she sells
components: [DestinyComponentType.VendorSales, DestinyComponentType.ItemStats],
})
).Response;
// now that we got the API data we wanted, store it in this component's state
setVendorResponse(fetchedVendorResponse);
})();
}, [authedClient]);
if (!vendorResponse) return "loading account and vendor info...";
// let's find a piece of armor that our favorite exo offers
const aPieceOfArmor = Object.values(vendorResponse.sales.data!).find((sale) => {
// a sale item has very little information about the item itself, mostly the sale.
// we'll use the item's definition for a fuller picture of it
const itemDef = getInventoryItemLiteDef(sale.itemHash);
// check its ItemType and stop searching when we find an armor
return itemDef?.itemType === DestinyItemType.Armor;
});
if (!aPieceOfArmor) return "hmm. is ada-1 not selling any armor?";
// vendor sales, components, etc, are in dictionaries, keyed by vendorItemIndex
// their property structure is a little silly and redundant, as you can see from the below path.
const armorStats = vendorResponse.itemComponents.stats.data![aPieceOfArmor.vendorItemIndex].stats!;
// this ^ is a dictionary of stat information, keyed by stat hash.
const armorDef = getInventoryItemLiteDef(aPieceOfArmor.itemHash)!;
return (
<>
ada-1 is selling a{" "}
{armorDef.displayProperties.name} ({armorDef.itemTypeDisplayName})
{Object.values(armorStats).map((s) => {
const statDef = getStatDef(s.statHash)!;
const statName = statDef.displayProperties.name;
return (
-
{statName}: {s.value}
);
})}
>
);
}
export default App;