Skip to content

custom elements - #804

Open
titerman wants to merge 4 commits into
mainfrom
custom-elements
Open

titerman wants to merge 4 commits into
mainfrom
custom-elements

Conversation

@titerman

@titerman titerman commented Nov 7, 2026

Copy link
Copy Markdown
Collaborator

No description provided.

@eandreeva-twr eandreeva-twr left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this, the feature is well covered and the recipes are great. Below is what I'd change before the release. SDK references are to AdaptySDK-iOS release/4.4.0.

Missing from the PR: other iOS 4.4 changes

  • Localized billing period for 12-month commitment plans. 4.4 adds subscriptionPricingTerms.localizedBillingPeriod and subscriptionPricingTerms.commitmentInfo.localizedPeriod, and deprecates product.localizedBillingPeriod in favor of the first one. Our docs still tell readers to display billingPeriod and commitmentInfo.period, which are period structs, not display strings:

    • set-up-12-month-commitment.mdx:75-76 and :147
    • fetch-paywalls-and-products.mdx:140

    Suggest: "To display the installment, use subscriptionPricingTerms.localizedBillingPrice and subscriptionPricingTerms.localizedBillingPeriod (SDK 4.4+). To display the yearly total, use commitmentInfo.localizedPrice and commitmentInfo.localizedPeriod (SDK 4.4+)."

Major

1. The two resolver sections describe the wrong reason to choose each

ios-flow-custom-elements.mdx:116-128, :84, and the recipes that link to these anchors.

"Display the same views to all users" vs. "Display user-specific data" isn't the real difference:

  • The app-wide resolver is looked up again every time an element renders (AdaptyUICustomElementsViewModel.swift:31-36), so its views can show the current user's data.
  • A per-flow resolver fully replaces the app-wide one in that flow, and it is stored in the flow configuration when you fetch it (AdaptyFlowConfiguration.swift:84,101). Values it holds stay fixed from that moment.

Suggested structure:

Which resolver to use

Start with the app-wide resolver. Register all your custom element views in one place, and every flow can use them, so the builder user can add them to new flows and A/B tests without a developer. A view registered this way can still show the current user's data, because it reads that data from your app each time it appears.

Add a resolver to a single flow only when that flow needs something your app knows only at the moment it opens the flow, such as answers the user just gave in a previous flow or the item they tapped to open it. You can also use one when this flow must show a different view for the same Type.

In short: if the view needs something you only know when you open this particular flow, attach a resolver to that flow. Otherwise, use the app-wide resolver.

Headings: ### Set an app-wide resolver and ### Attach a resolver to one flow. Examples for each:

  • App-wide: reusable app components (a branded animation, testimonials, a plan comparison, your video player), views that show current app data (a streak ring, the user's name, course progress), views that use the device (a camera preview, a map).
  • One flow: data passed from a previous flow (the chart recipe), data from where the flow opens (a paywall opened from "Export" previews the user's document), a different view for the same Type in one flow.

Under the per-flow heading, lead with the two caveats: it replaces the app-wide resolver in that flow, and its values are fixed when you fetch the flow, so fetch it again if they change.

Related:

  • :147 calls CustomElementsResolver().view(...), which builds a second copy of the app-wide resolver on every call. Apps can't read the registered resolver back, so inject it instead:

    struct StreakResolver: AdaptyUICustomElementsResolver {
        let streakProgress: AnimationProgressTime
        let base: any AdaptyUICustomElementsResolver // the resolver you set app-wide
    
        func view(forType type: String, id: String, context: AdaptyUICustomElementContext) -> AnyView? {
            if type == "lottie_animation", id == "streak_ring" { /* … */ }
            return base.view(forType: type, id: id, context: context)
        }
    }

    Also mention that dictionaries can be combined with merging, which the SDK suggests in AdaptyUICustomElementsViewModel.swift:17-18.

  • :84 ("Store the data that your app already has … in properties of the resolver") follows from the same framing. A resolver created once at launch freezes those values. Say the view should read current data from your app's model, for example a store object passed in.

  • The streak recipe works better with the app-wide resolver reading the streak from the app's model. The chart recipe stays the per-flow example.

  • :158: streakProgress is undefined in this snippet.

2. "What changes require an app release" table is wrong in two rows

flow-custom-elements.mdx:99-105

  • Row 1: when the native view changes, users who haven't updated see the previous native view, not the fallback. Their app still has the old resolver for the same Type and ID.
  • Row 3 ("custom element properties → the updated content"): this is true for layout, size and fallback, but not for Type or ID. Changing either sends every released build to the fallback. Split the row.
  • Add a row: placing a Type and ID your app already supports in a new screen, flow or A/B variant → publish the flow, no release.
  • Row 2 "modify the custom element to reference the new content" is hard to act on. Suggest "add a custom element with the new Type and ID, and publish the flow".
  • :99: "Custom element display" → "Custom elements display"; the final "can." doesn't resolve.

3. Reuse across flows needs the same ID too

flow-custom-elements.mdx:55 says to reuse a view on several screens or flows, "Enter the same Type". Every resolver in the PR also checks the ID, so a builder user who copies only the Type gets the fallback. Suggest: "On several screens or flows: Enter the same Type and ID in each custom element."

4. Platform scope

  • The builder article and the three recipes never say that only iOS displays the native view. Android requests an older schema version and shows the fallback; cross-platform SDKs have no resolver API, so they show it too. Add to the intro of flow-custom-elements.mdx and to the fallback list at :61-66: "Only the Adapty iOS SDK 4.4+ displays custom elements. On other platforms, users see the fallback."
  • ios-flow-custom-elements.mdx:19: "The resolver API for cross-platform frameworks is coming soon." Timing statements go stale. State only what's true: "Custom elements require a native iOS app."
  • The recipes are iOS-only, so move them from tutorial.json:393-407 to ios.json under Flows & paywalls, nested under or after Display custom elements.

5. Explain context

No snippet uses the context parameter, and the article never mentions it. Builder flows don't pass any data to it yet, so a sentence is enough: "The context parameter is for data that the Flow & Paywall Builder can't pass to custom elements yet. Build the view from your app's data."

6. Map recipe: permission handling and repeated work

custom-element-map.mdx:49, :93-130; ios-flow-custom-elements.mdx:82, :112

  • The resolver returns nil when access is denied, but it only runs again on an unrelated redraw (products loading, a purchase starting). If the user taps "Don't Allow", the map stays empty, then switches to the fallback at a random later moment. Handle denial inside the view, and return nil from the resolver only when access is already denied when the screen opens.
  • CLLocationManager() is created on every resolver call (:114) and in a @State initial value (:49), which SwiftUI evaluates on every struct init. Keep one manager in the resolver, and use @StateObject in the view.
  • Add a rule to ios-flow-custom-elements.mdx:82: "Create players, sessions, and network requests in .task or a @StateObject, not in @State initial values or in the resolver."

7. Publish, then test

flow-custom-elements.mdx:16, :91-95. Test devices fetch the latest published version, and every recipe starts testing with "Publish the flow", so the step is "Publish and test the flow". Recommend testing on a placement that real users don't see.

Minor

flow-custom-elements.mdx

  • Structure (:11-17, :23-95): five intro steps vs. ## Setup with four H3s, and step 5 links to a reference section. "Distribute" is an odd word here. Suggest numbered H2s that match the intro word for word:

    1. ## 1. Add a custom element to the screen
    2. ## 2. Add fallback content
    3. ## 3. Set up the native view in your app
    4. ## 4. Publish and test the flow
    5. "Release the app update with the native view", as one sentence that links to What changes require an app release.

    Reuse (:53-57) can be its own ### Reuse a native view in several custom elements.

  • :25 ### 1. Add and configure doesn't name the object.

  • :3, :7 vs :19-21: the intro offers "any view from your actual app" and a Lottie/Rive animation, then the tip says "only" for touch, data or camera, and "Video" for recorded animations. Align them, and drop "actual". Rive isn't covered anywhere else.

  • :31-33: the carousel warning is about placement but sits under "Open the Add element panel". Add a step to place the element, and attach the warning there.

  • :39: the builder prefills the ID (custom_1). Say so before :57 uses custom_1: "In the ID field, replace the default ID, such as custom_1, with a name your app uses, such as welcome_wave."

  • :43: Fixed (200) is already the default height. "Keep the default Fixed height, and set the height the native view needs." Same in the recipes (Lottie :26, chart :34, map :23).

  • :45 / iOS :44: two names for one setting ("alignment grid", "container's alignment setting"). The placement follows the custom element's own alignment in the Layout section (top-left by default).

  • :56: quote the warning and say it doesn't block publishing: "If two custom elements on one screen have the same Type and ID, the ID field shows a warning: Same type and ID as another element on this screen. You can still publish the flow."

  • :57: the builder renames a taken ID on duplicate, paste, and when you add a tab, within the screen or component, for any ID (hero → hero_2).

  • :68: "an empty area the size of the custom element" is true only with a Fixed height. With Hug, it takes no space.

  • :76: the position control already disables Fixed inside containers, so the first half documents the UI. Keep the part the UI doesn't say: "You can't add a footer, a sliding sheet, or a bottom sheet inside a custom element."

  • :87-89: "given that it was collected in an earlier flow" narrows the data source; the streak comes from the app. "A native view can show the user's data, such as answers from an earlier flow."

  • :95: purpose last, and it repeats the iOS test section. "To see the latest published flow instead of a cached one, register the device as a test device. For the full procedure, read Test on a device."

  • :118: "make sure that your app shows no view" → "make sure the resolver returns nil when the content is missing", with a link to Set conditions for fallback content.

ios-flow-custom-elements.mdx

  • :12, :18: fine as written. Older SDKs do get the fallback.

  • :44: "Flow & Paywall builder" → Builder; "will be determined by container's alignment setting" → "follows the alignment in the Layout section of the custom element".

  • :46: OK for builder flows. "Yet" would be accurate if you want it: "The native view can't run flow actions yet."

  • :47: unresolved "it", "terminate everything you may need", double space. "When the user moves to another screen, SwiftUI calls the view's onDisappear modifier. In it, stop the camera sessions and other processes that the view started. For async work, use .task, which SwiftUI cancels for you."

  • View requirements: add that inside a product card or another selectable element with a selected state, the native view is created again and loses its state each time the selection changes, so keep the custom element outside such elements.

  • :70: the first resolver calls .named(id) without a guard, which :90-110 then calls a defect. Show the guarded version only.

  • :78: the example doesn't illustrate the sentence. "In the default case, return nil. If you later add a custom element with a new Type, users of this app version see its fallback."

  • :83: "One native view per Type" → "One view type per custom element". State resets per element; different IDs of one Type can map to different views.

  • :86-88: add a three-line dictionary example:

    AdaptyUI.setCustomElementsResolver([
        "lottie_animation": .init { _, id, _ in LottieElementView(animation: .named(id)) },
    ])
  • :92: "malform" → "In some cases, the native view has nothing to show and leaves an empty area:"

  • :112: "ask for the permission inside it" links to #write-a-resolver-…, which says nothing about permissions; link to the map recipe. The last clause ("while the fallback stays…") reads as if the fallback is showing in the recommended path. Suggest: "If you return nil before the user answers, the fallback stays even after they grant access, until the SDK calls the resolver again."

  • :118, :175: "before you present a flow" isn't required (the resolver is looked up on every render). "Such as at app launch" is enough; drop the :175 checklist item.

  • The registration snippet (:121) needs import AdaptyUI.

Recipes

  • All three :13: "they will always render" → "Earlier SDK versions show the fallback content for every custom element."
  • Step 4 headings ("Display the animation/chart/map in the custom element") read like an outcome and like step 3. Use "Pass the resolver to the flow" (Lottie, chart) and "Set the resolver app-wide" (map).
  • Lottie :46-62 and the chart resolver return nil for every other Type, while the article tells readers to call the app-wide resolver. Make them consistent, or say why.
  • Chart :7: reorder the intro so it starts with the goal.
  • Chart :24: lead with the purpose: "To read the weights the same way in every locale, select Integer in the Format dropdown."
  • Chart :26: keep the prescription (end on a screen with no inputs) and drop "The Close flow action doesn't send them."
  • Chart :42: "Step 6 shows this method" → link text that says where it goes.
  • Chart :114: WeightPlan.entries is computed on every resolver call; make it a let.
  • Chart :132, :207: Chart needs iOS 16. Add @available(iOS 16.0, *) to WeightGoalChart, so apps with an iOS 15 deployment target compile.
  • Map :7: drop "straight".
  • Map :103: declares a second struct CustomElementsResolver, which clashes with the article's. Show only the new case to add to the existing resolver.

Other edited articles

  • flow-builder-recipes.mdx:9: "uses the standard Flow & Paywall Builder elements" now contradicts the new recipes. "Most guides use only the standard elements."
  • ios-flow-custom-tags.mdx:56: the link promises a BMI result as a chart; the recipe charts projected weight. "To show the user's entered weights as a chart, see […]."
  • custom-media.mdx:126: the new sentence interrupts the procedure. Move it to the end of the section.

Unrelated files

  • public/api-specs/*.yaml: prebuild copies of src/api-reference/specs/. Please drop them.
  • .claude/skills/find-stand/SKILL.md: a whitespace-only change that also removes a Markdown line break. Please revert.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants