Subscription Service: Ingame Subscriptions Made Easy



Now with pictures!


About


Subscription Service is a module that utilises Developer Products to allow developers to create Subscriptions costing Robux rather than real currency, unlike Roblox’s built-in feature for subscriptions. In short, it is a good alternative to Roblox’s flawed built in Subscription feature.


Features

  • Easy Subscription Registration: Adding one/multiple subscriptions could not be made any easier.

  • Purchase Management: Grant and revoke subscriptions seamlessly to players in-game.

  • Expiration Handling: Automatically handle when a subscription has expired.

  • Flexible API: The module comes with a range of functions for whatever you may need.

  • Robux-Based: As most people on the platform do not want to pay real currency for game subscriptions, Robux based subscriptions will gain more profit.

  • Memory based: No objects are created, meaning that preformance is not affected.


Limitations

  • No Automatic Renewal: This is not possible to implement but this module could be worked around by prompting the player with a UI letting them know that their subscription expired

  • Potential Data Loss: As with anything that uses DataStoreService, if something at Roblox HQ goes down, data loss could occur. You are free to convert this module to more reliable data saving methods such as Datastore2

  • No Automatic Benefits: You will have to add the subscription benefits yourself, which means you will need a bit of scripting skill to use this module


Installation

Installation could really not be made any easier. (I am not the best explainer but I’ll try anyway) Simply follow the steps below:

  1. Insert the module and put it in ServerStorage. You could get it HERE
    **image_2023-10-31_231500332

  2. Require it from a Script in ServerScriptService and adjust the settings as you wish.

local SubscriptionService = require(SubscriptionModuleObject)
SubscriptionService.Expiration_Check_Rate = 10 -- in seconds. how often players' subscriptions are checked for expiration ingame. default is 10 seconds.
SubscriptionService.Handle_Subscription_Purchases = true -- when true, if you prompt a player with the purchase of a subscription dev product, the module will handle it automatically.
SubscriptionService.Print_Credits = true -- do i even have to explain this

Now that the module is a part of the game, lets make some subscriptions!

Before you continue, make sure you know how to implement Developer Products. You could learn this from the Internet or read about it on the Developer Forum

  1. Create a table with all of the subscriptions you wish to add in sub-tables as such:
local ExistingSubscriptions = {
	{
		Name = "Test Subscription",
		ProductId = 1679061197,
		Duration = 7, -- In days
		Stackable = true, -- When true, if a subscription is purchased while already having one, the player will get an additional however many days.
	},

	{
		Name = "Test Subscription 2",
		ProductId = 1679085149,
		Duration = 30,
		Stackable = false -- When false and a subscription is bought, a subscription will no be granted if the player already has one. you will have to prevent sale manually since there is not a way to do this.
	}
}
  1. Now just simply use the SubscriptionModule.RegisterSubscription(Subscription) function of the module to register them!
for Index, Subscription in pairs(ExistingSubscriptions) do
	SubscriptionService.RegisterSubscription(Subscription)
end
  1. Now the subscriptions are set up to use! You will have to add the benefits yourself using the functions provided by the module.

Sample Usage


Brief API Rundown

With Handle_Subscription_Purchases set to true, all you have to do for the subscription to register is to prompt the Player with the developer product using MarketPlaceService


and the module will handle everything else for you!


API Reference

API Reference will be in the module. Go read it.


Uncopylocked Place

Uncopylocked place to copy from can be found here


Special thanks to Road_Gamer2 for helping me out during the creation process of this module.

Any feedback, bug reports or suggestions will be greately appreciated and considered!!

18 Likes

didnt they just release subscriptions?

1 Like

roblox’s subscriptions are to be bought only using USD as shown in their documentation and im pretty sure not too many people want to pay for an ingame subscription with real currency

10 Likes

Amazing module love the API and the overall clean understandable script design would definitely say that this is going to help a lot of people as Roblox doesn’t plan on non USD subscriptions. This modules goes above and beyond with features such as stackability and I am proud to say that yes I was apart of the creation of this module even slightly i am looking forward to see devs benefitting from ur ingenuity.

1 Like

Not sure why this isn’t included.

Then it’s immediately a no-go, imagine paying for a subscription but your data gets overridden by accident?

This isn’t a limitation when you clearly have to do it yourself anyway…


API is solid but is not battle-tested.

And no GitHub repo? This honestly seems like a very half-baked release.

+Subscriptions are still beta, this doesn’t mean feedback won’t change it.

1 Like

I said “Potential” Data Loss? That does not mean it is guaranteed. This problem goes with anything that uses DatastoreService. That is like saying that buying a house is immediately a no-go because it can “Potentially” burn down. Nothing comes without risk.

The module gives you tools to do that yourself, which I reckon would be useful for someone who is actually good at UI designing. A simple connection fires when a subscription has expired.

I do not exactly see a need for a Github repo if everything is already in the module.

The module could be used until subscriptions do change. It is good for the time being.

5 Likes

I need to chime in on this. Resources do not need a repo attached at all, it is purely up to the authors’ choice of a repo or not. Stating due to no repo this being a half-baked released is very childish. Go view other 30 second made resources in this category for those.

6 Likes

He really thinks that every resource needs a github repo, When you make a resource, It’s completely your choice on whether to make a github repo or not

Some people may ask these questions so I will answer them below:

How can you easily contribute?

This topic already exists, so you can reply with your contribution, or private message the OP with the contribution

How can we view the source code?

You can open the studio to view it (or use BTRoblox to view the files with the code), If you’re lazy then it’s your problem then

source code for those who are lazy

DISCLAIMER: The code is up to date as of November 1st, 2023, so it can be outdated in the future

--[[
[V1.0.0]


----------------------------------------------------------------------------------
  _________    ___.                       .__        __  .__                     |
 /   _____/__ _\_ |__   ______ ___________|__|______/  |_|__| ____   ____        |
 \_____  \|  |  \ __ \ /  ___// ___\_  __ \  \____ \   __\  |/  _ \ /    \       |
 /        \  |  / \_\ \\___ \\  \___|  | \/  |  |_> >  | |  (  <_> )   |  \      |
/_______  /____/|___  /____  >\___  >__|  |__|   __/|__| |__|\____/|___|  /      |
        \/          \/     \/     \/         |__|                       \/       |
  _________                  .__                                                 |
 /   _____/ ______________  _|__| ____  ____                                     |
 \_____  \_/ __ \_  __ \  \/ /  |/ ___\/ __ \                                    |
 /        \  ___/|  | \/\   /|  \  \__\  ___/                                    |
/_______  /\___  >__|    \_/ |__|\___  >___  >                                   |
        \/     \/                    \/    \/                                    |
                                                                                 |
                                                                                 |
----------------------------------------------------------------------------------


- Author: scope.jpg

- Contributors:
    r7.120k


==============================================================================
Subscription Service is an easy way to set up and handle subscriptions that take Robux. As you may know,
Roblox implemented a new feature that allows developers to create subscriptions for their games, but to use
this feature, they require ID verification and the game must be a certain age. Subscription Service is a good,
easy to use substitute for this as this modules allows developers to set up subscriptions with no complication.
==============================================================================

----- API REFERENCE -----


~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
----------------------------------------- Settings -------------------------------------------------------
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 .     Expiration_Check_Rate (number):                   Rate for checking subscription expirations.
 .     Handle_Subscription_Purchases (boolean):          When true, the module will handle the subscription purchases. All you have to do is prompt the player with the Developer product and the module will handle it                                                   
 .     Print_Credits (boolean):                          Print credits to console.
 .     Product_Handling_Yield (number):                  Yield for product handling.


~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
----------------------------------------- Signals --------------------------------------------------------
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 .     SubscriptionExpired:             Fired when a subscription expires.
             Args: (Player: Player, Subscription: table)
                   Player is just the player object of the subscription owner.
                  
                   Subscription: 
                   {
                        Name = "subscription name here", -- Name of the subscription in a string
                        PurchaseDate = 57939529247 -- The UNIX timestamp of when the subscription was purchased
                   }
 
 
 
 .     SubscriptionPurchased:          Fired when a subscription is purchased.
             Args: (Player: Player, Subscription: table)
             
                    Subscription: 
                   {
                        Name = "subscription name here", -- Name of the subscription in a string
                        PurchaseDate = 57939529247 -- The UNIX timestamp of when the subscription was purchased
                   }
                   
                   
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
----------------------------------------- Functions ------------------------------------------------------
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 .     Module.UnixToDDMMYY(Timestamp: number): Converts a Unix timestamp to DD/MM/YY format. Useful for making the expiration dates and stuff into readable dates.
             return: 31/10/2022 (the date of a timestamp.)
 
 .     Module.UnixToMMDDYY(Timestamp: number): Converts a Unix timestamp to MM/DD/YY format. Useful for making the expiration dates and stuff into readable dates.
             return: 10/31/2022 (freedom date format. the date of a timestamp)
 
 .     Module.UnixToReadableTime(Timestamp: number): Converts a Unix timestamp to a readable time format (UTC). Useful for making the expiration dates and stuff into readable dates.
             return: 22:20 (the readable time of a timestamp)
 
 .     Module.FetchPlayerSubscriptionData(Player: Player): Fetches subscription data for a player.
             return:
                   {
                     Name = "PlayerName"
                     
                     ActiveSubscriptions = {}
                   }
 
 .     Module.RegisterSubscription(SubscriptionData: any): Registers a subscription.
             no return
 
 .     Module.DoesPlayerOwnSubscription(Player: Player, SubscriptionName: string): Checks if a player owns a subscription.
             return: true/false depending if the player owns the subscription or not
 
 .     Module.GrantSubscription(Player: Player, SubscriptionName: string): Grants a subscription to a player.
             no return
 
 .     Module.RevokeSubscription(Player: Player, SubscriptionName: string): Revokes a subscription from a player.
             no return
 
 
 .     Module.AdjustSubscription(Player: Player, SubscriptionName: string, Days: number): Adjusts the expiration date of a subscription to grant extra days. if the number is negative, it will take days away
             no return
 
 .     Module.FetchSubscriptionInfo(SubscriptionName: string): Fetches subscription information.
              return:
                    {
                    
                     Name = "Test Subscription",
		           ProductId = 1679061197,
		           Duration = 7,
		           Stackable = true,
                    }
 
 .     Module.loadPlayer(Player: Player): Loads player subscription data.
             no return
 
 .     Module.unloadPlayer(Player: Player): Unloads player subscription data.
             no return
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
]]

-- Services --
local MarketplaceService = game:GetService("MarketplaceService") 
local DataStoreService = game:GetService("DataStoreService")
local HTTPService = game:GetService("HttpService")
local Players = game:GetService("Players")

-- Objects --
local Dependencies = script:WaitForChild("Dependencies")
local SignalModuleObject = Dependencies:WaitForChild("GoodSignal")

-- Tables --
local registeredSubscriptions = {}
local productFunctions = {}

-- Modules --
local Signal = require(SignalModuleObject)

-- Datastore --
local SubscriptionDataStore = DataStoreService:GetDataStore("uj6rtrtrjursjtrsjtrsxrtj")

-- Local Functions --
local function toSeconds(Days: number)
	return Days * 86400
end

local function processReceipt(receiptInfo)
	local userId = receiptInfo.PlayerId
	local productId = receiptInfo.ProductId

	local player = Players:GetPlayerByUserId(userId)

	if player then
		local handler = productFunctions[productId]
		local success, result = pcall(handler, receiptInfo, player)

		if success then
			return Enum.ProductPurchaseDecision.PurchaseGranted
		else
			warn("Failed to process receipt:", receiptInfo, result)
		end
	end

	return Enum.ProductPurchaseDecision.NotProcessedYet
end

local function FindSubscriptionByNameInTable(SubscriptionName: string, Table)
	for i, v in pairs(Table) do
		if v.Name == SubscriptionName then
			return v, i
		end
	end

	return nil
end

local function FindSubscriptionByName(SubscriptionName: string)
	local FoundSubscription = nil

	for i, v in pairs(registeredSubscriptions) do 
		if v.Name == SubscriptionName then 
			FoundSubscription = v
			break 
		end 
	end

	return FoundSubscription
end

local function CheckSubscriptionValidity(PurchaseDate, Duration)
	local CurrectTime = tick()

	local ExpirationDate = PurchaseDate + toSeconds(Duration)

	if ExpirationDate <= CurrectTime then
		return false
	else
		return true
	end
end

local SubscriptionModule = {
	-- Presets --
	Expiration_Check_Rate = 10, 
	Handle_Subscription_Purchases = true, 
	Print_Credits = true,
	Product_Handling_Yield = 0.5,

	-- DO NOT TOUCH BELOW --
	PlayerSubscriptions = {}
}

SubscriptionModule.SubscriptionExpired = Signal.new()
SubscriptionModule.SubscriptionPurchased = Signal.new()

-- Functions to handle date and time conversions --
function SubscriptionModule.UnixToDDMMYY(Timestamp: number)
	local t = os.date("*t", Timestamp)

	local DateString = tostring(t.day.."/"..t.month.."/"..t.year)

	return DateString
end

function SubscriptionModule.UnixToMMDDYY(Timestamp: number)
	local t = os.date("*t", Timestamp)

	local DateString = tostring(t.month.."/"..t.day.."/"..t.year)

	return DateString
end

function SubscriptionModule.UnixToReadableTime(Timestamp: number) -- This is in UTC
	local t = os.date("*t", Timestamp)

	return tostring(t.hour..":"..t.min)
end

function SubscriptionModule.FetchPlayerSubscriptionData(Player: Player)
	for Index, Value in pairs(SubscriptionModule.PlayerSubscriptions) do
		if Value.Name == Player.Name then
			return Value, Index
		end
	end

	return nil
end

-- A function to register a subscription using a SubscriptionData object --
function SubscriptionModule.RegisterSubscription(SubscriptionData: any)
	if not SubscriptionData.Name or not SubscriptionData.Duration then warn("Incomplete table detected when passing through a table! Are you sure that each subscription table has values called Name and Duration?") end

	table.insert(registeredSubscriptions, SubscriptionData)
end

-- A function to check if a player owns a certain subscription (Returns: true/false) --
function SubscriptionModule.DoesPlayerOwnSubscription(Player: Player, SubscriptionName: string)
	local PlayerData = SubscriptionModule.FetchPlayerSubscriptionData(Player)

	if PlayerData ~= nil then
		local SearchingForSubscription = FindSubscriptionByNameInTable(SubscriptionName, PlayerData.ActiveSubscriptions)

		if SearchingForSubscription ~= nil then
			return true
		else
			return false
		end
	else
		warn("Subscription data failed to fetch! ;(")
	end
end

function SubscriptionModule.AdjustSubscription(Player: Player, SubscriptionName: string, Days: number)
	local PlayerData = SubscriptionModule.FetchPlayerSubscriptionData(Player)

	if PlayerData ~= nil then
		local SearchingForSubscription = FindSubscriptionByNameInTable(SubscriptionName, PlayerData.ActiveSubscriptions)

		if SearchingForSubscription ~= nil then
			SearchingForSubscription.PurchaseDate += toSeconds(Days)
		else
			warn(Player.Name.." does not own subscription "..SubscriptionName.."! Try granting the subscription first.")
		end
	else
		warn("Subscription data failed to fetch! ;(;(")
	end
end

-- A function to grant a subscription to a player --
function SubscriptionModule.GrantSubscription(Player: Player, SubscriptionName: string)
	local PlayerSubscriptionData = SubscriptionModule.FetchPlayerSubscriptionData(Player)
	local SubscriptionInfo = nil

	SubscriptionInfo = FindSubscriptionByName(SubscriptionName)

	if PlayerSubscriptionData == nil then error("Player subscription data was not found for "..Player.Name..". Issue with the module. Please message scope.") end
	if SubscriptionInfo == nil then error('Subscription by the name "'..SubscriptionName..'" has not been found in the list of registered subscriptions. Did you register the intended subscription under the correct name?') end

	local SubscriptionWantedInTable = FindSubscriptionByNameInTable(SubscriptionName, PlayerSubscriptionData.ActiveSubscriptions)

	if SubscriptionWantedInTable == nil then
		local Subscription = {
			Name = SubscriptionName,
			PurchaseDate = tick(),
		}

		table.insert(PlayerSubscriptionData.ActiveSubscriptions, Subscription)
		SubscriptionModule.SubscriptionPurchased:Fire(Player, Subscription)
	else
		if SubscriptionInfo.Stackable == true then
			SubscriptionWantedInTable.PurchaseDate += toSeconds(SubscriptionInfo.Duration)
			SubscriptionModule.SubscriptionPurchased:Fire(Player, SubscriptionWantedInTable)
		else
			warn(Player.Name.." already owns subscription '"..SubscriptionName.."'!")
		end
	end
end

-- A function that that handles overall revoking of a subscription --
function SubscriptionModule.RevokeSubscription(Player: Player, SubscriptionName: string)
	local PlayerData = SubscriptionModule.FetchPlayerSubscriptionData(Player)

	if PlayerData ~= nil then
		local SearchingForSubscription, PositionInTable = FindSubscriptionByNameInTable(SubscriptionName, PlayerData.ActiveSubscriptions)

		if SearchingForSubscription ~= nil then
			SubscriptionModule.SubscriptionExpired:Fire(Player, {Name = SubscriptionName, PurchaseDate = SearchingForSubscription.PurchaseDate})
			table.remove(PlayerData.ActiveSubscriptions, PositionInTable)
		else
			warn(Player.Name.." does not own subscription "..SubscriptionName.." so there is no point in revoking the subscription!")
		end
	else
		warn("Subscription data failed to fetch! ;(")
	end
end

function SubscriptionModule.FetchSubscriptionInfo(SubscriptionName: string)
	local SubscriptionInfo = nil

	SubscriptionInfo = FindSubscriptionByName(SubscriptionName)

	if SubscriptionInfo ~= nil then
		return SubscriptionInfo
	else
		return nil
	end
end

function SubscriptionModule.loadPlayer(Player: Player) -- creates the subscription storage and filters whichever ones the player has and whichever ones are expired
	local PlayerData = SubscriptionDataStore:GetAsync(Player.UserId)

	local PlayerSubscriptions = {
		Name = Player.Name,
		ActiveSubscriptions = {}
	}

	if PlayerData then
		local decodeddata = HTTPService:JSONDecode(PlayerData)

		for i, v in pairs(decodeddata.ActiveSubscriptions) do
			local SubscriptionInfo = nil

			SubscriptionInfo = FindSubscriptionByName(v.Name)

			if SubscriptionInfo == nil then continue end

			if CheckSubscriptionValidity(v.PurchaseDate, SubscriptionInfo.Duration) == true then
				local Subscription = {
					Name = v.Name,
					PurchaseDate = v.PurchaseDate
				}

				table.insert(PlayerSubscriptions.ActiveSubscriptions, Subscription)
			else
				SubscriptionModule.SubscriptionExpired:Fire(Player, v)
			end
		end
	end

	table.insert(SubscriptionModule.PlayerSubscriptions, PlayerSubscriptions)

	local SubscriptionExpirationListener = coroutine.create(function() -- listens to every subscription that the player has and checks if it has expired
		repeat
			task.wait(SubscriptionModule.Expiration_Check_Rate)
			if Player == nil then break end

			local SubscriptionTable = SubscriptionModule.FetchPlayerSubscriptionData(Player)

			if SubscriptionTable == nil then break end

			for i, Subscription in pairs(SubscriptionTable.ActiveSubscriptions) do
				local SubscriptionInfo = FindSubscriptionByName(Subscription.Name) or nil

				if SubscriptionInfo == nil then warn("Subscription by the name '"..Subscription.Name.."' was not found while listening for expiration.") end

				if Subscription.PurchaseDate ~= nil then
					if CheckSubscriptionValidity(Subscription.PurchaseDate, SubscriptionInfo.Duration) == false then
						SubscriptionModule.SubscriptionExpired:Fire(Player, {
							Name = Subscription.Name,
							PurchaseDate = Subscription.PurchaseDate
						})

						table.remove(SubscriptionTable.ActiveSubscriptions, i)
					end
				end
			end
		until Player == nil
	end)

	coroutine.resume(SubscriptionExpirationListener)
end

function SubscriptionModule.unloadPlayer(Player: Player)
	local SubscriptionTable, Index = SubscriptionModule.FetchPlayerSubscriptionData(Player)

	if SubscriptionTable == nil then error(Player.Name.." not found in the main table of the SubscriptionService while saving data: Module error.") return end

	local SubscriptionsOwnedByPlayer = {}

	for _, Subscription in pairs(SubscriptionTable.ActiveSubscriptions) do
		local SubscriptionInfo = FindSubscriptionByName(Subscription.Name)

		if SubscriptionInfo == nil then warn("Subscription by the name '"..Subscription.Name.."' was not found in the saving process.") end

		table.insert(SubscriptionsOwnedByPlayer, {Name = Subscription.Name, PurchaseDate = Subscription.PurchaseDate})
	end

	local PlayerData = {
		ActiveSubscriptions = SubscriptionsOwnedByPlayer
	}

	local EncodedData = HTTPService:JSONEncode(PlayerData)

	SubscriptionDataStore:SetAsync(Player.UserId, EncodedData)
	table.remove(SubscriptionModule.PlayerSubscriptions, Index)
end

do
	if SubscriptionModule.Handle_Subscription_Purchases == true then
		task.spawn(function()
			task.wait(SubscriptionModule.Product_Handling_Yield)

			for _, Subscription in pairs(registeredSubscriptions) do
				productFunctions[Subscription.ProductId] = function(receipt, player)
					SubscriptionModule.GrantSubscription(player, Subscription.Name)
				end
			end

			MarketplaceService.ProcessReceipt = processReceipt
		end)
	end

	if SubscriptionModule.Print_Credits == true then
		print("SubscriptionService: Made by SECRXETT on the Developer Forum.")
	end
end

return SubscriptionModule
How can we put it in our repo?

Just paste the code there, not a difficult job to do

How can I insert this using Wally?

It’s literally just a model, A single click to insert isn’t a difficult job either, You don’t need to use advanced tools like Wally to import it


Onto the topic:

It’s a great resource to be honest, I am looking into this and will use it as I don’t want kids using their mom’s credit cards just to buy a subscription, but if roblox adds that as a feature then I will stop using it (it’s a temporary solution for robux subscription)

2 Likes