Skip to content
SourcePointUSAPublic

About

Your plug & play CMP for Unity

Resources

Stars

3 stars

Watchers

6 watching

Forks

Repository files navigation

Unity-SDK

Sourcepoint's plug and play Unity SDK can be integrated with both Android and iOS.

Note: If you have updated from version 2.x.x to 3.0.0, please read MIGRATION.md

Note: The Unity SDK can not be demoed using Unity's Editor since it embeds native SDKs and those only work in their respective platforms.

External Dependency Manager for Unity is required in every consuming Unity project. Install it before importing this SDK, then resolve the Android and iOS dependencies declared in Assets/ConsentManagementProvider/Editor/SourcepointDependencies.xml before building your application.

In Unity SDK version 2.3.3, the native Android SDK dependency was updated to version 7.8.3, so CMP SDK is now supported on Unity version 2022.3.0 and higher. This was done due to Java version upgrade as well as Gradle upgrade to version 7.5 in native Android SDK 7.8.2 which is a dependency of Unity SDK. However, we are able to build with Gradle v7.2 which is built-in in Unity 2022.3

Newtonsoft JSON dependency

When installing this SDK as a UPM package from Git, version 3.3.0 and later installs com.unity.nuget.newtonsoft-json automatically.

When importing ConsentManagementProvider.unitypackage, add the following entry to the existing dependencies object in the consuming project's Packages/manifest.json, then reopen Unity:

"com.unity.nuget.newtonsoft-json": "3.2.2"

Alternatively, install com.unity.nuget.newtonsoft-json version 3.2.2 from the Unity Package Manager before importing the .unitypackage.

iOS build requirements and native dependency upgrade

The declared native dependency pair on this branch is Android CMP 7.15.14 and iOS ConsentViewController 7.12.11. The iOS pod depends on SPMobileCore 0.1.17, which CocoaPods installs transitively; do not add it manually. Android and iOS have independent version numbers. The iOS migration is unreleased and awaits a fresh export, Xcode build, and complete iOS UI test run; this pair is not yet release-verified.

To build for iOS, use macOS with Unity's matching iOS Build Support module, Xcode and its command-line tools, CocoaPods, and External Dependency Manager for Unity. Unity 6.5 (6000.5.10f1) exports this sample with an iOS 15.0 deployment target, even though the historical project setting was 13.0. Use the actual minimum version supported by your Unity editor; raising it also raises the minimum iOS version for your customers. A supported Xcode/CocoaPods version combination will be recorded after the complete release gate passes.

When upgrading the SDK through UPM or a .unitypackage, update the package, make a fresh Unity iOS export, then run ordinary pod install in that export and build its generated .xcworkspace in Xcode. Install EDM4U before importing the SDK and allow it to resolve the declared dependencies. An older export or its Podfile.lock can retain the previous native libraries.

Existing C# calls, scenes, callbacks, and consent JSON models need no changes. The shared CMP/ICmpSdk/ISpSdk API continues to serve both platforms; new native Global CMP or Preferences APIs are not added to the Unity API by this update. No manual Podfile or Pod-project edit is required for the normal upgrade. The Unity package owns and configures its Swift bridge in the exported UnityFramework target. Before EDM4U installs Pods, the package adds one standard CocoaPods post_install hook to the generated Podfile. It raises only ConsentViewController, its resource bundle, and SPMobileCore deployment targets to the exported Unity app's minimum iOS version, preserving higher targets and unrelated dependencies. This avoids Xcode 27 rejecting those pods' historical iOS 10.0 targets; subsequent ordinary pod install runs retain the setting.

A customer-maintained Podfile that already defines post_install is not automatically composable with this generated hook. Export fails with an actionable message instead of silently replacing a hook or registering two. Use a fresh EDM4U-generated Podfile for the supported automatic integration; custom CocoaPods lifecycle hooks require a deliberately maintained integration.

If a customer-maintained Podfile explicitly pins an incompatible ConsentViewController or SPMobileCore version, normal CocoaPods resolution fails with a dependency conflict. Such incompatible pins are unsupported; reconcile the custom dependency constraints before building. The SDK does not hide the conflict or modify installed Pods to bypass it.

SpConsentsJsonBridge.java remains part of the Android package. It adapts Android's native consent callback/model format to the shared Unity consent JSON contract; updating the iOS pod does not remove that requirement. Package consumers do not need to configure or edit this adapter.


Instantiate consent UI

To start, include the following library namepsace in your script:

using ConsentManagementProvider;

Construct List<SpCampaign> which contains SpCampaign objects. Each SpCampaign object should consist of CAMPAIGN_TYPE along with the TargetingParams you need.

    List<SpCampaign> spCampaigns = new List<SpCampaign>();

    List<TargetingParam> gdprParams = new List<TargetingParam> { new TargetingParam("location", "EU") };
    SpCampaign gdpr = new SpCampaign(CAMPAIGN_TYPE.GDPR, gdprParams);
    spCampaigns.Add(gdpr);

    List<TargetingParam> ccpaParams = new List<TargetingParam> { new TargetingParam("location", "US") };
    SpCampaign ccpa = new SpCampaign(CAMPAIGN_TYPE.CCPA, ccpaParams);
    spCampaigns.Add(ccpa);

    List<TargetingParam> ios14Params = new List<TargetingParam>();
    SpCampaign ios14 = new SpCampaign(CAMPAIGN_TYPE.IOS14, ios14Params);
    spCampaigns.Add(ios14);

    List<TargetingParam> usnatParams = new List<TargetingParam> { new TargetingParam("location", "US") };
    SpCampaign usnat = new SpCampaign(CAMPAIGN_TYPE.USNAT, usnatParams);
    spCampaigns.Add(usnat);

In order to instantiate & trigger Consent Message Web View, you must call the CMP.Instance.Initialize function in Awake along with spCampaigns, accountId, propertyId, propertyName and language.

Additionally, you can also specify a messageTimeout which, by default, is set to 30 seconds.

    CMP.Instance.Initialize(spCampaigns: spCampaigns,
                   accountId: 22,
                   propertyId: 16893,
                   propertyName: "mobile.multicampaign.demo",
                   language: MESSAGE_LANGUAGE.ENGLISH,
                   campaignsEnvironment: CAMPAIGN_ENV.PUBLIC,
                   messageTimeoutInSeconds: 30);
Field Description
spCampaigns List with campaigns launched on the property through the Sourcepoint portal
accountId Organization's account ID found in the Sourcepoint portal
propertyId ID for property found in the Sourcepoint portal
propertyName Name of property found in the Sourcepoint portal
language Force a message to be displayed in a certain language. Look MESSAGE_LANGUAGE for all available languages

Note: It may take a frame to initialize the CMP library, so we strongly recommend that you Initialize in Awake separately from LoadMessage. We recommend that you LoadMessage in Start (see example below).

When the SDK receives the LoadMessage call, it will instantiate a webview if the end-user needs to see a message.

If there is a consent profile associated with authId, the SDK will bring the consent data from the server, overwriting whatever was stored in the device.

private void Start()
{
     CMP.Instance.LoadMessage(authId: null); // or pass it a String if you wish to use authenticated consent
}

Utilize the following method if an end-user requests to have their data deleted:

private void ClearData()
{
     CMP.Instance.ClearAllData();
}

In order to free memory, call Dispose as illustrated in the following example :

private void OnDestroy()
{
    CMP.Instance.Dispose();
}

If you need library logs, you can use this:

CmpDebugUtil.EnableCmpLogs(true)

Handle consent callbacks

Consent callbacks allow you to track progress and receive updates of user interaction. We provide the following interfaces:

Callback Description
IOnConsentUIReady Triggered when web view UI is ready and about to show
IOnConsentAction Triggered when user made an action, provides you instance of enum CONSENT_ACTION_TYPE. See below for more information.
IOnConsentError Triggered when something went wrong, provides you instance of Exception
IOnConsentUIFinished Triggered when user interaction with web view UI is done and view is about to disappear
IOnConsentReady Triggered when server successfully reacted to user's consent, provides you SpConsent object with consent info

Custom Action, OnConsentAction

IOnConsentAction is triggered when the user takes an action in the first layer message or privacy manager, and provides you an instance of SpAction which contains:

  1. The enumeration of type CONSENT_ACTION_TYPE which indicates the type of action:
public enum CONSENT_ACTION_TYPE
{
    SAVE_AND_EXIT = 1,
    PM_DISMISS = 2,
    CUSTOM_ACTION = 9,
    ACCEPT_ALL = 11,
    SHOW_OPTIONS = 12,
    REJECT_ALL = 13,
    MSG_CANCEL = 15,
}
  1. CustomActionId : String - If the type of action is Custom, this attribute will contain the id you assigned to it when building the message in our message builder. In other cases the line is empty.

Workflow to handle callbacks using interfaces

After you have created your own script which derives from MonoBehaviour and attached this component to your GameObject you should perform the following:

Inherit your script from any number of interfaces from the IConsentEventHandler list you are interested in and implement its method(s).

Example
Suppose you want to handle exception callback via IOnConsentErrorEventHandler, and you already implemented IOnConsentErrorEventHandler inheritance and OnConsentError method in your script and attached this script to generic GameObject in hierarchy. What's next?

public class ConsentEventHandler : MonoBehaviour, IOnConsentError
{
    public void OnConsentError(Exception exception)
    {
        Debug.LogError("Oh no, an error! " + exception.Message);
    }
}

Register your gameObject (which implements any inheritor of IConsentEventHandler interface) as an event listener with ConsentMessenger.AddListener static method. It can be registered any time before you call the LoadMessage method (Awake, Start is enough, but you can adopt the registration to your own logic).

In the example below, we have added the current gameObject as listener for IOnConsentError events.

The event will be executed on all components of the game object that can handle it, regardless of whether they are subscribed or not if at least one have registered the gameObject as a listener.

void Awake()
{
    ConsentMessenger.AddListener<IOnConsentError>(this.gameObject);
}

You should also unregister your listener when it becomes unnecessary due to garbage collection. OnDestroy is enough for our purposes:

private void OnDestroy()
{
    ConsentMessenger.RemoveListener<IOnConsentError>(this.gameObject);
}

The solution is ready. Configure it and deploy!

Both calling & handling workflows are implemented in the ConsentMessageProvider and ConsentEventHandler scripts of our example app accordingly. Feel free to use these components.

using ConsentManagementProvider;
using System;
using UnityEngine;

public class ConsentEventHandler : MonoBehaviour, IOnConsentUIReady, IOnConsentAction, IOnConsentError, IOnConsentUIFinished, IOnConsentReady
{
    void Awake()
    {
        ConsentMessenger.AddListener<IOnConsentUIReady>(this.gameObject);
        ConsentMessenger.AddListener<IOnConsentAction>(this.gameObject);
        ConsentMessenger.AddListener<IOnConsentError>(this.gameObject);
        ConsentMessenger.AddListener<IOnConsentUIFinished>(this.gameObject);
        ConsentMessenger.AddListener<IOnConsentReady>(this.gameObject);
    }

    public void OnConsentUIReady()
    {
        Debug.LogWarning("User will be shown the web view with series of consent messages!");
    }

    public void OnConsentAction(SpAction action)
    {
        Debug.LogWarning($"User made action={action.Type} and customActionId={action.CustomActionId} action with consent view!");
    }

    public void OnConsentError(Exception exception)
    {
        Debug.LogError("Oh no, an error! " + exception.Message);
    }

    public void OnConsentUIFinished()
    {
        Debug.LogWarning("User has interacted with the web view consent message and it is disappeared!");
    }

    public void OnConsentReady(SpConsents spConsents)
    {
       Debug.Log($"The user interaction on consent messages is done. You can use the spConsent info; " +
          $"\n If it was the last from the series of consents, you can continue user's gaming experience!" +
          $"\n {spConsents.ToString()}");
    }

    private void OnDestroy()
    {
        ConsentMessenger.RemoveListener<IOnConsentUIReady>(this.gameObject);
        ConsentMessenger.RemoveListener<IOnConsentAction>(this.gameObject);
        ConsentMessenger.RemoveListener<IOnConsentError>(this.gameObject);
        ConsentMessenger.RemoveListener<IOnConsentUIFinished>(this.gameObject);
        ConsentMessenger.RemoveListener<IOnConsentReady>(this.gameObject);
    }
}

Resurface Privacy Manager

Once a player has completed the consent flow, you might want to provide a way for them to resurface the privacy manager so they can see/manage their consents on an ongoing basis. To do this, we provide the LoadPrivacyManager method. The following code snippet will show a GDPR privacy manager with the default tab open.

    public void OnPrivacyManagerButtonClick()
    {
        CMP.Instance.LoadPrivacyManager(campaignType: CAMPAIGN_TYPE.GDPR,
                               pmId: "488393",
                               tab: PRIVACY_MANAGER_TAB.DEFAULT);
    }

Below is a list of available tabs in a GDPR privacy manager:

    public enum PRIVACY_MANAGER_TAB
    {
        DEFAULT = 0,
        PURPOSES = 1,
        VENDORS = 2,
        FEATURES = 3
    }

GetSpConsent

This getter is used to retrieve SpConsents data. After calling, it checks the platform (Android or iOS) and returns the SPConsents structure:

    SpConsents
        |-- gdpr?
        |   |-- applies: bool
        |   |-- consents: GdprConsent
        |       |-- applies: bool
        |       |-- uuid: String?
        |       |-- tcData: Map<String, String>
        |       |-- grants: Map<String, GDPRPurposeGrants>
        |       |-- euconsent: String
        |       |-- acceptedCategories: List<String>
        |       |-- consentStatus: ConsentStatus
        |       |-- googleConsentMode: SPGCMData?
        |           |-- adStorage: SPGCMData.Status?
        |           |-- analyticsStorage: SPGCMData.Status?
        |           |-- adUserData: SPGCMData.Status?
        |           |-- adPersonalization: SPGCMData.Status?
        |-- ccpa?
        |   |-- applies: bool
        |   |-- consents: CcpaConsent
        |       |-- applies: bool
        |       |-- uuid: String?
	    |       |-- rejectedCategories: List<String>
        |       |-- rejectedVendors: List<String>
        |       |-- status: String?
        |       |-- uspstring: String
        |       |-- childPmId: String
        |       |-- signedLspa: bool
        |       |-- consentStatus: ConsentStatus?
        |       |-- GPPData: Dictionary<string, object>?
        |-- usnat?
            |-- applies: bool
            |-- consents: CcpaConsent
                |-- uuid: String?
                |-- applies: bool
	            |-- consentStrings: List<ConsentString>
                |-- vendors: List<Consentable> //[{id: String, consented: bool }]
                |-- categories: List<Consentable> //[{id: String, consented: bool }]
                |-- statuses: StatusesUsnat
                    |-- rejectedAny: bool?
                    |-- consentedToAll: bool?
                    |-- consentedToAny: bool?
                    |-- hasConsentData: bool?
                    |-- sellStatus: bool?
                    |-- shareStatus: bool?
                    |-- sensitiveDataStatus: bool?
                    |-- gpcStatus: bool?
                |-- GPPData: Dictionary<string, object>?

This method may return null. Sample usage:

    CMP.Instance.GetSpConsents()

Verifying end-user consent for a given vendor

IAB Vendors

If the vendor you're interested in verifying consent for is part of the IAB group, you don't need to manually check for consent. Our SDK implements the Transparency Consent Framework (TCF) spec and IAB vendors know how to retrieve consent from the local storage.

Non-IAB vendors (aka custom vendors)

For vendors that are not part of the IAB, you can verify the user consented to the vendor with the following:

     consents = CMP.Instance.GetSpConsents();
     
     // for GDPR
     bool isMyGDPRVendorConsented = consents.gdpr.consents.grants["a_vendor_id"].vendorGrant;
     
     // for CCPA, notice vendors are "opted-in" by default
     bool isMyCCPAVendorRejected = consents.ccpa.consents.status == "rejectedAll" ||
                                       consents.ccpa.consents.rejectedVendors.Contains("a_vendor_id");

Google Consent Mode

Google Consent Mode 2.0 ensures that Google vendors on your property comply with an end-user's consent choices for purposes (called consent checks) defined by Google. It is implemented via Google Analytics for Firebase SDK.

Update consent checks

Use Google's setConsent method to update the relevant consent checks when the appropriate purposes are consented to/rejected. Be advised that the googleConsentMode object in GdprConsent will only return values for Google consent checks that are mapped to a custom purpose within your vendor list. For all other Google consent checks, the response will be null.

Adding or Removing custom consents

It's possible to programmatically consent the current user to a list of custom vendors, categories and legitimate interest categories with the method:

void CustomConsentGDPR(
    string[] vendors, 
    string[] categories, 
    string[] legIntCategories, 
    Action<GdprConsent> onSuccessDelegate)

The vendor grants will be re-generated, this time taking into consideration the list of vendors, categories and legitimate interest categories you pass as parameters. The method is asynchronous so you must pass a completion handler that will receive back an instance of GdprConsent in case of success or it'll call the delegate method onError in case of failure.

Using the same strategy for the custom consent, it's possible to programmatically delete the current user consent to a list of vendors, categories and legitimate interest categories by using the following method from the consent lib:

void DeleteCustomConsentGDPR(
    string[] vendors, 
    string[] categories, 
    string[] legIntCategories, 
    Action<GdprConsent> onSuccessDelegate)

It's important to notice, this methods are intended to be used for custom vendors and purposes only. For IAB vendors and purposes, it's still required to get consent via the consent message or privacy manager.

Transfer opt-in/opt out preferences from U.S. Privacy (Legacy) to U.S. Multi-State Privacy

For full information please visit the links: ios, android. Parameters work only for the usnat campaign. Usage:

SpCampaign usnat = new SpCampaign(campaignType: CAMPAIGN_TYPE.USNAT, targetingParams: usnatParams, transitionCCPAAuth: true);
spCampaigns.Add(usnat);

Support U.S. Privacy (Legacy) with U.S. Multi-State Privacy

For full information please visit the links: ios, android. Parameters work only for the usnat campaign. Usage:

SpCampaign usnat = new SpCampaign(campaignType: CAMPAIGN_TYPE.USNAT, targetingParams: usnatParams, supportLegacyUSPString: true);
spCampaigns.Add(usnat);

Programmatically rejecting all for a user

It's possible to programmatically issue a "reject all" action in behalf of the current user by calling the rejectAll(campaignType) function. The rejectAll function behaves exactly the same way as if a user would have pressed the "reject all" button on the 1st layer or privacy manager. Upon completion, the sdk will call either onConsentReady in case of success or onError in case of failure.

    CMP.Instance.rejectAll(campaignType: CAMPAIGN_TYPE.gdpr)

Build for iOS

Follow the iOS build requirements and native dependency upgrade instructions above: make a fresh Unity iOS export, run ordinary pod install in the export directory, and build the generated .xcworkspace in Xcode. The package's CMPPostProcessBuild automatically configures SwiftBridge.swift in the Unity-owned UnityFramework target. Consumers do not need to edit the postprocessor, bridge paths, bridging headers, Podfile, or Pods project.

Troubleshooting

If you use 2.3.3-3.0.0 versions of Unity SDK

You might to use a custom template for Gradle, please refer to "Plugins\Android\mainTemplate.gradle", lines 41-43.

UnityFramework-Swift.h file not found (iOS)

Unity.mm uses the generated UnityFramework-Swift.h header from the target that owns SwiftBridge.swift. Start with a fresh Unity export, run ordinary pod install, and open the generated .xcworkspace. Check the first Swift compiler error and confirm that the exported UnityFramework target contains SwiftBridge.swift; a header error can follow an earlier Swift compilation failure. Retain the Unity export, CocoaPods output, and Xcode build log when reporting the failure so the package's target configuration can be diagnosed. Do not move the bridge into Pods, edit a Pod project, or bypass dependency conflicts with manual Podfile changes. This integration has no blanket pod update or CocoaPods version workaround; a supported toolchain will be recorded after the pending release gate passes.

About

Your plug & play CMP for Unity

Resources

Stars

3 stars

Watchers

6 watching

Forks

Releases

Used by

Contributors

Languages