<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Team Banuba</title>
    <description>The latest articles on DEV Community by Team Banuba (@team_banuba).</description>
    <link>https://dev.to/team_banuba</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4082753%2Fc1c62964-e75a-4e5c-a2b1-4d1001031685.png</url>
      <title>DEV Community: Team Banuba</title>
      <link>https://dev.to/team_banuba</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/team_banuba"/>
    <language>en</language>
    <item>
      <title>Virtual Eyewear Try-On: 4 Jobs Between Frame Photos and Cart</title>
      <dc:creator>Team Banuba</dc:creator>
      <pubDate>Mon, 31 Aug 2026 20:44:28 +0000</pubDate>
      <link>https://dev.to/team_banuba/virtual-eyewear-try-on-4-jobs-between-frame-photos-and-cart-8ep</link>
      <guid>https://dev.to/team_banuba/virtual-eyewear-try-on-4-jobs-between-frame-photos-and-cart-8ep</guid>
      <description>&lt;p&gt;Putting eyewear try-on on a storefront breaks into four jobs: turning photographs of each frame into a try-on asset, deciding which browsers get the entry point, mounting the widget and choosing what opens it, and connecting the catalog check to the cart events. Three are front-end work. The first is a data pipeline, and it sets the launch date.&lt;/p&gt;

&lt;p&gt;The ticket usually arrives with a design attached, a button under the add-to-cart block, and no picture of what sits behind it. The code below uses the &lt;a href="https://www.banuba.com/glasses-virtual-try-on" rel="noopener noreferrer"&gt;Banuba virtual try-on widget&lt;/a&gt; because that is what we build, so the attribute and method names are ours, and every fact about them is cited to our own documentation. The four jobs hold for any web try-on engine you mount.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before you start
&lt;/h2&gt;

&lt;p&gt;Four things belong in hand before the first commit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An HTTPS origin for every environment the widget runs in, staging included.&lt;/li&gt;
&lt;li&gt;A merchant ID issued by Banuba, which the widget uses to load your configuration.&lt;/li&gt;
&lt;li&gt;Photographs of every frame in the launch collection, shot to the digitization spec rather than pulled from the web catalog.&lt;/li&gt;
&lt;li&gt;A decision about where the entry point lives on the product detail page, since it drives the gating logic in the second job.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Digitizing the frames
&lt;/h2&gt;

&lt;p&gt;Digitization turns photographs of a physical frame into the asset the widget renders on a face, and in the admin dashboard you do it one frame at a time. Per frame you supply a name, a brand picked from the list or created on the spot, a SKU, an optional product page URL, then three images in the try-on model step: left, body and right. A preview renders while the images upload, so a bad set shows itself before you save, &lt;a href="https://tintvto.com/docs/glasses-digitization.html" rel="noopener noreferrer"&gt;as our glasses dashboard guide describes&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Since widget v1.7.0, released 30 April 2026 according to &lt;a href="https://tintvto.com/docs/changelog.html" rel="noopener noreferrer"&gt;our widget changelog&lt;/a&gt;, that processing runs automatically. The consequence lands on whoever owns the photography. Our docs state that photos failing the requirements give a poor result, and that it cannot be repaired afterwards, because nothing about the processing is manual. Treat the photo spec as an input requirement with the same weight as a schema.&lt;/p&gt;

&lt;p&gt;Here is the part that reshapes a project plan. The bulk CSV path is makeup only. In &lt;a href="https://tintvto.com/docs/bulk_upload.html" rel="noopener noreferrer"&gt;our bulk upload template&lt;/a&gt; the Category column always takes the value MAKEUP, and the columns after it are Region, Finish, Color as a 6-digit hex, and Coverage, all parameters of a cosmetic product. Frames therefore go in through the Glasses tab individually, and our docs mark that tab Beta. For a catalog of any size that is a data entry line with a person's name on it, and sizing it comes before anyone commits to a launch date.&lt;/p&gt;

&lt;p&gt;Production work asks for more than the three try-on images. The commercial list in &lt;a href="https://tintvto.com/docs/knowledge-base.html" rel="noopener noreferrer"&gt;our knowledge base&lt;/a&gt; adds UPC number, a storefront image link, the product page URL, photos from multiple angles, and a 3D model, with frame size where it is known. Collect all of it in the same pass, since walking the collection twice costs the same hours twice.&lt;/p&gt;

&lt;p&gt;The decision point is ownership. Someone has to supply the photographs, either merchandising from existing shoots or a studio from a new one, and that answer moves the timeline more than any code in this article does. Pull one frame record out of your PIM as well and see whether the frame size fields exist there at all.&lt;/p&gt;

&lt;p&gt;What goes wrong here is scheduling. A team scopes the work as a front-end sprint, then finds in week one that the launch collection has to be entered by hand and reshot on top, because the existing photography was styled for hero banners at an angle the try-on step cannot use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deciding which browsers get the button
&lt;/h2&gt;

&lt;p&gt;The try-on button should render off a runtime check rather than a static support table, because camera access has hard requirements a shopper's browser either satisfies or fails. The first is transport. Webcam access needs an HTTPS/TLS connection, so a staging box on plain HTTP fails at the first call and looks exactly like a broken widget; MDN documents &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia" rel="noopener noreferrer"&gt;the secure context rule behind getUserMedia&lt;/a&gt;, which applies to every camera API on the page, ours included.&lt;/p&gt;

&lt;p&gt;The second is the engine. &lt;a href="https://tintvto.com/docs/introduction.html" rel="noopener noreferrer"&gt;Our documented system requirements&lt;/a&gt; specify the last 2 Safari major versions, the last 2 iOS major versions, and the last 5 versions of Chrome, Opera, Edge and Samsung Internet. Read that as the set we test against. A version floor describes a population, while the shopper in front of you is one device with one driver and one browser build.&lt;/p&gt;

&lt;p&gt;The answer for an individual shopper comes from feature detection at render time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;cameraIsAvailable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isSecureContext&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mediaDevices&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mediaDevices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getUserMedia&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;function&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tryOnButton&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;open-vto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;tryOnButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hidden&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;cameraIsAvailable&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;window.isSecureContext covers transport, navigator.mediaDevices is absent entirely on browsers without media capture, and tryOnButton is the entry point your PDP template renders. The button now appears only where both conditions hold.&lt;/p&gt;

&lt;p&gt;Then comes the branch, and both sides are defensible. Hiding the entry point on a failing check keeps the page honest, at the cost of a shopper who never learns the feature exists. Rendering it with a static model photo behind it keeps the feature discoverable. Eyewear weakens that second option in a way a cosmetic shade does not: a frame on a stock model shows how the style reads, while the question that made the shopper tap was how it sits on their own face.&lt;/p&gt;

&lt;p&gt;Skip the gate and the failure is uniform. Every shopper sees the button, and the ones on an old in-app browser or an HTTP staging link get a modal that opens onto nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mounting the widget and picking the trigger
&lt;/h2&gt;

&lt;p&gt;Mounting is two lines of markup, and the judgment call here is what opens the widget afterwards. The bundle loads from a CDN, and the element goes wherever the experience should appear, both covered in &lt;a href="https://tintvto.com/docs/integration.html" rel="noopener noreferrer"&gt;our integration guide&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"module"&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://tintvto.com/widget.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That path carries no version segment, so it resolves to the current release. Confirm a pinned URL with your technical manager before production, because a minor release can change behavior on a page you have not deployed to in months.&lt;/p&gt;

&lt;p&gt;On a product detail page you want the widget focused on the frame being viewed, which is what the isolated SKU attributes do:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;tint-vto&lt;/span&gt; &lt;span class="na"&gt;merchant-id=&lt;/span&gt;&lt;span class="s"&gt;"YOUR_MERCHANT_ID"&lt;/span&gt; &lt;span class="na"&gt;isolated-sku&lt;/span&gt; &lt;span class="na"&gt;sku=&lt;/span&gt;&lt;span class="s"&gt;"8028997081552"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/tint-vto&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"open-vto"&lt;/span&gt; &lt;span class="na"&gt;data-sku=&lt;/span&gt;&lt;span class="s"&gt;"8028997081552"&lt;/span&gt; &lt;span class="na"&gt;hidden&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Try these on&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;merchant-id is the identifier Banuba issues you, isolated-sku switches the widget into single product mode, and sku names the frame on this page. The token and sdk-token attributes are documented as deprecated and slated for removal, so check any snippet inherited from an older sample.&lt;/p&gt;

&lt;p&gt;That merchant ID ships in your page source and any visitor can read it in devtools, which is normal for a widget that loads its configuration client side and is also the first question a security reviewer will ask. The answer worth having in writing before launch is how the identifier is scoped: which origins it works from, how that list is configured, and what happens when someone copies it onto their own domain. Put those three questions to your technical manager during evaluation rather than the week before release.&lt;/p&gt;

&lt;p&gt;Our integration guide documents launch triggers for opening immediately, opening on the window load event, and opening on a customer action. A product detail page wants the third, so that the camera permission prompt follows an explicit tap and reads as an answer to something the shopper asked for.&lt;/p&gt;

&lt;p&gt;The camera itself can be declined, and the fallback is two method calls:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;script&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;module&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;widget&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tint-vto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tryOnButton&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;open-vto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;tryOnButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;widget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;widget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;useWebcam&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;modelPhoto&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/media/model-front.webp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;widget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;useImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;modelPhoto&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/script&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;useWebcam() resolves with the active stream, or with null when the shopper denies access, and useImage() accepts a Blob or File to render on instead. usePhoto() is a deprecated alias of useImage(). On a frames PDP that fallback stays a courtesy, for the fit reason above.&lt;/p&gt;

&lt;p&gt;Two things break here in practice. One is the inherited snippet still passing token, which keeps working until the release that removes it. The other is a widget wired to open on page load, which spends the camera prompt before the shopper has expressed any interest and trains them to dismiss it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connecting the catalog and the cart
&lt;/h2&gt;

&lt;p&gt;Two wires finish the integration: a catalog check deciding whether the button renders for this frame, and the events keeping the cart in step with the widget. The check is isCustomerSkuExist(customerSkuIds: string[]), which resolves true when every SKU you pass exists in the try-on catalog and can be called before open(), per &lt;a href="https://tintvto.com/docs/widget-api.html" rel="noopener noreferrer"&gt;our widget API reference&lt;/a&gt;. Put it in the PDP render path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;script&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;module&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;widget&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tint-vto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tryOnButton&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;open-vto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;currentSku&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tryOnButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isDigitized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;widget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isCustomerSkuExist&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;currentSku&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="nx"&gt;tryOnButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hidden&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;isDigitized&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;cameraIsAvailable&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/script&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;currentSku reads the data-sku attribute your template wrote onto the button, and cameraIsAvailable() is the detection function from the previous job, imported from wherever you keep it. A frame with no try-on asset yet never shows a button. &lt;/p&gt;

&lt;p&gt;That gate carries more weight for eyewear than for a cosmetic shade, where a missing asset still leaves a swatch and a product photo to look at.&lt;/p&gt;

&lt;p&gt;Cart sync runs on the events the widget emits. Attach the listeners once, wherever you mount the element:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storefrontCart&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// your own cart module&lt;/span&gt;

&lt;span class="nx"&gt;widget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;addToCart&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;detail&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nx"&gt;widget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;removeFromCart&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;detail&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nx"&gt;widget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;redirectToCart&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/cart&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our API reference documents detail as an array of Product objects on the first two events, and as null on redirectToCart, which fires when the shopper clicks Go to cart. To drive the widget from your own product data instead, applyProduct() takes fully defined product objects and applyProductByCustomerSku() takes your customer facing SKU identifiers.&lt;/p&gt;

&lt;p&gt;For device checks, widget v1.7.1 of 18 May 2026 added try-on links and QR codes generated per product or for a whole catalog, per the same changelog. QA becomes scanning a code: open it on the phones your analytics say people shop from, and walk the flow inside the Instagram and Facebook in-app browsers, with no deploy behind any of it. Rendering and camera behavior diverge across handsets much further than a support matrix suggests, which is what &lt;a href="https://www.banuba.com/blog/best-web-ar-apis-tested" rel="noopener noreferrer"&gt;our web AR testing kept running into&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The defects to watch for are quiet ones. Leave any of those events unhandled and the cart drifts, so a shopper adds a frame inside the try-on and finds the header count unchanged. Ship a frame to the PDP with a live button before anyone has processed its photos and you get the ungated version of the check above.&lt;/p&gt;

&lt;h2&gt;
  
  
  What carries over to another engine
&lt;/h2&gt;

&lt;p&gt;Swap the vendor and the identifiers change while the shapes survive. There is always an asset preparation step with a photography spec attached, and it is the long pole. A capability gate follows it, since camera access on the web is conditional everywhere. Then a mount point plus a trigger decision, where the case for the tap is the permission prompt. Last comes the boundary between the try-on surface and your cart, crossed by events one way and product identifiers the other.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom line
&lt;/h2&gt;

&lt;p&gt;The three front-end jobs are a normal week for someone holding the merchant ID and a PDP template. The variable is the photography, and better code will not compress it, because it is bounded by how fast someone shoots frames to spec and enters them one at a time through a Beta tab. So run the estimate in that order: count the frames in the launch collection, find out who is shooting them and whether the UPC and frame size fields exist in your PIM today, and put the code work after that number.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What do you upload for each frame?
&lt;/h3&gt;

&lt;p&gt;A name, a brand, a SKU, an optional product page URL, and three images of the frame: left, body and right. Going to production, collect the commercial set in the same pass, which adds UPC number, a storefront image link, photos from multiple angles, a 3D model, and frame size where you have it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can frames be bulk imported from a CSV?
&lt;/h3&gt;

&lt;p&gt;No. The bulk upload template is makeup only: its Category field takes the single value MAKEUP and its parameter columns are Region, Finish, Color and Coverage. Frames go in individually through the Glasses tab of the admin dashboard.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why does the try-on work locally but not on my staging server?
&lt;/h3&gt;

&lt;p&gt;Almost always because staging is served over plain HTTP. Browsers restrict camera access to secure contexts, and localhost counts as one while &lt;a href="http://staging.example.com" rel="noopener noreferrer"&gt;http://staging.example.com&lt;/a&gt; does not. Put a certificate on staging and the same build works.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does eyewear try-on need a native app?
&lt;/h3&gt;

&lt;p&gt;No. The widget is web based, loads from a CDN script tag, and runs in the browser on desktop and mobile, so no app store release sits in the critical path.&lt;/p&gt;

&lt;h3&gt;
  
  
  What happens on an unsupported browser?
&lt;/h3&gt;

&lt;p&gt;The widget opens and the camera call fails, which reads to the shopper as a broken page. Hence the feature detection in the second job: the entry point renders only where a secure context and a media devices API are both present.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which attributes should a new integration use?
&lt;/h3&gt;

&lt;p&gt;merchant-id, plus isolated-sku and sku on a product detail page. token and sdk-token are deprecated and scheduled for removal, and usePhoto() is a deprecated alias of useImage().&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>ecommerce</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>6 AR Makeup Try-On Failures a React Storefront Hits in Production</title>
      <dc:creator>Team Banuba</dc:creator>
      <pubDate>Wed, 19 Aug 2026 09:37:09 +0000</pubDate>
      <link>https://dev.to/team_banuba/6-ar-makeup-try-on-failures-a-react-storefront-hits-in-production-1e07</link>
      <guid>https://dev.to/team_banuba/6-ar-makeup-try-on-failures-a-react-storefront-hits-in-production-1e07</guid>
      <description>&lt;p&gt;AR makeup try-on rarely fails in the demo. It fails on the third page load, on a mid-range Android, in the Instagram in-app browser, or on a skin tone nobody on the team has. Six failure modes account for most of it: camera permission handling, double-mounted effects, shade fidelity, frame rate collapse, SKU mapping drift, and mobile browser policy.&lt;/p&gt;

&lt;p&gt;Each one below has the same shape: what actually breaks, why React or the mobile browser makes it worse, and the test you can run before a shopper finds it. &lt;/p&gt;

&lt;p&gt;The code examples use the &lt;a href="https://tintvto.com/docs/introduction.html" rel="noopener noreferrer"&gt;Banuba virtual try-on widget&lt;/a&gt; because that is what we build, but every test method here is vendor-neutral. If you are integrating a different engine, the checks still apply.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc36x1dvz5zhvy6cb6e8i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc36x1dvz5zhvy6cb6e8i.png" alt="banuba makeup virtual try-on widget preview on the frontend" width="799" height="534"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does a try-on demo pass and the storefront fail?
&lt;/h2&gt;

&lt;p&gt;A vendor demo runs one product, one browser, one lighting setup, and one face. A storefront runs your whole catalog across every device your analytics has ever seen. The gap between those two is where the six failures live.&lt;/p&gt;

&lt;p&gt;It is worth being precise about what a web try-on actually is before testing it. It is a camera stream, a face tracking model, a rendering layer, and a mapping from your SKUs to render parameters. Three of the six failures below are in code you wrote, not in the engine.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. What happens when the shopper denies camera access?
&lt;/h2&gt;

&lt;p&gt;This is the most common one and the cheapest to fix. getUserMedia can be denied, dismissed, blocked by enterprise policy, or unavailable because the page is not on a secure origin. Per MDN, getUserMedia is only available in a &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia" rel="noopener noreferrer"&gt;secure context&lt;/a&gt;, meaning HTTPS or localhost, so a staging box on plain HTTP will fail every time and look like a bug in the widget.&lt;/p&gt;

&lt;p&gt;A try-on with no fallback shows a black rectangle and the shopper leaves the page. A try-on with a fallback shows the product on a model photo and keeps the session alive.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;async function openTryOn(widget) {
  await widget.open();

 const stream = await widget.useWebcam();
  if (!stream) {
    // Camera denied, dismissed, or unavailable. Do not leave the modal empty.
    const model = await fetch("/model.webp").then((r) =&amp;gt; r.blob());
    await widget.useImage(model);
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  How to test it
&lt;/h3&gt;

&lt;p&gt;In Chrome DevTools, open the three-dot menu, go to More tools, then Sensors, and block camera permission for the origin. Then repeat on plain HTTP. Both paths must render something. Track the denial rate as an event, because if a third of your sessions never get a camera, the fallback image is your real try-on experience and deserves the same design attention.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Why does the camera light stay on after the modal closes?
&lt;/h2&gt;

&lt;p&gt;This one is specific to React and it is the failure most teams ship without noticing. React 18 in development deliberately mounts, unmounts, and remounts every component. The React documentation is blunt about the reason: Strict Mode calls effects twice so that "bugs caused by missing cleanup logic" surface in development instead of production.&lt;/p&gt;

&lt;p&gt;If your effect opens the widget and never releases it, you get two widget instances and two camera streams. In development the symptom is a doubled modal. &lt;/p&gt;

&lt;p&gt;In production the symptom is worse: the camera indicator stays lit after the shopper closes the try-on, which reads as spyware and generates support tickets.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { useEffect, useRef } from "react";
export function TryOn({ merchantId, sku }) {
  const ref = useRef(null);

  useEffect(() =&amp;gt; {
    const widget = ref.current;
    let cancelled = false;

    (async () =&amp;gt; {
      await widget.open();
      if (cancelled) await widget.close();
    })();

    return () =&amp;gt; {
      cancelled = true;
      widget.close();          // release the stream
    };
  }, [sku]);

  return &amp;lt;tint-vto ref={ref} merchant-id={merchantId} isolated-sku sku={sku} /&amp;gt;;
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  How to test it
&lt;/h3&gt;

&lt;p&gt;Open the try-on, close it, and watch the browser tab's camera indicator. It must go dark within a second. Then navigate between two product pages five times and confirm you still have exactly one widget instance in the DOM. Run this with Strict Mode on, not off. Turning Strict Mode off to make the double mount go away hides the bug rather than fixing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Does the shade look right on every skin tone, or only on yours?
&lt;/h2&gt;

&lt;p&gt;This is the failure that costs money rather than support tickets, and it is the hardest to catch because the team testing it is usually not diverse enough to catch it. &lt;/p&gt;

&lt;p&gt;The single most common complaint reported by virtual try-on buyers is filed, almost word for word, as "product color not displayed correctly": the shade on screen does not match the shade in the box. A naive color overlay washes out on deep skin and looks artificial on light skin, so the same lipstick reads as two different products depending on who is looking at it.&lt;/p&gt;

&lt;p&gt;The fix is not something you test by eye once. Build a fixed test matrix and run it every release. The dermatology standard for this is the Fitzpatrick scale, which defines six skin phototypes, and it gives you a defensible set of rows rather than an arbitrary one.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwtqmnc4iluvjnsbub0da.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwtqmnc4iluvjnsbub0da.jpg" alt="the rable comparing test axis (skin tone, lighting, finish, capture) versus minimum coverage" width="629" height="208"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Written out rather than only in the table: cover all six Fitzpatrick phototypes, four lighting conditions including backlit, at least four finishes including one high-coverage foundation, and both the live webcam path and the uploaded photo path. Twenty-four to forty-eight screenshots per release is a small price against a shade that only works on half your customers.&lt;/p&gt;

&lt;p&gt;On the engine side, skin-tone-aware rendering is the specific capability that separates a try-on from a filter. The &lt;a href="https://www.banuba.com/tint-makeup-virtual-try-on" rel="noopener noreferrer"&gt;Banuba virtual try-on widget&lt;/a&gt; applies 16 makeup product types with skin-tone-aware application for exactly this reason, and supports 16 or more product categories including eyewear, hair color, contacts, jewelry, and accessories. &lt;/p&gt;

&lt;p&gt;Whichever engine you pick, ask for its output across your matrix before you sign, not after.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. What is your frame rate on the cheapest phone in your analytics?
&lt;/h2&gt;

&lt;p&gt;Real-time face tracking plus rendering is a per-frame GPU and CPU budget. On a flagship phone you will never see the ceiling. On a three-year-old midrange Android the frame rate drops, the render lags behind the face by a few frames, and the try-on stops feeling like a mirror.&lt;/p&gt;

&lt;p&gt;Pick a floor and enforce it. A practical one is 24 frames per second sustained over sixty seconds, since that is where motion stops reading as stuttering to most people. &lt;/p&gt;

&lt;p&gt;Measure sustained rather than peak, because thermal throttling is what actually gets you: the first ten seconds look fine and second thirty do not.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;let frames = 0;
const start = performance.now();

function tick() {
  frames += 1;
  if (performance.now() - start &amp;lt; 60000) requestAnimationFrame(tick);
  else console.log("sustained fps:", frames / 60);
}
requestAnimationFrame(tick);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  How to test it
&lt;/h3&gt;

&lt;p&gt;run it on a real device, not the emulator, and not immediately after a charge. Pull the three most common device models out of your analytics and test the slowest one. If you only have flagships in the office, a cheap prepaid Android is the highest-value QA hardware you can buy.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Why does one SKU render as a generic tint?
&lt;/h2&gt;

&lt;p&gt;Try-on engines do not render your product. They render the parameters someone mapped to your product: a region, a color, a finish, a coverage level. &lt;/p&gt;

&lt;p&gt;When merchandising adds a shade and nobody digitizes it, the try-on either shows nothing or falls back to something approximate, and the shopper sees a color that is not the one on the box.&lt;/p&gt;

&lt;p&gt;This is a data integrity problem, so treat it as one. Add a contract test in CI that walks your live catalog and asserts every purchasable SKU exists on the try-on side.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Fails the build when merchandising adds a SKU that nobody digitized.
const skus = await getPurchasableSkus();          // your catalog
const ok = await widget.isCustomerSkuExist(skus); // try-on catalog
if (!ok) throw new Error("SKU present in catalog but missing from try-on");
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That isCustomerSkuExist call is from the Banuba widget API, and most engines expose an equivalent lookup; the &lt;a href="https://github.com/Banuba/beauty-web" rel="noopener noreferrer"&gt;open web sample&lt;/a&gt; has a minimal page you can diff your own wiring against.&lt;/p&gt;

&lt;h3&gt;
  
  
  How to test it
&lt;/h3&gt;

&lt;p&gt;Run that check nightly against production data, not just on the fixture set in your test suite. Fixtures never drift. Catalogs always do. Then decide the product behavior for a missing SKU: hiding the try-on button is almost always better than opening a try-on that shows the wrong color.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Does it work inside the Instagram browser?
&lt;/h2&gt;

&lt;p&gt;A large share of beauty traffic arrives from social, which means the page opens in an in-app webview rather than Safari or Chrome. &lt;/p&gt;

&lt;p&gt;Those webviews have their own camera permission behavior, and iOS has a long-standing quirk that catches teams out: WebKit requires the playsinline attribute on a video element, otherwise the stream takes over the whole screen in fullscreen playback instead of rendering in place.&lt;/p&gt;

&lt;p&gt;If you embed the try-on in your own native app rather than the mobile web, the same constraint applies from the other direction. Camera access inside a webview needs a real HTTPS origin, so loading inline HTML without setting a proper base URL will fail even though the identical markup works in a browser tab:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Android WebView: JS on, and a real HTTPS base URL or the camera never opens.
webView.settings.javaScriptEnabled = true
webView.settings.domStorageEnabled = true
webView.loadDataWithBaseURL("https://shop.example.com/", html, "text/html", "UTF-8", null)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  How to test it
&lt;/h3&gt;

&lt;p&gt;Send yourself the product URL through Instagram, Facebook, and TikTok on both iOS and Android, and open it from inside each app. Do not test by pasting the link into Safari. That is a different browser with different rules, and it is not where your traffic is.&lt;/p&gt;

&lt;h3&gt;
  
  
  What does a release checklist look like?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Pulling the six together, the pre-release pass is short:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Camera denied and plain-HTTP origin both render a fallback, not a black box.&lt;/li&gt;
&lt;li&gt;Camera indicator goes dark within a second of closing, with Strict Mode on.&lt;/li&gt;
&lt;li&gt;Shade matrix screenshots taken across six phototypes and four lighting setups.&lt;/li&gt;
&lt;li&gt;Sustained 24 fps over sixty seconds on the slowest device in your top three.&lt;/li&gt;
&lt;li&gt;Nightly SKU contract test green against production catalog data.&lt;/li&gt;
&lt;li&gt;Product URL opened from inside Instagram on iOS and Android.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the integration itself, the reference worth having open alongside the checklist is the &lt;a href="https://tintvto.com/docs/integration.html" rel="noopener noreferrer"&gt;widget integration guide&lt;/a&gt;, which documents the element, its attributes, and the lifecycle methods used above.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom line
&lt;/h2&gt;

&lt;p&gt;The engineering work in AR makeup try-on has moved. Face tracking and rendering are the vendor's problem now, and a first web integration is realistically a two-week front-end task rather than a computer vision project. &lt;/p&gt;

&lt;p&gt;What has not moved is everything around it: permissions, component lifecycle, device spread, catalog integrity, and the browsers your customers actually use.&lt;/p&gt;

&lt;p&gt;Those six are yours regardless of which engine you integrate, and they are the difference between a try-on that converts and one that quietly loses sessions. Build the test matrix before the launch, not after the first support ticket.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Do I need a native app to run AR makeup try-on?
&lt;/h3&gt;

&lt;p&gt;No. A web-based try-on loads as a script bundle and a custom HTML element on your existing product page, so it runs in the mobile browser without a separate app. A native app can embed the same widget through a webview when you want it inside an existing app experience.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why does the try-on work on desktop but not on my staging server?
&lt;/h3&gt;

&lt;p&gt;Almost always the secure context rule. Browsers only expose getUserMedia over HTTPS or localhost, so a staging box served over plain HTTP will never get a camera stream no matter how correct the integration is.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I test shade accuracy without a diverse test team?
&lt;/h3&gt;

&lt;p&gt;Use a fixed matrix instead of ad hoc checks. Six Fitzpatrick phototypes against four lighting conditions gives you 24 screenshots per finish, which you can collect from a stock photo set for the upload path and from contractors or colleagues for the live path. The point is that it is repeatable between releases.&lt;/p&gt;

&lt;h3&gt;
  
  
  What frame rate is good enough?
&lt;/h3&gt;

&lt;p&gt;Set a floor rather than chasing a number. Sustained 24 fps over a full minute on your slowest common device is a reasonable bar, because motion below that starts reading as stutter and thermal throttling shows up after the first few seconds.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can one integration cover more than makeup?
&lt;/h3&gt;

&lt;p&gt;Yes, if the engine supports it. The Banuba widget covers 16 or more product categories including eyewear, hair color, contacts, jewelry, and accessories, so a storefront that integrates for lipstick can enable additional categories later through configuration rather than a second integration.&lt;/p&gt;

&lt;h3&gt;
  
  
  What breaks most often after launch?
&lt;/h3&gt;

&lt;p&gt;Catalog drift. The integration is static once it ships, but merchandising keeps adding SKUs, and any SKU that is not digitized on the try-on side either disappears or renders approximately. A nightly contract test catches it before a customer does.&lt;/p&gt;

</description>
      <category>react</category>
      <category>javascript</category>
      <category>testing</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
