mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-18 19:12:45 +00:00
support for unlockable flags (#3479)
## Description: Add support for purchasable/gated flags. * Create a new "Store" modal that renders both skins & flags * move all store related logic out of TerritoryPatternsModal * use nation:code for existing nation flags & flag:key for gated flags * check if user has the appropriate flags before purchasing ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: evan
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
import { cosmeticRelationship } from "../src/client/Cosmetics";
|
||||
import { UserMeResponse } from "../src/core/ApiSchemas";
|
||||
|
||||
const product = { productId: "prod_123", priceId: "price_123", price: "$4.99" };
|
||||
|
||||
function makeUserMe(flares: string[]): UserMeResponse {
|
||||
return {
|
||||
player: { flares },
|
||||
} as unknown as UserMeResponse;
|
||||
}
|
||||
|
||||
describe("cosmeticRelationship", () => {
|
||||
it("returns owned when user has wildcard flare", () => {
|
||||
expect(
|
||||
cosmeticRelationship(
|
||||
{
|
||||
wildcardFlare: "flag:*",
|
||||
requiredFlare: "flag:cool",
|
||||
product,
|
||||
affiliateCode: null,
|
||||
itemAffiliateCode: null,
|
||||
},
|
||||
makeUserMe(["flag:*"]),
|
||||
),
|
||||
).toBe("owned");
|
||||
});
|
||||
|
||||
it("returns owned when user has the specific flare", () => {
|
||||
expect(
|
||||
cosmeticRelationship(
|
||||
{
|
||||
wildcardFlare: "flag:*",
|
||||
requiredFlare: "flag:cool",
|
||||
product,
|
||||
affiliateCode: null,
|
||||
itemAffiliateCode: null,
|
||||
},
|
||||
makeUserMe(["flag:cool"]),
|
||||
),
|
||||
).toBe("owned");
|
||||
});
|
||||
|
||||
it("returns blocked when no product and user does not own it", () => {
|
||||
expect(
|
||||
cosmeticRelationship(
|
||||
{
|
||||
wildcardFlare: "flag:*",
|
||||
requiredFlare: "flag:cool",
|
||||
product: null,
|
||||
affiliateCode: null,
|
||||
itemAffiliateCode: null,
|
||||
},
|
||||
makeUserMe([]),
|
||||
),
|
||||
).toBe("blocked");
|
||||
});
|
||||
|
||||
it("returns blocked when affiliate codes do not match", () => {
|
||||
expect(
|
||||
cosmeticRelationship(
|
||||
{
|
||||
wildcardFlare: "flag:*",
|
||||
requiredFlare: "flag:cool",
|
||||
product,
|
||||
affiliateCode: "storeA",
|
||||
itemAffiliateCode: "storeB",
|
||||
},
|
||||
makeUserMe([]),
|
||||
),
|
||||
).toBe("blocked");
|
||||
});
|
||||
|
||||
it("returns purchasable when product exists and affiliate matches", () => {
|
||||
expect(
|
||||
cosmeticRelationship(
|
||||
{
|
||||
wildcardFlare: "flag:*",
|
||||
requiredFlare: "flag:cool",
|
||||
product,
|
||||
affiliateCode: null,
|
||||
itemAffiliateCode: null,
|
||||
},
|
||||
makeUserMe([]),
|
||||
),
|
||||
).toBe("purchasable");
|
||||
});
|
||||
|
||||
it("returns purchasable when affiliate codes match", () => {
|
||||
expect(
|
||||
cosmeticRelationship(
|
||||
{
|
||||
wildcardFlare: "pattern:*",
|
||||
requiredFlare: "pattern:stripes:red",
|
||||
product,
|
||||
affiliateCode: "storeA",
|
||||
itemAffiliateCode: "storeA",
|
||||
},
|
||||
makeUserMe([]),
|
||||
),
|
||||
).toBe("purchasable");
|
||||
});
|
||||
|
||||
it("returns blocked when user is not logged in and no product", () => {
|
||||
expect(
|
||||
cosmeticRelationship(
|
||||
{
|
||||
wildcardFlare: "flag:*",
|
||||
requiredFlare: "flag:cool",
|
||||
product: null,
|
||||
affiliateCode: null,
|
||||
itemAffiliateCode: null,
|
||||
},
|
||||
false,
|
||||
),
|
||||
).toBe("blocked");
|
||||
});
|
||||
|
||||
it("returns purchasable when user is not logged in but product exists", () => {
|
||||
expect(
|
||||
cosmeticRelationship(
|
||||
{
|
||||
wildcardFlare: "flag:*",
|
||||
requiredFlare: "flag:cool",
|
||||
product,
|
||||
affiliateCode: null,
|
||||
itemAffiliateCode: null,
|
||||
},
|
||||
false,
|
||||
),
|
||||
).toBe("purchasable");
|
||||
});
|
||||
|
||||
it("returns owned when user has wildcard flare for patterns", () => {
|
||||
expect(
|
||||
cosmeticRelationship(
|
||||
{
|
||||
wildcardFlare: "pattern:*",
|
||||
requiredFlare: "pattern:stripes:red",
|
||||
product,
|
||||
affiliateCode: null,
|
||||
itemAffiliateCode: null,
|
||||
},
|
||||
makeUserMe(["pattern:*"]),
|
||||
),
|
||||
).toBe("owned");
|
||||
});
|
||||
});
|
||||
+79
-1
@@ -18,7 +18,7 @@ const bannedWords = [
|
||||
const matcher = createMatcher(bannedWords);
|
||||
|
||||
// Create a minimal PrivilegeCheckerImpl for testing censorUsername
|
||||
const mockCosmetics = { patterns: {}, colorPalettes: {} };
|
||||
const mockCosmetics = { patterns: {}, colorPalettes: {}, flags: {} };
|
||||
const mockDecoder = () => new Uint8Array();
|
||||
const checker = new PrivilegeCheckerImpl(
|
||||
mockCosmetics,
|
||||
@@ -27,6 +27,24 @@ const checker = new PrivilegeCheckerImpl(
|
||||
);
|
||||
const emptyChecker = new PrivilegeCheckerImpl(mockCosmetics, mockDecoder, []);
|
||||
|
||||
const flagCosmetics = {
|
||||
patterns: {},
|
||||
colorPalettes: {},
|
||||
flags: {
|
||||
cool_flag: {
|
||||
name: "cool_flag",
|
||||
url: "https://example.com/cool.png",
|
||||
affiliateCode: null,
|
||||
product: { productId: "prod_1", priceId: "price_1", price: "$4.99" },
|
||||
},
|
||||
},
|
||||
};
|
||||
const flagChecker = new PrivilegeCheckerImpl(
|
||||
flagCosmetics,
|
||||
mockDecoder,
|
||||
bannedWords,
|
||||
);
|
||||
|
||||
describe("UsernameCensor", () => {
|
||||
describe("isProfane (via matcher.hasMatch)", () => {
|
||||
test("detects exact banned words", () => {
|
||||
@@ -145,3 +163,63 @@ describe("UsernameCensor", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Flag validation in isAllowed", () => {
|
||||
test("allows valid country flag and resolves to SVG path", () => {
|
||||
const result = flagChecker.isAllowed([], { flag: "country:us" });
|
||||
expect(result.type).toBe("allowed");
|
||||
if (result.type === "allowed") {
|
||||
expect(result.cosmetics.flag).toBe("/flags/us.svg");
|
||||
}
|
||||
});
|
||||
|
||||
test("rejects invalid country code", () => {
|
||||
const result = flagChecker.isAllowed([], { flag: "country:zzzz" });
|
||||
expect(result.type).toBe("forbidden");
|
||||
});
|
||||
|
||||
test("rejects flag with no prefix", () => {
|
||||
const result = flagChecker.isAllowed([], { flag: "us" });
|
||||
expect(result.type).toBe("forbidden");
|
||||
});
|
||||
|
||||
test("allows cosmetic flag when user has wildcard flare", () => {
|
||||
const result = flagChecker.isAllowed(["flag:*"], {
|
||||
flag: "flag:cool_flag",
|
||||
});
|
||||
expect(result.type).toBe("allowed");
|
||||
if (result.type === "allowed") {
|
||||
expect(result.cosmetics.flag).toBe("https://example.com/cool.png");
|
||||
}
|
||||
});
|
||||
|
||||
test("allows cosmetic flag when user has specific flare", () => {
|
||||
const result = flagChecker.isAllowed(["flag:cool_flag"], {
|
||||
flag: "flag:cool_flag",
|
||||
});
|
||||
expect(result.type).toBe("allowed");
|
||||
if (result.type === "allowed") {
|
||||
expect(result.cosmetics.flag).toBe("https://example.com/cool.png");
|
||||
}
|
||||
});
|
||||
|
||||
test("rejects cosmetic flag when user lacks flare", () => {
|
||||
const result = flagChecker.isAllowed([], { flag: "flag:cool_flag" });
|
||||
expect(result.type).toBe("forbidden");
|
||||
});
|
||||
|
||||
test("rejects cosmetic flag that does not exist", () => {
|
||||
const result = flagChecker.isAllowed(["flag:*"], {
|
||||
flag: "flag:nonexistent",
|
||||
});
|
||||
expect(result.type).toBe("forbidden");
|
||||
});
|
||||
|
||||
test("allows no flag", () => {
|
||||
const result = flagChecker.isAllowed([], {});
|
||||
expect(result.type).toBe("allowed");
|
||||
if (result.type === "allowed") {
|
||||
expect(result.cosmetics.flag).toBeUndefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user