Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions .env-example
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
API_URL=http://localhost:3001
DEFAULT_EGG_IMAGE_URL=https://topiaimages.s3.us-west-1.amazonaws.com/arva_egg.png
DEFAULT_KEY_ASSET_IMAGE_URL=https://topiaimages.s3.us-west-1.amazonaws.com/arva_egg.png
DEV_URL=https://your-ngrok-url.ngrok-free.app
EMOTE_NAME=quest_1
INSTANCE_DOMAIN=api.topia.io
INSTANCE_PROTOCOL=https
INTERACTIVE_KEY=xxxxxxx
INTERACTIVE_SECRET=xxxxxxx
INTERACTIVE_KEY=your_interactive_key
INTERACTIVE_SECRET=your_interactive_secret
NODE_ENV="development"
PORT=3000
WEB_IMAGE_ASSET_ID=webImageAsset
GOOGLESHEETS_CLIENT_EMAILxxxxxxx
GOOGLESHEETS_SHEET_IDxxxxxxx
GOOGLESHEETS_PRIVATE_KEY=xxxxxxx
GOOGLESHEETS_CLIENT_EMAIL=your_google_service_account_email
GOOGLESHEETS_SHEET_ID=your_google_sheet_id
GOOGLESHEETS_PRIVATE_KEY=your_google_private_key
39 changes: 22 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,16 +51,29 @@ Quest is a dynamic hide and seek game where an admin can drop multiple quest ite

### Required Assets with Unique Names

| Unique Name Pattern | Description |
|---------------------|-------------|
| Unique Name Pattern | Description |
| ------------------------- | -------------------------------------------------------------------------------------------- |
| `questItem_{sceneDropId}` | Quest items dropped in the world. The app uses this pattern to track and manage quest items. |

### Environment Variables

| Variable | Description |
|----------|-------------|
| `WEB_IMAGE_ASSET_ID` | The asset ID used to create web image assets for quest items. Default: `webImageAsset` |
| `DEFAULT_QUEST_ITEM_IMAGE_URL` | Default image URL for quest items when no custom image is set |
Create a `.env` file in the root directory. See `.env-example` for a template.

| Variable | Description | Required |
| ----------------------------- | ----------------------------------------------------------------------------------- | -------- |
| `NODE_ENV` | Node environment (`development` or `production`) | No |
| `PORT` | Server port (default: `3000`) | No |
| `INSTANCE_DOMAIN` | Topia API domain (`api.topia.io` for production, `api-stage.topia.io` for staging) | Yes |
| `INTERACTIVE_KEY` | Topia interactive app key | Yes |
| `INTERACTIVE_SECRET` | Topia interactive app secret | Yes |
| `WEB_IMAGE_ASSET_ID` | Asset ID used to create web image assets for quest items (default: `webImageAsset`) | No |
| `DEFAULT_EGG_IMAGE_URL` | Default image URL for quest egg items | No |
| `DEFAULT_KEY_ASSET_IMAGE_URL` | Default image URL for the key asset | No |
| `DEV_URL` | Development URL (e.g., ngrok URL for webhook forwarding) | No |
| `EMOTE_NAME` | Name of the emote to trigger on quest completion (e.g., `quest_1`) | No |
| `GOOGLESHEETS_CLIENT_EMAIL` | Google service account email for analytics | No |
| `GOOGLESHEETS_SHEET_ID` | Google Sheet ID for analytics | No |
| `GOOGLESHEETS_PRIVATE_KEY` | Google service account private key | No |

---

Expand All @@ -76,17 +89,9 @@ Quest is a dynamic hide and seek game where an admin can drop multiple quest ite

### Add your .env environmental variables

```json
API_KEY=xxxxxxxxxxxxx
DEFAULT_QUEST_ITEM_IMAGE_URL=pathtodefaultimage
INSTANCE_DOMAIN=api.topia.io
INSTANCE_PROTOCOL=https
INTERACTIVE_KEY=xxxxxxxxxxxxx
INTERACTIVE_SECRET=xxxxxxxxxxxxxx
WEB_IMAGE_ASSET_ID=webImageAsset
```

### Where to find API_KEY, INTERACTIVE_KEY and INTERACTIVE_SECRET
See [Environment Variables](#environment-variables) above.

### Where to find INTERACTIVE_KEY and INTERACTIVE_SECRET

[Topia Dev Account Dashboard](https://dev.topia.io/t/dashboard/integrations)

Expand Down
5 changes: 4 additions & 1 deletion client/src/pages/Home.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useContext, useEffect, useState } from "react";
import { useSearchParams } from "react-router-dom";

// components
import { Leaderboard, PageContainer } from "@/components";
Expand All @@ -12,6 +13,8 @@ import { backendAPI, setErrorMessage } from "@/utils";

export const Home = () => {
const [isLoading, setIsLoading] = useState(true);
const [searchParams] = useSearchParams();
const forceRefreshInventory = searchParams.get("forceRefreshInventory") === "true";

// context
const dispatch = useContext(GlobalDispatchContext);
Expand All @@ -20,7 +23,7 @@ export const Home = () => {
useEffect(() => {
if (hasInteractiveParams) {
backendAPI
.get("/quest")
.get("/quest", { params: { forceRefreshInventory } })
.then((response) => {
const { questDetails, visitor, badges, visitorInventory } = response.data;
dispatch!({
Expand Down
3 changes: 2 additions & 1 deletion server/controllers/droppedAssets/handleGetQuestDetails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ import { errorHandler, getBadges, getCredentials, getVisitor, getWorldDetails }
export const handleGetQuestDetails = async (req: Request, res: Response) => {
try {
const credentials = getCredentials(req.query);
const forceRefreshInventory = req.query.forceRefreshInventory === "true";

const { dataObject } = await getWorldDetails(credentials, false);

const { visitor, visitorInventory } = await getVisitor(credentials, credentials.assetId);

const badges = await getBadges(credentials);
const badges = await getBadges(credentials, forceRefreshInventory);

return res.json({
questDetails: dataObject,
Expand Down
4 changes: 2 additions & 2 deletions server/utils/getBadges.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import { Credentials } from "../types/Credentials.js";
import { getCachedInventoryItems } from "./inventoryCache.js";
import { standardizeError } from "./standardizeError.js";

export const getBadges = async (credentials: Credentials) => {
export const getBadges = async (credentials: Credentials, forceRefresh = false) => {
try {
const inventoryItems = await getCachedInventoryItems({ credentials });
const inventoryItems = await getCachedInventoryItems({ credentials, forceRefresh });

const badges: {
[name: string]: {
Expand Down
4 changes: 2 additions & 2 deletions server/utils/inventoryCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ interface CachedInventory {
timestamp: number;
}

// Cache duration: 24 hours in milliseconds
const CACHE_DURATION_MS = 24 * 60 * 60 * 1000;
// Cache duration: 6 hours in milliseconds
const CACHE_DURATION_MS = 6 * 60 * 60 * 1000;

// In-memory cache
let inventoryCache: CachedInventory | null = null;
Expand Down