From ec0055928b7bea816f440cc72e98a4c440a9efa5 Mon Sep 17 00:00:00 2001 From: Jiralite <33201955+Jiralite@users.noreply.github.com> Date: Tue, 18 Jun 2024 09:57:17 +0100 Subject: [PATCH 1/8] feat: premium buttons --- .../builders/src/components/button/Button.ts | 13 +++++++++ .../interfaces/InteractionResponses.js | 1 + .../formatters/__tests__/formatters.test.ts | 15 ++++++++++ packages/formatters/src/formatters.ts | 28 +++++++++++++++++++ 4 files changed, 57 insertions(+) diff --git a/packages/builders/src/components/button/Button.ts b/packages/builders/src/components/button/Button.ts index b89172613ce2..03bc7c3f20cd 100644 --- a/packages/builders/src/components/button/Button.ts +++ b/packages/builders/src/components/button/Button.ts @@ -6,6 +6,7 @@ import { type APIButtonComponentWithURL, type APIMessageComponentEmoji, type ButtonStyle, + type Snowflake, } from 'discord-api-types/v10'; import { buttonLabelValidator, @@ -89,6 +90,18 @@ export class ButtonBuilder extends ComponentBuilder { return this; } + /** + * Sets the SKU id that represents a purchasable SKU for this button. + * + * @remarks Only available when using premium-style buttons. + * @param skuId - The SKU id to use + */ + public setSKUId(skuId: Snowflake) { + // @ts-expect-error discord-api-types. + (this.data as APIButtonComponentWithSKUId).sku_id = skuId; + return this; + } + /** * Sets the emoji to display on this button. * diff --git a/packages/discord.js/src/structures/interfaces/InteractionResponses.js b/packages/discord.js/src/structures/interfaces/InteractionResponses.js index c52ba133f5ef..f33179c11837 100644 --- a/packages/discord.js/src/structures/interfaces/InteractionResponses.js +++ b/packages/discord.js/src/structures/interfaces/InteractionResponses.js @@ -266,6 +266,7 @@ class InteractionResponses { /** * Responds to the interaction with an upgrade button. * Only available for applications with monetization enabled. + * @deprecated Using a premium button is the new Discord behaviour. * @returns {Promise} */ async sendPremiumRequired() { diff --git a/packages/formatters/__tests__/formatters.test.ts b/packages/formatters/__tests__/formatters.test.ts index 362543692333..1b917dad701a 100644 --- a/packages/formatters/__tests__/formatters.test.ts +++ b/packages/formatters/__tests__/formatters.test.ts @@ -2,6 +2,7 @@ import { URL } from 'node:url'; import { describe, test, expect, vitest } from 'vitest'; import { + applicationDirectory, chatInputApplicationCommandMention, blockQuote, bold, @@ -313,6 +314,20 @@ describe('Message formatters', () => { }); }); + describe('applicationDirectoryStore', () => { + test('GIVEN application id THEN returns application directory store', () => { + expect(applicationDirectory('123456789012345678')).toEqual( + 'https://discord.com/application-directory/123456789012345678/store', + ); + }); + + test('GIVEN application id AND SKU id THEN returns SKU within the application directory store', () => { + expect(applicationDirectory('123456789012345678', '123456789012345678')).toEqual( + 'https://discord.com/application-directory/123456789012345678/store/123456789012345678', + ); + }); + }); + describe('Faces', () => { test('GIVEN Faces.Shrug THEN returns "¯\\_(ツ)_/¯"', () => { expect<'¯\\_(ツ)_/¯'>(Faces.Shrug).toEqual('¯\\_(ツ)_/¯'); diff --git a/packages/formatters/src/formatters.ts b/packages/formatters/src/formatters.ts index eec15ed140a0..2cdd4cd944cd 100644 --- a/packages/formatters/src/formatters.ts +++ b/packages/formatters/src/formatters.ts @@ -615,6 +615,34 @@ export function time(timeOrSeconds?: Date | number, style?: TimestampStylesStrin return typeof style === 'string' ? `` : ``; } +/** + * Formats an application directory link. + * + * @typeParam ApplicationId - This is inferred by the supplied application id + * @param applicationId - The application id + */ +export function applicationDirectory(applicationId: ApplicationId): string; + +/** + * Formats an application directory SKU link. + * + * @typeParam ApplicationId - This is inferred by the supplied application id + * @typeParam SKUId - This is inferred by the supplied SKU id + * @param applicationId - The application id + * @param skuId - The SKU id + */ +export function applicationDirectory( + applicationId: ApplicationId, + skuId: SKUId, +): string; + +export function applicationDirectory( + applicationId: ApplicationId, + skuId?: SKUId, +): string { + return `https://discord.com/application-directory/${applicationId}/store${skuId ? `/${skuId}` : ''}`; +} + /** * The {@link https://discord.com/developers/docs/reference#message-formatting-timestamp-styles | message formatting timestamp styles} * supported by Discord. From 94fea30d3403a6e7395bfeb3d22426c4bec450e5 Mon Sep 17 00:00:00 2001 From: Jiralite <33201955+Jiralite@users.noreply.github.com> Date: Tue, 18 Jun 2024 10:05:32 +0100 Subject: [PATCH 2/8] docs: deprecation string --- packages/core/src/api/interactions.ts | 1 + .../src/structures/interfaces/InteractionResponses.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/core/src/api/interactions.ts b/packages/core/src/api/interactions.ts index 2cd0a9d18b1c..797e71caa6ea 100644 --- a/packages/core/src/api/interactions.ts +++ b/packages/core/src/api/interactions.ts @@ -258,6 +258,7 @@ export class InteractionsAPI { * @param interactionId - The id of the interaction * @param interactionToken - The token of the interaction * @param options - The options for sending the premium required response + * @deprecated Sending a premium-style button is the new Discord behaviour. */ public async sendPremiumRequired( interactionId: Snowflake, diff --git a/packages/discord.js/src/structures/interfaces/InteractionResponses.js b/packages/discord.js/src/structures/interfaces/InteractionResponses.js index f33179c11837..f7cfce1c901c 100644 --- a/packages/discord.js/src/structures/interfaces/InteractionResponses.js +++ b/packages/discord.js/src/structures/interfaces/InteractionResponses.js @@ -266,7 +266,7 @@ class InteractionResponses { /** * Responds to the interaction with an upgrade button. * Only available for applications with monetization enabled. - * @deprecated Using a premium button is the new Discord behaviour. + * @deprecated Sending a premium-style button is the new Discord behaviour. * @returns {Promise} */ async sendPremiumRequired() { From 67a144574f0fa80c3b3c41a30f63f5cb8301e69d Mon Sep 17 00:00:00 2001 From: Jiralite <33201955+Jiralite@users.noreply.github.com> Date: Tue, 18 Jun 2024 10:18:39 +0100 Subject: [PATCH 3/8] feat(InteractionResponses): add deprecation message --- .../src/structures/interfaces/InteractionResponses.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/discord.js/src/structures/interfaces/InteractionResponses.js b/packages/discord.js/src/structures/interfaces/InteractionResponses.js index f7cfce1c901c..9f711b517b49 100644 --- a/packages/discord.js/src/structures/interfaces/InteractionResponses.js +++ b/packages/discord.js/src/structures/interfaces/InteractionResponses.js @@ -1,5 +1,6 @@ 'use strict'; +const { deprecate } = require('node:util'); const { isJSONEncodable } = require('@discordjs/util'); const { InteractionResponseType, MessageFlags, Routes, InteractionType } = require('discord-api-types/v10'); const { DiscordjsError, ErrorCodes } = require('../../errors'); @@ -338,4 +339,10 @@ class InteractionResponses { } } +InteractionResponses.prototype.sendPremiumRequired = deprecate( + InteractionResponses.prototype.sendPremiumRequired, + // eslint-disable-next-line max-len + 'InteractionResponses#sendPremiumRequired() is deprecated. Sending a premium-style button is the new Discord behaviour.', +); + module.exports = InteractionResponses; From 9c55454b7b9f65341fcf83872a5fc1eb55a2d4d6 Mon Sep 17 00:00:00 2001 From: Jiralite <33201955+Jiralite@users.noreply.github.com> Date: Tue, 18 Jun 2024 10:53:59 +0100 Subject: [PATCH 4/8] feat(builders): add tests --- .../__tests__/components/button.test.ts | 47 +++++++++++++++++++ .../builders/src/components/Assertions.ts | 38 ++++++++++----- .../builders/src/components/button/Button.ts | 2 + 3 files changed, 76 insertions(+), 11 deletions(-) diff --git a/packages/builders/__tests__/components/button.test.ts b/packages/builders/__tests__/components/button.test.ts index 29da7b4720b0..410b49bb4896 100644 --- a/packages/builders/__tests__/components/button.test.ts +++ b/packages/builders/__tests__/components/button.test.ts @@ -50,6 +50,12 @@ describe('Button Components', () => { button.toJSON(); }).not.toThrowError(); + expect(() => { + // @ts-expect-error: discord-api-types. + const button = buttonComponent().setSKUId('123456789012345678').setStyle(ButtonStyle.Premium); + button.toJSON(); + }).not.toThrowError(); + expect(() => buttonComponent().setURL('https://google.com')).not.toThrowError(); }); @@ -101,6 +107,47 @@ describe('Button Components', () => { button.toJSON(); }).toThrowError(); + expect(() => { + const button = buttonComponent().setStyle(ButtonStyle.Primary).setSKUId('123456789012345678'); + button.toJSON(); + }).toThrowError(); + + expect(() => { + const button = buttonComponent() + .setStyle(ButtonStyle.Secondary) + .setLabel('button') + .setSKUId('123456789012345678'); + + button.toJSON(); + }).toThrowError(); + + expect(() => { + const button = buttonComponent() + .setStyle(ButtonStyle.Success) + .setEmoji({ name: '😇' }) + .setSKUId('123456789012345678'); + + button.toJSON(); + }).toThrowError(); + + expect(() => { + const button = buttonComponent() + .setStyle(ButtonStyle.Danger) + .setCustomId('test') + .setSKUId('123456789012345678'); + + button.toJSON(); + }).toThrowError(); + + expect(() => { + const button = buttonComponent() + .setStyle(ButtonStyle.Link) + .setURL('https://google.com') + .setSKUId('123456789012345678'); + + button.toJSON(); + }).toThrowError(); + // @ts-expect-error: Invalid style expect(() => buttonComponent().setStyle(24)).toThrowError(); expect(() => buttonComponent().setLabel(longStr)).toThrowError(); diff --git a/packages/builders/src/components/Assertions.ts b/packages/builders/src/components/Assertions.ts index 960efd706c7c..1f42d8279d74 100644 --- a/packages/builders/src/components/Assertions.ts +++ b/packages/builders/src/components/Assertions.ts @@ -81,21 +81,37 @@ export function validateRequiredButtonParameters( label?: string, emoji?: APIMessageComponentEmoji, customId?: string, + skuId?: string, url?: string, ) { - if (url && customId) { - throw new RangeError('URL and custom id are mutually exclusive'); - } + // @ts-expect-error discord-api-types. + if (style === ButtonStyle.Premium) { + if (!skuId) { + throw new RangeError('Premium buttons must have an SKU id.'); + } - if (!label && !emoji) { - throw new RangeError('Buttons must have a label and/or an emoji'); - } + if (customId || label || url || emoji) { + throw new RangeError('Premium buttons cannot have a custom id, label, URL, or emoji.'); + } + } else { + if (skuId) { + throw new RangeError('Non-premium buttons must not have an SKU id.'); + } + + if (url && customId) { + throw new RangeError('URL and custom id are mutually exclusive.'); + } + + if (!label && !emoji) { + throw new RangeError('Non-premium buttons must have a label and/or an emoji.'); + } - if (style === ButtonStyle.Link) { - if (!url) { - throw new RangeError('Link buttons must have a url'); + if (style === ButtonStyle.Link) { + if (!url) { + throw new RangeError('Link buttons must have a URL.'); + } + } else if (url) { + throw new RangeError('Non-premium and non-link buttons cannot have a URL.'); } - } else if (url) { - throw new RangeError('Non-link buttons cannot have a url'); } } diff --git a/packages/builders/src/components/button/Button.ts b/packages/builders/src/components/button/Button.ts index 03bc7c3f20cd..e3db4a4f3ad9 100644 --- a/packages/builders/src/components/button/Button.ts +++ b/packages/builders/src/components/button/Button.ts @@ -141,6 +141,8 @@ export class ButtonBuilder extends ComponentBuilder { (this.data as Exclude).label, (this.data as Exclude).emoji, (this.data as APIButtonComponentWithCustomId).custom_id, + // @ts-expect-error discord-api-types. + (this.data as APIButtonComponentWithSKUId).sku_id, (this.data as APIButtonComponentWithURL).url, ); From 0dd5d6faed025697bf5a3fcdbb60af9f6b993c18 Mon Sep 17 00:00:00 2001 From: Jiralite <33201955+Jiralite@users.noreply.github.com> Date: Tue, 18 Jun 2024 19:41:53 +0100 Subject: [PATCH 5/8] chore: remove @ts-expect-errors --- packages/builders/__tests__/components/button.test.ts | 1 - packages/builders/src/components/Assertions.ts | 1 - packages/builders/src/components/button/Button.ts | 2 -- 3 files changed, 4 deletions(-) diff --git a/packages/builders/__tests__/components/button.test.ts b/packages/builders/__tests__/components/button.test.ts index 410b49bb4896..0eb5134d4312 100644 --- a/packages/builders/__tests__/components/button.test.ts +++ b/packages/builders/__tests__/components/button.test.ts @@ -51,7 +51,6 @@ describe('Button Components', () => { }).not.toThrowError(); expect(() => { - // @ts-expect-error: discord-api-types. const button = buttonComponent().setSKUId('123456789012345678').setStyle(ButtonStyle.Premium); button.toJSON(); }).not.toThrowError(); diff --git a/packages/builders/src/components/Assertions.ts b/packages/builders/src/components/Assertions.ts index 1f42d8279d74..793e6b5bca44 100644 --- a/packages/builders/src/components/Assertions.ts +++ b/packages/builders/src/components/Assertions.ts @@ -84,7 +84,6 @@ export function validateRequiredButtonParameters( skuId?: string, url?: string, ) { - // @ts-expect-error discord-api-types. if (style === ButtonStyle.Premium) { if (!skuId) { throw new RangeError('Premium buttons must have an SKU id.'); diff --git a/packages/builders/src/components/button/Button.ts b/packages/builders/src/components/button/Button.ts index e3db4a4f3ad9..cc36d80dabcb 100644 --- a/packages/builders/src/components/button/Button.ts +++ b/packages/builders/src/components/button/Button.ts @@ -97,7 +97,6 @@ export class ButtonBuilder extends ComponentBuilder { * @param skuId - The SKU id to use */ public setSKUId(skuId: Snowflake) { - // @ts-expect-error discord-api-types. (this.data as APIButtonComponentWithSKUId).sku_id = skuId; return this; } @@ -141,7 +140,6 @@ export class ButtonBuilder extends ComponentBuilder { (this.data as Exclude).label, (this.data as Exclude).emoji, (this.data as APIButtonComponentWithCustomId).custom_id, - // @ts-expect-error discord-api-types. (this.data as APIButtonComponentWithSKUId).sku_id, (this.data as APIButtonComponentWithURL).url, ); From 21bc4e8bbf4242b51e43afaae196fe780609cb3d Mon Sep 17 00:00:00 2001 From: Jiralite <33201955+Jiralite@users.noreply.github.com> Date: Wed, 19 Jun 2024 00:13:09 +0100 Subject: [PATCH 6/8] test: update method name --- packages/formatters/__tests__/formatters.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/formatters/__tests__/formatters.test.ts b/packages/formatters/__tests__/formatters.test.ts index 1b917dad701a..645d9b817140 100644 --- a/packages/formatters/__tests__/formatters.test.ts +++ b/packages/formatters/__tests__/formatters.test.ts @@ -314,7 +314,7 @@ describe('Message formatters', () => { }); }); - describe('applicationDirectoryStore', () => { + describe('applicationDirectory', () => { test('GIVEN application id THEN returns application directory store', () => { expect(applicationDirectory('123456789012345678')).toEqual( 'https://discord.com/application-directory/123456789012345678/store', From 9eb879164df443d150239b09295104ded10c68c4 Mon Sep 17 00:00:00 2001 From: Jiralite <33201955+Jiralite@users.noreply.github.com> Date: Thu, 27 Jun 2024 22:36:16 +0100 Subject: [PATCH 7/8] refactor(formatters): stricter types --- packages/formatters/src/formatters.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/formatters/src/formatters.ts b/packages/formatters/src/formatters.ts index 2cdd4cd944cd..37bfbaf9a2c2 100644 --- a/packages/formatters/src/formatters.ts +++ b/packages/formatters/src/formatters.ts @@ -621,7 +621,9 @@ export function time(timeOrSeconds?: Date | number, style?: TimestampStylesStrin * @typeParam ApplicationId - This is inferred by the supplied application id * @param applicationId - The application id */ -export function applicationDirectory(applicationId: ApplicationId): string; +export function applicationDirectory( + applicationId: ApplicationId, +): `https://discord.com/application-directory/${ApplicationId}/store`; /** * Formats an application directory SKU link. @@ -634,13 +636,16 @@ export function applicationDirectory(applicatio export function applicationDirectory( applicationId: ApplicationId, skuId: SKUId, -): string; +): `https://discord.com/application-directory/${ApplicationId}/store/${SKUId}`; export function applicationDirectory( applicationId: ApplicationId, skuId?: SKUId, -): string { - return `https://discord.com/application-directory/${applicationId}/store${skuId ? `/${skuId}` : ''}`; +): + | `https://discord.com/application-directory/${ApplicationId}/store/${SKUId}` + | `https://discord.com/application-directory/${ApplicationId}/store` { + const url = `https://discord.com/application-directory/${applicationId}/store` as const; + return skuId ? `${url}/${skuId}` : url; } /** From cfb105a7c73e969d20d6c08a1bd33f9f69133926 Mon Sep 17 00:00:00 2001 From: Jiralite <33201955+Jiralite@users.noreply.github.com> Date: Thu, 27 Jun 2024 22:47:19 +0100 Subject: [PATCH 8/8] docs: deprecate method in typings --- packages/discord.js/typings/index.d.ts | 3 +++ packages/discord.js/typings/index.test-d.ts | 8 +++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index 5545e4f31cb7..fd03fa0e5ad7 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -604,6 +604,7 @@ export abstract class CommandInteraction e | ModalComponentData | APIModalInteractionResponseCallbackData, ): Promise; + /** @deprecated Sending a premium-style button is the new Discord behaviour. */ public sendPremiumRequired(): Promise; public awaitModalSubmit( options: AwaitModalSubmitOptions, @@ -2261,6 +2262,7 @@ export class MessageComponentInteraction e | ModalComponentData | APIModalInteractionResponseCallbackData, ): Promise; + /** @deprecated Sending a premium-style button is the new Discord behaviour. */ public sendPremiumRequired(): Promise; public awaitModalSubmit( options: AwaitModalSubmitOptions, @@ -2460,6 +2462,7 @@ export class ModalSubmitInteraction extend options: InteractionDeferUpdateOptions & { fetchReply: true }, ): Promise>>; public deferUpdate(options?: InteractionDeferUpdateOptions): Promise>>; + /** @deprecated Sending a premium-style button is the new Discord behaviour. */ public sendPremiumRequired(): Promise; public inGuild(): this is ModalSubmitInteraction<'raw' | 'cached'>; public inCachedGuild(): this is ModalSubmitInteraction<'cached'>; diff --git a/packages/discord.js/typings/index.test-d.ts b/packages/discord.js/typings/index.test-d.ts index d9b212f23807..407f6023e8d2 100644 --- a/packages/discord.js/typings/index.test-d.ts +++ b/packages/discord.js/typings/index.test-d.ts @@ -206,7 +206,7 @@ import { MentionableSelectMenuComponent, Poll, } from '.'; -import { expectAssignable, expectNotAssignable, expectNotType, expectType } from 'tsd'; +import { expectAssignable, expectDeprecated, expectNotAssignable, expectNotType, expectType } from 'tsd'; import type { ContextMenuCommandBuilder, SlashCommandBuilder } from '@discordjs/builders'; import { ReadonlyCollection } from '@discordjs/collection'; @@ -1763,6 +1763,7 @@ client.on('interactionCreate', async interaction => { expectType(interaction); expectType(interaction.component); expectType(interaction.message); + expectDeprecated(interaction.sendPremiumRequired()); if (interaction.inCachedGuild()) { expectAssignable(interaction); expectType(interaction.component); @@ -1950,6 +1951,7 @@ client.on('interactionCreate', async interaction => { interaction.type === InteractionType.ApplicationCommand && interaction.commandType === ApplicationCommandType.ChatInput ) { + expectDeprecated(interaction.sendPremiumRequired()); if (interaction.inRawGuild()) { expectNotAssignable>(interaction); expectAssignable(interaction); @@ -2073,6 +2075,10 @@ client.on('interactionCreate', async interaction => { expectType>(interaction.followUp({ content: 'a' })); } } + + if (interaction.isModalSubmit()) { + expectDeprecated(interaction.sendPremiumRequired()); + } }); declare const shard: Shard;