We use SteamVR it does not use any unity specific stuff at all, on top of that we have our own abstraction for Rift and Vive and in the future other hardware as well.
protected abstract HashSet<ButtonInput> GetMappableButtons();
public Button? CheckForPlayerMappableButtonPress(NVRHand[] hands)
{
var possibleButtons = GetMappableButtons();
foreach (var hand in hands)
{
foreach (var input in possibleButtons)
{
if (VirtualButtons.Contains((int)input.Button))
{
if (CheckForVirtualButton(hand, input)) return input.Button;
continue;
}
if (CheckForSteamVRButton(hand, input)) return input.Button;
}
}
return null;
}
GetMappableButtons is a abstract method implemented by Vive and Rift subclasses it returns the possible buttons that the user can map, for Rift it looks like this
private static readonly HashSet<ButtonInput> mappableButtons = new HashSet<ButtonInput>(new[] { Button.A, Button.ApplicationMenu, Button.VirtualButtonCenter, Button.VirtualButtonUp, Button.VirtualButtonDown }.Select(btn => new ButtonInput{ Button = btn, Action = ButtonAction.PressDown}));
protected override HashSet<ButtonInput> GetMappableButtons()
{
return mappableButtons;
}
We also define default configs, here for rift
protected override void ApplyDefaultCommandMap(CommandMap commandMap)
{
commandMap
.Add(Command.BoltRelease, Button.VirtualButtonUp.PressDown())
.Add(Command.MagRelease, Button.VirtualButtonDown.PressDown())
.Add(Command.ChangeFireMode, Button.VirtualButtonCenter.PressDown())
.Add(Command.Calibrate, Button.Trigger.PressDown())
.Add(Command.GripItem, Button.Grip.PressDown())
.Add(Command.GripWeapon, Button.Grip.PressDown())
.Add(Command.GripFrontGrip, Button.Grip.PressDown())
.Add(Command.GripInteractable, Button.Trigger.PressDown())
.Add(Command.DropItem, Button.Grip.PressDown())
.Add(Command.DropWeapon, Button.Grip.PressDown())
.Add(Command.DropFrontGrip, Button.Grip.PressDown())
.Add(Command.DropInteractable, Button.Grip.PressDown())
.Add(Command.PullGrenadeLever, Button.Touchpad.PressDown())
.Add(Command.ReleaseAction, Button.Touchpad.IsPressed())
.Add(Command.SameHandMagRelease, Button.Touchpad.PressDown())
.Add(Command.ShowMenu, Button.ApplicationMenu.PressDown())
.Add(Command.DetachAttachment, Button.Grip.PressDown())
.Add(Command.StartPhysicalHand, Button.Trigger.PressDown())
.Add(Command.StopPhysicalHand, Button.Trigger.PressUp())
.Add(Command.Sprint, Button.Touchpad.IsPressed())
.Add(Command.ToggleAttachment, Button.Touchpad.PressUp())
.Add(Command.ReleaseDrum, Button.VirtualButtonUp.PressDown())
.Add(Command.CockHammer, Button.VirtualButtonDown.PressDown())
.Add(Command.UncockHammer, Button.VirtualButtonDown.IsPressed());
}
Lastly we have a table over mappable commands
private static readonly Command[] mappableCommands = new[] { Command.BoltRelease, Command.ChangeFireMode, Command.MagRelease, Command.ReleaseAction };
Now the UI can query the underlying domain and wait for a button to be pressed, we also have something we call commandGrouping, commands that cant share the same button. If the user select the same button the config turns read and he cant save the config.

edit: And offcourse the UI isnt hardcoded, the list of command mappings is databound
protected virtual void Start()
{
EventBus.Instance.Subscribe(this);
var template = GetComponentInChildren<CommandMapping>();
template.gameObject.SetActive(false);
var invalidColor = GetComponentInParent<MainMenu>().InvalidColor;
maps = mappableCommands
.Select(cmd => Instantiate(template).Init(cmd, invalidColor))
.ToDictionary(cm => cm.Command, cm => cm);
foreach (var item in maps.Values)
item.transform.SetParent(transform, false);
}
The missing component now is UI friendly captions both for commands and buttons, for commands we just āWordifyā the enums so Command.BoltRelease becomes Bolt release, you can also completely override the caption if you add the command to a Command > string dictionary. For the buttons we only have a button > string dictionary.
private static readonly Dictionary<Button, string> captions = new Dictionary<Button, string>
{
{ Button.VirtualButtonUp, "Stick up" },
{ Button.VirtualButtonRight, "Stick right" },
{ Button.VirtualButtonDown, "Stick down" },
{ Button.VirtualButtonLeft, "Stick left" },
{ Button.Touchpad, "Stick" },
{ Button.VirtualButtonCenter, "Stick" },
{ Button.A, "A" },
{ Button.ApplicationMenu, "B" },
};
So there sadly aint no magic going on that we can call a singel method in some Unity API 
Oh then from the Tutorial we do something like this
[CreateAssetMenu(menuName = "Tutorial/ReleaseBoltStep")]
public class ReleaseBoltStep : TutorialStep
{
public bool HasBoltRelease;
public override IEnumerator Execute()
{
var firearm = Get<Firearm>();
ShowPopup(firearm.Slide.transform, firearm.Slide.Ready || !HasBoltRelease ? string.Format("Grab the slide by {0}, draw the slide and release.", GetInteractionCaption(firearm.Slide.ItemType)) : string.Format("Release the bolt by pressing {0}", GetCommandCaption(Command.BoltRelease)) );
while (firearm.Bullet == null)
yield return null;
}
}