{"$schema": "https://raw.githubusercontent.com/guitaripod/wwdc-sessions/master/schema/transcript.schema.json", "id": "wwdc2026-216", "language": "eng", "source": "https://developer.apple.com/videos/play/wwdc2026/216", "wordCount": 3967, "segments": [{"start": 7.0, "text": "Hi! I'm Kiara, an engineer on the Safari team."}, {"start": 11.0, "text": "If you've ever had an idea for a feature you wanted to see in Safari, and wanted to"}, {"start": 15.0, "text": "turn your idea into a reality, this session is for you."}, {"start": 19.0, "text": "I'll be breaking down everything you need to know to build and distribute"}, {"start": 23.0, "text": "a web extension for Safari."}, {"start": 25.0, "text": "There's a lot to cover, so feel free to take breaks or skip to sections"}, {"start": 29.0, "text": "that are most useful for you."}, {"start": 31.0, "text": "Safari web extensions are packaged within an app."}, {"start": 34.0, "text": "And honestly, they're some of my favorite things to get from the App Store."}, {"start": 38.0, "text": "Whether it's blocking ads, building a custom new tab page,"}, {"start": 42.0, "text": "or enhancing the playback experience on your favorite streaming site."}, {"start": 46.0, "text": "They're small but they can meaningfully improve how you experience the web."}, {"start": 51.0, "text": "Apple is working with other browsers in the W3C Web Extensions Working Group"}, {"start": 56.0, "text": "to standardize the APIs used to build web extensions across browsers."}, {"start": 61.0, "text": "So if you have an extension that you've built for another browser,"}, {"start": 64.0, "text": "you can bring your extension to Safari."}, {"start": 67.0, "text": "Jump to the Packaging and Distribution section, where I'll show you how you can"}, {"start": 72.0, "text": "use App Store Connect to distribute your extension."}, {"start": 75.0, "text": "Today, I'm covering all the bases by building a web extension"}, {"start": 80.0, "text": "from the ground up."}, {"start": 81.0, "text": "I'll take you through the process of developing an extension and highlight"}, {"start": 85.0, "text": "the key APIs and features you can use to bring customizable experiences to Safari."}, {"start": 91.0, "text": "I'll also show you how to test web extensions in Safari and how you can use"}, {"start": 96.0, "text": "TestFlight to share beta versions of your extension with users."}, {"start": 100.0, "text": "And once I'm ready to share my creation with the world, I'll submit my extension"}, {"start": 104.0, "text": "to the App Store."}, {"start": 106.0, "text": "To get started, download the sample code project for this session."}, {"start": 110.0, "text": "In it, you'll find all of the resources for the extension that I'll be building today."}, {"start": 115.0, "text": "So you can follow along."}, {"start": 118.0, "text": "In this session, I'll guide you through building a real extension"}, {"start": 122.0, "text": "— one that blocks content and modifies web pages."}, {"start": 126.0, "text": "Then, I'll cover a couple of different options for how you can package"}, {"start": 130.0, "text": "and distribute your extension to the App Store."}, {"start": 133.0, "text": "And to take the capabilities of your extension even further, I'll show you how"}, {"start": 138.0, "text": "your extension can work in tandem with its containing app."}, {"start": 143.0, "text": "And at the end, the extension will work in Safari on iOS, iPadOS, macOS,"}, {"start": 149.0, "text": "and visionOS simultaneously."}, {"start": 152.0, "text": "Because the beauty of web extensions is that they're all made up"}, {"start": 155.0, "text": "of HTML, CSS, and JavaScript."}, {"start": 159.0, "text": "So if you've done any web development before,"}, {"start": 161.0, "text": "you already know most of what you need."}, {"start": 164.0, "text": "In today's session, I'll be building an extension that allows people to block"}, {"start": 168.0, "text": "distracting sites while they browse the web."}, {"start": 171.0, "text": "And as you know, there are many rabbit holes you can go down while browsing."}, {"start": 176.0, "text": "Take webkit.org for example."}, {"start": 178.0, "text": "There are hundreds of articles on there and I can easily lose hours"}, {"start": 182.0, "text": "just reading about what's new in WebKit."}, {"start": 185.0, "text": "So, I need an extension like this."}, {"start": 187.0, "text": "Today, I'm going to build it, and it has two different blocking modes:"}, {"start": 192.0, "text": "a Light mode that allows up to 10 minutes of browsing on a site,"}, {"start": 195.0, "text": "perfect for reading a few WebKit articles,"}, {"start": 198.0, "text": "and a Full mode that redirects users the moment"}, {"start": 201.0, "text": "they try to navigate there."}, {"start": 203.0, "text": "To get started, I'll open up my favorite code editor."}, {"start": 206.0, "text": "You can also use Xcode, but any editor works to build an extension."}, {"start": 211.0, "text": "In my code editor, I'll create a folder to add all of the files for this extension."}, {"start": 217.0, "text": "To lay the ground work, the first file every extension needs is a manifest."}, {"start": 222.0, "text": "The manifest is a JSON formatted file that tells the browser what your extension is"}, {"start": 228.0, "text": "and what it can do."}, {"start": 230.0, "text": "Think of the manifest as an ID card for you extension."}, {"start": 233.0, "text": "It'll contain information, like the extension's name,"}, {"start": 237.0, "text": "description, and version number."}, {"start": 239.0, "text": "Next, I'm going to add the images folder that will hold my extension's icon."}, {"start": 245.0, "text": "The icon can appear in a variety of places."}, {"start": 247.0, "text": "Like the toolbar..."}, {"start": 249.0, "text": "or Extensions Settings."}, {"start": 251.0, "text": "Depending on where it appears, different sizes will be needed."}, {"start": 255.0, "text": "So, I'll add the icon as an svg."}, {"start": 259.0, "text": "Safari handles scaling the icon perfectly so I can focus on what's important"}, {"start": 264.0, "text": "like hopping into my code editor to show what this looks like in practice."}, {"start": 269.0, "text": "In the manifest file, I've added the extension's icon."}, {"start": 273.0, "text": "And the icon is located in my images folder."}, {"start": 276.0, "text": "To see what this looks like, I'll save my changes and I'll head over to Safari"}, {"start": 281.0, "text": "to load the extension."}, {"start": 283.0, "text": "And it's so easy to load an extension in Safari."}, {"start": 286.0, "text": "All I have to do is open Safari Settings using Command Comma,"}, {"start": 290.0, "text": "and click on the Advanced Settings pane"}, {"start": 292.0, "text": "and check \"Show features for web developers\"."}, {"start": 296.0, "text": "This will enable the developer pane"}, {"start": 298.0, "text": "and from there I can add a temporary extension."}, {"start": 302.0, "text": "Since it's an extension that hasn't had its code signature verified,"}, {"start": 306.0, "text": "I'll need to allow unsigned extensions."}, {"start": 309.0, "text": "After allowing, I'll select the folder containing my extension's resources."}, {"start": 314.0, "text": "And just like that, I've got my extension loaded in Safari!"}, {"start": 318.0, "text": "Most extensions will have some sort of UI for people to interact with."}, {"start": 322.0, "text": "For my extension, I want to have a way for people to add"}, {"start": 325.0, "text": "distracting sites to a block list."}, {"start": 328.0, "text": "So I'll need to add some custom UI."}, {"start": 331.0, "text": "There are a couple of ways I can do this."}, {"start": 334.0, "text": "One way is with the extension's action button."}, {"start": 337.0, "text": "This is the button that's added for the extension in Safari's toolbar."}, {"start": 341.0, "text": "When clicked, Safari will display a popup with the UI that you've defined for it."}, {"start": 347.0, "text": "The file name for the popup, or any resource that you define in the manifest,"}, {"start": 352.0, "text": "can have any name you'd like, as long as it's associated with the right manifest key"}, {"start": 358.0, "text": "so that Safari knows what it is and how it should be used."}, {"start": 363.0, "text": "In this example, the file used to load the default popup is set to \"popup.html\"."}, {"start": 369.0, "text": "For my extension, displaying a blocklist in a popup"}, {"start": 373.0, "text": "might make the UI look a bit cramped,"}, {"start": 375.0, "text": "so instead, I'll display it using the extension's options page."}, {"start": 380.0, "text": "This will be a full page where users can set settings for my extension."}, {"start": 385.0, "text": "Now, I'm going to jump back into the code editor to add this change."}, {"start": 390.0, "text": "In the manifest, I have the options page defined."}, {"start": 394.0, "text": "And I'll add a file for it in my extension's folder."}, {"start": 397.0, "text": "Before adding the full UI for this page,"}, {"start": 400.0, "text": "I'll start off with something simple, like Hello World."}, {"start": 404.0, "text": "In Safari, I can test my changes by reloading the extension."}, {"start": 409.0, "text": "Nice!"}, {"start": 410.0, "text": "My new changes worked since my extension now has this Settings button."}, {"start": 415.0, "text": "And clicking on the button opens my extension's options page!"}, {"start": 419.0, "text": "Now that I've got that set up, my extension will need to do more"}, {"start": 422.0, "text": "than just show \"Hello World\" for it to be of much use."}, {"start": 426.0, "text": "So I've designed a page that allows users to switch between"}, {"start": 429.0, "text": "a Light mode and a Full mode."}, {"start": 432.0, "text": "I've already written the interface for this using HTML, CSS, and JavaScript."}, {"start": 438.0, "text": "It's pretty and interactive, but I need to wire it up."}, {"start": 442.0, "text": "So now I'll take a look at how to upgrade my extension to start blocking content."}, {"start": 448.0, "text": "I'll do this using the declarative net request API, which will give my extension"}, {"start": 453.0, "text": "the ability to block, modify or redirect network requests."}, {"start": 459.0, "text": "This API is what powers my favorite type of web extensions."}, {"start": 464.0, "text": "With this capability, extensions can filter web content such as ads"}, {"start": 468.0, "text": "and trackers that can target users while they browse the web."}, {"start": 473.0, "text": "But in order for my extension to access these features,"}, {"start": 476.0, "text": "I'll need to add a permission for it."}, {"start": 479.0, "text": "Permissions are how extensions tell Safari what they need access to."}, {"start": 483.0, "text": "It can be things like accessing cookies,"}, {"start": 485.0, "text": "saving data to storage or writing to the clipboard."}, {"start": 490.0, "text": "For content blocking I'll need to add the declarative net request permission"}, {"start": 495.0, "text": "and I can do this in my extension's manifest."}, {"start": 498.0, "text": "With this in place, I can start defining rules."}, {"start": 503.0, "text": "A rule has an ID, a priority, and the type of action that should occur"}, {"start": 508.0, "text": "when the conditions are met."}, {"start": 509.0, "text": "This rule, for example, will block all navigations to webkit.org."}, {"start": 514.0, "text": "There are two ways I can define rules."}, {"start": 517.0, "text": "One option is to define them in the manifest."}, {"start": 521.0, "text": "These are called static rules."}, {"start": 523.0, "text": "They're great when you already know what rules you want to use."}, {"start": 527.0, "text": "But, if you need a little flexibility, you can add rules dynamically at runtime"}, {"start": 532.0, "text": "using JavaScript."}, {"start": 534.0, "text": "I'm going the go with the dynamic approach because I won't know which sites to block"}, {"start": 539.0, "text": "until the user adds them to the list."}, {"start": 542.0, "text": "I'll put this logic in a file named rules.js, inside the utilities folder."}, {"start": 548.0, "text": "Then I'll use my host.js file to create the rule"}, {"start": 552.0, "text": "when a user adds a site to the blocklist."}, {"start": 555.0, "text": "I'm going to jump back into my code and wire this up."}, {"start": 559.0, "text": "In my extension's manifest, I've added the declarative net request permission."}, {"start": 563.0, "text": "And I've replaced the previous options page with the HTML, CSS, and JavaScript files"}, {"start": 570.0, "text": "that I already created for my extension."}, {"start": 572.0, "text": "I also added the utilities folder with my two new files."}, {"start": 577.0, "text": "To add a rule, I head over to my rules.js file."}, {"start": 581.0, "text": "And since these rules specify an ID, I've added a helper method"}, {"start": 586.0, "text": "to map the host for the site to a unique integer ID."}, {"start": 590.0, "text": "Now, I create a rule specifying the ID, the type as \"block\", and the urlFilter"}, {"start": 598.0, "text": "to match the host for the site."}, {"start": 600.0, "text": "And then use the declarative net request updateDynamicRules API"}, {"start": 605.0, "text": "to add the rule to my extension."}, {"start": 607.0, "text": "In my host file, when a site is added to the list,"}, {"start": 611.0, "text": "I can add the rule if the extension"}, {"start": 614.0, "text": "is in the full blocking mode."}, {"start": 616.0, "text": "Going back to Safari, I reload to update my extension."}, {"start": 621.0, "text": "In the options page, I'll add webkit.org to the list."}, {"start": 628.0, "text": "And when I go to the site,"}, {"start": 631.0, "text": "it's been blocked!"}, {"start": 633.0, "text": "My extension can now block navigations to sites added to the list."}, {"start": 637.0, "text": "But, I don't really love that error page that shows up."}, {"start": 641.0, "text": "I'd rather send users somewhere more intentional, like a custom page"}, {"start": 646.0, "text": "that I've designed for my extension."}, {"start": 648.0, "text": "That's where redirect rules come into play."}, {"start": 651.0, "text": "This rule is similar to the previous block rule, except the type is redirect,"}, {"start": 657.0, "text": "and I can specify an extensionPath for the page the user will land on."}, {"start": 662.0, "text": "But before making this change, I need to add host permissions for my extension."}, {"start": 668.0, "text": "To block a network request, extensions don't need access to the page."}, {"start": 673.0, "text": "But for redirecting network requests, the extension does need access."}, {"start": 677.0, "text": "So, in my extension's manifest I'll use the declarativeNetRequestWithHostAccess"}, {"start": 683.0, "text": "permission instead."}, {"start": 685.0, "text": "And since my extension doesn't need to request access to any site upfront,"}, {"start": 690.0, "text": "I can use optional host permissions and request access to the site at runtime."}, {"start": 696.0, "text": "Host permissions tell Safari which sites your extension wants access to."}, {"start": 701.0, "text": "You can set them up as an array of match patterns,"}, {"start": 704.0, "text": "with each pattern consisting of a scheme, a host, and a path."}, {"start": 710.0, "text": "Since any site can be added to the list,"}, {"start": 713.0, "text": "I'll use a pattern that can match against all URLs."}, {"start": 717.0, "text": "If your extension needs explicit access to a site to work,"}, {"start": 720.0, "text": "you can use host permissions instead."}, {"start": 723.0, "text": "But the extension doesn't automatically get access to the site."}, {"start": 727.0, "text": "We designed the permissions model for extensions to respect user privacy."}, {"start": 732.0, "text": "Since a user's browsing experience can expose personal data,"}, {"start": 737.0, "text": "we put the user in control and they decide which sites the extension can access."}, {"start": 743.0, "text": "If you explicitly request access,"}, {"start": 745.0, "text": "Safari will show a badge on the extension's action button."}, {"start": 749.0, "text": "Clicking on the button brings up an alert, asking the user"}, {"start": 754.0, "text": "if they want to grant the extension access to the page."}, {"start": 757.0, "text": "If the user chooses to allow access, the icon will become tinted,"}, {"start": 762.0, "text": "notifying them that the extension is active on that page."}, {"start": 766.0, "text": "My extension doesn't need access to any site upfront,"}, {"start": 770.0, "text": "which is why I've gone with optional host permissions."}, {"start": 773.0, "text": "This way, I can request access for any site when my extension needs it."}, {"start": 778.0, "text": "In the manifest, I've changed the permission to declarativeNetRequestWithHostAccess."}, {"start": 784.0, "text": "And my extension can now request access to any site at runtime."}, {"start": 789.0, "text": "Now, in my rules file, I'll create the redirect rule."}, {"start": 794.0, "text": "It's very similar to the previous block rule, but now the type is redirect."}, {"start": 800.0, "text": "And it has the path to my custom extension page."}, {"start": 804.0, "text": "And I've added the resources for the page in my extension's folder."}, {"start": 809.0, "text": "Now instead of blocking the navigation, I'll redirect users to a page"}, {"start": 815.0, "text": "that I've designed for my extension."}, {"start": 818.0, "text": "And since my extension will need access to the site, I'll request access"}, {"start": 822.0, "text": "to the domain and subdomains using the permissions.request API."}, {"start": 828.0, "text": "To see what this experience looks like, in Safari, I'll update my extension."}, {"start": 834.0, "text": "In the options page, I'll add webkit.org."}, {"start": 839.0, "text": "And now, before the site is added, I'm prompted to grant the extension access."}, {"start": 845.0, "text": "Great!"}, {"start": 846.0, "text": "That's exactly what I was expecting."}, {"start": 849.0, "text": "I'll allow access and refresh the page."}, {"start": 854.0, "text": "Amazing!"}, {"start": 855.0, "text": "The navigation was redirected to my custom extension page."}, {"start": 859.0, "text": "My extension is really coming along!"}, {"start": 862.0, "text": "It can now redirect navigations to distracting sites."}, {"start": 866.0, "text": "But let's be real."}, {"start": 868.0, "text": "Going no-contact with some of your favorite sites can be hard."}, {"start": 872.0, "text": "So I'm going to add a mode that allows up 10 minutes of browsing"}, {"start": 877.0, "text": "— with a countdown timer right on the page."}, {"start": 880.0, "text": "To make that happen, I'll need a way to inject content directly onto the page."}, {"start": 885.0, "text": "This is where content scripts come into play."}, {"start": 888.0, "text": "Content scripts give extensions the ability to read and modify"}, {"start": 893.0, "text": "the contents of a web page."}, {"start": 896.0, "text": "Scripts can be static — declared right in the manifest"}, {"start": 900.0, "text": "with the files and match patterns for the sites they'll run on."}, {"start": 904.0, "text": "This works well if you already know which sites to target."}, {"start": 908.0, "text": "But in my case, I won't know the sites until it's added to the list."}, {"start": 913.0, "text": "So I'll add them on the fly, using the registered content scripts API."}, {"start": 918.0, "text": "These work just like static scripts but they have two additional fields:"}, {"start": 923.0, "text": "an ID and a persistence flag."}, {"start": 926.0, "text": "Setting this to true means that the scripts will remain"}, {"start": 929.0, "text": "after Safari relaunches."}, {"start": 931.0, "text": "To use this API, I'll add the scripting permission in the manifest"}, {"start": 936.0, "text": "and new scripting.js file in my utilities folder."}, {"start": 941.0, "text": "Here, I'll define the content script."}, {"start": 943.0, "text": "I'll give it an ID, the javascript file for the timer, the CSS for styling,"}, {"start": 950.0, "text": "and match patterns that cover the domain and any subdomains of the site."}, {"start": 954.0, "text": "And I'll set this flag to true."}, {"start": 957.0, "text": "Then, I'll add the script using the register content scripts API."}, {"start": 962.0, "text": "Going back to my addHost method, now, when the user adds a site to the block list,"}, {"start": 969.0, "text": "I'll add a content script to show a timer for that site."}, {"start": 973.0, "text": "When the extension is in the full blocking mode, the redirect will trigger before"}, {"start": 978.0, "text": "the page loads so I can always register the scripts."}, {"start": 982.0, "text": "Now with the light blocking mode selected, I'll add webkit.org to the list."}, {"start": 989.0, "text": "And when I go to the site, a 10-minute timer is shown on the page."}, {"start": 995.0, "text": "My extension is so close to being something I want to get out into the world,"}, {"start": 1000.0, "text": "but there's something I want to fix first."}, {"start": 1002.0, "text": "As you may have noticed, every time I reload the extension,"}, {"start": 1006.0, "text": "I keep having to add the same site back to the list."}, {"start": 1011.0, "text": "This is happening because I'm storing all of this information in memory,"}, {"start": 1015.0, "text": "so when the extension reloads, that state is gone."}, {"start": 1019.0, "text": "I can keep this data around using the storage API."}, {"start": 1023.0, "text": "Safari supports two kinds of storage areas."}, {"start": 1026.0, "text": "Session storage is great for quick, in-memory stuff"}, {"start": 1030.0, "text": "that doesn't need to survive a restart."}, {"start": 1033.0, "text": "But I want my blocklist to stick around"}, {"start": 1036.0, "text": "so local storage, which writes data to disk, is the right call here."}, {"start": 1041.0, "text": "To use the storage API, I'll add the permission in the manifest."}, {"start": 1046.0, "text": "Then, I'll add a new file in my utilities folder."}, {"start": 1051.0, "text": "In this file, I'll define a few helper methods"}, {"start": 1054.0, "text": "to update and get the hosts in storage."}, {"start": 1057.0, "text": "And a couple more to save and get the block mode."}, {"start": 1061.0, "text": "Going back to my addHost method, now when the user adds a new site, I can update"}, {"start": 1067.0, "text": "the list of hosts in storage."}, {"start": 1070.0, "text": "And I can use the stored list to display the block list."}, {"start": 1074.0, "text": "With this change, the blocklist will always render"}, {"start": 1077.0, "text": "with the full list of sites that were added!"}, {"start": 1080.0, "text": "Similarly, when the user switches between the two modes,"}, {"start": 1084.0, "text": "I'll save the change to storage"}, {"start": 1087.0, "text": "and if the extension is in the full blocking mode,"}, {"start": 1090.0, "text": "I can create the redirect rules for all of the sites."}, {"start": 1094.0, "text": "To see the benefits of using the storage API, in Safari,"}, {"start": 1098.0, "text": "I'll change the blocking mode to \"Full\""}, {"start": 1101.0, "text": "and add a site to the list."}, {"start": 1105.0, "text": "Now, when I reload the extension,"}, {"start": 1107.0, "text": "the changes that I just made are still there!"}, {"start": 1111.0, "text": "Great!"}, {"start": 1112.0, "text": "Using the storage API has tackled one persistence problem with my extension,"}, {"start": 1118.0, "text": "but there's another one I need to fix."}, {"start": 1120.0, "text": "Registered content scripts persist across Safari restarts,"}, {"start": 1124.0, "text": "but not across extension updates."}, {"start": 1127.0, "text": "So if a user updates my extension, they'll lose the content scripts."}, {"start": 1132.0, "text": "To fix that, I need a way for my extension to know it's been updated"}, {"start": 1137.0, "text": "so that it can read the hosts from storage and recreate the scripts."}, {"start": 1142.0, "text": "The perfect place for that is in a background page or a service worker."}, {"start": 1148.0, "text": "Both can do the same things: like manage your extension's lifecycle,"}, {"start": 1153.0, "text": "listen for browser events and pass messages between parts of your extension."}, {"start": 1159.0, "text": "Safari supports both, so it's really your preference!"}, {"start": 1163.0, "text": "I like background pages since they have access to the DOM, so I'll go with that."}, {"start": 1169.0, "text": "To add the background page, I'll specify it in the manifest."}, {"start": 1174.0, "text": "Then, I'll add the file to my extension's folder."}, {"start": 1178.0, "text": "Here, I'll register for the onInstalled event."}, {"start": 1182.0, "text": "This will let my extension know that it's been updated to a newer version."}, {"start": 1186.0, "text": "When this happens, I'll read the hosts from storage"}, {"start": 1189.0, "text": "and re-register the content scripts."}, {"start": 1192.0, "text": "Getting my extension hooked up to read and write from storage"}, {"start": 1196.0, "text": "has made such a huge improvement."}, {"start": 1198.0, "text": "I think it's time to take a look at how I can get my extension on the App Store."}, {"start": 1204.0, "text": "One way for me to do this is with App Store Connect."}, {"start": 1208.0, "text": "App Store Connect is where you can upload, submit, and manage your extensions,"}, {"start": 1215.0, "text": "whether you've just built yours or your looking to bring"}, {"start": 1218.0, "text": "an existing one to Safari."}, {"start": 1220.0, "text": "And the best part?"}, {"start": 1221.0, "text": "You can do this from any browser, without using a Mac."}, {"start": 1225.0, "text": "To get started, I'll head over to developer.apple.com and enroll"}, {"start": 1231.0, "text": "in the Apple Developer Program."}, {"start": 1233.0, "text": "After enrolling, I'll go to appstoreconnect.apple.com."}, {"start": 1238.0, "text": "Since Safari web extensions need to be packaged within a containing app,"}, {"start": 1243.0, "text": "I can use App Store Connect to create this app for me."}, {"start": 1247.0, "text": "When creating the app, I'll need to specify a few things,"}, {"start": 1251.0, "text": "such as the platforms I want my extension available on."}, {"start": 1255.0, "text": "I'll choose iOS and macOS,"}, {"start": 1258.0, "text": "which will make my extension available on iPhone, iPad, Mac,"}, {"start": 1263.0, "text": "and as a compatible app on Apple Vision Pro."}, {"start": 1267.0, "text": "I'll also set the bundle identifier which is a unique ID for my app."}, {"start": 1272.0, "text": "After adding all the details, I'll switch over to the tab for Xcode Cloud"}, {"start": 1278.0, "text": "and scroll down to the Safari Web Extension Packager"}, {"start": 1281.0, "text": "to upload my extension's resources."}, {"start": 1284.0, "text": "Once it's uploaded, the Safari Web Extension Packager"}, {"start": 1288.0, "text": "will handle packaging my extension in a matter of minutes!"}, {"start": 1292.0, "text": "Once it's finished, I can view any issues or take next steps to test my extension"}, {"start": 1298.0, "text": "using TestFlight."}, {"start": 1300.0, "text": "TestFlight allows me distribute beta builds"}, {"start": 1303.0, "text": "so I can continuously make improvements and implement user feedback"}, {"start": 1307.0, "text": "before submitting my extension to the App Store."}, {"start": 1310.0, "text": "Once I'm ready to submit,"}, {"start": 1312.0, "text": "I'll head over to the Distribution tab to add my finishing touches."}, {"start": 1318.0, "text": "Like a screenshot of my extension in action."}, {"start": 1321.0, "text": "And a description to help users understand"}, {"start": 1323.0, "text": "the features and capabilities of my extension."}, {"start": 1327.0, "text": "After adding all the details, I'll select the build, and submit it for review."}, {"start": 1333.0, "text": "And that's how you can distribute your extension using App Store Connect!"}, {"start": 1338.0, "text": "Throughout this session, I've shown how standard WebExtension APIs and features"}, {"start": 1343.0, "text": "come together to build a customizable browsing experience."}, {"start": 1347.0, "text": "But what if I told you that your extension can go beyond just the web platform."}, {"start": 1353.0, "text": "I'm going to guide you on how you can use native messaging to access features"}, {"start": 1358.0, "text": "that the platform offers."}, {"start": 1360.0, "text": "From here, I'll need to use Xcode."}, {"start": 1363.0, "text": "The simplest way for me to do this is with the Safari Web Extension Packager tool."}, {"start": 1369.0, "text": "Running this command in Terminal will create and launch an Xcode project for me."}, {"start": 1375.0, "text": "It'll contain my app and web extension."}, {"start": 1378.0, "text": "From there, I can hook them up to send and receive messages."}, {"start": 1382.0, "text": "We call this native messaging."}, {"start": 1385.0, "text": "Think of it as three people passing notes."}, {"start": 1389.0, "text": "The JavaScript in my extension kicks things off."}, {"start": 1392.0, "text": "The App Extension in the middle catches that message"}, {"start": 1395.0, "text": "and hands it to the native app."}, {"start": 1397.0, "text": "Then the app does its thing and sends the result back the same way."}, {"start": 1403.0, "text": "For my app, I'm going to have it help the extension protect its settings"}, {"start": 1407.0, "text": "by requiring bio authentication before a change is made to the block list."}, {"start": 1412.0, "text": "To get started, I'll add the nativeMessaging permission"}, {"start": 1416.0, "text": "in my extension's manifest."}, {"start": 1418.0, "text": "Then, in my extension's background page, I'll add a method to send a message"}, {"start": 1424.0, "text": "from my extension to its native app."}, {"start": 1426.0, "text": "The message I get back will let me know if the authentication succeeded."}, {"start": 1432.0, "text": "In order for my app to receive the message,"}, {"start": 1434.0, "text": "I'll need to modify the SafariWebExtensionHandler,"}, {"start": 1438.0, "text": "a class contained in a file the packager tool created for me."}, {"start": 1443.0, "text": "And the great thing is, it comes with a template that already allows my app"}, {"start": 1447.0, "text": "to receive messages from my extension."}, {"start": 1450.0, "text": "All I need to do is make a few tweaks,"}, {"start": 1453.0, "text": "like parsing the message for the requestBioAuth key."}, {"start": 1457.0, "text": "Then, I'll use a System API to request user authentication with biometrics"}, {"start": 1464.0, "text": "and once the user has authenticated, the app sends the message back"}, {"start": 1468.0, "text": "to the web extension with the result."}, {"start": 1471.0, "text": "I'm going to hop into Xcode to build the extension and test my changes in Safari."}, {"start": 1476.0, "text": "In Xcode, I have the changes made to send and receive messages to my extension."}, {"start": 1483.0, "text": "And in the Project Navigator,"}, {"start": 1484.0, "text": "my project has all of the resources for my web extension."}, {"start": 1489.0, "text": "I'll use the Command+B shortcut to build the extension."}, {"start": 1492.0, "text": "And in Safari, I'll enable the extension,"}, {"start": 1497.0, "text": "open the options page,"}, {"start": 1500.0, "text": "and add webkit.org to the list."}, {"start": 1504.0, "text": "And before it gets added, I'm prompted to authenticate with touch ID."}, {"start": 1508.0, "text": "And after authenticating, the site is added to the list."}, {"start": 1512.0, "text": "And that's how you can use native messaging to have your app"}, {"start": 1517.0, "text": "and web extension work together."}, {"start": 1520.0, "text": "And with that, I can proudly say my extension is ready for distribution."}, {"start": 1525.0, "text": "I'll do this using Xcode."}, {"start": 1528.0, "text": "I'll start by building an archive."}, {"start": 1531.0, "text": "And since I previously used App Store Connect to create a build,"}, {"start": 1535.0, "text": "I'll want to make sure this build number is one step higher than my last build."}, {"start": 1540.0, "text": "And in the Organizer window I'll distribute my extension."}, {"start": 1545.0, "text": "And that's how you can create and distribute a Safari web extension"}, {"start": 1549.0, "text": "from the ground up."}, {"start": 1550.0, "text": "We covered a lot today."}, {"start": 1552.0, "text": "Whether you're just starting out or looking to take an extension further,"}, {"start": 1557.0, "text": "I hope you feel ready to take your idea and turn it into something real."}, {"start": 1562.0, "text": "I can't wait to see what you build."}, {"start": 1565.0, "text": "If you haven't already,"}, {"start": 1566.0, "text": "download the sample code project"}, {"start": 1568.0, "text": "to play around with some of the APIs we featured today."}, {"start": 1572.0, "text": "You can learn more about them by checking out the cross-browser documentation"}, {"start": 1576.0, "text": "for web extensions on MDN."}, {"start": 1579.0, "text": "And finally, provide feedback through Feedback Assistant."}, {"start": 1583.0, "text": "Or file a bug on bugs.webkit.org"}, {"start": 1587.0, "text": "as you test your web extensions on Safari 27."}, {"start": 1591.0, "text": "Thanks for joining me on this journey and have a great WWDC!"}]}