React Native Tickets SDK
  • 14 Nov 2024
  • 12 Minutes to read
  • Contributors
  • Dark
    Light

React Native Tickets SDK

  • Dark
    Light

Article summary

Tickets SDK

Tickets SDK React Native (iOS)


Create a new .swift file, the name in this guide is "TicketsSdkViewController" but call it whatever you like 

In React Native iOS there are two ways to present the Tickets SDK which are Embedded (inside a React Native View) or a Modal above the current screen, discussed later on.

If it is the first time making a .swift file, Xcode will prompt you to make a Bridging-Header.h file, press "Create Bridging Header" 


 A  Bridging-Header.h will expose Objective-C public headers to Swift


In [PROJECT-NAME]-Bridging-Header.h file add 

#import <React/RCTBridgeModule.h>

In your TicketsViewController.swift create a class and extend UIViewController like below (you can remove the auto-generated Foundation import):

class TicketsViewController: UIViewController {

}


import TicketmasterAuthentication and TicketmasterTickets which should have been made available after adding the Ticketmaster Frameworks

import TicketmasterAuthentication
import TicketmasterTickets


Override a viewDidLoad function 

override func viewDidLoad() {
      super.viewDidLoad() 
    }

Presenting the TicketsSDK

In React Native iOS there are two ways to present the Tickets SDK which are Embedded (inside a React Native View) or a Modal above the current screen

Embedded View Approach


Declare TMTicketsView at the top of the class

class TicketsSdkViewController: UIViewController {
 var ticketsView: TMTicketsView!


In viewDidLoad() under super.viewDidLoad(), present the Embedded Tickets SDK View Controller by pasting in the following code 

        TMTickets.shared.configure {
            print(" - Tickets SDK Configured")
            self.ticketsView = TMTicketsView.init(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: self.view.frame.height))
            self.view.addSubview(self.ticketsView)
            TMTickets.shared.start(ticketsView: self.ticketsView)
        } failure: { error in
            print(" - Tickets SDK Configuration Error: \(error.localizedDescription)")
        }


 

Modal View Approach

In viewDidLoad() under super.viewDidLoad(), present the Embedded Tickets SDK View Controller by pasting in the following code

        TMTickets.shared.configure {
            print(" - Tickets SDK Configured")
            let ticketsVC = TMTicketsViewController()
            ticketsVC.modalPresentationStyle = .fullScreen
            self.present(ticketsVC, animated: false, completion: nil)
        } failure: { error in
            print(" - Tickets SDK Configuration Error: \(error.localizedDescription)")
        }


You can remove the line which sets the modal to fullscreen if you want a modal in which user can swipe down to dismiss

Exposing Native View to React Native

Create a new .m file, the name in this guide is "RNTTicketsSdkViewManager" you can change the name but note prefixing with "RNT" and appending "Manager" are common conventions and you could run into issues if you don't follow them.

Paste in the below inside RNTTicketsSdkViewManager.m:

#import <React/RCTViewManager.h>
// Below import is eeded to use Swift files in Objective-C files i.e. TicketsSdkViewController
#import “[PROJECT-NAME]-Swift.h"


@interface RNTTicketsSdkViewManager : RCTViewManager
@end

@implementation RNTTicketsSdkViewManager

- (dispatch_queue_t)methodQueue
{
  return dispatch_get_main_queue();
}

+ (BOOL)requiresMainQueueSetup {
  return true;
}

RCT_EXPORT_MODULE(RNTTicketsSdkView)

- (UIView *)view
{
  TicketsSdkViewController *vc = [[TicketsSdkViewController alloc] init];
  return vc.view;
}

@end




Show the View in React Native


In your React Native project, create a file called TicketsSdk.tsx and paste in the below:

import {requireNativeComponent} from 'react-native'

module.exports = requireNativeComponent('RNTTicketsSdkView')


Embedded View approach

Create a file called MyEvents.tsx and paste in the below:

import React, {useCallback, useState} from 'react'
import {useFocusEffect} from '@react-navigation/native'
import TicketsSdk from '../components/TicketsSdk'

const MyTickets = () => {
return <TicketsSdk style={{flex: 1}} />
}

export default MyTickets



Modal View Approach

import React, {useCallback, useState} from 'react'
import {View, Text, TouchableOpacity} from 'react-native'
import {useFocusEffect} from '@react-navigation/native'
import TicketsSdk from '../components/TicketsSdk'

const MyTickets = () => {
  const [initialFocus, setInitialFocus] = useState(false)
  const [showTicketsSdk, setShowTicketsSdk] = useState(false)

  useFocusEffect(
    useCallback(() => {
      // Show SDK's when screen focuses
      setInitialFocus(true)
      // unmount the RN components after the Native SDK's are launched,
      // so that the SDK can be reinitialized via RN's button onPress after the SDK's Native close button is pressed
      setTimeout(() => {
        setInitialFocus(false)
      }, 500)
      console.log('useFocusEffect mount called')
      return () => {
        setInitialFocus(false)
        console.log('useFocusEffect unmount called')
      }
    }, []),
  )

  const onShowTicketsSdk = () => {
    setShowTicketsSdk(true)
    setTimeout(() => {
      setShowTicketsSdk(false)
    }, 500)
  }

  return (
    <View style={{flex: 1}}>
      <View style={{margin: 4, width: 170, alignItems: 'center'}}>
        <Text>Tickets SDK</Text>
      </View>
      <TouchableOpacity
        onPress={() => onShowTicketsSdk()}
        style={{
          backgroundColor: '#07a',
          width: 170,
          height: 30,
          borderRadius: 4,
          alignItems: 'center',
          margin: 4,
          justifyContent: 'center',
        }}>
        <Text>Show Tickets SDK:</Text>
      </TouchableOpacity>
      {(initialFocus || showTicketsSdk) && (
        // Wrapper the SDK modal view in an additional view prevents the main view from being unresponsive after the SDK is presented
        <View>
          <TicketsSdk />
        </View>
      )}
    </View>
  )
}

export default MyTickets


React Navigation (Embedded View Approach)  


If you are using React Navigation you will have varying Header and Bottom Tab heights. in this case you can use height instead of flex and do the below:

const [initialFocus, setInitialFocus] = useState(true);

 useEffect(() => {
    // Initially, the altered Bottom Tabs View frame height is not available for Native code on iOS, this becomes available after a rerender.
    // If needed an additional delay can be used before setting to false
    setInitialFocus(false);
  }, []);

  return <>{!initialFocus && <TicketsSdkEmbedded style={{height: '90%'}} />}</>;


You can then set a height on your bottom tabs so that height of the embedded view takes into account the height of your bottom tabs: 

const BottomTab = createBottomTabNavigator()

...

 <BottomTab.Navigator
  screenOptions={{
        tabBarStyle: {
...
        height: '10%',
...
        },
      }}>

Another approach would be to set the height in the .swift file instead of React Native. In this case, keep the changes to BottomTab.Navigator but instead of the solution involving initial focus include an extra line in TicketsSdkViewController.swift:

self.ticketsView = TMTicketsView.init(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: self.view.frame.height))
// extra line below
self.ticketsView.frame.size.height *= 0.9
// extra line above
self.view.addSubview(self.ticketsView)

Analytics  

To setup delegates in React Native iOS see: https://ignite.ticketmaster.com/docs/react-native-accounts-information



After setting up Delegates in your Accounts SDK NSObject/RCTEventEmitter class you can follow: https://ignite.ticketmaster.com/v1/docs/analytics-ios-1






React Native Tickets Page (Android)

Create a custom view that extends FrameLayout. content of this view can be anything you want to render

class CustomView(context: Context) : FrameLayout(context) {
  init {
    // set padding and background color
    setPadding(16,16,16,16)
    setBackgroundColor(Color.parseColor("#5FD3F3"))

    // add default text view
    addView(TextView(context).apply {
      text = "Welcome to Retail SDK with React Native."
    })
  }
}

Create a fragment with FrameLayout

class TicketsFragment : Fragment() {

    private val mProgressDialog: AlertDialog by lazy {
        AlertDialog
            .Builder(this@MyFragment.requireActivity())
            .setView(LayoutInflater.from(this@MyFragment.requireActivity()).inflate(R.layout.layout_loading_view, null, false))
            .setCancelable(false)
            .create()
            .apply {
                setCanceledOnTouchOutside(false)
            }
    }

    private lateinit var customView: CustomView
    private fun createTicketsColors(color: Int): TicketsColors =
        TicketsColors(
            lightColors(primary = Color(color), primaryVariant = Color(color), secondary = Color(color)),
            darkColors(primary = Color(color), primaryVariant = Color(color), secondary = Color(color))
        )

    private suspend fun validateAuthToken(authentication: TMAuthentication): Map<AuthSource, String> {
        val tokenMap = mutableMapOf<AuthSource, String>()
        AuthSource.values().forEach {
            //Validate if there is an active token for the AuthSource, if not it returns null.
            authentication.getToken(it)?.let { token ->
                tokenMap[it] = token
            }
        }
        return tokenMap
    }

    private fun launchTicketsView() {
        mProgressDialog.dismiss()
        //Retrieve an EventFragment
        TicketsSDKSingleton.getEventsFragment(requireContext())?.let {
           childFragmentManager.beginTransaction().add(R.id.container, it).commit()
        }
    }

    private val resultLauncher = registerForActivityResult(
        ActivityResultContracts.StartActivityForResult()
    ) { result ->
        when (result.resultCode) {
            AppCompatActivity.RESULT_OK -> launchTicketsView()
            AppCompatActivity.RESULT_CANCELED -> {
            }
        }
    }

    override fun onAttach(context: Context) {
        super.onAttach(context)
        mProgressDialog.show()
        val coroutineScope = CoroutineScope(Dispatchers.IO)
        coroutineScope.launch {
            val authentication = TMAuthentication.Builder()
                .apiKey("[API_KEY]")
                .clientName("[TEAM_NAME]") // Team name to be displayed
                .colors(TMAuthentication.ColorTheme())
                .environment(TMXDeploymentEnvironment.Production) // Environment that the SDK will use. Default is Production
                .region(TMXDeploymentRegion.US) // Region that the SDK will use. Default is US
                .build(this@MyFragment.requireActivity())
            val tokenMap = validateAuthToken(authentication)

            TicketsSDKClient
                .Builder()
                .authenticationSDKClient(authentication) //Authentication object
                //Optional value to define the colors for the Tickets page
                .colors(createTicketsColors(android.graphics.Color.parseColor("#231F20")))
                //Function that generates a TicketsSDKClient object
                .build(this@MyFragment.requireActivity())
                .apply {
                    //After creating the TicketsSDKClient object, add it into the TicketsSDKSingleton
                    TicketsSDKSingleton.setTicketsSdkClient(this)

                    //Validate if there is an active token.
                    if (tokenMap.isNotEmpty()) {
                        //If there is an active token, it launches the event fragment
                        launchTicketsView()
                    } else {
                        //If there is no active token, it launches a login intent. Launch an ActivityForResult, if result
                        //is RESULT_OK, there is an active token to be retrieved.

                        val activity = this@MyFragment.activity // Use activity property instead of requireActivity()
                        //It is important to null check activity because 
                        //The method getLoginIntent(context: FragmentActivity) requires a non-null FragmentActivity as its parameter   
                        if (activity != null) {
                           resultLauncher.launch(TicketsSDKSingleton.getLoginIntent(activity))
                        } else {
                           // Handle the case where the activity is null
                           Log.e("MyFragment", "Activity is null, unable to launch login intent.")
                        }                  
                    }
                }
        }
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        customView = CustomView(requireNotNull(context))
        return customView // this CustomView could be any view that you want to render

    }
}

Create a ViewManager, this is used to bring the fragment to react native side.

class TicketsViewManager(
    private val reactContext: ReactApplicationContext
) : ViewGroupManager<FrameLayout>() {
  private var propWidth: Int? = null
  private var propHeight: Int? = null

  override fun getName() = REACT_CLASS

  /**
   * Return a FrameLayout which will later hold the Fragment
   */
  override fun createViewInstance(reactContext: ThemedReactContext) =
      FrameLayout(reactContext)

  /**
   * Map the "create" command to an integer
   */
  override fun getCommandsMap() = mapOf("create" to COMMAND_CREATE)

  /**
   * Handle "create" command (called from JS) and call createFragment method
   */
  override fun receiveCommand(
      root: FrameLayout,
      commandId: String,
      args: ReadableArray?
  ) {
    super.receiveCommand(root, commandId, args)
    val reactNativeViewId = requireNotNull(args).getInt(0)

    when (commandId.toInt()) {
      COMMAND_CREATE -> createFragment(root, reactNativeViewId)
    }
  }

  @ReactPropGroup(names = ["width", "height"], customType = "Style")
  fun setStyle(view: FrameLayout, index: Int, value: Int) {
    if (index == 0) propWidth = value
    if (index == 1) propHeight = value
  }

  /**
   * Replace your React Native view with a custom fragment
   */
  fun createFragment(root: FrameLayout, reactNativeViewId: Int) {
    val parentView = root.findViewById<ViewGroup>(reactNativeViewId)
    setupLayout(parentView)

    val myFragment = TicketsFragment()
    val activity = reactContext.currentActivity as FragmentActivity
    activity.supportFragmentManager
        .beginTransaction()
        .replace(reactNativeViewId, myFragment, reactNativeViewId.toString())
        .commit()
  }

  fun setupLayout(view: View) {
    Choreographer.getInstance().postFrameCallback(object: Choreographer.FrameCallback {
      override fun doFrame(frameTimeNanos: Long) {
        manuallyLayoutChildren(view)
        view.viewTreeObserver.dispatchOnGlobalLayout()
        Choreographer.getInstance().postFrameCallback(this)
      }
    })
  }

  /**
   * Layout all children properly
   */
  private fun manuallyLayoutChildren(view: View) {
    // propWidth and propHeight coming from react-native props
    val width = requireNotNull(propWidth)
    val height = requireNotNull(propHeight)

    view.measure(
        View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
        View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY))

    view.layout(0, 0, width, height)
  }

  companion object {
    private const val REACT_CLASS = "TicketsViewManager"
    private const val COMMAND_CREATE = 1
  }
}

Create a React Package and add your view manager

class MyPackage : ReactPackage {

    override fun createViewManagers(
        reactContext: ReactApplicationContext
    ) = listOf(TicketsViewManager(reactContext))
}

Register the Package in your MainApplication.kt

    override fun getPackages() = PackageList(this).packages.apply {
      add(MyPackage())
    }

Setup Javascript module

import {requireNativeComponent} from 'react-native'

export const TicketsViewManager = requireNativeComponent('TicketsViewManager')

Create a custom View

import React, {useEffect, useRef} from 'react';
import {
  PixelRatio,
  UIManager,
  findNodeHandle,
} from 'react-native';

import {TicketsViewManager} from './tickets-view-manager';

const createFragment = viewId =>
  UIManager.dispatchViewManagerCommand(
    viewId,
    // we are calling the 'create' command
    UIManager.TicketsViewManager.Commands.create.toString(),
    [viewId],
  );

export const TicketsView = () => {
  const ref = useRef(null);

  useEffect(() => {
    const viewId = findNodeHandle(ref.current);
    createFragment(viewId);
  }, []);

  return (
    <TicketsViewManager
      style={{
        // converts dpi to px, provide desired height
        height: PixelRatio.getPixelSizeForLayoutSize(200),
        // converts dpi to px, provide desired width
        width: PixelRatio.getPixelSizeForLayoutSize(200),
      }}
      ref={ref}
    />
  );
};


Was this article helpful?