XBorg SDK
  • Introduction
  • Community
    • Details
    • Resources
    • Inventory
    • Prizes
    • Store
    • Achievements
  • User
    • Profile
    • Authentication
    • Socials
    • Wallets
  • Quests
    • User
    • Community
  • Quest Events
    • Details
    • Participation
    • Leaderboard
  • Blockchain
  • Configuration
  • Shared Library SDK
    • Authentication & Setup
    • Quest Management
    • Event Management
      • Working with Events
      • Events Lifecycle
      • Events Requirements
      • Events Quests
      • Events Rewards
    • Quest Rewards
    • 3D Avatar
Powered by GitBook
On this page
  • Claiming Rewards
  • Claiming Rewards
  • Attaching requirements to payload
  • Determining prize status
  1. Shared Library SDK

Quest Rewards

Each quest can have many types of rewards, most of them are stream lined and require no action from Frontend, but for some other specific types like prizes, there are some additional actions requird.

Different types of Quest Rewards

  • Give Resources

  • Give Assets

  • Give Prizes

  • Give Discord Roles

Each of the above rewards can be identified and dealt with in FE from the following data types:

// Shared properties of a reward
const baseRewardSchema = z.object({
  rewardConfigurationId: z.string(),
  rewardId: z.string(),
  createdAt: z.string().datetime().optional(),
  updatedAt: z.string().datetime().optional(),
  questId: z.string(),
});

In the Quest object, look for the rewardfield. Depending on the reward defined in the admin panel, we need to use the field slugto identify the type of reward. in the SDK you will find the following zod schemas you can use to validate/identify rewards:

const questEventWithRewardsSchema = z.union([
  giveAssetsRewardSchema,
  manualClaimRewardSchema,
  giveResourceRewardSchema,
  assignDiscordRoleRewardSchema,
  giveResourceProgressiveRewardSchema,
]);

Of course if you don't want to opt-in to zod, you can also use plain typescript to identify these rewards with:

export type CreditResourceType = z.infer<typeof giveResourceRewardSchema>;
export type AssignDiscordRoleType = z.infer<
  typeof assignDiscordRoleRewardSchema
>;

export type ManualClaimRewardType = z.infer<typeof manualClaimRewardSchema>;
export type GivePrizesRewardType = z.infer<typeof givePrizesRewardSchema>;
export type GiveAssetsType = z.infer<typeof giveAssetsRewardSchema>;
export type GiveResourcesProgressive = z.infer<
  typeof giveResourceProgressiveRewardSchema
>;

Claiming Rewards

Quest reward allocation is manual and require a user action to claim rewards once their quest is completed. Once the quest is completed, use the following methods to identify rewards allocated to the user to prepare for the claim phase:

In server components

function getQuestRewardAllocations(
  api: AxiosInstance,
  questId: string,
): Promise<{
    allocations: QuestRewardAllocationsType[];
    total: number;
    offset: number;
}>;

The above function will fetch all reward allocations for the given quest id per user. Remember to populate Authorization headers in the axios config beforehand.

In client components

In the client components use the following hook to receive all rewards allocated to the user. an enabled option is provided to pass in when the quest is completed.

function useQuestRewardAllocations(
  questId?: string,
  options?: Partial<{ enabled: boolean }>,
);

Claiming Rewards

Once you have the reward allocation, you can start claiming the rewards by checking whether or not a reward has a requirement, currently only one reward types might have requirements, but you can streamline this by checking the requirementsfield in the allocation.

If there is no requirement, the claiming process is easy as what follows:

In server components

function postClaimQuestRewardAllocation(
  api: AxiosInstance,
  questId: string,
  allocationId: string,
  payload?: any,
): Promise<QuestRewardAllocationsType>;

In client components

function useMutateQuestReward();

NOTE: the payload field is not required when there is no requirement, it is mainly used for passing requirements.

Claiming Prizes

Since prizes are custom rewards, their delivery mechanism is a bit different, some of them are automatically distributed, but some other require admin approval. In addition to that, custom rewards may require some additional information from the user before they can be delivered. For example, a Steam gift card may require email address from the user, or for some other prizes a postal address may be required. This information is defined in the requirements field.

In the following section we will deal with prize requirements and how can we prepare data to send to our API.

Currently following requirements are supported via SDK:

const rewardRequirementSchema = z.discriminatedUnion('name', [
  emailQuestRewardRequirementSchema,
  addressQuestRewardRequirementSchema,
  walletAddressQuestRewardRequirementSchema,
]);

Since currently only Prize rewards contain requirements, we can use a similar approach to determine prize requirements:

function RewardRequirements({ quest, }) {
  const reward = first(quest?.rewards);
  const prizeReward = givePrizesRewardSchema.safeParse(reward);

  if (prizeReward.error) {
    console.log(prizeReward.error); // Keeping this for debugging purposes
    return null;
  }

  const requirements = prizeReward.data.rewardHandlerArgs.requirements ?? [];

  if (requirements.length === 0 || quest?.status !== QUEST_STATUS.COMPLETED) {
    return null;
  }

  return (
    <RequirementForm requirements={requirements} />
  );
}

Then we can identify required fields based on the type definitions we cover previously, note that this example is using react-hook-form, but you can use any form library you prefer:

  <form onChange={form.handleSubmit(onSuccessHandler, onInvalidHandler)}>
        {requirements.map((requirement) =>
          match(requirement)
            {/* Address is a compound field, we are using nesting with dot notation here. */}
            .with({ name: 'address' }, () => (
              <div>
                {Object.entries(requirement.args).map(([key, value]) => (
                  <FormField
                    key={key}
                    control={form.control}
                    name={`address.${key}`}
                    render={({ field, fieldState }) => (
                      <FormItem>
                        <FormLabel>{value.label}</FormLabel>
                        <FormControl>
                          <Input
                            autoFocus
                            placeholder={value.label}
                            {...field}
                          />
                        </FormControl>
                        <FormMessage>{fieldState.error?.message}</FormMessage>
                      </FormItem>
                    )}
                  />
                ))}
              </div>
            ))
            .with({ name: 'wallet_address' }, () => (
              <FormField
                key={requirement.requirementId}
                control={form.control}
                name={'walletAddress'}
                render={({ field, fieldState }) => (
                  <FormItem>
                    <FormLabel>{requirement.displayName}</FormLabel>
                    <FormControl>
                      <Input placeholder={requirement.displayName} {...field} />
                    </FormControl>
                    <FormDescription>{requirement.description}</FormDescription>
                    <FormMessage>{fieldState.error?.message}</FormMessage>
                  </FormItem>
                )}
              />
            ))
            .otherwise(() => (
              <FormField
                key={requirement.requirementId}
                control={form.control}
                name={requirement.name}
                render={({ field, fieldState }) => (
                  <FormItem>
                    <FormLabel>{requirement.displayName}</FormLabel>
                    <FormControl>
                      <Input placeholder={requirement.displayName} {...field} />
                    </FormControl>
                    <FormDescription>{requirement.description}</FormDescription>
                    <FormMessage>{fieldState.error?.message}</FormMessage>
                  </FormItem>
                )}
              />
            )),
        )}
  </form>

Each requirement field has its own structure but follows a similar structure:

Email Requirement

const emailQuestRewardRequirementSchema =
  baseQuestRewardRequirementSchema.extend({
    name: z.literal('email'),
    data: z
      .object({
        email: z.string().optional(),
      })
      .optional(),
    args: z.object({
      email: z.object({
        type: z.literal('email'),
        label: z.string(),
        hidden: z.boolean(),
        required: z.boolean(),
      }),
    }),
  });

Note: data.email might be empty, but if it has value, it means the user has already populated this information, and it can be re-used as a default value.

Wallet Address Requirement

const walletAddressQuestRewardRequirementSchema =
  baseQuestRewardRequirementSchema.extend({
    name: z.literal('wallet_address'),
    data: z
      .object({
        walletAddress: z.string().optional().nullable(),
      })
      .optional(),
    args: z.object({
      walletAddress: z.object({
        type: z.literal('string'),
        label: z.string(),
        hidden: z.boolean(),
        required: z.boolean(),
      }),
    }),
  });

Note: There are no "validation" on the accepted wallet address format, it should be done in the client.

Address Requirement

const addressQuestRewardRequirementSchema =
  baseQuestRewardRequirementSchema.extend({
    name: z.literal('address'),
    data: z
      .object({
        addressLine1: z.string().optional(),
        addressLine2: z.string().optional(),
        addressLine3: z.string().optional(),
        city: z.string().optional(),
        state: z.string().optional(),
        country: z.string().optional(),
        postalCode: z.string().optional(),
      })
      .optional(),
    args: z.object({
      addressLine1: z.object({
        type: z.literal('string'),
        label: z.string(),
        hidden: z.boolean(),
        required: z.boolean(),
      }),
      addressLine2: z.object({
        type: z.literal('string'),
        label: z.string(),
        hidden: z.boolean(),
        required: z.boolean(),
      }),
      addressLine3: z.object({
        type: z.literal('string'),
        label: z.string(),
        hidden: z.boolean(),
        required: z.boolean(),
      }),
      city: z.object({
        type: z.literal('string'),
        label: z.string(),
        hidden: z.boolean(),
        required: z.boolean(),
      }),
      state: z
        .object({
          type: z.literal('string'),
          label: z.string(),
          hidden: z.boolean(),
          required: z.boolean(),
        })
        .optional(),
      country: z.object({
        type: z.literal('string'),
        label: z.string(),
        hidden: z.boolean(),
        required: z.boolean(),
      }),
      postalCode: z.object({
        type: z.literal('string'),
        label: z.string(),
        hidden: z.boolean(),
        required: z.boolean(),
      }),
    }),
  });

Attaching requirements to payload

Once you gather all required information, you can pass these information in the format of Record<string, string | Record<string, string>> as the payloadargument we've discussed above in the claiming rewards section.

Determining prize status

Once the claim is completed, make sure you are hooking to distributionStatusfield in the allocation entity.

Distribution status can be one of the following

enum QUEST_REWARD_DISTRIBUTION_STATUS {
    DISTRIBUTED = "DISTRIBUTED",
    ALLOCATED = "ALLOCATED"
}

Note: Only when the status is QUEST_REWARD_DISTRIBUTION_STATUS.DISTRIBUTEDyou can assume that the reward is delivered, otherwise, the reward is pending in the queue to be delivered.

PreviousEvents RewardsNext3D Avatar

Last updated 3 months ago